mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
79 Commits
v0.10.3
...
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 | ||
|
|
fa053b4d1f | ||
|
|
bcc7afa3e4 | ||
|
|
647a5d05a6 | ||
|
|
e15d41f80d | ||
|
|
6c7c919d49 | ||
|
|
dfdedf9e48 | ||
|
|
7e76eb4dd1 | ||
|
|
c1f777e23e | ||
|
|
975d13c7e1 | ||
|
|
017bdd2778 | ||
|
|
548df8c0f4 | ||
|
|
1e6dbb5e8e | ||
|
|
431dadb6c2 | ||
|
|
22e42b62ad | ||
|
|
49ee8ce132 | ||
|
|
9bace8e58b | ||
|
|
c0afcaf3f6 | ||
|
|
53edf06476 | ||
|
|
255e9e4095 | ||
|
|
03f923c6e2 | ||
|
|
4685ef7439 | ||
|
|
626cfb80b4 | ||
|
|
9591fbff08 | ||
|
|
fbda00f059 | ||
|
|
1907e7e59c | ||
|
|
ffe7b04bea | ||
|
|
fe0a662afd | ||
|
|
319584d911 | ||
|
|
2d40b2dfe5 | ||
|
|
e32b713742 | ||
|
|
988357fb55 |
10
.github/workflows/deploy.yml
vendored
10
.github/workflows/deploy.yml
vendored
@@ -70,8 +70,12 @@ jobs:
|
||||
file: ./Dockerfile.cloud
|
||||
push: true
|
||||
tags: |
|
||||
siumauricio/cloud:${{ github.ref_name == 'main' && 'main' || 'canary' }}
|
||||
siumauricio/cloud:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
|
||||
platforms: linux/amd64
|
||||
build-args: |
|
||||
NEXT_PUBLIC_UMAMI_HOST=${{ secrets.NEXT_PUBLIC_UMAMI_HOST }}
|
||||
NEXT_PUBLIC_UMAMI_WEBSITE_ID=${{ secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}
|
||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${{ secrets.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY }}
|
||||
|
||||
build-and-push-schedule-image:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -93,7 +97,7 @@ jobs:
|
||||
file: ./Dockerfile.schedule
|
||||
push: true
|
||||
tags: |
|
||||
siumauricio/schedule:${{ github.ref_name == 'main' && 'main' || 'canary' }}
|
||||
siumauricio/schedule:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
|
||||
platforms: linux/amd64
|
||||
|
||||
|
||||
@@ -117,5 +121,5 @@ jobs:
|
||||
file: ./Dockerfile.server
|
||||
push: true
|
||||
tags: |
|
||||
siumauricio/server:${{ github.ref_name == 'main' && 'main' || 'canary' }}
|
||||
siumauricio/server:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
|
||||
platforms: linux/amd64
|
||||
@@ -71,10 +71,9 @@ Run the command that will spin up all the required services and files.
|
||||
pnpm run dokploy:setup
|
||||
```
|
||||
|
||||
Build the server package (If you make any changes after in the packages/server folder, you need to rebuild and run this command)
|
||||
|
||||
Run this script
|
||||
```bash
|
||||
pnpm run server:build
|
||||
pnpm run server:script
|
||||
```
|
||||
|
||||
Now run the development server.
|
||||
|
||||
@@ -12,7 +12,16 @@ RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib
|
||||
# Install dependencies
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/dokploy install --frozen-lockfile
|
||||
|
||||
|
||||
# Deploy only the dokploy app
|
||||
ARG NEXT_PUBLIC_UMAMI_HOST
|
||||
ENV NEXT_PUBLIC_UMAMI_HOST=$NEXT_PUBLIC_UMAMI_HOST
|
||||
|
||||
ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
||||
ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
||||
|
||||
ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
|
||||
ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=$NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN pnpm --filter=@dokploy/server build
|
||||
|
||||
22
README.md
22
README.md
@@ -15,29 +15,29 @@
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
Dokploy is a free self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
|
||||
Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
|
||||
|
||||
### Features
|
||||
|
||||
Dokploy include multiples features to make your life easier.
|
||||
Dokploy includes multiple features to make your life easier.
|
||||
|
||||
- **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.).
|
||||
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, Redis.
|
||||
- **Backups**: Automate backups for databases to a external storage destination.
|
||||
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, and Redis.
|
||||
- **Backups**: Automate backups for databases to an external storage destination.
|
||||
- **Docker Compose**: Native support for Docker Compose to manage complex applications.
|
||||
- **Multi Node**: Scale applications to multiples nodes using docker swarm to manage the cluster.
|
||||
- **Templates**: Deploy in a single click open source templates (Plausible, Pocketbase, Calcom, etc.).
|
||||
- **Multi Node**: Scale applications to multiple nodes using Docker Swarm to manage the cluster.
|
||||
- **Templates**: Deploy open-source templates (Plausible, Pocketbase, Calcom, etc.) with a single click.
|
||||
- **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
|
||||
- **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage, for every resource.
|
||||
- **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage for every resource.
|
||||
- **Docker Management**: Easily deploy and manage Docker containers.
|
||||
- **CLI/API**: Manage your applications and databases using the command line or through the API.
|
||||
- **Notifications**: Get notified when your deployments are successful or failed (Slack, Discord, Telegram, Email, etc.)
|
||||
- **Multi Server**: Deploy and manager your applications remotely to external servers.
|
||||
- **Notifications**: Get notified when your deployments succeed or fail (via Slack, Discord, Telegram, Email, etc.).
|
||||
- **Multi Server**: Deploy and manage your applications remotely to external servers.
|
||||
- **Self-Hosted**: Self-host Dokploy on your VPS.
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
To get started run the following command in a VPS:
|
||||
To get started, run the following command on a VPS:
|
||||
|
||||
```bash
|
||||
curl -sSL https://dokploy.com/install.sh | sh
|
||||
@@ -90,6 +90,8 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||
<a href="https://steamsets.com/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a>
|
||||
<a href="https://rivo.gg/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/126797452?s=200&v=4" width="60px" alt="Rivo.gg"/></a>
|
||||
<a href="https://photoquest.wedding/?ref=dokploy"><img src="https://photoquest.wedding/favicon/android-chrome-512x512.png" width="60px" alt="Rivo.gg"/></a>
|
||||
|
||||
</div>
|
||||
|
||||
#### Organizations:
|
||||
|
||||
@@ -8,7 +8,7 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
||||
server: z.boolean().optional(),
|
||||
type: z.enum(["deploy", "redeploy"]),
|
||||
applicationType: z.literal("application"),
|
||||
serverId: z.string(),
|
||||
serverId: z.string().min(1),
|
||||
}),
|
||||
z.object({
|
||||
composeId: z.string(),
|
||||
@@ -17,7 +17,7 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
||||
server: z.boolean().optional(),
|
||||
type: z.enum(["deploy", "redeploy"]),
|
||||
applicationType: z.literal("compose"),
|
||||
serverId: z.string(),
|
||||
serverId: z.string().min(1),
|
||||
}),
|
||||
]);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
deployApplication,
|
||||
deployCompose,
|
||||
deployRemoteApplication,
|
||||
deployRemoteCompose,
|
||||
rebuildApplication,
|
||||
rebuildCompose,
|
||||
rebuildRemoteApplication,
|
||||
rebuildRemoteCompose,
|
||||
updateApplicationStatus,
|
||||
updateCompose,
|
||||
} from "@dokploy/server";
|
||||
import type { DeployJob } from "./schema";
|
||||
import type { LemonSqueezyLicenseResponse } from "./types";
|
||||
|
||||
// const LEMON_SQUEEZY_API_KEY = process.env.LEMON_SQUEEZY_API_KEY;
|
||||
// const LEMON_SQUEEZY_STORE_ID = process.env.LEMON_SQUEEZY_STORE_ID;
|
||||
// export const validateLemonSqueezyLicense = async (
|
||||
// licenseKey: string,
|
||||
// ): Promise<LemonSqueezyLicenseResponse> => {
|
||||
// try {
|
||||
// const response = await fetch(
|
||||
// "https://api.lemonsqueezy.com/v1/licenses/validate",
|
||||
// {
|
||||
// method: "POST",
|
||||
// headers: {
|
||||
// "Content-Type": "application/json",
|
||||
// "x-api-key": LEMON_SQUEEZY_API_KEY as string,
|
||||
// },
|
||||
// body: JSON.stringify({
|
||||
// license_key: licenseKey,
|
||||
// store_id: LEMON_SQUEEZY_STORE_ID as string,
|
||||
// }),
|
||||
// },
|
||||
// );
|
||||
|
||||
// return response.json();
|
||||
// } catch (error) {
|
||||
// console.error("Error validating license:", error);
|
||||
// return { valid: false, error: "Error validating license" };
|
||||
// }
|
||||
// };
|
||||
|
||||
export const deploy = async (job: DeployJob) => {
|
||||
try {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
PlugZapIcon,
|
||||
TerminalIcon,
|
||||
} from "lucide-react";
|
||||
import Script from "next/script";
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
});
|
||||
@@ -63,6 +64,10 @@ export default function Layout({
|
||||
className={inter.className}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<Script
|
||||
src="https://umami.dokploy.com/script.js"
|
||||
data-website-id="6ad2aa56-6d38-4f39-97a8-1a8fcdda8d51"
|
||||
/>
|
||||
<GoogleAnalytics />
|
||||
<body>
|
||||
<I18nProvider
|
||||
|
||||
@@ -15,13 +15,13 @@ description: '学习如何在服务器上手动安装 Dokploy。'
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 确保以根用户身份运行脚本
|
||||
# Ensure the script is run as root
|
||||
if [ "$(id -u)" != "0" ]; then
|
||||
echo "This script must be run as root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查 Linux 操作系统(非 macOS 或 Docker 容器内的操作系统)
|
||||
# Check for Linux OS (not macOS or inside a Docker container)
|
||||
if [ "$(uname)" = "Darwin" ]; then
|
||||
echo "This script must be run on Linux" >&2
|
||||
exit 1
|
||||
@@ -32,7 +32,7 @@ if [ -f /.dockerenv ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查端口是否被占用
|
||||
# Check for occupied ports
|
||||
if ss -tulnp | grep ':80 ' >/dev/null; then
|
||||
echo "Error: Port 80 is already in use" >&2
|
||||
exit 1
|
||||
@@ -43,32 +43,53 @@ if ss -tulnp | grep ':443 ' >/dev/null; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查命令是否存在
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$@" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
# 如果未安装 Docker 则安装
|
||||
# Install Docker if it is not installed
|
||||
if command_exists docker; then
|
||||
echo "Docker already installed"
|
||||
else
|
||||
curl -sSL https://get.docker.com | sh
|
||||
fi
|
||||
|
||||
# 初始化 Docker Swarm
|
||||
# Initialize Docker Swarm
|
||||
docker swarm leave --force 2>/dev/null
|
||||
advertise_addr=$(curl -s ifconfig.me)
|
||||
docker swarm init --advertise-addr $advertise_addr
|
||||
echo "Swarm initialized"
|
||||
|
||||
# 创建网络
|
||||
docker network rm -f dokploy-network 2>/dev/null
|
||||
docker network create --driver overlay --attachable dokploy-network
|
||||
echo "Network created"
|
||||
get_ip() {
|
||||
# Try to get IPv4
|
||||
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
|
||||
|
||||
# 准备配置目录
|
||||
mkdir -p /etc/dokploy
|
||||
chmod -R 777 /etc/dokploy
|
||||
if [ -n "$ipv4" ]; then
|
||||
echo "$ipv4"
|
||||
else
|
||||
# Try to get IPv6
|
||||
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
|
||||
if [ -n "$ipv6" ]; then
|
||||
echo "$ipv6"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
advertise_addr="${ADVERTISE_ADDR:-$(get_ip)}"
|
||||
|
||||
docker swarm init --advertise-addr $advertise_addr
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to initialize Docker Swarm" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker network rm -f dokploy-network 2>/dev/null
|
||||
docker network create --driver overlay --attachable dokploy-network
|
||||
|
||||
echo "Network created"
|
||||
|
||||
mkdir -p /etc/dokploy
|
||||
|
||||
chmod 777 /etc/dokploy
|
||||
|
||||
# Pull and deploy Dokploy
|
||||
docker pull dokploy/dokploy:latest
|
||||
@@ -84,9 +105,10 @@ docker service create \
|
||||
-e PORT=<Value For PORT eg(3000)> \
|
||||
-e TRAEFIK_SSL_PORT=<Value For SSL PORT eg(444)> \
|
||||
-e TRAEFIK_PORT=<VALUE FOR TRAEFIK HTTP PORT eg(81)> \
|
||||
-e ADVERTISE_ADDR=$advertise_addr \
|
||||
dokploy/dokploy:latest
|
||||
|
||||
# 输出成功消息
|
||||
# Output success message
|
||||
GREEN="\033[0;32m"
|
||||
YELLOW="\033[1;33m"
|
||||
BLUE="\033[0;34m"
|
||||
|
||||
@@ -57,18 +57,39 @@ fi
|
||||
|
||||
# Initialize Docker Swarm
|
||||
docker swarm leave --force 2>/dev/null
|
||||
advertise_addr=$(curl -s ifconfig.me)
|
||||
docker swarm init --advertise-addr $advertise_addr
|
||||
echo "Swarm initialized"
|
||||
|
||||
# Create network
|
||||
docker network rm -f dokploy-network 2>/dev/null
|
||||
docker network create --driver overlay --attachable dokploy-network
|
||||
echo "Network created"
|
||||
get_ip() {
|
||||
# Try to get IPv4
|
||||
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
|
||||
|
||||
# Prepare configuration directory
|
||||
mkdir -p /etc/dokploy
|
||||
chmod -R 777 /etc/dokploy
|
||||
if [ -n "$ipv4" ]; then
|
||||
echo "$ipv4"
|
||||
else
|
||||
# Try to get IPv6
|
||||
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
|
||||
if [ -n "$ipv6" ]; then
|
||||
echo "$ipv6"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
advertise_addr="${ADVERTISE_ADDR:-$(get_ip)}"
|
||||
|
||||
docker swarm init --advertise-addr $advertise_addr
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to initialize Docker Swarm" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker network rm -f dokploy-network 2>/dev/null
|
||||
docker network create --driver overlay --attachable dokploy-network
|
||||
|
||||
echo "Network created"
|
||||
|
||||
mkdir -p /etc/dokploy
|
||||
|
||||
chmod 777 /etc/dokploy
|
||||
|
||||
# Pull and deploy Dokploy
|
||||
docker pull dokploy/dokploy:latest
|
||||
@@ -84,6 +105,7 @@ docker service create \
|
||||
-e PORT=<Value For PORT eg(3000)> \
|
||||
-e TRAEFIK_SSL_PORT=<Value For SSL PORT eg(444)> \
|
||||
-e TRAEFIK_PORT=<VALUE FOR TRAEFIK HTTP PORT eg(81)> \
|
||||
-e ADVERTISE_ADDR=$advertise_addr \
|
||||
dokploy/dokploy:latest
|
||||
|
||||
# Output success message
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { paths } from "@dokploy/server/dist/constants";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
const { APPLICATIONS_PATH } = paths();
|
||||
import type { ApplicationNested } from "@dokploy/server";
|
||||
import { unzipDrop } from "@dokploy/server";
|
||||
import AdmZip from "adm-zip";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@dokploy/server/constants", async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
// @ts-ignore
|
||||
...actual,
|
||||
paths: () => ({
|
||||
APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
if (typeof window === "undefined") {
|
||||
const undici = require("undici");
|
||||
globalThis.File = undici.File as any;
|
||||
@@ -82,16 +93,6 @@ const baseApp: ApplicationNested = {
|
||||
dockerContextPath: null,
|
||||
};
|
||||
|
||||
vi.mock("@dokploy/server/dist/constants", async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
// @ts-ignore
|
||||
...actual,
|
||||
paths: () => ({
|
||||
APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
||||
}),
|
||||
};
|
||||
});
|
||||
describe("unzipDrop using real zip files", () => {
|
||||
// const { APPLICATIONS_PATH } = paths();
|
||||
beforeAll(async () => {
|
||||
|
||||
@@ -24,6 +24,9 @@ const baseAdmin: Admin = {
|
||||
sshPrivateKey: null,
|
||||
enableDockerCleanup: false,
|
||||
enableLogRotation: false,
|
||||
serversQuantity: 0,
|
||||
stripeCustomerId: "",
|
||||
stripeSubscriptionId: "",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import path from "node:path";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
root: "./",
|
||||
projects: ["tsconfig.json"],
|
||||
}),
|
||||
],
|
||||
test: {
|
||||
include: ["__test__/**/*.test.ts"], // Incluir solo los archivos de test en el directorio __test__
|
||||
exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"],
|
||||
@@ -18,4 +13,12 @@ export default defineConfig({
|
||||
NODE: "test",
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@dokploy/server": path.resolve(
|
||||
__dirname,
|
||||
"../../../packages/server/src",
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 { Server } from "lucide-react";
|
||||
import { AddRegistry } from "./add-docker-registry";
|
||||
import { AddSelfHostedRegistry } from "./add-self-docker-registry";
|
||||
import { DeleteRegistry } from "./delete-registry";
|
||||
import { UpdateDockerRegistry } from "./update-docker-registry";
|
||||
|
||||
@@ -31,8 +30,6 @@ export const ShowRegistry = () => {
|
||||
<div className="flex flex-row gap-2">
|
||||
{data && data?.length > 0 && (
|
||||
<>
|
||||
{!haveSelfHostedRegistry && <AddSelfHostedRegistry />}
|
||||
|
||||
<AddRegistry />
|
||||
</>
|
||||
)}
|
||||
@@ -47,7 +44,6 @@ export const ShowRegistry = () => {
|
||||
</span>
|
||||
|
||||
<div className="flex flex-row md:flex-row gap-2 flex-wrap w-full justify-center">
|
||||
<AddSelfHostedRegistry />
|
||||
<AddRegistry />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,16 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
@@ -32,12 +42,15 @@ const addDestination = z.object({
|
||||
bucket: z.string(),
|
||||
region: z.string(),
|
||||
endpoint: z.string(),
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
type AddDestination = z.infer<typeof addDestination>;
|
||||
|
||||
export const AddDestination = () => {
|
||||
const utils = api.useUtils();
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
const { mutateAsync, isError, error, isLoading } =
|
||||
api.destination.create.useMutation();
|
||||
@@ -189,30 +202,106 @@ export const AddDestination = () => {
|
||||
/>
|
||||
</form>
|
||||
|
||||
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
||||
<Button
|
||||
isLoading={isLoadingConnection}
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
await testConnection({
|
||||
accessKey: form.getValues("accessKeyId"),
|
||||
bucket: form.getValues("bucket"),
|
||||
endpoint: form.getValues("endpoint"),
|
||||
name: "Test",
|
||||
region: form.getValues("region"),
|
||||
secretAccessKey: form.getValues("secretAccessKey"),
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Connection Success");
|
||||
<DialogFooter
|
||||
className={cn(
|
||||
isCloud ? "!flex-col" : "flex-row",
|
||||
"flex w-full !justify-between pt-3 gap-4",
|
||||
)}
|
||||
>
|
||||
{isCloud ? (
|
||||
<div className="flex flex-col gap-4 border p-2 rounded-lg">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Select a server to test the destination. If you don't have a
|
||||
server choose the default one.
|
||||
</span>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Server (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select a server" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Servers</SelectLabel>
|
||||
{servers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
value={server.serverId}
|
||||
>
|
||||
{server.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value={"none"}>None</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant={"secondary"}
|
||||
isLoading={isLoading}
|
||||
onClick={async () => {
|
||||
await testConnection({
|
||||
accessKey: form.getValues("accessKeyId"),
|
||||
bucket: form.getValues("bucket"),
|
||||
endpoint: form.getValues("endpoint"),
|
||||
name: "Test",
|
||||
region: form.getValues("region"),
|
||||
secretAccessKey: form.getValues("secretAccessKey"),
|
||||
serverId: form.getValues("serverId"),
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Connection Success");
|
||||
})
|
||||
.catch((e) => {
|
||||
toast.error("Error to connect the provider", {
|
||||
description: e.message,
|
||||
});
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test Connection
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
isLoading={isLoadingConnection}
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
await testConnection({
|
||||
accessKey: form.getValues("accessKeyId"),
|
||||
bucket: form.getValues("bucket"),
|
||||
endpoint: form.getValues("endpoint"),
|
||||
name: "Test",
|
||||
region: form.getValues("region"),
|
||||
secretAccessKey: form.getValues("secretAccessKey"),
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to connect the provider");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test connection
|
||||
</Button>
|
||||
.then(async () => {
|
||||
toast.success("Connection Success");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to connect the provider");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test connection
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-destination-add"
|
||||
|
||||
@@ -18,6 +18,16 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PenBoxIcon } from "lucide-react";
|
||||
@@ -33,6 +43,7 @@ const updateDestination = z.object({
|
||||
bucket: z.string(),
|
||||
region: z.string(),
|
||||
endpoint: z.string(),
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
type UpdateDestination = z.infer<typeof updateDestination>;
|
||||
@@ -43,6 +54,8 @@ interface Props {
|
||||
|
||||
export const UpdateDestination = ({ destinationId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { data, refetch } = api.destination.one.useQuery(
|
||||
{
|
||||
@@ -220,34 +233,107 @@ export const UpdateDestination = ({ destinationId }: Props) => {
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
||||
<Button
|
||||
isLoading={isLoadingConnection}
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
await testConnection({
|
||||
accessKey: form.getValues("accessKeyId"),
|
||||
bucket: form.getValues("bucket"),
|
||||
endpoint: form.getValues("endpoint"),
|
||||
name: "Test",
|
||||
region: form.getValues("region"),
|
||||
secretAccessKey: form.getValues("secretAccessKey"),
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Connection Success");
|
||||
<DialogFooter
|
||||
className={cn(
|
||||
isCloud ? "!flex-col" : "flex-row",
|
||||
"flex w-full !justify-between pt-3 gap-4",
|
||||
)}
|
||||
>
|
||||
{isCloud ? (
|
||||
<div className="flex flex-col gap-4 border p-2 rounded-lg">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Select a server to test the destination. If you don't have a
|
||||
server choose the default one.
|
||||
</span>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Server (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select a server" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Servers</SelectLabel>
|
||||
{servers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
value={server.serverId}
|
||||
>
|
||||
{server.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value={"none"}>None</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant={"secondary"}
|
||||
onClick={async () => {
|
||||
await testConnection({
|
||||
accessKey: form.getValues("accessKeyId"),
|
||||
bucket: form.getValues("bucket"),
|
||||
endpoint: form.getValues("endpoint"),
|
||||
name: "Test",
|
||||
region: form.getValues("region"),
|
||||
secretAccessKey: form.getValues("secretAccessKey"),
|
||||
serverId: form.getValues("serverId"),
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Connection Success");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to connect the provider");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test Connection
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
isLoading={isLoadingConnection}
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
await testConnection({
|
||||
accessKey: form.getValues("accessKeyId"),
|
||||
bucket: form.getValues("bucket"),
|
||||
endpoint: form.getValues("endpoint"),
|
||||
name: "Test",
|
||||
region: form.getValues("region"),
|
||||
secretAccessKey: form.getValues("secretAccessKey"),
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to connect the provider");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test connection
|
||||
</Button>
|
||||
.then(async () => {
|
||||
toast.success("Connection Success");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to connect the provider");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test connection
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
isLoading={form.formState.isSubmitting}
|
||||
form="hook-form"
|
||||
type="submit"
|
||||
isLoading={form.formState.isSubmitting}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
|
||||
@@ -109,7 +109,7 @@ export type NotificationSchema = z.infer<typeof notificationSchema>;
|
||||
export const AddNotification = () => {
|
||||
const utils = api.useUtils();
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } =
|
||||
api.notification.testSlackConnection.useMutation();
|
||||
|
||||
@@ -660,26 +660,28 @@ export const AddNotification = () => {
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dokployRestart"
|
||||
render={({ field }) => (
|
||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Dokploy Restart</FormLabel>
|
||||
<FormDescription>
|
||||
Trigger the action when a dokploy is restarted.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{!isCloud && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dokployRestart"
|
||||
render={({ field }) => (
|
||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Dokploy Restart</FormLabel>
|
||||
<FormDescription>
|
||||
Trigger the action when a dokploy is restarted.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -63,7 +63,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
|
||||
const telegramMutation = api.notification.updateTelegram.useMutation();
|
||||
const discordMutation = api.notification.updateDiscord.useMutation();
|
||||
const emailMutation = api.notification.updateEmail.useMutation();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const form = useForm<NotificationSchema>({
|
||||
defaultValues: {
|
||||
type: "slack",
|
||||
@@ -618,27 +618,29 @@ export const UpdateNotification = ({ notificationId }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
defaultValue={form.control._defaultValues.dokployRestart}
|
||||
name="dokployRestart"
|
||||
render={({ field }) => (
|
||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Dokploy Restart</FormLabel>
|
||||
<FormDescription>
|
||||
Trigger the action when a dokploy is restarted.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{!isCloud && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
defaultValue={form.control._defaultValues.dokployRestart}
|
||||
name="dokployRestart"
|
||||
render={({ field }) => (
|
||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Dokploy Restart</FormLabel>
|
||||
<FormDescription>
|
||||
Trigger the action when a dokploy is restarted.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -52,8 +52,6 @@ export const ProfileForm = () => {
|
||||
const { data, refetch } = api.auth.get.useQuery();
|
||||
const { mutateAsync, isLoading } = api.auth.update.useMutation();
|
||||
|
||||
const { mutateAsync: generateToken, isLoading: isLoadingToken } =
|
||||
api.auth.generateToken.useMutation();
|
||||
const form = useForm<Profile>({
|
||||
defaultValues: {
|
||||
email: data?.email || "",
|
||||
@@ -76,7 +74,7 @@ export const ProfileForm = () => {
|
||||
|
||||
const onSubmit = async (values: Profile) => {
|
||||
await mutateAsync({
|
||||
email: values.email,
|
||||
email: values.email.toLowerCase(),
|
||||
password: values.password,
|
||||
image: values.image,
|
||||
})
|
||||
|
||||
@@ -31,6 +31,7 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -57,6 +58,9 @@ type Schema = z.infer<typeof Schema>;
|
||||
export const AddServer = () => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { data: canCreateMoreServers, refetch } =
|
||||
api.stripe.canCreateMoreServers.useQuery();
|
||||
|
||||
const { data: sshKeys } = api.sshKey.all.useQuery();
|
||||
const { mutateAsync, error, isError } = api.server.create.useMutation();
|
||||
const form = useForm<Schema>({
|
||||
@@ -82,6 +86,10 @@ export const AddServer = () => {
|
||||
});
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [isOpen]);
|
||||
|
||||
const onSubmit = async (data: Schema) => {
|
||||
await mutateAsync({
|
||||
name: data.name,
|
||||
@@ -116,6 +124,14 @@ export const AddServer = () => {
|
||||
Add a server to deploy your applications remotely.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{!canCreateMoreServers && (
|
||||
<AlertBlock type="warning">
|
||||
You cannot create more servers,{" "}
|
||||
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||
Please upgrade your plan
|
||||
</Link>
|
||||
</AlertBlock>
|
||||
)}
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<Form {...form}>
|
||||
<form
|
||||
@@ -254,6 +270,7 @@ export const AddServer = () => {
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={form.formState.isSubmitting}
|
||||
disabled={!canCreateMoreServers}
|
||||
form="hook-form-add-server"
|
||||
type="submit"
|
||||
>
|
||||
|
||||
@@ -36,6 +36,9 @@ export const ShowServers = () => {
|
||||
const { data, refetch } = api.server.all.useQuery();
|
||||
const { mutateAsync } = api.server.remove.useMutation();
|
||||
const { data: sshKeys } = api.sshKey.all.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: canCreateMoreServers } =
|
||||
api.stripe.canCreateMoreServers.useQuery();
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
@@ -74,8 +77,22 @@ export const ShowServers = () => {
|
||||
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||
<ServerIcon className="size-8" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
No Servers found. Add a server to deploy your applications
|
||||
remotely.
|
||||
{!canCreateMoreServers ? (
|
||||
<div>
|
||||
You cannot create more servers,{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/billing"
|
||||
className="text-primary"
|
||||
>
|
||||
Please upgrade your plan
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<span>
|
||||
No Servers found. Add a server to deploy your applications
|
||||
remotely.
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
@@ -87,6 +104,9 @@ export const ShowServers = () => {
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[100px]">Name</TableHead>
|
||||
{isCloud && (
|
||||
<TableHead className="text-center">Status</TableHead>
|
||||
)}
|
||||
<TableHead className="text-center">IP Address</TableHead>
|
||||
<TableHead className="text-center">Port</TableHead>
|
||||
<TableHead className="text-center">Username</TableHead>
|
||||
@@ -98,9 +118,23 @@ export const ShowServers = () => {
|
||||
<TableBody>
|
||||
{data?.map((server) => {
|
||||
const canDelete = server.totalSum === 0;
|
||||
const isActive = server.serverStatus === "active";
|
||||
return (
|
||||
<TableRow key={server.serverId}>
|
||||
<TableCell className="w-[100px]">{server.name}</TableCell>
|
||||
{isCloud && (
|
||||
<TableHead className="text-center">
|
||||
<Badge
|
||||
variant={
|
||||
server.serverStatus === "active"
|
||||
? "default"
|
||||
: "destructive"
|
||||
}
|
||||
>
|
||||
{server.serverStatus}
|
||||
</Badge>
|
||||
</TableHead>
|
||||
)}
|
||||
<TableCell className="text-center">
|
||||
<Badge>{server.ipAddress}</Badge>
|
||||
</TableCell>
|
||||
@@ -131,18 +165,25 @@ export const ShowServers = () => {
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
{server.sshKeyId && (
|
||||
<TerminalModal serverId={server.serverId}>
|
||||
<span>Enter the terminal</span>
|
||||
</TerminalModal>
|
||||
|
||||
{isActive && (
|
||||
<>
|
||||
{server.sshKeyId && (
|
||||
<TerminalModal serverId={server.serverId}>
|
||||
<span>Enter the terminal</span>
|
||||
</TerminalModal>
|
||||
)}
|
||||
<SetupServer serverId={server.serverId} />
|
||||
|
||||
<UpdateServer serverId={server.serverId} />
|
||||
{server.sshKeyId && (
|
||||
<ShowServerActions
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<SetupServer serverId={server.serverId} />
|
||||
|
||||
<UpdateServer serverId={server.serverId} />
|
||||
{server.sshKeyId && (
|
||||
<ShowServerActions serverId={server.serverId} />
|
||||
)}
|
||||
<DialogAction
|
||||
disabled={!canDelete}
|
||||
title={
|
||||
@@ -187,17 +228,21 @@ export const ShowServers = () => {
|
||||
</DropdownMenuItem>
|
||||
</DialogAction>
|
||||
|
||||
{server.sshKeyId && (
|
||||
{isActive && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>Extra</DropdownMenuLabel>
|
||||
{server.sshKeyId && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>Extra</DropdownMenuLabel>
|
||||
|
||||
<ShowTraefikFileSystemModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
<ShowDockerContainersModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
<ShowTraefikFileSystemModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
<ShowDockerContainersModal
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -54,7 +54,7 @@ export const AddUser = () => {
|
||||
|
||||
const onSubmit = async (data: AddUser) => {
|
||||
await mutateAsync({
|
||||
email: data.email,
|
||||
email: data.email.toLowerCase(),
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Invitation created");
|
||||
|
||||
@@ -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,
|
||||
href: "/dashboard/settings/registry",
|
||||
},
|
||||
|
||||
...(!isCloud
|
||||
? [
|
||||
{
|
||||
@@ -102,6 +103,16 @@ export const SettingsLayout = ({ children }: Props) => {
|
||||
icon: Server,
|
||||
href: "/dashboard/settings/servers",
|
||||
},
|
||||
...(isCloud
|
||||
? [
|
||||
{
|
||||
title: "Billing",
|
||||
label: "",
|
||||
icon: CreditCardIcon,
|
||||
href: "/dashboard/settings/billing",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
: []),
|
||||
...(user?.canAccessToSSHKeys
|
||||
@@ -137,6 +148,7 @@ import {
|
||||
Activity,
|
||||
Bell,
|
||||
BoxesIcon,
|
||||
CreditCardIcon,
|
||||
Database,
|
||||
GitBranch,
|
||||
KeyIcon,
|
||||
|
||||
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,
|
||||
"tag": "0040_graceful_wolfsbane",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 41,
|
||||
"version": "6",
|
||||
"when": 1729667438853,
|
||||
"tag": "0041_huge_bruce_banner",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 42,
|
||||
"version": "6",
|
||||
"when": 1729984439862,
|
||||
"tag": "0042_fancy_havok",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.10.3",
|
||||
"version": "v0.10.4",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -11,7 +11,7 @@
|
||||
"build-next": "next build",
|
||||
"setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run",
|
||||
"reset-password": "node -r dotenv/config dist/reset-password.mjs",
|
||||
"dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
|
||||
"dev": "TURBOPACK=1 tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
|
||||
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
||||
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
|
||||
"migration:run": "tsx -r dotenv/config migration.ts",
|
||||
@@ -34,12 +34,12 @@
|
||||
"test": "vitest --config __test__/vitest.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dokploy/server": "workspace:*",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-yaml": "^6.1.1",
|
||||
"@codemirror/language": "^6.10.1",
|
||||
"@codemirror/legacy-modes": "6.4.0",
|
||||
"@codemirror/view": "6.29.0",
|
||||
"@dokploy/server": "workspace:*",
|
||||
"@dokploy/trpc-openapi": "0.0.4",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@octokit/webhooks": "^13.2.7",
|
||||
@@ -61,6 +61,7 @@
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toggle": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@stripe/stripe-js": "4.8.0",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@tanstack/react-table": "^8.16.0",
|
||||
"@trpc/client": "^10.43.6",
|
||||
@@ -89,7 +90,7 @@
|
||||
"lucia": "^3.0.1",
|
||||
"lucide-react": "^0.312.0",
|
||||
"nanoid": "3",
|
||||
"next": "^14.1.3",
|
||||
"next": "^15.0.1",
|
||||
"next-themes": "^0.2.1",
|
||||
"node-pty": "1.0.0",
|
||||
"node-schedule": "2.1.1",
|
||||
@@ -102,6 +103,8 @@
|
||||
"recharts": "^2.12.7",
|
||||
"slugify": "^1.6.6",
|
||||
"sonner": "^1.4.0",
|
||||
"ssh2": "1.15.0",
|
||||
"stripe": "17.2.0",
|
||||
"superjson": "^2.2.1",
|
||||
"swagger-ui-react": "^5.17.14",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
@@ -111,11 +114,9 @@
|
||||
"ws": "8.16.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"zod": "^3.23.4",
|
||||
"zod-form-data": "^2.0.2",
|
||||
"ssh2": "1.15.0"
|
||||
"zod-form-data": "^2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "10.4.12",
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
"@types/bcrypt": "5.0.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
@@ -124,8 +125,10 @@
|
||||
"@types/node-schedule": "2.1.6",
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@types/ssh2": "1.15.1",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
"@types/ws": "8.5.10",
|
||||
"autoprefixer": "10.4.12",
|
||||
"drizzle-kit": "^0.21.1",
|
||||
"esbuild": "0.20.2",
|
||||
"lint-staged": "^15.2.7",
|
||||
@@ -134,8 +137,7 @@
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.4.2",
|
||||
"vite-tsconfig-paths": "4.3.2",
|
||||
"vitest": "^1.6.0",
|
||||
"@types/ssh2": "1.15.1"
|
||||
"vitest": "^1.6.0"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
"initVersion": "7.25.2"
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ThemeProvider } from "next-themes";
|
||||
import type { AppProps } from "next/app";
|
||||
import { Inter } from "next/font/google";
|
||||
import Head from "next/head";
|
||||
import Script from "next/script";
|
||||
import type { ReactElement, ReactNode } from "react";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
@@ -36,6 +37,14 @@ const MyApp = ({
|
||||
<Head>
|
||||
<title>Dokploy</title>
|
||||
</Head>
|
||||
{process.env.NEXT_PUBLIC_UMAMI_HOST &&
|
||||
process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID && (
|
||||
<Script
|
||||
src={process.env.NEXT_PUBLIC_UMAMI_HOST}
|
||||
data-website-id={process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
|
||||
@@ -3,8 +3,9 @@ import { Head, Html, Main, NextScript } from "next/document";
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html lang="en" className="font-sans">
|
||||
<Head />
|
||||
|
||||
<Head>
|
||||
<link rel="icon" href="/icon.svg" />
|
||||
</Head>
|
||||
<body className="flex h-full flex-col font-sans">
|
||||
<Main />
|
||||
<NextScript />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { db } from "@/server/db";
|
||||
import { applications } from "@/server/db/schema";
|
||||
import type { DeploymentJob } from "@/server/queues/deployments-queue";
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { deploy } from "@/server/utils/deploy";
|
||||
import { IS_CLOUD } from "@dokploy/server";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { db } from "@/server/db";
|
||||
import { compose } from "@/server/db/schema";
|
||||
import type { DeploymentJob } from "@/server/queues/deployments-queue";
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { deploy } from "@/server/utils/deploy";
|
||||
import { IS_CLOUD } from "@dokploy/server";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { db } from "@/server/db";
|
||||
import { applications, compose, github } from "@/server/db/schema";
|
||||
import type { DeploymentJob } from "@/server/queues/deployments-queue";
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { deploy } from "@/server/utils/deploy";
|
||||
import { IS_CLOUD } from "@dokploy/server";
|
||||
|
||||
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,
|
||||
BreadcrumbLink,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { GlobeIcon } from "lucide-react";
|
||||
import { GlobeIcon, HelpCircle, ServerOff } from "lucide-react";
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
InferGetServerSidePropsType,
|
||||
@@ -100,8 +107,40 @@ const Service = (
|
||||
</h1>
|
||||
<span className="text-sm">{data?.appName}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||
<div className="flex flex-row h-fit w-fit gap-2">
|
||||
<Badge
|
||||
variant={
|
||||
!data?.serverId
|
||||
? "default"
|
||||
: data?.server?.serverStatus === "active"
|
||||
? "default"
|
||||
: "destructive"
|
||||
}
|
||||
>
|
||||
{data?.server?.name || "Dokploy Server"}
|
||||
</Badge>
|
||||
{data?.server?.serverStatus === "inactive" && (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-[999] w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
<span>
|
||||
You cannot, deploy this application because the server
|
||||
is inactive, please upgrade your plan to add more
|
||||
servers.
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{data?.description && (
|
||||
@@ -119,90 +158,111 @@ const Service = (
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/application/${applicationId}?tab=${e}`;
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateApplication applicationId={applicationId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteApplication applicationId={applicationId} />
|
||||
)}
|
||||
|
||||
{data?.server?.serverStatus === "inactive" ? (
|
||||
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
This service is hosted on the server {data.server.name}, but this
|
||||
server has been disabled because your current plan doesn't include
|
||||
enough servers. Please purchase more servers to regain access to
|
||||
this application.
|
||||
</span>
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
Go to{" "}
|
||||
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||
Billing
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/application/${applicationId}?tab=${e}`;
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateApplication applicationId={applicationId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteApplication applicationId={applicationId} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowGeneralApplication applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowEnvironment applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
<ShowGeneralApplication applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowEnvironment applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="deployments" className="w-full">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDeployments applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="domains" className="w-full">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDomains applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<AddCommand applicationId={applicationId} />
|
||||
<ShowClusterSettings applicationId={applicationId} />
|
||||
<ShowApplicationResources applicationId={applicationId} />
|
||||
<ShowVolumes applicationId={applicationId} />
|
||||
<ShowRedirects applicationId={applicationId} />
|
||||
<ShowSecurity applicationId={applicationId} />
|
||||
<ShowPorts applicationId={applicationId} />
|
||||
<ShowTraefikConfig applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="deployments" className="w-full">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDeployments applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="domains" className="w-full">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDomains applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<AddCommand applicationId={applicationId} />
|
||||
<ShowClusterSettings applicationId={applicationId} />
|
||||
<ShowApplicationResources applicationId={applicationId} />
|
||||
<ShowVolumes applicationId={applicationId} />
|
||||
<ShowRedirects applicationId={applicationId} />
|
||||
<ShowSecurity applicationId={applicationId} />
|
||||
<ShowPorts applicationId={applicationId} />
|
||||
<ShowTraefikConfig applicationId={applicationId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,13 +16,21 @@ import {
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { CircuitBoard } from "lucide-react";
|
||||
import { CircuitBoard, ServerOff } from "lucide-react";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
InferGetServerSidePropsType,
|
||||
@@ -94,8 +102,40 @@ const Service = (
|
||||
</h1>
|
||||
<span className="text-sm">{data?.appName}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||
<div className="flex flex-row h-fit w-fit gap-2">
|
||||
<Badge
|
||||
variant={
|
||||
!data?.serverId
|
||||
? "default"
|
||||
: data?.server?.serverStatus === "active"
|
||||
? "default"
|
||||
: "destructive"
|
||||
}
|
||||
>
|
||||
{data?.server?.name || "Dokploy Server"}
|
||||
</Badge>
|
||||
{data?.server?.serverStatus === "inactive" && (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-[999] w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
<span>
|
||||
You cannot, deploy this application because the server
|
||||
is inactive, please upgrade your plan to add more
|
||||
servers.
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
{data?.description && (
|
||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||
@@ -113,98 +153,118 @@ const Service = (
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/compose/${composeId}?tab=${e}`;
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
|
||||
data?.composeType === "docker-compose" ? "" : "md:grid-cols-6",
|
||||
data?.serverId && data?.composeType === "stack"
|
||||
? "md:grid-cols-5"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
{data?.composeType === "docker-compose" && (
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
)}
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateCompose composeId={composeId} />
|
||||
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteCompose composeId={composeId} />
|
||||
)}
|
||||
{data?.server?.serverStatus === "inactive" ? (
|
||||
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
This service is hosted on the server {data.server.name}, but this
|
||||
server has been disabled because your current plan doesn't include
|
||||
enough servers. Please purchase more servers to regain access to
|
||||
this application.
|
||||
</span>
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
Go to{" "}
|
||||
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||
Billing
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/compose/${composeId}?tab=${e}`;
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
|
||||
data?.composeType === "docker-compose" ? "" : "md:grid-cols-6",
|
||||
data?.serverId && data?.composeType === "stack"
|
||||
? "md:grid-cols-5"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
{data?.composeType === "docker-compose" && (
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
)}
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateCompose composeId={composeId} />
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowGeneralCompose composeId={composeId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteCompose composeId={composeId} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowEnvironmentCompose composeId={composeId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowMonitoringCompose
|
||||
<ShowGeneralCompose composeId={composeId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowEnvironmentCompose composeId={composeId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowMonitoringCompose
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
appType={data?.composeType || "docker-compose"}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogsCompose
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
appType={data?.composeType || "docker-compose"}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogsCompose
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
appType={data?.composeType || "docker-compose"}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="deployments">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDeploymentsCompose composeId={composeId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="deployments">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDeploymentsCompose composeId={composeId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="domains">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDomainsCompose composeId={composeId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<AddCommandCompose composeId={composeId} />
|
||||
<ShowVolumesCompose composeId={composeId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<TabsContent value="domains">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDomainsCompose composeId={composeId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<AddCommandCompose composeId={composeId} />
|
||||
<ShowVolumesCompose composeId={composeId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,12 +17,20 @@ import {
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { HelpCircle, ServerOff } from "lucide-react";
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
InferGetServerSidePropsType,
|
||||
@@ -82,8 +90,40 @@ const Mariadb = (
|
||||
</h1>
|
||||
<span className="text-sm">{data?.appName}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||
<div className="flex flex-row h-fit w-fit gap-2">
|
||||
<Badge
|
||||
variant={
|
||||
!data?.serverId
|
||||
? "default"
|
||||
: data?.server?.serverStatus === "active"
|
||||
? "default"
|
||||
: "destructive"
|
||||
}
|
||||
>
|
||||
{data?.server?.name || "Dokploy Server"}
|
||||
</Badge>
|
||||
{data?.server?.serverStatus === "inactive" && (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-[999] w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
<span>
|
||||
You cannot, deploy this application because the server
|
||||
is inactive, please upgrade your plan to add more
|
||||
servers.
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
{data?.description && (
|
||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||
@@ -99,79 +139,99 @@ const Mariadb = (
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/mariadb/${mariadbId}?tab=${e}`;
|
||||
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateMariadb mariadbId={mariadbId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteMariadb mariadbId={mariadbId} />
|
||||
)}
|
||||
{data?.server?.serverStatus === "inactive" ? (
|
||||
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
This service is hosted on the server {data.server.name}, but this
|
||||
server has been disabled because your current plan doesn't include
|
||||
enough servers. Please purchase more servers to regain access to
|
||||
this application.
|
||||
</span>
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
Go to{" "}
|
||||
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||
Billing
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/mariadb/${mariadbId}?tab=${e}`;
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowGeneralMariadb mariadbId={mariadbId} />
|
||||
<ShowInternalMariadbCredentials mariadbId={mariadbId} />
|
||||
<ShowExternalMariadbCredentials mariadbId={mariadbId} />
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateMariadb mariadbId={mariadbId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteMariadb mariadbId={mariadbId} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowMariadbEnvironment mariadbId={mariadbId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
<ShowGeneralMariadb mariadbId={mariadbId} />
|
||||
<ShowInternalMariadbCredentials mariadbId={mariadbId} />
|
||||
<ShowExternalMariadbCredentials mariadbId={mariadbId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="backups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowBackupMariadb mariadbId={mariadbId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowAdvancedMariadb mariadbId={mariadbId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowMariadbEnvironment mariadbId={mariadbId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="backups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowBackupMariadb mariadbId={mariadbId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowAdvancedMariadb mariadbId={mariadbId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,12 +17,20 @@ import {
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { HelpCircle, ServerOff } from "lucide-react";
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
InferGetServerSidePropsType,
|
||||
@@ -83,8 +91,40 @@ const Mongo = (
|
||||
</h1>
|
||||
<span className="text-sm">{data?.appName}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||
<div className="flex flex-row h-fit w-fit gap-2">
|
||||
<Badge
|
||||
variant={
|
||||
!data?.serverId
|
||||
? "default"
|
||||
: data?.server?.serverStatus === "active"
|
||||
? "default"
|
||||
: "destructive"
|
||||
}
|
||||
>
|
||||
{data?.server?.name || "Dokploy Server"}
|
||||
</Badge>
|
||||
{data?.server?.serverStatus === "inactive" && (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-[999] w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
<span>
|
||||
You cannot, deploy this application because the server
|
||||
is inactive, please upgrade your plan to add more
|
||||
servers.
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
{data?.description && (
|
||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||
@@ -100,80 +140,100 @@ const Mongo = (
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/mongo/${mongoId}?tab=${e}`;
|
||||
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateMongo mongoId={mongoId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteMongo mongoId={mongoId} />
|
||||
)}
|
||||
{data?.server?.serverStatus === "inactive" ? (
|
||||
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
This service is hosted on the server {data.server.name}, but this
|
||||
server has been disabled because your current plan doesn't include
|
||||
enough servers. Please purchase more servers to regain access to
|
||||
this application.
|
||||
</span>
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
Go to{" "}
|
||||
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||
Billing
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/mongo/${mongoId}?tab=${e}`;
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowGeneralMongo mongoId={mongoId} />
|
||||
<ShowInternalMongoCredentials mongoId={mongoId} />
|
||||
<ShowExternalMongoCredentials mongoId={mongoId} />
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateMongo mongoId={mongoId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteMongo mongoId={mongoId} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowMongoEnvironment mongoId={mongoId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
<ShowGeneralMongo mongoId={mongoId} />
|
||||
<ShowInternalMongoCredentials mongoId={mongoId} />
|
||||
<ShowExternalMongoCredentials mongoId={mongoId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="backups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowBackupMongo mongoId={mongoId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowAdvancedMongo mongoId={mongoId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowMongoEnvironment mongoId={mongoId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="backups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowBackupMongo mongoId={mongoId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowAdvancedMongo mongoId={mongoId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,12 +17,20 @@ import {
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { HelpCircle, ServerOff } from "lucide-react";
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
InferGetServerSidePropsType,
|
||||
@@ -81,8 +89,40 @@ const MySql = (
|
||||
</h1>
|
||||
<span className="text-sm">{data?.appName}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||
<div className="flex flex-row h-fit w-fit gap-2">
|
||||
<Badge
|
||||
variant={
|
||||
!data?.serverId
|
||||
? "default"
|
||||
: data?.server?.serverStatus === "active"
|
||||
? "default"
|
||||
: "destructive"
|
||||
}
|
||||
>
|
||||
{data?.server?.name || "Dokploy Server"}
|
||||
</Badge>
|
||||
{data?.server?.serverStatus === "inactive" && (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-[999] w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
<span>
|
||||
You cannot, deploy this application because the server
|
||||
is inactive, please upgrade your plan to add more
|
||||
servers.
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
{data?.description && (
|
||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||
@@ -99,80 +139,100 @@ const MySql = (
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/mysql/${mysqlId}?tab=${e}`;
|
||||
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateMysql mysqlId={mysqlId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteMysql mysqlId={mysqlId} />
|
||||
)}
|
||||
{data?.server?.serverStatus === "inactive" ? (
|
||||
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
This service is hosted on the server {data.server.name}, but this
|
||||
server has been disabled because your current plan doesn't include
|
||||
enough servers. Please purchase more servers to regain access to
|
||||
this application.
|
||||
</span>
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
Go to{" "}
|
||||
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||
Billing
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/mysql/${mysqlId}?tab=${e}`;
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowGeneralMysql mysqlId={mysqlId} />
|
||||
<ShowInternalMysqlCredentials mysqlId={mysqlId} />
|
||||
<ShowExternalMysqlCredentials mysqlId={mysqlId} />
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateMysql mysqlId={mysqlId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteMysql mysqlId={mysqlId} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="environment" className="w-full">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowMysqlEnvironment mysqlId={mysqlId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
<ShowGeneralMysql mysqlId={mysqlId} />
|
||||
<ShowInternalMysqlCredentials mysqlId={mysqlId} />
|
||||
<ShowExternalMysqlCredentials mysqlId={mysqlId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="backups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowBackupMySql mysqlId={mysqlId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowAdvancedMysql mysqlId={mysqlId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<TabsContent value="environment" className="w-full">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowMysqlEnvironment mysqlId={mysqlId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="backups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowBackupMySql mysqlId={mysqlId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowAdvancedMysql mysqlId={mysqlId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,12 +17,20 @@ import {
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { HelpCircle, ServerOff } from "lucide-react";
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
InferGetServerSidePropsType,
|
||||
@@ -82,8 +90,40 @@ const Postgresql = (
|
||||
</h1>
|
||||
<span className="text-sm">{data?.appName}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||
<div className="flex flex-row h-fit w-fit gap-2">
|
||||
<Badge
|
||||
variant={
|
||||
!data?.serverId
|
||||
? "default"
|
||||
: data?.server?.serverStatus === "active"
|
||||
? "default"
|
||||
: "destructive"
|
||||
}
|
||||
>
|
||||
{data?.server?.name || "Dokploy Server"}
|
||||
</Badge>
|
||||
{data?.server?.serverStatus === "inactive" && (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-[999] w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
<span>
|
||||
You cannot, deploy this application because the server
|
||||
is inactive, please upgrade your plan to add more
|
||||
servers.
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
{data?.description && (
|
||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||
@@ -100,80 +140,100 @@ const Postgresql = (
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/postgres/${postgresId}?tab=${e}`;
|
||||
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdatePostgres postgresId={postgresId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeletePostgres postgresId={postgresId} />
|
||||
)}
|
||||
{data?.server?.serverStatus === "inactive" ? (
|
||||
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
This service is hosted on the server {data.server.name}, but this
|
||||
server has been disabled because your current plan doesn't include
|
||||
enough servers. Please purchase more servers to regain access to
|
||||
this application.
|
||||
</span>
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
Go to{" "}
|
||||
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||
Billing
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/postgres/${postgresId}?tab=${e}`;
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowGeneralPostgres postgresId={postgresId} />
|
||||
<ShowInternalPostgresCredentials postgresId={postgresId} />
|
||||
<ShowExternalPostgresCredentials postgresId={postgresId} />
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdatePostgres postgresId={postgresId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeletePostgres postgresId={postgresId} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowPostgresEnvironment postgresId={postgresId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
<ShowGeneralPostgres postgresId={postgresId} />
|
||||
<ShowInternalPostgresCredentials postgresId={postgresId} />
|
||||
<ShowExternalPostgresCredentials postgresId={postgresId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="backups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowBackupPostgres postgresId={postgresId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowAdvancedPostgres postgresId={postgresId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowPostgresEnvironment postgresId={postgresId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="backups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowBackupPostgres postgresId={postgresId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowAdvancedPostgres postgresId={postgresId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,12 +16,20 @@ import {
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { HelpCircle, ServerOff } from "lucide-react";
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
InferGetServerSidePropsType,
|
||||
@@ -81,8 +89,40 @@ const Redis = (
|
||||
</h1>
|
||||
<span className="text-sm">{data?.appName}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
||||
<div className="flex flex-row h-fit w-fit gap-2">
|
||||
<Badge
|
||||
variant={
|
||||
!data?.serverId
|
||||
? "default"
|
||||
: data?.server?.serverStatus === "active"
|
||||
? "default"
|
||||
: "destructive"
|
||||
}
|
||||
>
|
||||
{data?.server?.name || "Dokploy Server"}
|
||||
</Badge>
|
||||
{data?.server?.serverStatus === "inactive" && (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-[999] w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
<span>
|
||||
You cannot, deploy this application because the server
|
||||
is inactive, please upgrade your plan to add more
|
||||
servers.
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
{data?.description && (
|
||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||
@@ -99,74 +139,94 @@ const Redis = (
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/redis/${redisId}?tab=${e}`;
|
||||
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-4" : "md:grid-cols-5",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateRedis redisId={redisId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteRedis redisId={redisId} />
|
||||
)}
|
||||
{data?.server?.serverStatus === "inactive" ? (
|
||||
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
This service is hosted on the server {data.server.name}, but this
|
||||
server has been disabled because your current plan doesn't include
|
||||
enough servers. Please purchase more servers to regain access to
|
||||
this application.
|
||||
</span>
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
Go to{" "}
|
||||
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||
Billing
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/services/redis/${redisId}?tab=${e}`;
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowGeneralRedis redisId={redisId} />
|
||||
<ShowInternalRedisCredentials redisId={redisId} />
|
||||
<ShowExternalRedisCredentials redisId={redisId} />
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
data?.serverId ? "md:grid-cols-4" : "md:grid-cols-5",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
{!data?.serverId && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateRedis redisId={redisId} />
|
||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||
<DeleteRedis redisId={redisId} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowRedisEnvironment redisId={redisId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
<ShowGeneralRedis redisId={redisId} />
|
||||
<ShowInternalRedisCredentials redisId={redisId} />
|
||||
<ShowExternalRedisCredentials redisId={redisId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowAdvancedRedis redisId={redisId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowRedisEnvironment redisId={redisId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
{!data?.serverId && (
|
||||
<TabsContent value="monitoring">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerMonitoring appName={data?.appName || ""} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowAdvancedRedis redisId={redisId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { ShowProjects } from "@/components/dashboard/projects/show";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Dashboard = () => {
|
||||
return <ShowProjects />;
|
||||
@@ -16,7 +19,22 @@ Dashboard.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
@@ -27,6 +45,8 @@ export async function getServerSideProps(
|
||||
};
|
||||
}
|
||||
return {
|
||||
props: {},
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { AppearanceForm } from "@/components/dashboard/settings/appearance-form";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
@@ -25,7 +28,30 @@ Page.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
await helpers.auth.get.prefetch();
|
||||
if (user?.rol === "user") {
|
||||
await helpers.user.byAuthId.prefetch({
|
||||
authId: user.authId,
|
||||
});
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -34,8 +60,9 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {},
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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 { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
|
||||
import superjson from "superjson";
|
||||
const Page = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
@@ -25,7 +27,8 @@ Page.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user || user.rol === "user") {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -35,7 +38,23 @@ export async function getServerSideProps(
|
||||
};
|
||||
}
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
await helpers.auth.get.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
return {
|
||||
props: {},
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { ShowNodes } from "@/components/dashboard/settings/cluster/nodes/show-nodes";
|
||||
import { ShowRegistry } from "@/components/dashboard/settings/cluster/registry/show-registry";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
@@ -26,6 +28,7 @@ Page.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
if (IS_CLOUD) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -43,8 +46,22 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
await helpers.auth.get.prefetch();
|
||||
|
||||
return {
|
||||
props: {},
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { ShowDestinations } from "@/components/dashboard/settings/destination/show-destinations";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
@@ -25,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user || user.rol === "user") {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -35,7 +39,23 @@ export async function getServerSideProps(
|
||||
};
|
||||
}
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
await helpers.auth.get.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
return {
|
||||
props: {},
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -49,9 +49,10 @@ export async function getServerSideProps(
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
await helpers.auth.get.prefetch();
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
const auth = await helpers.auth.get.fetch();
|
||||
|
||||
if (auth.rol === "user") {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { ShowDestinations } from "@/components/dashboard/settings/destination/show-destinations";
|
||||
import { ShowNotifications } from "@/components/dashboard/settings/notifications/show-notifications";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
@@ -26,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user || user.rol === "user") {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -36,7 +39,23 @@ export async function getServerSideProps(
|
||||
};
|
||||
}
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
await helpers.auth.get.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
return {
|
||||
props: {},
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@ import { GenerateToken } from "@/components/dashboard/settings/profile/generate-
|
||||
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
const { data } = api.auth.get.useQuery();
|
||||
@@ -37,7 +40,29 @@ Page.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
await helpers.auth.get.prefetch();
|
||||
if (user?.rol === "user") {
|
||||
await helpers.user.byAuthId.prefetch({
|
||||
authId: user.authId,
|
||||
});
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -48,6 +73,8 @@ export async function getServerSideProps(
|
||||
}
|
||||
|
||||
return {
|
||||
props: {},
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { ShowRegistry } from "@/components/dashboard/settings/cluster/registry/show-registry";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
@@ -25,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user || user.rol === "user") {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -34,8 +38,23 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
await helpers.auth.get.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
return {
|
||||
props: {},
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@ import { WebDomain } from "@/components/dashboard/settings/web-domain";
|
||||
import { WebServer } from "@/components/dashboard/settings/web-server";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
@@ -27,6 +30,7 @@ Page.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
if (IS_CLOUD) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -35,7 +39,7 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -53,7 +57,22 @@ export async function getServerSideProps(
|
||||
};
|
||||
}
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
await helpers.auth.get.prefetch();
|
||||
|
||||
return {
|
||||
props: {},
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { ShowServers } from "@/components/dashboard/settings/servers/show-servers";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
@@ -25,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -43,7 +47,23 @@ export async function getServerSideProps(
|
||||
};
|
||||
}
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
await helpers.auth.get.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
return {
|
||||
props: {},
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ export async function getServerSideProps(
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
const auth = await helpers.auth.get.fetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
if (auth.rol === "user") {
|
||||
const user = await helpers.user.byAuthId.fetch({
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
@@ -25,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req, res);
|
||||
if (!user || user.rol === "user") {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -35,7 +39,23 @@ export async function getServerSideProps(
|
||||
};
|
||||
}
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
await helpers.auth.get.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
return {
|
||||
props: {},
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Login2FA } from "@/components/auth/login-2fa";
|
||||
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Logo } from "@/components/shared/logo";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -55,12 +56,16 @@ type AuthResponse = {
|
||||
authId: string;
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
interface Props {
|
||||
IS_CLOUD: boolean;
|
||||
}
|
||||
export default function Home({ IS_CLOUD }: Props) {
|
||||
const [temp, setTemp] = useState<AuthResponse>({
|
||||
is2FAEnabled: false,
|
||||
authId: "",
|
||||
});
|
||||
const { mutateAsync, isLoading } = api.auth.login.useMutation();
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.auth.login.useMutation();
|
||||
const router = useRouter();
|
||||
const form = useForm<Login>({
|
||||
defaultValues: {
|
||||
@@ -112,6 +117,12 @@ export default function Home() {
|
||||
</CardDescription>
|
||||
<Card className="mx-auto w-full max-w-lg bg-transparent ">
|
||||
<div className="p-3.5" />
|
||||
{isError && (
|
||||
<AlertBlock type="error" className="mx-4 my-2">
|
||||
<span>{error?.message}</span>
|
||||
</AlertBlock>
|
||||
)}
|
||||
|
||||
<CardContent>
|
||||
{!temp.is2FAEnabled ? (
|
||||
<Form {...form}>
|
||||
@@ -167,22 +178,33 @@ export default function Home() {
|
||||
|
||||
<div className="flex flex-row justify-between flex-wrap">
|
||||
<div className="mt-4 text-center text-sm flex flex-row justify-center gap-2">
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="/register"
|
||||
>
|
||||
Create an account
|
||||
</Link>
|
||||
{IS_CLOUD && (
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="/register"
|
||||
>
|
||||
Create an account
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-sm flex flex-row justify-center gap-2">
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="https://docs.dokploy.com/docs/core/get-started/reset-password"
|
||||
target="_blank"
|
||||
>
|
||||
Lost your password?
|
||||
</Link>
|
||||
{IS_CLOUD ? (
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="/send-reset-password"
|
||||
>
|
||||
Lost your password?
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="https://docs.dokploy.com/docs/core/get-started/reset-password"
|
||||
target="_blank"
|
||||
>
|
||||
Lost your password?
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2" />
|
||||
@@ -212,7 +234,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
} catch (error) {}
|
||||
|
||||
return {
|
||||
props: {},
|
||||
props: {
|
||||
IS_CLOUD: IS_CLOUD,
|
||||
},
|
||||
};
|
||||
}
|
||||
const hasAdmin = await isAdminPresent();
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { getUserByToken } from "@dokploy/server";
|
||||
import { IS_CLOUD, getUserByToken } from "@dokploy/server";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
@@ -69,9 +69,10 @@ type Register = z.infer<typeof registerSchema>;
|
||||
interface Props {
|
||||
token: string;
|
||||
invitation: Awaited<ReturnType<typeof getUserByToken>>;
|
||||
isCloud: boolean;
|
||||
}
|
||||
|
||||
const Invitation = ({ token, invitation }: Props) => {
|
||||
const Invitation = ({ token, invitation, isCloud }: Props) => {
|
||||
const router = useRouter();
|
||||
const { data } = api.admin.getUserByToken.useQuery(
|
||||
{
|
||||
@@ -83,7 +84,8 @@ const Invitation = ({ token, invitation }: Props) => {
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync, error, isError } = api.auth.createUser.useMutation();
|
||||
const { mutateAsync, error, isError, isSuccess } =
|
||||
api.auth.createUser.useMutation();
|
||||
|
||||
const form = useForm<Register>({
|
||||
defaultValues: {
|
||||
@@ -112,7 +114,9 @@ const Invitation = ({ token, invitation }: Props) => {
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("User registration succesfuly", {
|
||||
duration: 2000,
|
||||
description:
|
||||
"Please check your inbox or spam folder to confirm your account.",
|
||||
duration: 100000,
|
||||
});
|
||||
router.push("/dashboard/projects");
|
||||
})
|
||||
@@ -146,6 +150,7 @@ const Invitation = ({ token, invitation }: Props) => {
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form
|
||||
@@ -210,6 +215,25 @@ const Invitation = ({ token, invitation }: Props) => {
|
||||
Register
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-sm flex flex-row justify-between gap-2 w-full">
|
||||
{isCloud && (
|
||||
<>
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="/"
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
<Link
|
||||
className="hover:underline text-muted-foreground"
|
||||
href="/send-reset-password"
|
||||
>
|
||||
Lost your password?
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
@@ -250,6 +274,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||
|
||||
return {
|
||||
props: {
|
||||
isCloud: IS_CLOUD,
|
||||
token: token,
|
||||
invitation: invitation,
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Logo } from "@/components/shared/logo";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -72,7 +73,8 @@ interface Props {
|
||||
|
||||
const Register = ({ isCloud }: Props) => {
|
||||
const router = useRouter();
|
||||
const { mutateAsync, error, isError } = api.auth.createAdmin.useMutation();
|
||||
const { mutateAsync, error, isError, data } =
|
||||
api.auth.createAdmin.useMutation();
|
||||
|
||||
const form = useForm<Register>({
|
||||
defaultValues: {
|
||||
@@ -89,14 +91,16 @@ const Register = ({ isCloud }: Props) => {
|
||||
|
||||
const onSubmit = async (values: Register) => {
|
||||
await mutateAsync({
|
||||
email: values.email,
|
||||
email: values.email.toLowerCase(),
|
||||
password: values.password,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("User registration succesfuly", {
|
||||
duration: 2000,
|
||||
});
|
||||
router.push("/");
|
||||
if (!isCloud) {
|
||||
router.push("/");
|
||||
}
|
||||
})
|
||||
.catch((e) => e);
|
||||
};
|
||||
@@ -130,6 +134,14 @@ const Register = ({ isCloud }: Props) => {
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{data && (
|
||||
<AlertBlock type="success" className="mx-4 my-2">
|
||||
<span>
|
||||
Registration succesfuly, Please check your inbox or spam
|
||||
folder to confirm your account.
|
||||
</span>
|
||||
</AlertBlock>
|
||||
)}
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form
|
||||
|
||||
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({});
|
||||
|
||||
useEffect(() => {
|
||||
// Esto solo se ejecutará en el cliente
|
||||
if (data) {
|
||||
const protocolAndHost = `${window.location.protocol}//${window.location.host}/api`;
|
||||
const newSpec = {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
@@ -29,6 +29,7 @@ import { securityRouter } from "./routers/security";
|
||||
import { serverRouter } from "./routers/server";
|
||||
import { settingsRouter } from "./routers/settings";
|
||||
import { sshRouter } from "./routers/ssh-key";
|
||||
import { stripeRouter } from "./routers/stripe";
|
||||
import { userRouter } from "./routers/user";
|
||||
|
||||
/**
|
||||
@@ -69,6 +70,7 @@ export const appRouter = createTRPCRouter({
|
||||
gitlab: gitlabRouter,
|
||||
github: githubRouter,
|
||||
server: serverRouter,
|
||||
stripe: stripeRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
apiRemoveUser,
|
||||
users,
|
||||
} from "@/server/db/schema";
|
||||
|
||||
import {
|
||||
createInvitation,
|
||||
findAdminById,
|
||||
|
||||
@@ -19,11 +19,8 @@ import {
|
||||
apiUpdateApplication,
|
||||
applications,
|
||||
} from "@/server/db/schema";
|
||||
import {
|
||||
type DeploymentJob,
|
||||
cleanQueuesByApplication,
|
||||
} from "@/server/queues/deployments-queue";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import { cleanQueuesByApplication, myQueue } from "@/server/queues/queueSetup";
|
||||
import { deploy } from "@/server/utils/deploy";
|
||||
import { uploadFileSchema } from "@/utils/schema";
|
||||
import {
|
||||
|
||||
@@ -4,11 +4,13 @@ import {
|
||||
apiFindOneAuth,
|
||||
apiLogin,
|
||||
apiUpdateAuth,
|
||||
apiUpdateAuthByAdmin,
|
||||
apiVerify2FA,
|
||||
apiVerifyLogin2FA,
|
||||
auth,
|
||||
} from "@/server/db/schema";
|
||||
import { WEBSITE_URL } from "@/server/utils/stripe";
|
||||
import {
|
||||
type Auth,
|
||||
IS_CLOUD,
|
||||
createAdmin,
|
||||
createUser,
|
||||
@@ -18,12 +20,18 @@ import {
|
||||
getUserByToken,
|
||||
lucia,
|
||||
luciaToken,
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
updateAuthById,
|
||||
validateRequest,
|
||||
verify2FA,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import * as bcrypt from "bcrypt";
|
||||
import { isBefore } from "date-fns";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { db } from "../../db";
|
||||
import {
|
||||
adminProcedure,
|
||||
@@ -47,6 +55,12 @@ export const authRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
const newAdmin = await createAdmin(input);
|
||||
|
||||
if (IS_CLOUD) {
|
||||
await sendDiscordNotificationWelcome(newAdmin);
|
||||
await sendVerificationEmail(newAdmin.id);
|
||||
return true;
|
||||
}
|
||||
const session = await lucia.createSession(newAdmin.id || "", {});
|
||||
ctx.res.appendHeader(
|
||||
"Set-Cookie",
|
||||
@@ -54,7 +68,12 @@ export const authRouter = createTRPCRouter({
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
// @ts-ignore
|
||||
message: `Error: ${error?.code === "23505" ? "Email already exists" : "Error to create admin"}`,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
createUser: publicProcedure
|
||||
@@ -68,7 +87,13 @@ export const authRouter = createTRPCRouter({
|
||||
message: "Invalid token",
|
||||
});
|
||||
}
|
||||
|
||||
const newUser = await createUser(input);
|
||||
|
||||
if (IS_CLOUD) {
|
||||
await sendVerificationEmail(token.authId);
|
||||
return true;
|
||||
}
|
||||
const session = await lucia.createSession(newUser?.authId || "", {});
|
||||
ctx.res.appendHeader(
|
||||
"Set-Cookie",
|
||||
@@ -100,6 +125,15 @@ export const authRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
if (auth?.confirmationToken && IS_CLOUD) {
|
||||
await sendVerificationEmail(auth.id);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message:
|
||||
"Email not confirmed, we have sent you a confirmation email please check your inbox.",
|
||||
});
|
||||
}
|
||||
|
||||
if (auth?.is2FAEnabled) {
|
||||
return {
|
||||
is2FAEnabled: true,
|
||||
@@ -120,7 +154,7 @@ export const authRouter = createTRPCRouter({
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Credentials do not match",
|
||||
message: `Error: ${error instanceof Error ? error.message : "Error to login"}`,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
@@ -145,7 +179,7 @@ export const authRouter = createTRPCRouter({
|
||||
.input(apiUpdateAuth)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const auth = await updateAuthById(ctx.user.authId, {
|
||||
...(input.email && { email: input.email }),
|
||||
...(input.email && { email: input.email.toLowerCase() }),
|
||||
...(input.password && {
|
||||
password: bcrypt.hashSync(input.password, 10),
|
||||
}),
|
||||
@@ -177,19 +211,6 @@ export const authRouter = createTRPCRouter({
|
||||
return auth;
|
||||
}),
|
||||
|
||||
updateByAdmin: protectedProcedure
|
||||
.input(apiUpdateAuthByAdmin)
|
||||
.mutation(async ({ input }) => {
|
||||
const auth = await updateAuthById(input.id, {
|
||||
...(input.email && { email: input.email }),
|
||||
...(input.password && {
|
||||
password: bcrypt.hashSync(input.password, 10),
|
||||
}),
|
||||
...(input.image && { image: input.image }),
|
||||
});
|
||||
|
||||
return auth;
|
||||
}),
|
||||
generate2FASecret: protectedProcedure.query(async ({ ctx }) => {
|
||||
return await generate2FASecret(ctx.user.authId);
|
||||
}),
|
||||
@@ -230,7 +251,208 @@ export const authRouter = createTRPCRouter({
|
||||
});
|
||||
return auth;
|
||||
}),
|
||||
verifyToken: protectedProcedure.mutation(async () => {
|
||||
return true;
|
||||
}),
|
||||
sendResetPasswordEmail: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
email: z.string().min(1).email(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (!IS_CLOUD) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "This feature is only available in the cloud version",
|
||||
});
|
||||
}
|
||||
const authR = await db.query.auth.findFirst({
|
||||
where: eq(auth.email, input.email),
|
||||
});
|
||||
if (!authR) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
const token = nanoid();
|
||||
await updateAuthById(authR.id, {
|
||||
resetPasswordToken: token,
|
||||
// Make resetPassword in 24 hours
|
||||
resetPasswordExpiresAt: new Date(
|
||||
new Date().getTime() + 24 * 60 * 60 * 1000,
|
||||
).toISOString(),
|
||||
});
|
||||
|
||||
await sendEmailNotification(
|
||||
{
|
||||
fromAddress: process.env.SMTP_FROM_ADDRESS || "",
|
||||
toAddresses: [authR.email],
|
||||
smtpServer: process.env.SMTP_SERVER || "",
|
||||
smtpPort: Number(process.env.SMTP_PORT),
|
||||
username: process.env.SMTP_USERNAME || "",
|
||||
password: process.env.SMTP_PASSWORD || "",
|
||||
},
|
||||
"Reset Password",
|
||||
`
|
||||
Reset your password by clicking the link below:
|
||||
The link will expire in 24 hours.
|
||||
<a href="${WEBSITE_URL}/reset-password?token=${token}">
|
||||
Reset Password
|
||||
</a>
|
||||
|
||||
`,
|
||||
);
|
||||
}),
|
||||
|
||||
resetPassword: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
resetPasswordToken: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (!IS_CLOUD) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "This feature is only available in the cloud version",
|
||||
});
|
||||
}
|
||||
const authR = await db.query.auth.findFirst({
|
||||
where: eq(auth.resetPasswordToken, input.resetPasswordToken),
|
||||
});
|
||||
|
||||
if (!authR || authR.resetPasswordExpiresAt === null) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Token not found",
|
||||
});
|
||||
}
|
||||
|
||||
const isExpired = isBefore(
|
||||
new Date(authR.resetPasswordExpiresAt),
|
||||
new Date(),
|
||||
);
|
||||
|
||||
if (isExpired) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Token expired",
|
||||
});
|
||||
}
|
||||
|
||||
await updateAuthById(authR.id, {
|
||||
resetPasswordExpiresAt: null,
|
||||
resetPasswordToken: null,
|
||||
password: bcrypt.hashSync(input.password, 10),
|
||||
});
|
||||
|
||||
return true;
|
||||
}),
|
||||
confirmEmail: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
confirmationToken: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (!IS_CLOUD) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Functionality not available in cloud version",
|
||||
});
|
||||
}
|
||||
const authR = await db.query.auth.findFirst({
|
||||
where: eq(auth.confirmationToken, input.confirmationToken),
|
||||
});
|
||||
if (!authR || authR.confirmationExpiresAt === null) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Token not found",
|
||||
});
|
||||
}
|
||||
if (authR.confirmationToken !== input.confirmationToken) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Confirmation Token not found",
|
||||
});
|
||||
}
|
||||
|
||||
const isExpired = isBefore(
|
||||
new Date(authR.confirmationExpiresAt),
|
||||
new Date(),
|
||||
);
|
||||
|
||||
if (isExpired) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Confirmation Token expired",
|
||||
});
|
||||
}
|
||||
1;
|
||||
await updateAuthById(authR.id, {
|
||||
confirmationToken: null,
|
||||
confirmationExpiresAt: null,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
});
|
||||
|
||||
export const sendVerificationEmail = async (authId: string) => {
|
||||
const token = nanoid();
|
||||
const result = await updateAuthById(authId, {
|
||||
confirmationToken: token,
|
||||
confirmationExpiresAt: new Date(
|
||||
new Date().getTime() + 24 * 60 * 60 * 1000,
|
||||
).toISOString(),
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
await sendEmailNotification(
|
||||
{
|
||||
fromAddress: process.env.SMTP_FROM_ADDRESS || "",
|
||||
toAddresses: [result?.email],
|
||||
smtpServer: process.env.SMTP_SERVER || "",
|
||||
smtpPort: Number(process.env.SMTP_PORT),
|
||||
username: process.env.SMTP_USERNAME || "",
|
||||
password: process.env.SMTP_PASSWORD || "",
|
||||
},
|
||||
"Confirm your email | Dokploy",
|
||||
`
|
||||
Welcome to Dokploy!
|
||||
Please confirm your email by clicking the link below:
|
||||
<a href="${WEBSITE_URL}/confirm-email?token=${result?.confirmationToken}">
|
||||
Confirm Email
|
||||
</a>
|
||||
`,
|
||||
);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const sendDiscordNotificationWelcome = async (newAdmin: Auth) => {
|
||||
await sendDiscordNotification(
|
||||
{
|
||||
webhookUrl: process.env.DISCORD_WEBHOOK_URL || "",
|
||||
},
|
||||
{
|
||||
title: "✅ New User Registered",
|
||||
color: 0x00ff00,
|
||||
fields: [
|
||||
{
|
||||
name: "Email",
|
||||
value: newAdmin.email,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
timestamp: newAdmin.createdAt,
|
||||
footer: {
|
||||
text: "Dokploy User Registration Notification",
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
findMongoByBackupId,
|
||||
findMySqlByBackupId,
|
||||
findPostgresByBackupId,
|
||||
findServerById,
|
||||
removeBackupById,
|
||||
removeScheduleBackup,
|
||||
runMariadbBackup,
|
||||
@@ -36,6 +37,25 @@ export const backupRouter = createTRPCRouter({
|
||||
const backup = await findBackupById(newBackup.backupId);
|
||||
|
||||
if (IS_CLOUD && backup.enabled) {
|
||||
const databaseType = backup.databaseType;
|
||||
let serverId = "";
|
||||
if (databaseType === "postgres" && backup.postgres?.serverId) {
|
||||
serverId = backup.postgres.serverId;
|
||||
} else if (databaseType === "mysql" && backup.mysql?.serverId) {
|
||||
serverId = backup.mysql.serverId;
|
||||
} else if (databaseType === "mongo" && backup.mongo?.serverId) {
|
||||
serverId = backup.mongo.serverId;
|
||||
} else if (databaseType === "mariadb" && backup.mariadb?.serverId) {
|
||||
serverId = backup.mariadb.serverId;
|
||||
}
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
if (server.serverStatus === "inactive") {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server is inactive",
|
||||
});
|
||||
}
|
||||
await schedule({
|
||||
cronSchedule: backup.schedule,
|
||||
backupId: backup.backupId,
|
||||
|
||||
@@ -9,11 +9,7 @@ import {
|
||||
apiUpdateCompose,
|
||||
compose,
|
||||
} from "@/server/db/schema";
|
||||
import {
|
||||
type DeploymentJob,
|
||||
cleanQueuesByCompose,
|
||||
} from "@/server/queues/deployments-queue";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
|
||||
import { templates } from "@/templates/templates";
|
||||
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
||||
import {
|
||||
@@ -28,6 +24,7 @@ import _ from "lodash";
|
||||
import { nanoid } from "nanoid";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import { deploy } from "@/server/utils/deploy";
|
||||
import {
|
||||
IS_CLOUD,
|
||||
@@ -41,7 +38,6 @@ import {
|
||||
createComposeByTemplate,
|
||||
createDomain,
|
||||
createMount,
|
||||
findAdmin,
|
||||
findAdminById,
|
||||
findComposeById,
|
||||
findDomainsByComposeId,
|
||||
@@ -252,7 +248,6 @@ export const composeRouter = createTRPCRouter({
|
||||
descriptionLog: "",
|
||||
server: !!compose.serverId,
|
||||
};
|
||||
console.log(jobData);
|
||||
|
||||
if (IS_CLOUD && compose.serverId) {
|
||||
jobData.serverId = compose.serverId;
|
||||
|
||||
@@ -12,9 +12,10 @@ import {
|
||||
destinations,
|
||||
} from "@/server/db/schema";
|
||||
import {
|
||||
IS_CLOUD,
|
||||
createDestintation,
|
||||
execAsync,
|
||||
findAdmin,
|
||||
execAsyncRemote,
|
||||
findDestinationById,
|
||||
removeDestinationById,
|
||||
updateDestinationById,
|
||||
@@ -53,11 +54,26 @@ export const destinationRouter = createTRPCRouter({
|
||||
];
|
||||
const rcloneDestination = `:s3:${bucket}`;
|
||||
const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||
await execAsync(rcloneCommand);
|
||||
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (IS_CLOUD) {
|
||||
await execAsyncRemote(input.serverId || "", rcloneCommand);
|
||||
} else {
|
||||
await execAsync(rcloneCommand);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to connect to bucket",
|
||||
message:
|
||||
error instanceof Error
|
||||
? error?.message
|
||||
: "Error to connect to bucket",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
deployMariadb,
|
||||
findMariadbById,
|
||||
findProjectById,
|
||||
findServerById,
|
||||
removeMariadbById,
|
||||
removeService,
|
||||
startService,
|
||||
@@ -151,6 +152,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
message: "You are not authorized to deploy this mariadb",
|
||||
});
|
||||
}
|
||||
|
||||
return deployMariadb(input.mariadbId);
|
||||
}),
|
||||
changeStatus: protectedProcedure
|
||||
|
||||
@@ -148,12 +148,6 @@ export const notificationRouter = createTRPCRouter({
|
||||
.input(apiCreateDiscord)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
// go to your discord server
|
||||
// go to settings
|
||||
// go to integrations
|
||||
// add a new integration
|
||||
// select webhook
|
||||
// copy the webhook url
|
||||
return await createDiscordNotification(input, ctx.user.adminId);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
|
||||
@@ -20,10 +20,12 @@ import { and, desc, eq, sql } from "drizzle-orm";
|
||||
import type { AnyPgColumn } from "drizzle-orm/pg-core";
|
||||
|
||||
import {
|
||||
IS_CLOUD,
|
||||
addNewProject,
|
||||
checkProjectAccess,
|
||||
createProject,
|
||||
deleteProject,
|
||||
findAdminById,
|
||||
findProjectById,
|
||||
findUserByAuthId,
|
||||
updateProjectById,
|
||||
@@ -38,6 +40,15 @@ export const projectRouter = createTRPCRouter({
|
||||
await checkProjectAccess(ctx.user.authId, "create");
|
||||
}
|
||||
|
||||
const admin = await findAdminById(ctx.user.adminId);
|
||||
|
||||
if (admin.serversQuantity === 0 && IS_CLOUD) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "No servers available, Please subscribe to a plan",
|
||||
});
|
||||
}
|
||||
|
||||
const project = await createProject(input, ctx.user.adminId);
|
||||
if (ctx.user.rol === "user") {
|
||||
await addNewProject(ctx.user.authId, project.projectId);
|
||||
@@ -47,7 +58,7 @@ export const projectRouter = createTRPCRouter({
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the project",
|
||||
message: `Error to create the project: ${error instanceof Error ? error.message : error}`,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
@@ -128,9 +139,9 @@ export const projectRouter = createTRPCRouter({
|
||||
)})`,
|
||||
with: {
|
||||
applications: {
|
||||
where: buildServiceFilter(
|
||||
applications.applicationId,
|
||||
accesedServices,
|
||||
where: and(
|
||||
buildServiceFilter(applications.applicationId, accesedServices),
|
||||
eq(projects.adminId, ctx.user.adminId),
|
||||
),
|
||||
with: { domains: true },
|
||||
},
|
||||
@@ -230,5 +241,5 @@ function buildServiceFilter(fieldName: AnyPgColumn, accesedServices: string[]) {
|
||||
accesedServices.map((serviceId) => sql`${serviceId}`),
|
||||
sql`, `,
|
||||
)})`
|
||||
: sql`1 = 0`; // Always false condition
|
||||
: sql`1 = 0`;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
apiCreateRegistry,
|
||||
apiEnableSelfHostedRegistry,
|
||||
apiFindOneRegistry,
|
||||
apiRemoveRegistry,
|
||||
apiTestRegistry,
|
||||
@@ -13,8 +12,6 @@ import {
|
||||
execAsyncRemote,
|
||||
findAllRegistryByAdminId,
|
||||
findRegistryById,
|
||||
initializeRegistry,
|
||||
manageRegistry,
|
||||
removeRegistry,
|
||||
updateRegistry,
|
||||
} from "@dokploy/server";
|
||||
@@ -84,6 +81,13 @@ export const registryRouter = createTRPCRouter({
|
||||
try {
|
||||
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
|
||||
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Select a server to test the registry",
|
||||
});
|
||||
}
|
||||
|
||||
if (input.serverId && input.serverId !== "none") {
|
||||
await execAsyncRemote(input.serverId, loginCommand);
|
||||
} else {
|
||||
@@ -96,34 +100,4 @@ export const registryRouter = createTRPCRouter({
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
|
||||
enableSelfHostedRegistry: adminProcedure
|
||||
.input(apiEnableSelfHostedRegistry)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (IS_CLOUD) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Self Hosted Registry is not available in the cloud version",
|
||||
});
|
||||
}
|
||||
const selfHostedRegistry = await createRegistry(
|
||||
{
|
||||
...input,
|
||||
registryName: "Self Hosted Registry",
|
||||
registryType: "selfHosted",
|
||||
registryUrl:
|
||||
process.env.NODE_ENV === "production"
|
||||
? input.registryUrl
|
||||
: "dokploy-registry.docker.localhost",
|
||||
imagePrefix: null,
|
||||
serverId: undefined,
|
||||
},
|
||||
ctx.user.adminId,
|
||||
);
|
||||
|
||||
await manageRegistry(selfHostedRegistry);
|
||||
await initializeRegistry(input.username, input.password);
|
||||
|
||||
return selfHostedRegistry;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { updateServersBasedOnQuantity } from "@/pages/api/stripe/webhook";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
@@ -15,15 +16,17 @@ import {
|
||||
server,
|
||||
} from "@/server/db/schema";
|
||||
import {
|
||||
IS_CLOUD,
|
||||
createServer,
|
||||
deleteServer,
|
||||
findAdminById,
|
||||
findServerById,
|
||||
findServersByAdminId,
|
||||
haveActiveServices,
|
||||
removeDeploymentsByServerId,
|
||||
serverSetup,
|
||||
updateServerById,
|
||||
} from "@dokploy/server";
|
||||
// import { serverSetup } from "@/server/setup/server-setup";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, getTableColumns, isNotNull, sql } from "drizzle-orm";
|
||||
|
||||
@@ -32,6 +35,14 @@ export const serverRouter = createTRPCRouter({
|
||||
.input(apiCreateServer)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
const admin = await findAdminById(ctx.user.adminId);
|
||||
const servers = await findServersByAdminId(admin.adminId);
|
||||
if (IS_CLOUD && servers.length >= admin.serversQuantity) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "You cannot create more servers",
|
||||
});
|
||||
}
|
||||
const project = await createServer(input, ctx.user.adminId);
|
||||
return project;
|
||||
} catch (error) {
|
||||
@@ -77,13 +88,17 @@ export const serverRouter = createTRPCRouter({
|
||||
return result;
|
||||
}),
|
||||
withSSHKey: protectedProcedure.query(async ({ ctx }) => {
|
||||
return await db.query.server.findMany({
|
||||
const result = await db.query.server.findMany({
|
||||
orderBy: desc(server.createdAt),
|
||||
where: and(
|
||||
isNotNull(server.sshKeyId),
|
||||
eq(server.adminId, ctx.user.adminId),
|
||||
),
|
||||
where: IS_CLOUD
|
||||
? and(
|
||||
isNotNull(server.sshKeyId),
|
||||
eq(server.adminId, ctx.user.adminId),
|
||||
eq(server.serverStatus, "active"),
|
||||
)
|
||||
: and(isNotNull(server.sshKeyId), eq(server.adminId, ctx.user.adminId)),
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
setup: protectedProcedure
|
||||
.input(apiFindOneServer)
|
||||
@@ -125,6 +140,15 @@ export const serverRouter = createTRPCRouter({
|
||||
await removeDeploymentsByServerId(currentServer);
|
||||
await deleteServer(input.serverId);
|
||||
|
||||
if (IS_CLOUD) {
|
||||
const admin = await findAdminById(ctx.user.adminId);
|
||||
|
||||
await updateServersBasedOnQuantity(
|
||||
admin.adminId,
|
||||
admin.serversQuantity,
|
||||
);
|
||||
}
|
||||
|
||||
return currentServer;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -141,6 +165,13 @@ export const serverRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this server",
|
||||
});
|
||||
}
|
||||
|
||||
if (server.serverStatus === "inactive") {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server is inactive",
|
||||
});
|
||||
}
|
||||
const currentServer = await updateServerById(input.serverId, {
|
||||
...input,
|
||||
});
|
||||
|
||||
@@ -221,6 +221,13 @@ export const settingsRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
if (server.enableDockerCleanup) {
|
||||
const server = await findServerById(input.serverId);
|
||||
if (server.serverStatus === "inactive") {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server is inactive",
|
||||
});
|
||||
}
|
||||
if (IS_CLOUD) {
|
||||
await schedule({
|
||||
cronSchedule: "0 0 * * *",
|
||||
@@ -503,7 +510,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
if (input?.serverId) {
|
||||
const result = await execAsyncRemote(input.serverId, command);
|
||||
stdout = result.stdout;
|
||||
} else {
|
||||
} else if (!IS_CLOUD) {
|
||||
const result = await execAsync(
|
||||
"docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik",
|
||||
);
|
||||
@@ -635,7 +642,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
|
||||
return true;
|
||||
}),
|
||||
isCloud: adminProcedure.query(async () => {
|
||||
isCloud: protectedProcedure.query(async () => {
|
||||
return IS_CLOUD;
|
||||
}),
|
||||
health: publicProcedure.query(async () => {
|
||||
|
||||
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,
|
||||
} from "@dokploy/server";
|
||||
import { type Job, Worker } from "bullmq";
|
||||
import { myQueue, redisConfig } from "./queueSetup";
|
||||
|
||||
type DeployJob =
|
||||
| {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
server?: boolean;
|
||||
type: "deploy" | "redeploy";
|
||||
applicationType: "application";
|
||||
serverId?: string;
|
||||
}
|
||||
| {
|
||||
composeId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
server?: boolean;
|
||||
type: "deploy" | "redeploy";
|
||||
applicationType: "compose";
|
||||
serverId?: string;
|
||||
};
|
||||
|
||||
export type DeploymentJob = DeployJob;
|
||||
import type { DeploymentJob } from "./queue-types";
|
||||
import { redisConfig } from "./redis-connection";
|
||||
|
||||
export const deploymentWorker = new Worker(
|
||||
"deployments",
|
||||
@@ -114,25 +93,3 @@ export const deploymentWorker = new Worker(
|
||||
connection: redisConfig,
|
||||
},
|
||||
);
|
||||
|
||||
export const cleanQueuesByApplication = async (applicationId: string) => {
|
||||
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
|
||||
|
||||
for (const job of jobs) {
|
||||
if (job?.data?.applicationId === applicationId) {
|
||||
await job.remove();
|
||||
console.log(`Removed job ${job.id} for application ${applicationId}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const cleanQueuesByCompose = async (composeId: string) => {
|
||||
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
|
||||
|
||||
for (const job of jobs) {
|
||||
if (job?.data?.composeId === composeId) {
|
||||
await job.remove();
|
||||
console.log(`Removed job ${job.id} for compose ${composeId}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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", {
|
||||
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 };
|
||||
|
||||
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 { setupDockerStatsMonitoringSocketServer } from "./wss/docker-stats";
|
||||
import { setupDeploymentLogsWebSocketServer } from "./wss/listen-deployment";
|
||||
import {
|
||||
getPublicIpWithFallback,
|
||||
setupTerminalWebSocketServer,
|
||||
} from "./wss/terminal";
|
||||
import { setupTerminalWebSocketServer } from "./wss/terminal";
|
||||
|
||||
config({ path: ".env" });
|
||||
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
|
||||
const dev = process.env.NODE_ENV !== "production";
|
||||
const app = next({ dev });
|
||||
const app = next({ dev, turbopack: dev });
|
||||
const handle = app.getRequestHandler();
|
||||
void app.prepare().then(async () => {
|
||||
try {
|
||||
@@ -40,7 +37,9 @@ void app.prepare().then(async () => {
|
||||
setupDockerContainerLogsWebSocketServer(server);
|
||||
setupDockerContainerTerminalWebSocketServer(server);
|
||||
setupTerminalWebSocketServer(server);
|
||||
setupDockerStatsMonitoringSocketServer(server);
|
||||
if (!IS_CLOUD) {
|
||||
setupDockerStatsMonitoringSocketServer(server);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === "production" && !IS_CLOUD) {
|
||||
setupDirectories();
|
||||
@@ -53,7 +52,6 @@ void app.prepare().then(async () => {
|
||||
await initializeRedis();
|
||||
|
||||
initCronJobs();
|
||||
welcomeServer();
|
||||
|
||||
// Timeout to wait for the database to be ready
|
||||
await new Promise((resolve) => setTimeout(resolve, 7000));
|
||||
@@ -76,18 +74,3 @@ void app.prepare().then(async () => {
|
||||
console.error("Main Server Error", e);
|
||||
}
|
||||
});
|
||||
|
||||
async function welcomeServer() {
|
||||
const ip = await getPublicIpWithFallback();
|
||||
console.log(
|
||||
[
|
||||
"",
|
||||
"",
|
||||
"Dokploy server is up and running!",
|
||||
"Please wait for 15 seconds before opening the browser.",
|
||||
` http://${ip}:${PORT}`,
|
||||
"",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import type { DeploymentJob } from "../queues/deployments-queue";
|
||||
import { findServerById } from "@dokploy/server";
|
||||
import type { DeploymentJob } from "../queues/queue-types";
|
||||
|
||||
export const deploy = async (jobData: DeploymentJob) => {
|
||||
try {
|
||||
const server = await findServerById(jobData.serverId as string);
|
||||
if (server.serverStatus === "inactive") {
|
||||
throw new Error("Server is inactive");
|
||||
}
|
||||
const result = await fetch(`${process.env.SERVER_URL}/deploy`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
||||
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,
|
||||
createDefaultTraefikConfig,
|
||||
initializeTraefik,
|
||||
} from "@dokploy/server/dist/setup/traefik-setup";
|
||||
} from "@dokploy/server/setup/traefik-setup";
|
||||
|
||||
import { setupDirectories } from "@dokploy/server/dist/setup/config-paths";
|
||||
import { initializePostgres } from "@dokploy/server/dist/setup/postgres-setup";
|
||||
import { initializeRedis } from "@dokploy/server/dist/setup/redis-setup";
|
||||
import { setupDirectories } from "@dokploy/server/setup/config-paths";
|
||||
import { initializePostgres } from "@dokploy/server/setup/postgres-setup";
|
||||
import { initializeRedis } from "@dokploy/server/setup/redis-setup";
|
||||
import {
|
||||
initializeNetwork,
|
||||
initializeSwarm,
|
||||
} from "@dokploy/server/dist/setup/setup";
|
||||
} from "@dokploy/server/setup/setup";
|
||||
(async () => {
|
||||
try {
|
||||
setupDirectories();
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
/* Path Aliases */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
"@/*": ["./*"],
|
||||
"@dokploy/server/*": ["../../packages/server/src/*"]
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
"moduleResolution": "Node",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
"@/*": ["./*"],
|
||||
"@dokploy/server/*": ["../../packages/server/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "./server/**/*"]
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
cleanUpSystemPrune,
|
||||
cleanUpUnusedImages,
|
||||
findBackupById,
|
||||
findServerById,
|
||||
runMariadbBackup,
|
||||
runMongoBackup,
|
||||
runMySqlBackup,
|
||||
@@ -21,22 +22,47 @@ export const runJobs = async (job: QueueJob) => {
|
||||
const { backupId } = job;
|
||||
const backup = await findBackupById(backupId);
|
||||
const { databaseType, postgres, mysql, mongo, mariadb } = backup;
|
||||
|
||||
if (databaseType === "postgres" && postgres) {
|
||||
const server = await findServerById(postgres.serverId as string);
|
||||
if (server.serverStatus === "inactive") {
|
||||
logger.info("Server is inactive");
|
||||
return;
|
||||
}
|
||||
await runPostgresBackup(postgres, backup);
|
||||
} else if (databaseType === "mysql" && mysql) {
|
||||
const server = await findServerById(mysql.serverId as string);
|
||||
if (server.serverStatus === "inactive") {
|
||||
logger.info("Server is inactive");
|
||||
return;
|
||||
}
|
||||
await runMySqlBackup(mysql, backup);
|
||||
} else if (databaseType === "mongo" && mongo) {
|
||||
const server = await findServerById(mongo.serverId as string);
|
||||
if (server.serverStatus === "inactive") {
|
||||
logger.info("Server is inactive");
|
||||
return;
|
||||
}
|
||||
await runMongoBackup(mongo, backup);
|
||||
} else if (databaseType === "mariadb" && mariadb) {
|
||||
const server = await findServerById(mariadb.serverId as string);
|
||||
if (server.serverStatus === "inactive") {
|
||||
logger.info("Server is inactive");
|
||||
return;
|
||||
}
|
||||
await runMariadbBackup(mariadb, backup);
|
||||
}
|
||||
}
|
||||
if (job.type === "server") {
|
||||
const { serverId } = job;
|
||||
const server = await findServerById(serverId);
|
||||
if (server.serverStatus === "inactive") {
|
||||
logger.info("Server is inactive");
|
||||
return;
|
||||
}
|
||||
await cleanUpUnusedImages(serverId);
|
||||
await cleanUpDockerBuilder(serverId);
|
||||
await cleanUpSystemPrune(serverId);
|
||||
// await sendDockerCleanupNotifications();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import clsx from "clsx";
|
||||
import { Inter, Lexend } from "next/font/google";
|
||||
import "@/styles/tailwind.css";
|
||||
import GoogleAnalytics from "@/components/analitycs/google";
|
||||
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getMessages } from "next-intl/server";
|
||||
|
||||
@@ -88,7 +86,14 @@ export default async function RootLayout({
|
||||
lang={locale}
|
||||
className={clsx("h-full scroll-smooth", inter.variable, lexend.variable)}
|
||||
>
|
||||
<GoogleAnalytics />
|
||||
<head>
|
||||
<script
|
||||
defer
|
||||
src="https://umami.dokploy.com/script.js"
|
||||
data-website-id="7d1422e4-3776-4870-8145-7d7b2075d470"
|
||||
/>
|
||||
</head>
|
||||
{/* <GoogleAnalytics /> */}
|
||||
<body className="flex h-full flex-col">
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<Header />
|
||||
|
||||
@@ -1,11 +1,40 @@
|
||||
import Link from "next/link";
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "@/components/ui/select";
|
||||
import { Link, useRouter } from "@/i18n/routing";
|
||||
import { useLocale, useTranslations } from "next-intl";
|
||||
import type { SVGProps } from "react";
|
||||
import { Container } from "./Container";
|
||||
import { NavLink } from "./NavLink";
|
||||
import { Logo } from "./shared/Logo";
|
||||
import { buttonVariants } from "./ui/button";
|
||||
|
||||
const I18nIcon = (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth={0}
|
||||
viewBox="0 0 512 512"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
stroke="none"
|
||||
d="m478.33 433.6-90-218a22 22 0 0 0-40.67 0l-90 218a22 22 0 1 0 40.67 16.79L316.66 406h102.67l18.33 44.39A22 22 0 0 0 458 464a22 22 0 0 0 20.32-30.4zM334.83 362 368 281.65 401.17 362zm-66.99-19.08a22 22 0 0 0-4.89-30.7c-.2-.15-15-11.13-36.49-34.73 39.65-53.68 62.11-114.75 71.27-143.49H330a22 22 0 0 0 0-44H214V70a22 22 0 0 0-44 0v20H54a22 22 0 0 0 0 44h197.25c-9.52 26.95-27.05 69.5-53.79 108.36-31.41-41.68-43.08-68.65-43.17-68.87a22 22 0 0 0-40.58 17c.58 1.38 14.55 34.23 52.86 83.93.92 1.19 1.83 2.35 2.74 3.51-39.24 44.35-77.74 71.86-93.85 80.74a22 22 0 1 0 21.07 38.63c2.16-1.18 48.6-26.89 101.63-85.59 22.52 24.08 38 35.44 38.93 36.1a22 22 0 0 0 30.75-4.9z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export function Footer() {
|
||||
const router = useRouter();
|
||||
const locale = useLocale();
|
||||
const t = useTranslations("HomePage");
|
||||
const linkT = useTranslations("Link");
|
||||
|
||||
@@ -31,7 +60,7 @@ export function Footer() {
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex flex-col items-center border-t border-slate-400/10 py-10 sm:flex-row-reverse sm:justify-between">
|
||||
<div className="flex gap-x-6">
|
||||
<div className="flex gap-x-6 items-center">
|
||||
<Link
|
||||
href="https://x.com/getdokploy"
|
||||
className="group"
|
||||
@@ -56,6 +85,30 @@ export function Footer() {
|
||||
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2Z" />
|
||||
</svg>
|
||||
</Link>
|
||||
<Select
|
||||
onValueChange={(locale) => {
|
||||
router.replace("/", {
|
||||
locale: locale as "en" | "zh-Hans",
|
||||
});
|
||||
}}
|
||||
value={locale}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={buttonVariants({
|
||||
variant: "outline",
|
||||
className:
|
||||
" flex items-center gap-2 !rounded-full visited:outline-none focus-within:outline-none focus:outline-none",
|
||||
})}
|
||||
>
|
||||
<I18nIcon width={20} height={20} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="en">{t("navigation.i18nEn")}</SelectItem>
|
||||
<SelectItem value="zh-Hans">
|
||||
{t("navigation.i18nZh-Hans")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<p className="mt-6 text-sm text-muted-foreground sm:mt-0">
|
||||
{t("footer.copyright", {
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "@/components/ui/select";
|
||||
import { Link, useRouter } from "@/i18n/routing";
|
||||
import { Link } from "@/i18n/routing";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { HeartIcon } from "lucide-react";
|
||||
import { useLocale, useTranslations } from "next-intl";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Fragment, type JSX, type SVGProps } from "react";
|
||||
import { Container } from "./Container";
|
||||
import { NavLink } from "./NavLink";
|
||||
@@ -125,10 +119,7 @@ function MobileNavigation() {
|
||||
as="div"
|
||||
className="absolute inset-x-0 top-full mt-4 flex origin-top flex-col rounded-2xl border border-border bg-background p-4 text-lg tracking-tight text-primary shadow-xl ring-1 ring-border/5"
|
||||
>
|
||||
<MobileNavLink href="/#features">
|
||||
{t("navigation.features")}
|
||||
</MobileNavLink>
|
||||
{/* <MobileNavLink href="/#testimonials">Testimonials</MobileNavLink> */}
|
||||
<MobileNavLink href="/pricing">Pricing</MobileNavLink>
|
||||
<MobileNavLink href="/#faqs">{t("navigation.faqs")}</MobileNavLink>
|
||||
<MobileNavLink href={linkT("docs.intro")} target="_blank">
|
||||
{t("navigation.docs")}
|
||||
@@ -141,8 +132,6 @@ function MobileNavigation() {
|
||||
}
|
||||
|
||||
export function Header() {
|
||||
const router = useRouter();
|
||||
const locale = useLocale();
|
||||
const t = useTranslations("HomePage");
|
||||
const linkT = useTranslations("Link");
|
||||
|
||||
@@ -155,8 +144,7 @@ export function Header() {
|
||||
<Logo className="h-10 w-auto" />
|
||||
</Link>
|
||||
<div className="hidden md:flex md:gap-x-6">
|
||||
<NavLink href="/#features">{t("navigation.features")}</NavLink>
|
||||
{/* <NavLink href="/#testimonials">Testimonials</NavLink> */}
|
||||
<NavLink href="/pricing">{t("navigation.pricing")}</NavLink>
|
||||
<NavLink href="/#faqs">{t("navigation.faqs")}</NavLink>
|
||||
<NavLink href={linkT("docs.intro")} target="_blank">
|
||||
{t("navigation.docs")}
|
||||
@@ -164,31 +152,6 @@ export function Header() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2 md:gap-x-5">
|
||||
<Select
|
||||
onValueChange={(locale) => {
|
||||
router.replace("/", {
|
||||
locale: locale as "en" | "zh-Hans",
|
||||
});
|
||||
}}
|
||||
value={locale}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={buttonVariants({
|
||||
variant: "outline",
|
||||
className:
|
||||
" flex items-center gap-2 !rounded-full visited:outline-none focus-within:outline-none focus:outline-none",
|
||||
})}
|
||||
>
|
||||
<I18nIcon width={20} height={20} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="en">{t("navigation.i18nEn")}</SelectItem>
|
||||
<SelectItem value="zh-Hans">
|
||||
{t("navigation.i18nZh-Hans")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Link
|
||||
className={buttonVariants({
|
||||
variant: "outline",
|
||||
@@ -202,25 +165,14 @@ export function Header() {
|
||||
</span>
|
||||
<HeartIcon className="animate-heartbeat size-4 fill-red-600 text-red-500 " />
|
||||
</Link>
|
||||
<Button
|
||||
className="rounded-full bg-[#5965F2] hover:bg-[#4A55E0]"
|
||||
asChild
|
||||
>
|
||||
<Button className="rounded-xl" asChild>
|
||||
<Link
|
||||
href="https://discord.gg/2tBnJ3jDJc"
|
||||
href="https://app.dokploy.com"
|
||||
aria-label="Dokploy on GitHub"
|
||||
target="_blank"
|
||||
className="flex flex-row items-center gap-2 text-white"
|
||||
// className="flex flex-row items-center gap-2 text-white"
|
||||
>
|
||||
<svg
|
||||
role="img"
|
||||
className="h-6 w-6 fill-white"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
|
||||
</svg>
|
||||
{t("navigation.discord")}
|
||||
{t("navigation.dashboard")}
|
||||
</Link>
|
||||
</Button>
|
||||
<div className="-mr-1 md:hidden">
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
"use client";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ArrowRight, ArrowRightIcon, Check, Copy } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Container } from "./Container";
|
||||
import AnimatedShinyText from "./ui/animated-shiny-text";
|
||||
import { Button } from "./ui/button";
|
||||
import { HoverBorderGradient } from "./ui/hover-border-gradient";
|
||||
|
||||
const ProductHunt = () => {
|
||||
return (
|
||||
@@ -69,7 +72,22 @@ export function Hero() {
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<h1 className="mx-auto max-w-4xl font-display text-5xl font-medium tracking-tight text-muted-foreground sm:text-7xl">
|
||||
<Link href="/pricing" className="relative z-10 mb-4 inline-block">
|
||||
<div className="flex items-center justify-center">
|
||||
<div
|
||||
className={cn(
|
||||
"group rounded-full border border-black/5 bg-neutral-100 text-sm font-medium text-white transition-all ease-in hover:cursor-pointer hover:bg-neutral-200 dark:border-white/5 dark:bg-neutral-900 dark:hover:bg-neutral-800",
|
||||
)}
|
||||
>
|
||||
<AnimatedShinyText className="inline-flex items-center justify-center px-4 py-1 text-neutral-800 transition ease-out hover:text-neutral-900 hover:duration-300 hover:dark:text-neutral-400">
|
||||
<span>🚀 {t("hero.cloud")} </span>
|
||||
<ArrowRightIcon className="ml-1 size-3 transition-transform duration-300 ease-in-out group-hover:translate-x-0.5" />
|
||||
</AnimatedShinyText>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<h1 className="mx-auto max-w-4xl font-display text-5xl font-medium tracking-tight text-muted-foreground sm:text-7xl">
|
||||
{t("hero.deploy")}{" "}
|
||||
<span className="relative whitespace-nowrap text-primary">
|
||||
<svg
|
||||
@@ -125,7 +143,9 @@ export function Hero() {
|
||||
Discord
|
||||
</Link>
|
||||
</Button> */}
|
||||
<Button className="rounded-xl" asChild>
|
||||
</div>
|
||||
<div className="mx-auto flex w-full max-w-sm flex-wrap items-center justify-center gap-3 md:flex-nowrap">
|
||||
<Button className="w-full rounded-xl" asChild>
|
||||
<Link
|
||||
href="https://github.com/dokploy/dokploy"
|
||||
aria-label="Dokploy on GitHub"
|
||||
@@ -138,6 +158,27 @@ export function Hero() {
|
||||
Github
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full rounded-xl bg-[#5965F2] hover:bg-[#4A55E0]"
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
href="https://discord.gg/2tBnJ3jDJc"
|
||||
aria-label="Dokploy on GitHub"
|
||||
target="_blank"
|
||||
className="flex flex-row items-center gap-2 text-white"
|
||||
>
|
||||
<svg
|
||||
role="img"
|
||||
className="h-6 w-6 fill-white"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
|
||||
</svg>
|
||||
{t("navigation.discord")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-16 flex flex-row justify-center gap-x-8 rounded-lg sm:gap-x-0 sm:gap-y-10 xl:gap-x-12 xl:gap-y-0">
|
||||
@@ -182,22 +223,20 @@ export function Hero() {
|
||||
}
|
||||
|
||||
export const ShowSponsors = () => {
|
||||
const t = useTranslations("HomePage");
|
||||
return (
|
||||
<div className="mt-20 flex flex-col justify-center gap-y-10">
|
||||
<div className="flex flex-col gap-4 justify-start">
|
||||
<div className="flex flex-col justify-start gap-4">
|
||||
<h1 className="mx-auto max-w-2xl font-display text-3xl font-medium tracking-tight text-primary sm:text-5xl">
|
||||
Sponsors
|
||||
{t("hero.sponsors.title")}
|
||||
</h1>
|
||||
<p className="mx-auto max-w-2xl text-lg tracking-tight text-muted-foreground">
|
||||
Dokploy is an open source project that is maintained by a community of
|
||||
volunteers. We would like to thank our sponsors for their support and
|
||||
contributions to the project, which help us to continue to develop and
|
||||
improve Dokploy.
|
||||
{t("hero.sponsors.description")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 md:gap-6 justify-start">
|
||||
<h2 className="font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl text-left">
|
||||
Hero Sponsors 🎖
|
||||
<div className="flex flex-col items-center justify-start gap-4 md:gap-6">
|
||||
<h2 className="text-left font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl">
|
||||
{t("hero.sponsors.level.hero")} 🎖
|
||||
</h2>
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<a
|
||||
@@ -209,7 +248,7 @@ export const ShowSponsors = () => {
|
||||
<img
|
||||
src="https://raw.githubusercontent.com/Dokploy/dokploy/canary/.github/sponsors/hostinger.jpg"
|
||||
alt="hostinger.com"
|
||||
className="rounded-xl w-[190px] h-auto"
|
||||
className="h-auto w-[190px] rounded-xl"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
@@ -221,16 +260,16 @@ export const ShowSponsors = () => {
|
||||
<img
|
||||
src="https://raw.githubusercontent.com/Dokploy/dokploy/canary/.github/sponsors/lxaer.png"
|
||||
alt="lxaer.com"
|
||||
className="rounded-xl w-[70px] h-auto"
|
||||
className="h-auto w-[70px] rounded-xl"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 md:gap-8 justify-start">
|
||||
<h2 className="font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl text-left">
|
||||
Premium Supporters 🥇
|
||||
<div className="flex flex-col items-center justify-start gap-4 md:gap-8">
|
||||
<h2 className="text-left font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl">
|
||||
{t("hero.sponsors.level.premium")} 🥇
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
<div className="flex flex-col items-center justify-start gap-4 md:gap-6">
|
||||
<a
|
||||
href="https://supafort.com/?ref=dokploy"
|
||||
target="_blank"
|
||||
@@ -245,9 +284,9 @@ export const ShowSponsors = () => {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 md:gap-8 justify-start">
|
||||
<h2 className="font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl text-left">
|
||||
Supporting Members 🥉
|
||||
<div className="flex flex-col items-center justify-start gap-4 md:gap-8">
|
||||
<h2 className="text-left font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl">
|
||||
{t("hero.sponsors.level.supporting")} 🥉
|
||||
</h2>
|
||||
<div className="flex flex-row gap-10">
|
||||
<a
|
||||
@@ -276,9 +315,9 @@ export const ShowSponsors = () => {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 md:gap-8 justify-start">
|
||||
<h2 className="font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl text-left">
|
||||
Community Backers 🤝
|
||||
<div className="justify-star flex flex-col items-center gap-4 md:gap-8">
|
||||
<h2 className="text-left font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl">
|
||||
{t("hero.sponsors.level.community")} 🤝
|
||||
</h2>
|
||||
<div className="flex flex-row gap-10">
|
||||
<a
|
||||
@@ -305,11 +344,24 @@ export const ShowSponsors = () => {
|
||||
alt="Rivo.gg"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://photoquest.wedding/?ref=dokploy"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<img
|
||||
src="https://photoquest.wedding/favicon/android-chrome-512x512.png"
|
||||
className="rounded-xl"
|
||||
width="60px"
|
||||
alt="Rivo.gg"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 md:gap-8 justify-start">
|
||||
<h2 className="font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl text-left">
|
||||
Organizations:
|
||||
<div className="flex flex-col items-center justify-start gap-4 md:gap-8">
|
||||
<h2 className="text-left font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl">
|
||||
{t("hero.sponsors.level.organizations")}
|
||||
</h2>
|
||||
<div className="flex flex-row gap-10">
|
||||
<a
|
||||
@@ -324,9 +376,9 @@ export const ShowSponsors = () => {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 md:gap-8 justify-start">
|
||||
<h2 className="font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl text-left">
|
||||
Individuals:
|
||||
<div className="flex flex-col items-center justify-start gap-4 md:gap-8">
|
||||
<h2 className="text-left font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl">
|
||||
{t("hero.sponsors.level.individuals")}
|
||||
</h2>
|
||||
<div className="flex flex-row gap-10">
|
||||
<a
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { Link } from "@/i18n/routing";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Footer } from "./Footer";
|
||||
import { Header } from "./Header";
|
||||
|
||||
export function SlimLayout() {
|
||||
const t = useTranslations("404");
|
||||
@@ -16,6 +14,7 @@ export function SlimLayout() {
|
||||
<Link href="/" className="text-primary">
|
||||
{t("action")}
|
||||
</Link>
|
||||
p{" "}
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user