Compare commits

...

50 Commits

Author SHA1 Message Date
Mauricio Siu
1a6bf06929 Merge pull request #606 from PaiJi/chore/i18n-zh-hans
feat(i18n): add zh-Hans for Pricing and Sponsor
2024-10-27 02:05:24 -06:00
JiPai
ecc98b0944 feat(i18n): add zh-Hans for Pricing page 2024-10-27 15:38:22 +08:00
JiPai
950ee77a49 feat(i18n): add zh-Hans translation for homepage 2024-10-27 14:32:23 +08:00
Mauricio Siu
38a72806a7 refactor(dokploy): add webhook url for new users 2024-10-26 23:06:00 -06:00
Mauricio Siu
fa70696c71 refactor(website): update const value 2024-10-26 22:46:15 -06:00
Mauricio Siu
b123baafa4 refactor(dokploy): add analytics and pricing section to website 2024-10-26 22:44:21 -06:00
Mauricio Siu
d6fa416a3f chore(ci/cd): set secrets 2024-10-26 20:31:17 -06:00
Mauricio Siu
d2c0f19353 refactor(dokploy): add missing import 2024-10-26 19:56:34 -06:00
Mauricio Siu
3ef6c711e6 chore(ci/cd): set live key 2024-10-26 19:51:23 -06:00
Mauricio Siu
64a4d51f9c Merge pull request #605 from Dokploy/feat/add-email-verification
Feat/add email verification
2024-10-26 19:39:59 -06:00
Mauricio Siu
1a20e4f813 refactor: disable endpoints from cloud in self hosted 2024-10-26 19:35:49 -06:00
Mauricio Siu
250c14738c refactor: remove register when is self hosted 2024-10-26 19:32:29 -06:00
Mauricio Siu
28221a4e7a refactor(dokploy): add umami and update favicon 2024-10-26 19:27:01 -06:00
Mauricio Siu
680c22a41e feat(dokploy): add verification email to cloud version 2024-10-26 18:09:55 -06:00
Mauricio Siu
428b9cf0ec Merge pull request #603 from Dokploy/fix/start-schedules-and-server
fix(server): update build paths
2024-10-26 16:24:12 -06:00
Mauricio Siu
78659b2ad9 chore: exclude package.json to format 2024-10-26 16:17:20 -06:00
Mauricio Siu
4b5408c050 chore: lint 2024-10-26 16:04:43 -06:00
Mauricio Siu
5417f6376b chore(server): update switch prod in build command 2024-10-26 15:56:03 -06:00
Mauricio Siu
27c33c7661 fix(server): update build paths 2024-10-26 15:48:30 -06:00
Mauricio Siu
0873618c63 chore(dokploy): set latest tag in cloud 2024-10-26 15:08:49 -06:00
Mauricio Siu
e3b20268d5 refactor(dokploy): add ssr for settings to show instantly the sidebar 2024-10-25 23:56:33 -06:00
Mauricio Siu
742e5a244d refactor(dokploy): prevent to create projects when the user doesn't have servers buyed 2024-10-25 23:44:39 -06:00
Mauricio Siu
1f208f5f5b refactor(dokploy): add next public shareable key 2024-10-25 22:53:58 -06:00
Mauricio Siu
a53ade88e7 refactor(dokploy): add SITE_URL 2024-10-25 22:35:35 -06:00
Mauricio Siu
77e9617770 chore(version): bump version 2024-10-25 21:18:04 -06:00
Mauricio Siu
21e97b0175 Merge pull request #598 from Dokploy/593-use-advertise_addr-from-environment-variable-in-dokpoy-service
feat(dokploy): add env for ADVERTISE_ADDR to installation #593
2024-10-25 20:50:58 -06:00
Mauricio Siu
a6618a14d5 feat(dokploy): add env for ADVERTISE_ADDR to installation #593 2024-10-25 20:48:46 -06:00
Mauricio Siu
59308ab013 Merge pull request #596 from Dokploy/595-dokploy-internal-postgres-database-is-publicly-accessible-by-default
fix(dokploy): remove expose ports in production
2024-10-25 20:19:14 -06:00
Mauricio Siu
e19c8d7a7a fix(dokploy): remove expose ports in production 2024-10-25 17:09:06 -06:00
Mauricio Siu
421c93795b refactor(dokploy): prevent start worker when is cloud 2024-10-25 16:56:32 -06:00
Mauricio Siu
182f908c31 chore: add new sponsor 2024-10-25 10:20:56 -06:00
Mauricio Siu
20616363e9 refactor: update badge server 2024-10-25 00:39:34 -06:00
Mauricio Siu
d85073b26d Merge pull request #591 from Dokploy/feat/remove-build-on-server
Feat/remove build on server
2024-10-25 00:23:56 -06:00
Mauricio Siu
303d1b1b87 chore: add missing command 2024-10-25 00:21:12 -06:00
Mauricio Siu
60d4e1ba63 chore: update dockerfiles 2024-10-25 00:17:10 -06:00
Mauricio Siu
83d52b68f0 refactor: update 2024-10-25 00:05:55 -06:00
Mauricio Siu
af3b1a27f4 refactor: update tests 2024-10-24 23:57:40 -06:00
Mauricio Siu
7f94593c07 chore: revert ci/cd 2024-10-24 23:35:21 -06:00
Mauricio Siu
5df7654873 chore: update imports 2024-10-24 23:33:15 -06:00
Mauricio Siu
054836fd4c chore: update workflows 2024-10-24 23:28:52 -06:00
Mauricio Siu
484ead1f1f chore: update workflows 2024-10-24 23:26:06 -06:00
Mauricio Siu
fbada4c5de chore: set right filter pnpm dockerfile 2024-10-24 23:22:23 -06:00
Mauricio Siu
491113416b chore: remove server build 2024-10-24 23:20:51 -06:00
Mauricio Siu
c42f5cb799 refactor: update 2024-10-24 23:18:23 -06:00
Mauricio Siu
47aa223f87 refactor: remove save on build on next app and integrate turbopack 2024-10-24 23:13:24 -06:00
Mauricio Siu
554ac59b97 Merge pull request #589 from alok-debnath/typo-fixes
Typo fixes in README.md
2024-10-24 13:43:28 -06:00
Alok Debnath
0247898876 minor typo fixes in README.md for better readability 2024-10-25 00:06:09 +05:30
Mauricio Siu
fc2778db35 Merge pull request #585 from Dokploy/505-mongodb-backup-is-empty-in-output-pipeline
fix(dokploy): add missing --archive to mongodump
2024-10-24 01:14:10 -06:00
Mauricio Siu
85d6ff9012 chore(version): bump version 2024-10-24 01:11:43 -06:00
Mauricio Siu
522f8baec7 fix(dokploy): add missing --archive to mongodump 2024-10-24 01:07:02 -06:00
172 changed files with 6384 additions and 1199 deletions

View File

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

View File

@@ -71,10 +71,9 @@ Run the command that will spin up all the required services and files.
pnpm run dokploy:setup
```
Build the server package (If you make any changes after in the packages/server folder, you need to rebuild and run this command)
Run this script
```bash
pnpm run server:build
pnpm run server:script
```
Now run the development server.

View File

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

View File

@@ -15,29 +15,29 @@
</div>
</div>
<br />
Dokploy is a free self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
### Features
Dokploy include multiples features to make your life easier.
Dokploy includes multiple features to make your life easier.
- **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.).
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, Redis.
- **Backups**: Automate backups for databases to a external storage destination.
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, and Redis.
- **Backups**: Automate backups for databases to an external storage destination.
- **Docker Compose**: Native support for Docker Compose to manage complex applications.
- **Multi Node**: Scale applications to multiples nodes using docker swarm to manage the cluster.
- **Templates**: Deploy in a single click open source templates (Plausible, Pocketbase, Calcom, etc.).
- **Multi Node**: Scale applications to multiple nodes using Docker Swarm to manage the cluster.
- **Templates**: Deploy open-source templates (Plausible, Pocketbase, Calcom, etc.) with a single click.
- **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
- **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage, for every resource.
- **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage for every resource.
- **Docker Management**: Easily deploy and manage Docker containers.
- **CLI/API**: Manage your applications and databases using the command line or 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:

View File

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

View File

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

View File

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

View File

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

View File

@@ -57,18 +57,39 @@ fi
# Initialize Docker Swarm
docker swarm leave --force 2>/dev/null
advertise_addr=$(curl -s ifconfig.me)
docker swarm init --advertise-addr $advertise_addr
echo "Swarm initialized"
# Create network
docker network rm -f dokploy-network 2>/dev/null
docker network create --driver overlay --attachable dokploy-network
echo "Network created"
get_ip() {
# Try to get IPv4
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
# Prepare configuration directory
mkdir -p /etc/dokploy
chmod -R 777 /etc/dokploy
if [ -n "$ipv4" ]; then
echo "$ipv4"
else
# Try to get IPv6
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
if [ -n "$ipv6" ]; then
echo "$ipv6"
fi
fi
}
advertise_addr="${ADVERTISE_ADDR:-$(get_ip)}"
docker swarm init --advertise-addr $advertise_addr
if [ $? -ne 0 ]; then
echo "Error: Failed to initialize Docker Swarm" >&2
exit 1
fi
docker network rm -f dokploy-network 2>/dev/null
docker network create --driver overlay --attachable dokploy-network
echo "Network created"
mkdir -p /etc/dokploy
chmod 777 /etc/dokploy
# Pull and deploy Dokploy
docker pull dokploy/dokploy:latest
@@ -84,6 +105,7 @@ docker service create \
-e PORT=<Value For PORT eg(3000)> \
-e TRAEFIK_SSL_PORT=<Value For SSL PORT eg(444)> \
-e TRAEFIK_PORT=<VALUE FOR TRAEFIK HTTP PORT eg(81)> \
-e ADVERTISE_ADDR=$advertise_addr \
dokploy/dokploy:latest
# Output success message

View File

@@ -1,12 +1,23 @@
import fs from "node:fs/promises";
import path from "node:path";
import { paths } from "@dokploy/server/dist/constants";
import { paths } from "@dokploy/server/constants";
const { APPLICATIONS_PATH } = paths();
import type { ApplicationNested } from "@dokploy/server";
import { unzipDrop } from "@dokploy/server";
import AdmZip from "adm-zip";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
vi.mock("@dokploy/server/constants", async (importOriginal) => {
const actual = await importOriginal();
return {
// @ts-ignore
...actual,
paths: () => ({
APPLICATIONS_PATH: "./__test__/drop/zips/output",
}),
};
});
if (typeof window === "undefined") {
const undici = require("undici");
globalThis.File = undici.File as any;
@@ -82,16 +93,6 @@ const baseApp: ApplicationNested = {
dockerContextPath: null,
};
vi.mock("@dokploy/server/dist/constants", async (importOriginal) => {
const actual = await importOriginal();
return {
// @ts-ignore
...actual,
paths: () => ({
APPLICATIONS_PATH: "./__test__/drop/zips/output",
}),
};
});
describe("unzipDrop using real zip files", () => {
// const { APPLICATIONS_PATH } = paths();
beforeAll(async () => {

View File

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

View File

@@ -6,11 +6,18 @@ import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { loadStripe } from "@stripe/stripe-js";
import clsx from "clsx";
import { AlertTriangle, CheckIcon, MinusIcon, PlusIcon } from "lucide-react";
import {
AlertTriangle,
CheckIcon,
Loader2,
MinusIcon,
PlusIcon,
} from "lucide-react";
import Link from "next/link";
import React, { useState } from "react";
const stripePromise = loadStripe(
"pk_test_51QAm7bF3cxQuHeOz0xg04o9teeyTbbNHQPJ5Tr98MlTEan9MzewT3gwh0jSWBNvrRWZ5vASoBgxUSF4gPWsJwATk00Ir2JZ0S1",
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || "",
);
export const calculatePrice = (count: number, isAnnual = false) => {
@@ -21,11 +28,10 @@ export const calculatePrice = (count: number, isAnnual = false) => {
if (count <= 1) return 4.5;
return count * 3.5;
};
// 178.156.147.118
export const ShowBilling = () => {
const { data: servers } = api.server.all.useQuery(undefined);
const { data: admin } = api.admin.one.useQuery();
const { data } = api.stripe.getProducts.useQuery();
const { data, isLoading } = api.stripe.getProducts.useQuery();
const { mutateAsync: createCheckoutSession } =
api.stripe.createCheckoutSession.useMutation();
@@ -97,145 +103,186 @@ export const ShowBilling = () => {
)}
</div>
)}
{products?.map((product) => {
const featured = true;
return (
<div key={product.id}>
<section
className={clsx(
"flex flex-col rounded-3xl border-dashed border-2 px-4 max-w-sm",
featured
? "order-first bg-black border py-8 lg:order-none"
: "lg:py-8",
)}
<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"
>
{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",
<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",
)}
>
{admin?.stripeCustomerId && (
<Button
variant="secondary"
className="w-full"
onClick={async () => {
const session = await createCustomerPortalSession();
window.open(session.url);
}}
>
Manage Subscription
</Button>
{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>
{data?.subscriptions?.length === 0 && (
<div className="justify-end w-full">
<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
className="w-full"
onClick={async () => {
handleCheckout(product.id);
disabled={serverQuantity <= 1}
variant="outline"
onClick={() => {
if (serverQuantity <= 1) return;
setServerQuantity(serverQuantity - 1);
}}
disabled={serverQuantity < 1}
>
Subscribe
<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>
<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>
</section>
</div>
);
})}
);
})}
</>
)}
</div>
);
};

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -295,6 +295,13 @@
"when": 1729667438853,
"tag": "0041_huge_bruce_banner",
"breakpoints": true
},
{
"idx": 42,
"version": "6",
"when": 1729984439862,
"tag": "0042_fancy_havok",
"breakpoints": true
}
]
}

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.10.2",
"version": "v0.10.4",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -11,7 +11,7 @@
"build-next": "next build",
"setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run",
"reset-password": "node -r dotenv/config dist/reset-password.mjs",
"dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
"dev": "TURBOPACK=1 tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
"migration:run": "tsx -r dotenv/config migration.ts",
@@ -90,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",

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { db } from "@/server/db";
import { applications } from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/deployments-queue";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { IS_CLOUD } from "@dokploy/server";

View File

@@ -1,6 +1,6 @@
import { db } from "@/server/db";
import { compose } from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/deployments-queue";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { IS_CLOUD } from "@dokploy/server";

View File

@@ -1,6 +1,6 @@
import { db } from "@/server/db";
import { applications, compose, github } from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/deployments-queue";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { IS_CLOUD } from "@dokploy/server";

View File

@@ -0,0 +1,96 @@
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
import { Logo } from "@/components/shared/logo";
import { CardDescription, CardTitle } from "@/components/ui/card";
import { db } from "@/server/db";
import { auth } from "@/server/db/schema";
import { IS_CLOUD, updateAuthById } from "@dokploy/server";
import { isBefore } from "date-fns";
import { eq } from "drizzle-orm";
import type { GetServerSidePropsContext } from "next";
import Link from "next/link";
import type { ReactElement } from "react";
export default function Home() {
return (
<div className="flex h-screen w-full items-center justify-center ">
<div className="flex flex-col items-center gap-4 w-full">
<Link href="/" className="flex flex-row items-center gap-2">
<Logo />
<span className="font-medium text-sm">Dokploy</span>
</Link>
<CardTitle className="text-2xl font-bold">Email Confirmed</CardTitle>
<CardDescription>
Congratulations, your email is confirmed.
</CardDescription>
<div>
<Link href="/" className="w-full text-primary">
Click here to login
</Link>
</div>
</div>
</div>
);
}
Home.getLayout = (page: ReactElement) => {
return <OnboardingLayout>{page}</OnboardingLayout>;
};
export async function getServerSideProps(context: GetServerSidePropsContext) {
if (!IS_CLOUD) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
const { token } = context.query;
if (typeof token !== "string") {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
const authR = await db.query.auth.findFirst({
where: eq(auth.confirmationToken, token),
});
if (
!authR ||
authR?.confirmationToken === null ||
authR?.confirmationExpiresAt === null
) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
const isExpired = isBefore(new Date(authR.confirmationExpiresAt), new Date());
if (isExpired) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
await updateAuthById(authR.id, {
confirmationToken: null,
confirmationExpiresAt: null,
});
return {
props: {
token: authR.confirmationToken,
},
};
}

View File

@@ -110,9 +110,11 @@ const Service = (
<div className="flex flex-row h-fit w-fit gap-2">
<Badge
variant={
data?.server?.serverStatus === "active"
!data?.serverId
? "default"
: "destructive"
: data?.server?.serverStatus === "active"
? "default"
: "destructive"
}
>
{data?.server?.name || "Dokploy Server"}

View File

@@ -105,9 +105,11 @@ const Service = (
<div className="flex flex-row h-fit w-fit gap-2">
<Badge
variant={
data?.server?.serverStatus === "active"
!data?.serverId
? "default"
: "destructive"
: data?.server?.serverStatus === "active"
? "default"
: "destructive"
}
>
{data?.server?.name || "Dokploy Server"}

View File

@@ -93,9 +93,11 @@ const Mariadb = (
<div className="flex flex-row h-fit w-fit gap-2">
<Badge
variant={
data?.server?.serverStatus === "active"
!data?.serverId
? "default"
: "destructive"
: data?.server?.serverStatus === "active"
? "default"
: "destructive"
}
>
{data?.server?.name || "Dokploy Server"}

View File

@@ -94,9 +94,11 @@ const Mongo = (
<div className="flex flex-row h-fit w-fit gap-2">
<Badge
variant={
data?.server?.serverStatus === "active"
!data?.serverId
? "default"
: "destructive"
: data?.server?.serverStatus === "active"
? "default"
: "destructive"
}
>
{data?.server?.name || "Dokploy Server"}

View File

@@ -92,9 +92,11 @@ const MySql = (
<div className="flex flex-row h-fit w-fit gap-2">
<Badge
variant={
data?.server?.serverStatus === "active"
!data?.serverId
? "default"
: "destructive"
: data?.server?.serverStatus === "active"
? "default"
: "destructive"
}
>
{data?.server?.name || "Dokploy Server"}

View File

@@ -93,9 +93,11 @@ const Postgresql = (
<div className="flex flex-row h-fit w-fit gap-2">
<Badge
variant={
data?.server?.serverStatus === "active"
!data?.serverId
? "default"
: "destructive"
: data?.server?.serverStatus === "active"
? "default"
: "destructive"
}
>
{data?.server?.name || "Dokploy Server"}

View File

@@ -92,9 +92,11 @@ const Redis = (
<div className="flex flex-row h-fit w-fit gap-2">
<Badge
variant={
data?.server?.serverStatus === "active"
!data?.serverId
? "default"
: "destructive"
: data?.server?.serverStatus === "active"
? "default"
: "destructive"
}
>
{data?.server?.name || "Dokploy Server"}

View File

@@ -45,6 +45,13 @@ export async function getServerSideProps(
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: {

View File

@@ -55,6 +55,8 @@ export async function getServerSideProps(
transformer: superjson,
});
await helpers.auth.get.prefetch();
await helpers.settings.isCloud.prefetch();
return {

View File

@@ -49,7 +49,7 @@ export async function getServerSideProps(
},
transformer: superjson,
});
await helpers.auth.get.prefetch();
await helpers.settings.isCloud.prefetch();
return {

View File

@@ -1,9 +1,12 @@
import { ShowNodes } from "@/components/dashboard/settings/cluster/nodes/show-nodes";
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 (
@@ -25,6 +28,7 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
if (IS_CLOUD) {
return {
redirect: {
@@ -42,8 +46,22 @@ export async function getServerSideProps(
},
};
}
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
},
transformer: superjson,
});
await helpers.auth.get.prefetch();
return {
props: {},
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -50,7 +50,7 @@ export async function getServerSideProps(
},
transformer: superjson,
});
await helpers.auth.get.prefetch();
await helpers.settings.isCloud.prefetch();
return {

View File

@@ -49,7 +49,7 @@ export async function getServerSideProps(
},
transformer: superjson,
});
await helpers.auth.get.prefetch();
try {
await helpers.project.all.prefetch();
await helpers.settings.isCloud.prefetch();

View File

@@ -50,7 +50,7 @@ export async function getServerSideProps(
},
transformer: superjson,
});
await helpers.auth.get.prefetch();
await helpers.settings.isCloud.prefetch();
return {

View File

@@ -56,6 +56,13 @@ export async function getServerSideProps(
});
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: {

View File

@@ -49,7 +49,7 @@ export async function getServerSideProps(
},
transformer: superjson,
});
await helpers.auth.get.prefetch();
await helpers.settings.isCloud.prefetch();
return {

View File

@@ -2,9 +2,12 @@ import { WebDomain } from "@/components/dashboard/settings/web-domain";
import { WebServer } from "@/components/dashboard/settings/web-server";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root";
import { IS_CLOUD, validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
return (
@@ -27,6 +30,7 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
if (IS_CLOUD) {
return {
redirect: {
@@ -35,7 +39,7 @@ export async function getServerSideProps(
},
};
}
const { user } = await validateRequest(ctx.req, ctx.res);
const { user, session } = await validateRequest(ctx.req, ctx.res);
if (!user) {
return {
redirect: {
@@ -53,7 +57,22 @@ export async function getServerSideProps(
};
}
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
},
transformer: superjson,
});
await helpers.auth.get.prefetch();
return {
props: {},
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -58,7 +58,7 @@ export async function getServerSideProps(
},
transformer: superjson,
});
await helpers.auth.get.prefetch();
await helpers.settings.isCloud.prefetch();
return {

View File

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

View File

@@ -50,7 +50,7 @@ export async function getServerSideProps(
},
transformer: superjson,
});
await helpers.auth.get.prefetch();
await helpers.settings.isCloud.prefetch();
return {

View File

@@ -1,5 +1,6 @@
import { Login2FA } from "@/components/auth/login-2fa";
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
import { AlertBlock } from "@/components/shared/alert-block";
import { Logo } from "@/components/shared/logo";
import { Button } from "@/components/ui/button";
import {
@@ -63,7 +64,8 @@ export default function Home({ IS_CLOUD }: Props) {
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: {
@@ -115,6 +117,12 @@ export default function Home({ IS_CLOUD }: Props) {
</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}>
@@ -170,12 +178,14 @@ export default function Home({ IS_CLOUD }: Props) {
<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">

View File

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

View File

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

View File

@@ -17,6 +17,7 @@ import {
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";
@@ -194,7 +195,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
};
}
const authR = await db?.query.auth.findFirst({
const authR = await db.query.auth.findFirst({
where: eq(auth.resetPasswordToken, token),
});

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

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

View File

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

View File

@@ -4,12 +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,
@@ -19,6 +20,7 @@ import {
getUserByToken,
lucia,
luciaToken,
sendDiscordNotification,
sendEmailNotification,
updateAuthById,
validateRequest,
@@ -53,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",
@@ -60,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
@@ -74,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",
@@ -106,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,
@@ -126,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,
});
}
@@ -151,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),
}),
@@ -183,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);
}),
@@ -236,9 +251,6 @@ export const authRouter = createTRPCRouter({
});
return auth;
}),
verifyToken: protectedProcedure.mutation(async () => {
return true;
}),
sendResetPasswordEmail: publicProcedure
.input(
z.object({
@@ -270,7 +282,7 @@ export const authRouter = createTRPCRouter({
).toISOString(),
});
const email = await sendEmailNotification(
await sendEmailNotification(
{
fromAddress: process.env.SMTP_FROM_ADDRESS || "",
toAddresses: [authR.email],
@@ -283,7 +295,7 @@ export const authRouter = createTRPCRouter({
`
Reset your password by clicking the link below:
The link will expire in 24 hours.
<a href="http://localhost:3000/reset-password?token=${token}">
<a href="${WEBSITE_URL}/reset-password?token=${token}">
Reset Password
</a>
@@ -334,6 +346,113 @@ export const authRouter = createTRPCRouter({
password: bcrypt.hashSync(input.password, 10),
});
return true;
}),
confirmEmail: adminProcedure
.input(
z.object({
confirmationToken: z.string().min(1),
}),
)
.mutation(async ({ ctx, input }) => {
if (!IS_CLOUD) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Functionality not available in cloud version",
});
}
const authR = await db.query.auth.findFirst({
where: eq(auth.confirmationToken, input.confirmationToken),
});
if (!authR || authR.confirmationExpiresAt === null) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Token not found",
});
}
if (authR.confirmationToken !== input.confirmationToken) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Confirmation Token not found",
});
}
const isExpired = isBefore(
new Date(authR.confirmationExpiresAt),
new Date(),
);
if (isExpired) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Confirmation Token expired",
});
}
1;
await updateAuthById(authR.id, {
confirmationToken: null,
confirmationExpiresAt: null,
});
return true;
}),
});
export const sendVerificationEmail = async (authId: string) => {
const token = nanoid();
const result = await updateAuthById(authId, {
confirmationToken: token,
confirmationExpiresAt: new Date(
new Date().getTime() + 24 * 60 * 60 * 1000,
).toISOString(),
});
if (!result) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "User not found",
});
}
await sendEmailNotification(
{
fromAddress: process.env.SMTP_FROM_ADDRESS || "",
toAddresses: [result?.email],
smtpServer: process.env.SMTP_SERVER || "",
smtpPort: Number(process.env.SMTP_PORT),
username: process.env.SMTP_USERNAME || "",
password: process.env.SMTP_PASSWORD || "",
},
"Confirm your email | Dokploy",
`
Welcome to Dokploy!
Please confirm your email by clicking the link below:
<a href="${WEBSITE_URL}/confirm-email?token=${result?.confirmationToken}">
Confirm Email
</a>
`,
);
return true;
};
export const sendDiscordNotificationWelcome = async (newAdmin: Auth) => {
await sendDiscordNotification(
{
webhookUrl: process.env.DISCORD_WEBHOOK_URL || "",
},
{
title: "✅ New User Registered",
color: 0x00ff00,
fields: [
{
name: "Email",
value: newAdmin.email,
inline: true,
},
],
timestamp: newAdmin.createdAt,
footer: {
text: "Dokploy User Registration Notification",
},
},
);
};

View File

@@ -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,

View File

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

View File

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

View File

@@ -11,29 +11,8 @@ import {
updateCompose,
} from "@dokploy/server";
import { type Job, Worker } from "bullmq";
import { myQueue, redisConfig } from "./queueSetup";
type DeployJob =
| {
applicationId: string;
titleLog: string;
descriptionLog: string;
server?: boolean;
type: "deploy" | "redeploy";
applicationType: "application";
serverId?: string;
}
| {
composeId: string;
titleLog: string;
descriptionLog: string;
server?: boolean;
type: "deploy" | "redeploy";
applicationType: "compose";
serverId?: string;
};
export type DeploymentJob = DeployJob;
import type { DeploymentJob } from "./queue-types";
import { redisConfig } from "./redis-connection";
export const deploymentWorker = new Worker(
"deployments",
@@ -114,25 +93,3 @@ export const deploymentWorker = new Worker(
connection: redisConfig,
},
);
export const cleanQueuesByApplication = async (applicationId: string) => {
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
for (const job of jobs) {
if (job?.data?.applicationId === applicationId) {
await job.remove();
console.log(`Removed job ${job.id} for application ${applicationId}`);
}
}
};
export const cleanQueuesByCompose = async (composeId: string) => {
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
for (const job of jobs) {
if (job?.data?.composeId === composeId) {
await job.remove();
console.log(`Removed job ${job.id} for compose ${composeId}`);
}
}
};

View File

@@ -0,0 +1,21 @@
type DeployJob =
| {
applicationId: string;
titleLog: string;
descriptionLog: string;
server?: boolean;
type: "deploy" | "redeploy";
applicationType: "application";
serverId?: string;
}
| {
composeId: string;
titleLog: string;
descriptionLog: string;
server?: boolean;
type: "deploy" | "redeploy";
applicationType: "compose";
serverId?: string;
};
export type DeploymentJob = DeployJob;

View File

@@ -1,8 +1,6 @@
import { type ConnectionOptions, Queue } from "bullmq";
import { Queue } from "bullmq";
import { redisConfig } from "./redis-connection";
export const redisConfig: ConnectionOptions = {
host: process.env.NODE_ENV === "production" ? "dokploy-redis" : "127.0.0.1",
};
const myQueue = new Queue("deployments", {
connection: redisConfig,
});
@@ -21,4 +19,26 @@ myQueue.on("error", (error) => {
}
});
export const cleanQueuesByApplication = async (applicationId: string) => {
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
for (const job of jobs) {
if (job?.data?.applicationId === applicationId) {
await job.remove();
console.log(`Removed job ${job.id} for application ${applicationId}`);
}
}
};
export const cleanQueuesByCompose = async (composeId: string) => {
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
for (const job of jobs) {
if (job?.data?.composeId === composeId) {
await job.remove();
console.log(`Removed job ${job.id} for compose ${composeId}`);
}
}
};
export { myQueue };

View File

@@ -0,0 +1,5 @@
import type { ConnectionOptions } from "bullmq";
export const redisConfig: ConnectionOptions = {
host: process.env.NODE_ENV === "production" ? "dokploy-redis" : "127.0.0.1",
};

View File

@@ -19,15 +19,12 @@ import { setupDockerContainerLogsWebSocketServer } from "./wss/docker-container-
import { setupDockerContainerTerminalWebSocketServer } from "./wss/docker-container-terminal";
import { setupDockerStatsMonitoringSocketServer } from "./wss/docker-stats";
import { setupDeploymentLogsWebSocketServer } from "./wss/listen-deployment";
import {
getPublicIpWithFallback,
setupTerminalWebSocketServer,
} from "./wss/terminal";
import { setupTerminalWebSocketServer } from "./wss/terminal";
config({ path: ".env" });
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const app = next({ dev, turbopack: dev });
const handle = app.getRequestHandler();
void app.prepare().then(async () => {
try {
@@ -55,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));
@@ -78,18 +74,3 @@ void app.prepare().then(async () => {
console.error("Main Server Error", e);
}
});
async function welcomeServer() {
const ip = await getPublicIpWithFallback();
console.log(
[
"",
"",
"Dokploy server is up and running!",
"Please wait for 15 seconds before opening the browser.",
` http://${ip}:${PORT}`,
"",
"",
].join("\n"),
);
}

View File

@@ -1,5 +1,5 @@
import { findServerById } from "@dokploy/server";
import type { DeploymentJob } from "../queues/deployments-queue";
import type { DeploymentJob } from "../queues/queue-types";
export const deploy = async (jobData: DeploymentJob) => {
try {

View File

@@ -1,7 +1,7 @@
export const WEBSITE_URL =
process.env.NODE_ENV === "development"
? "http://localhost:3000"
: "https://app.dokploy.com";
: process.env.SITE_URL;
const BASE_PRICE_MONTHLY_ID = process.env.BASE_PRICE_MONTHLY_ID || ""; // $4.00

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,40 @@
import type { CSSProperties, FC, ReactNode } from "react";
import { cn } from "@/lib/utils";
interface AnimatedShinyTextProps {
children: ReactNode;
className?: string;
shimmerWidth?: number;
}
const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({
children,
className,
shimmerWidth = 100,
}) => {
return (
<p
style={
{
"--shiny-width": `${shimmerWidth}px`,
} as CSSProperties
}
className={cn(
"mx-auto max-w-md text-neutral-600/70 dark:text-neutral-400/70",
// Shine effect
"animate-shiny-text bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--shiny-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]",
// Shine gradient
"bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80",
className,
)}
>
{children}
</p>
);
};
export default AnimatedShinyText;

View File

@@ -0,0 +1,100 @@
"use client";
import type React from "react";
import { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";
import { motion } from "framer-motion";
type Direction = "TOP" | "LEFT" | "BOTTOM" | "RIGHT";
export function HoverBorderGradient({
children,
containerClassName,
className,
as: Tag = "button",
duration = 1,
clockwise = true,
...props
}: React.PropsWithChildren<
{
as?: React.ElementType;
containerClassName?: string;
className?: string;
duration?: number;
clockwise?: boolean;
} & React.HTMLAttributes<HTMLElement>
>) {
const [hovered, setHovered] = useState<boolean>(false);
const [direction, setDirection] = useState<Direction>("TOP");
const rotateDirection = (currentDirection: Direction): Direction => {
const directions: Direction[] = ["TOP", "LEFT", "BOTTOM", "RIGHT"];
const currentIndex = directions.indexOf(currentDirection);
const nextIndex = clockwise
? (currentIndex - 1 + directions.length) % directions.length
: (currentIndex + 1) % directions.length;
return directions[nextIndex];
};
const movingMap: Record<Direction, string> = {
TOP: "radial-gradient(20.7% 50% at 50% 0%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)",
LEFT: "radial-gradient(16.6% 43.1% at 0% 50%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)",
BOTTOM:
"radial-gradient(20.7% 50% at 50% 100%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)",
RIGHT:
"radial-gradient(16.2% 41.199999999999996% at 100% 50%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)",
};
const highlight =
"radial-gradient(75% 181.15942028985506% at 50% 50%, #3275F8 0%, rgba(255, 255, 255, 0) 100%)";
useEffect(() => {
if (!hovered) {
const interval = setInterval(() => {
setDirection((prevState) => rotateDirection(prevState));
}, duration * 1000);
return () => clearInterval(interval);
}
}, [hovered]);
return (
<Tag
onMouseEnter={(event: React.MouseEvent<HTMLDivElement>) => {
setHovered(true);
}}
onMouseLeave={() => setHovered(false)}
className={cn(
"relative flex rounded-full border content-center bg-black/20 hover:bg-black/10 transition duration-500 dark:bg-white/20 items-center flex-col flex-nowrap gap-10 h-min justify-center overflow-visible p-px decoration-clone w-fit",
containerClassName,
)}
{...props}
>
<div
className={cn(
"w-auto text-white z-10 bg-black px-4 py-2 rounded-[inherit]",
className,
)}
>
{children}
</div>
<motion.div
className={cn(
"flex-none inset-0 overflow-hidden absolute z-0 rounded-[inherit]",
)}
style={{
filter: "blur(2px)",
position: "absolute",
width: "100%",
height: "100%",
}}
initial={{ background: movingMap[direction] }}
animate={{
background: hovered
? [movingMap[direction], highlight]
: movingMap[direction],
}}
transition={{ ease: "linear", duration: duration ?? 1 }}
/>
<div className="bg-black absolute z-1 flex-none inset-[2px] rounded-[100px]" />
</Tag>
);
}

View File

@@ -4,18 +4,33 @@
"features": "Features",
"faqs": "FAQ",
"docs": "Docs",
"pricing": "Pricing",
"support": "Support",
"dashboard": "Dashboard",
"discord": "Discord",
"i18nButtonPlaceholder": "Language",
"i18nEn": "English",
"i18nZh-Hans": "简体中文"
},
"hero": {
"cloud": "Introducing Dokploy Cloud",
"deploy": "Deploy",
"anywhere": "Anywhere",
"with": "with Total Freedom and Ease.",
"des": "Streamline your operations with our all-in-one platform—perfect for managing projects, data, and system health with simplicity and efficiency.",
"featuredIn": "Featured in"
"featuredIn": "Featured in",
"sponsors": {
"title": "Sponsors",
"description": "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.",
"level": {
"hero": "Hero Sponsors",
"premium": "Premium Supporters",
"supporting": "Supporting Members",
"community": "Community Backers",
"organizations": "Organizations",
"individuals": "Individuals"
}
}
},
"primaryFeatures": {
"title": "Comprehensive Control for Your Digital Ecosystem",
@@ -92,5 +107,70 @@
"intro": "https://docs.dokploy.com/get-started/introduction",
"install": "https://docs.dokploy.com/en/docs/core/get-started/introduction"
}
},
"Pricing": {
"swirlyDoodleTitle": "Simple & Affordable,",
"restTitle": "Pricing.",
"description": "Deploy Smarter, Scale Faster Without Breaking the Bank",
"billingCycle": {
"monthly": "Monthly",
"annual": "Annual"
},
"plan": {
"free": {
"title": "Free",
"subTitle": "Open Source",
"section": {
"title": "Dokploy Open Source",
"description": "Manager your own infrastructure installing dokploy ui in your own server."
},
"features": {
"f1": "Complete Flexibility: Install Dokploy UI on your own infrastructure",
"f2": "Unlimited Deployments",
"f3": "Self-hosted Infrastructure",
"f4": "Community Support",
"f5": "Access to Core Features",
"f6": "Dokploy Integration",
"f7": "Basic Backups",
"f8": "Access to All Updates",
"f9": "Unlimited Servers"
},
"go": "Installation"
},
"cloud": {
"title": "Recommended",
"section": {
"title": "Dokploy Plan",
"description": " to manage Dokploy UI infrastructure, we take care of it for you."
},
"servers": "{serverQuantity}Servers (You bring the servers)",
"features": {
"f1": "Managed Hosting: No need to manage your own servers",
"f2": "Priority Support",
"f3": "Future-Proof Features"
},
"go": "Subscribe"
}
},
"faq": {
"title": "Frequently asked questions",
"description": "If you cant find what youre looking for, please send us an email to",
"q1": "How does Dokploy's Open Source plan work?",
"a1": "You can host Dokploy UI on your own infrastructure and you will be responsible for the maintenance and updates.",
"q2": "Do I need to provide my own server for the managed plan?",
"a2": "Yes, in the managed plan, you provide your own server eg(Hetzner, Hostinger, AWS, ETC.) VPS, and we manage the Dokploy UI infrastructure for you.",
"q3": "What happens if I need more than one server?",
"a3": "The first server costs $4.50/month, if you buy more than one it will be $3.50/month per server.",
"q4": "Is there a limit on the number of deployments?",
"a4": "No, there is no limit on the number of deployments in any of the plans.",
"q5": "What happens if I exceed my purchased server limit?",
"a5": "The most recently added servers will be deactivated. You won't be able to create services on inactive servers until they are reactivated.",
"q6": "Do you offer a refunds?",
"a6": "We do not offer refunds. However, you can cancel your subscription at any time. Feel free to try our open-source version for free before making a purchase.",
"q7": "What kind of support do you offer?",
"a7": "We offer community support for the open source version and priority support for paid plans.",
"q8": "Is Dokploy open-source?",
"a8": "Yes, Dokploy is fully open-source. You can contribute or modify it as needed for your projects."
}
}
}

View File

@@ -4,18 +4,33 @@
"features": "特性",
"faqs": "FAQ",
"docs": "文档",
"pricing": "价格",
"support": "赞助",
"dashboard": "控制台",
"discord": "Discord",
"i18nButtonPlaceholder": "语言",
"i18nEn": "English",
"i18nZh-Hans": "简体中文"
},
"hero": {
"cloud": "隆重介绍 Dokploy 云",
"deploy": "部署在",
"anywhere": "任何设施之上",
"with": "",
"des": "以前所未有的简洁和高效提供一站式项目、数据的管理以及系统监控。",
"featuredIn": "发布于"
"featuredIn": "发布于",
"sponsors": {
"title": "赞助名单",
"description": "Dokploy 是由社区成员共同支持的完全免费的开源项目,您的慷慨解囊将帮助我们继续开发和改进 Dokploy。",
"level": {
"hero": "特别赞助",
"premium": "金牌赞助",
"supporting": "银牌赞助",
"community": "铜牌赞助",
"organizations": "组织赞助",
"individuals": "个人赞助"
}
}
},
"primaryFeatures": {
"title": "全面掌控您的基础设施",
@@ -92,5 +107,70 @@
"intro": "https://docs.dokploy.com/cn/docs/core/get-started/introduction",
"install": "https://docs.dokploy.com/cn/docs/core/get-started/introduction"
}
},
"Pricing": {
"swirlyDoodleTitle": "简洁明了的,",
"restTitle": "定价",
"description": "更聪明的部署方式,更快的扩容速度,以及不会让你破产的账单。",
"billingCycle": {
"monthly": "按月",
"annual": "按年"
},
"plan": {
"free": {
"title": "免费",
"subTitle": "开源版本",
"section": {
"title": "部署Dokploy的开源版本",
"description": "自行管理您的基础设施,不收取任何费用"
},
"features": {
"f1": "灵活的架构,您可以单独部署 Dokploy 管理端",
"f2": "无限数量的部署",
"f3": "自维护的基础设施",
"f4": "来自社区的有限支持",
"f5": "所有功能可用",
"f6": "Dokploy 集成",
"f7": "基础备份服务",
"f8": "跟随开源版本更新",
"f9": "无限服务器数量"
},
"go": "立即开始"
},
"cloud": {
"title": "推荐(年付更优惠)",
"section": {
"title": "Dokploy 云",
"description": "使用我们的云服务,一站式管理您所有的部署。"
},
"servers": "{serverQuantity}台受控服务器",
"features": {
"f1": "由 Dokploy 云提供支持的独立控制面板",
"f2": "优先技术支持",
"f3": "优先体验先行功能"
},
"go": "订阅"
}
},
"faq": {
"title": "常见问题",
"description": "如果您的问题不在以下列表,请随时向我们致信",
"q1": "Dokploy 的开源版本是什么?",
"a1": "您可以免费安装 Dokploy 开源版本,并自行负责 Dokploy 的日后维护工作",
"q2": "付费计划还需要自己的服务器吗?",
"a2": "是的在付费计划中您依然需要提供您用于实际运行服务的服务器如阿里云、腾讯云、AWS 等),我们会为您管理 Dokploy 控制面板。",
"q3": "如果我需要管理更多的服务器怎么办?",
"a3": "第一台服务器的费用为 $4.5/月,之后每台的费用是 $3.5/月。",
"q4": "部署服务的数量有限制吗?",
"a4": "不管您如何使用 Dokploy我们都不会限制您部署服务的数量。",
"q5": "如果我意外超过了最大服务器数量怎么办?",
"a5": "最新添加的服务器将被停用,您将不能向其创建服务,直到它们被重新激活。",
"q6": "关于退款服务的政策?",
"a6": "您可以随时取消您的订阅,但请原谅我们无法提供退款服务,您可以无限试用开源版本,然后再购买。",
"q7": "关于技术支持?",
"a7": "付费计划可以得到优先的技术支持,开源版本则是由社区提供技术支持。",
"q8": "Dokploy 开源吗?",
"a8": "是的Dokploy 完全开源,您可以参与贡献或者是 Fork 后自行修改以用于您的私人需求。"
}
}
}

View File

@@ -57,7 +57,7 @@ install_dokploy() {
fi
}
advertise_addr=$(get_ip)
advertise_addr="${ADVERTISE_ADDR:-$(get_ip)}"
docker swarm init --advertise-addr $advertise_addr
@@ -92,6 +92,7 @@ install_dokploy() {
--update-order stop-first \
--constraint 'node.role == manager' \
-e RELEASE_TAG=canary \
-e ADVERTISE_ADDR=$advertise_addr \
dokploy/dokploy:canary
GREEN="\033[0;32m"

View File

@@ -41,7 +41,23 @@ install_dokploy() {
fi
docker swarm leave --force 2>/dev/null
advertise_addr=$(curl -s ifconfig.me)
get_ip() {
# Try to get IPv4
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
if [ -n "$ipv4" ]; then
echo "$ipv4"
else
# Try to get IPv6
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
if [ -n "$ipv6" ]; then
echo "$ipv6"
fi
fi
}
advertise_addr="${ADVERTISE_ADDR:-$(get_ip)}"
docker swarm init --advertise-addr $advertise_addr
@@ -76,6 +92,7 @@ install_dokploy() {
--update-order stop-first \
--constraint 'node.role == manager' \
-e RELEASE_TAG=feature \
-e ADVERTISE_ADDR=$advertise_addr \
dokploy/dokploy:feature
GREEN="\033[0;32m"

View File

@@ -56,7 +56,7 @@ install_dokploy() {
fi
}
advertise_addr=$(get_ip)
advertise_addr="${ADVERTISE_ADDR:-$(get_ip)}"
docker swarm init --advertise-addr $advertise_addr
@@ -90,6 +90,7 @@ install_dokploy() {
--update-parallelism 1 \
--update-order stop-first \
--constraint 'node.role == manager' \
-e ADVERTISE_ADDR=$advertise_addr \
dokploy/dokploy:latest
GREEN="\033[0;32m"

View File

@@ -10,9 +10,6 @@ const config = {
],
prefix: "",
theme: {
// fontFamily: {
// sans: ["var(--font-sans)", ...fontFamily.sans],
// },
fontSize: {
xs: ["0.75rem", { lineHeight: "1rem" }],
sm: ["0.875rem", { lineHeight: "1.5rem" }],
@@ -75,7 +72,6 @@ const config = {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
"4xl": "2rem",
},
fontFamily: {
@@ -84,17 +80,34 @@ const config = {
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
from: {
height: "0",
},
to: {
height: "var(--radix-accordion-content-height)",
},
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
from: {
height: "var(--radix-accordion-content-height)",
},
to: {
height: "0",
},
},
"shiny-text": {
"0%, 90%, 100%": {
"background-position": "calc(-100% - var(--shiny-width)) 0",
},
"30%, 60%": {
"background-position": "calc(100% + var(--shiny-width)) 0",
},
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"shiny-text": "shiny-text 8s infinite",
},
},
},

View File

@@ -1,7 +1,14 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"files": {
"ignore": ["node_modules/**", ".next/**", "drizzle/**", ".docker", "dist"]
"ignore": [
"node_modules/**",
".next/**",
"drizzle/**",
".docker",
"dist",
"packages/server/package.json"
]
},
"organizeImports": {
"enabled": true

View File

@@ -8,6 +8,7 @@
"dokploy:build": "pnpm --filter=dokploy run build",
"dokploy:start": "pnpm --filter=dokploy run start",
"test": "pnpm --filter=dokploy run test",
"server:script": "pnpm --filter=server run switch:dev",
"server:dev": "pnpm --filter=server run dev",
"server:build": "pnpm --filter=server run build",
"docker:build:canary": "./apps/dokploy/docker/build.sh canary",

View File

@@ -1,28 +1,28 @@
// import { build } from "esbuild";
// import alias from "esbuild-plugin-alias";
// import path from "node:path";
// import { fileURLToPath } from "node:url";
// const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file
// const __dirname = path.dirname(__filename);
import path from "node:path";
import { fileURLToPath } from "node:url";
import { build } from "esbuild";
import alias from "esbuild-plugin-alias";
const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file
const __dirname = path.dirname(__filename);
// build({
// entryPoints: ["./src/**/*.ts"],
// // outfile: "./dist/index.js",
// outdir: "./dist",
// bundle: true,
// minify: false,
// platform: "node",
// target: "esnext",
// format: "esm",
// plugins: [
// alias({
// "@/server": path.resolve(__dirname, "src"),
// }),
// ],
// packages: "external",
// // Opcional: si deseas emitir declaraciones de tipos con esbuild-plugin-dts
// })
// .then(() => {
// console.log("Build successful");
// })
// .catch(() => process.exit(1));
build({
entryPoints: ["./src/**/*.ts"],
// outfile: "./dist/index.js",
outdir: "./dist",
bundle: true,
minify: false,
platform: "node",
target: "esnext",
format: "esm",
plugins: [
alias({
"@dokploy/server": path.resolve(__dirname, "src"),
}),
],
packages: "external",
// Opcional: si deseas emitir declaraciones de tipos con esbuild-plugin-dts
})
.then(() => {
console.log("Build successful");
})
.catch(() => process.exit(1));

View File

@@ -1,74 +1,91 @@
{
"name": "@dokploy/server",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"scripts": {
"build": "rm -rf ./dist && tsc --project tsconfig.server.json && tsc-alias -p tsconfig.server.json",
"build:types": "tsc --emitDeclarationOnly --experimenta-dts",
"dev": "rm -rf ./dist && pnpm esbuild && tsc --emitDeclarationOnly --outDir dist -p tsconfig.server.json",
"esbuild": "tsx ./esbuild.config.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"rotating-file-stream": "3.2.3",
"@faker-js/faker": "^8.4.1",
"@lucia-auth/adapter-drizzle": "1.0.7",
"@octokit/auth-app": "^6.0.4",
"@react-email/components": "^0.0.21",
"@trpc/server": "^10.43.6",
"adm-zip": "^0.5.14",
"bcrypt": "5.1.1",
"bl": "6.0.11",
"boxen": "^7.1.1",
"date-fns": "3.6.0",
"dockerode": "4.0.2",
"dotenv": "16.4.5",
"drizzle-orm": "^0.30.8",
"drizzle-zod": "0.5.1",
"hi-base32": "^0.5.1",
"js-yaml": "4.1.0",
"lodash": "4.17.21",
"lucia": "^3.0.1",
"nanoid": "3",
"node-os-utils": "1.3.7",
"node-pty": "1.0.0",
"node-schedule": "2.1.1",
"nodemailer": "6.9.14",
"octokit": "3.1.2",
"otpauth": "^9.2.3",
"postgres": "3.4.4",
"public-ip": "6.0.2",
"qrcode": "^1.5.3",
"react": "18.2.0",
"react-dom": "18.2.0",
"slugify": "^1.6.6",
"ws": "8.16.0",
"zod": "^3.23.4",
"ssh2": "1.15.0"
},
"devDependencies": {
"tailwindcss": "^3.4.1",
"tsx": "^4.7.1",
"tsc-alias": "1.8.10",
"@types/adm-zip": "^0.5.5",
"@types/bcrypt": "5.0.2",
"@types/dockerode": "3.3.23",
"@types/js-yaml": "4.0.9",
"@types/lodash": "4.17.4",
"@types/node": "^18.17.0",
"@types/node-os-utils": "1.3.4",
"@types/node-schedule": "2.1.6",
"@types/nodemailer": "^6.4.15",
"@types/qrcode": "^1.5.5",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@types/ws": "8.5.10",
"drizzle-kit": "^0.21.1",
"esbuild": "0.20.2",
"postcss": "^8.4.31",
"typescript": "^5.4.2",
"@types/ssh2": "1.15.1"
}
}
"name": "@dokploy/server",
"version": "1.0.0",
"main": "./src/index.ts",
"type": "module",
"exports": {
".": "./src/index.ts",
"./db": {
"import": "./src/db/index.ts",
"require": "./dist/db/index.cjs.js"
},
"./setup/*": {
"import": "./src/setup/*.ts",
"require": "./dist/setup/index.cjs.js"
},
"./constants": {
"import": "./src/constants/index.ts",
"require": "./dist/constants.cjs.js"
}
},
"scripts": {
"build": "npm run switch:prod && rm -rf ./dist && tsc --project tsconfig.server.json && tsc-alias -p tsconfig.server.json",
"build:types": "tsc --emitDeclarationOnly --experimenta-dts",
"switch:dev": "node scripts/switchToSrc.js",
"switch:prod": "node scripts/switchToDist.js",
"dev": "rm -rf ./dist && pnpm esbuild && tsc --emitDeclarationOnly --outDir dist -p tsconfig.server.json",
"esbuild": "tsx ./esbuild.config.ts && tsc --project tsconfig.server.json --emitDeclarationOnly ",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"rotating-file-stream": "3.2.3",
"@faker-js/faker": "^8.4.1",
"@lucia-auth/adapter-drizzle": "1.0.7",
"@octokit/auth-app": "^6.0.4",
"@react-email/components": "^0.0.21",
"@trpc/server": "^10.43.6",
"adm-zip": "^0.5.14",
"bcrypt": "5.1.1",
"bl": "6.0.11",
"boxen": "^7.1.1",
"date-fns": "3.6.0",
"dockerode": "4.0.2",
"dotenv": "16.4.5",
"drizzle-orm": "^0.30.8",
"drizzle-zod": "0.5.1",
"hi-base32": "^0.5.1",
"js-yaml": "4.1.0",
"lodash": "4.17.21",
"lucia": "^3.0.1",
"nanoid": "3",
"node-os-utils": "1.3.7",
"node-pty": "1.0.0",
"node-schedule": "2.1.1",
"nodemailer": "6.9.14",
"octokit": "3.1.2",
"otpauth": "^9.2.3",
"postgres": "3.4.4",
"public-ip": "6.0.2",
"qrcode": "^1.5.3",
"react": "18.2.0",
"react-dom": "18.2.0",
"slugify": "^1.6.6",
"ws": "8.16.0",
"zod": "^3.23.4",
"ssh2": "1.15.0"
},
"devDependencies": {
"esbuild-plugin-alias": "0.2.1",
"tailwindcss": "^3.4.1",
"tsx": "^4.7.1",
"tsc-alias": "1.8.10",
"@types/adm-zip": "^0.5.5",
"@types/bcrypt": "5.0.2",
"@types/dockerode": "3.3.23",
"@types/js-yaml": "4.0.9",
"@types/lodash": "4.17.4",
"@types/node": "^18.17.0",
"@types/node-os-utils": "1.3.4",
"@types/node-schedule": "2.1.6",
"@types/nodemailer": "^6.4.15",
"@types/qrcode": "^1.5.5",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@types/ws": "8.5.10",
"drizzle-kit": "^0.21.1",
"esbuild": "0.20.2",
"postcss": "^8.4.31",
"typescript": "^5.4.2",
"@types/ssh2": "1.15.1"
}
}

View File

@@ -0,0 +1,41 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packagePath = path.resolve(__dirname, "../package.json");
const pkg = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
pkg.main = "./dist/index.js";
pkg.exports = {
".": {
import: "./dist/index.js",
require: "./dist/index.cjs.js",
},
"./db": {
import: "./dist/db/index.js",
require: "./dist/db/index.cjs.js",
},
"./*": {
import: "./dist/*",
require: "./dist/*.cjs",
},
"./dist": {
import: "./dist/index.js",
require: "./dist/index.cjs.js",
},
"./dist/db": {
import: "./dist/db/index.js",
require: "./dist/db/index.cjs.js",
},
"./dist/db/schema": {
import: "./dist/db/schema/index.js",
require: "./dist/db/schema/index.cjs.js",
},
};
fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2));
console.log("Switched exports to use dist for production");

View File

@@ -0,0 +1,34 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packagePath = path.resolve(__dirname, "../package.json");
// Leer el archivo package.json
const pkg = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
pkg.main = "./src/index.ts";
// Modificar los exports
pkg.exports = {
".": "./src/index.ts",
"./db": {
import: "./src/db/index.ts",
require: "./dist/db/index.cjs.js",
},
"./setup/*": {
import: "./src/setup/*.ts",
require: "./dist/setup/index.cjs.js",
},
"./constants": {
import: "./src/constants/index.ts",
require: "./dist/constants.cjs.js",
},
};
// Guardar los cambios en package.json
fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2));
console.log("Switched exports to use src for development");

View File

@@ -1,7 +1,7 @@
import { webcrypto } from "node:crypto";
import type { IncomingMessage, ServerResponse } from "node:http";
import { findAdminByAuthId } from "@/server/services/admin";
import { findUserByAuthId } from "@/server/services/user";
import { findAdminByAuthId } from "@dokploy/server/services/admin";
import { findUserByAuthId } from "@dokploy/server/services/user";
import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle";
import { TimeSpan } from "lucia";
import { Lucia } from "lucia/dist/core.js";

View File

@@ -50,6 +50,8 @@ export const auth = pgTable("auth", {
.$defaultFn(() => new Date().toISOString()),
resetPasswordToken: text("resetPasswordToken"),
resetPasswordExpiresAt: text("resetPasswordExpiresAt"),
confirmationToken: text("confirmationToken"),
confirmationExpiresAt: text("confirmationExpiresAt"),
});
export const authRelations = relations(auth, ({ many }) => ({

View File

@@ -1,11 +1,11 @@
import { randomBytes } from "node:crypto";
import { db } from "@/server/db";
import { db } from "@dokploy/server/db";
import {
admins,
type apiCreateUserInvitation,
auth,
users,
} from "@/server/db/schema";
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt";
import { eq } from "drizzle-orm";
@@ -20,7 +20,7 @@ export const createInvitation = async (
const result = await tx
.insert(auth)
.values({
email: input.email,
email: input.email.toLowerCase(),
rol: "user",
password: bcrypt.hashSync("01231203012312", 10),
})

View File

@@ -1,38 +1,41 @@
import { docker } from "@/server/constants";
import { db } from "@/server/db";
import { type apiCreateApplication, applications } from "@/server/db/schema";
import { generateAppName } from "@/server/db/schema";
import { getAdvancedStats } from "@/server/monitoring/utilts";
import { generatePassword } from "@/server/templates/utils";
import { docker } from "@dokploy/server/constants";
import { db } from "@dokploy/server/db";
import {
type apiCreateApplication,
applications,
} from "@dokploy/server/db/schema";
import { generateAppName } from "@dokploy/server/db/schema";
import { getAdvancedStats } from "@dokploy/server/monitoring/utilts";
import { generatePassword } from "@dokploy/server/templates/utils";
import {
buildApplication,
getBuildCommand,
mechanizeDockerContainer,
} from "@/server/utils/builders";
import { sendBuildErrorNotifications } from "@/server/utils/notifications/build-error";
import { sendBuildSuccessNotifications } from "@/server/utils/notifications/build-success";
import { execAsyncRemote } from "@/server/utils/process/execAsync";
} from "@dokploy/server/utils/builders";
import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error";
import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import {
cloneBitbucketRepository,
getBitbucketCloneCommand,
} from "@/server/utils/providers/bitbucket";
} from "@dokploy/server/utils/providers/bitbucket";
import {
buildDocker,
buildRemoteDocker,
} from "@/server/utils/providers/docker";
} from "@dokploy/server/utils/providers/docker";
import {
cloneGitRepository,
getCustomGitCloneCommand,
} from "@/server/utils/providers/git";
} from "@dokploy/server/utils/providers/git";
import {
cloneGithubRepository,
getGithubCloneCommand,
} from "@/server/utils/providers/github";
} from "@dokploy/server/utils/providers/github";
import {
cloneGitlabRepository,
getGitlabCloneCommand,
} from "@/server/utils/providers/gitlab";
import { createTraefikConfig } from "@/server/utils/traefik/application";
} from "@dokploy/server/utils/providers/gitlab";
import { createTraefikConfig } from "@dokploy/server/utils/traefik/application";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { encodeBase64 } from "../utils/docker/utils";

View File

@@ -1,13 +1,13 @@
import { randomBytes } from "node:crypto";
import { db } from "@/server/db";
import { db } from "@dokploy/server/db";
import {
admins,
type apiCreateAdmin,
type apiCreateUser,
auth,
users,
} from "@/server/db/schema";
import { getPublicIpWithFallback } from "@/server/wss/terminal";
} from "@dokploy/server/db/schema";
import { getPublicIpWithFallback } from "@dokploy/server/wss/terminal";
import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt";
import { eq } from "drizzle-orm";
@@ -24,7 +24,7 @@ export const createAdmin = async (input: typeof apiCreateAdmin._type) => {
const newAuth = await tx
.insert(auth)
.values({
email: input.email,
email: input.email.toLowerCase(),
password: hashedPassword,
rol: "admin",
})
@@ -43,7 +43,8 @@ export const createAdmin = async (input: typeof apiCreateAdmin._type) => {
.values({
authId: newAuth.id,
...(!IS_CLOUD && {
serverIp: await getPublicIpWithFallback(),
serverIp:
process.env.ADVERTISE_ADDR || (await getPublicIpWithFallback()),
}),
})
.returning();
@@ -92,7 +93,7 @@ export const findAuthByEmail = async (email: string) => {
if (!result) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Auth not found",
message: "User not found",
});
}
return result;

View File

@@ -1,5 +1,5 @@
import { db } from "@/server/db";
import { type apiCreateBackup, backups } from "@/server/db/schema";
import { db } from "@dokploy/server/db";
import { type apiCreateBackup, backups } from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";

View File

@@ -1,10 +1,10 @@
import { db } from "@/server/db";
import { db } from "@dokploy/server/db";
import {
type apiCreateBitbucket,
type apiUpdateBitbucket,
bitbucket,
gitProvider,
} from "@/server/db/schema";
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";

View File

@@ -1,9 +1,12 @@
import fs from "node:fs";
import path from "node:path";
import { paths } from "@/server/constants";
import { db } from "@/server/db";
import { type apiCreateCertificate, certificates } from "@/server/db/schema";
import { removeDirectoryIfExistsContent } from "@/server/utils/filesystem/directory";
import { paths } from "@dokploy/server/constants";
import { db } from "@dokploy/server/db";
import {
type apiCreateCertificate,
certificates,
} from "@dokploy/server/db/schema";
import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { dump } from "js-yaml";

View File

@@ -1,44 +1,47 @@
import { join } from "node:path";
import { paths } from "@/server/constants";
import { db } from "@/server/db";
import { type apiCreateCompose, compose } from "@/server/db/schema";
import { generateAppName } from "@/server/db/schema";
import { generatePassword } from "@/server/templates/utils";
import { paths } from "@dokploy/server/constants";
import { db } from "@dokploy/server/db";
import { type apiCreateCompose, compose } from "@dokploy/server/db/schema";
import { generateAppName } from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates/utils";
import {
buildCompose,
getBuildComposeCommand,
} from "@/server/utils/builders/compose";
import { randomizeSpecificationFile } from "@/server/utils/docker/compose";
} from "@dokploy/server/utils/builders/compose";
import { randomizeSpecificationFile } from "@dokploy/server/utils/docker/compose";
import {
cloneCompose,
cloneComposeRemote,
loadDockerCompose,
loadDockerComposeRemote,
} from "@/server/utils/docker/domain";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { sendBuildErrorNotifications } from "@/server/utils/notifications/build-error";
import { sendBuildSuccessNotifications } from "@/server/utils/notifications/build-success";
import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync";
} from "@dokploy/server/utils/docker/domain";
import type { ComposeSpecification } from "@dokploy/server/utils/docker/types";
import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error";
import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success";
import {
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
import {
cloneBitbucketRepository,
getBitbucketCloneCommand,
} from "@/server/utils/providers/bitbucket";
} from "@dokploy/server/utils/providers/bitbucket";
import {
cloneGitRepository,
getCustomGitCloneCommand,
} from "@/server/utils/providers/git";
} from "@dokploy/server/utils/providers/git";
import {
cloneGithubRepository,
getGithubCloneCommand,
} from "@/server/utils/providers/github";
} from "@dokploy/server/utils/providers/github";
import {
cloneGitlabRepository,
getGitlabCloneCommand,
} from "@/server/utils/providers/gitlab";
} from "@dokploy/server/utils/providers/gitlab";
import {
createComposeFile,
getCreateComposeFileCommand,
} from "@/server/utils/providers/raw";
} from "@dokploy/server/utils/providers/raw";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { encodeBase64 } from "../utils/docker/utils";

View File

@@ -1,14 +1,14 @@
import { existsSync, promises as fsPromises } from "node:fs";
import path from "node:path";
import { paths } from "@/server/constants";
import { db } from "@/server/db";
import { paths } from "@dokploy/server/constants";
import { db } from "@dokploy/server/db";
import {
type apiCreateDeployment,
type apiCreateDeploymentCompose,
type apiCreateDeploymentServer,
deployments,
} from "@/server/db/schema";
import { removeDirectoryIfExistsContent } from "@/server/utils/filesystem/directory";
} from "@dokploy/server/db/schema";
import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory";
import { TRPCError } from "@trpc/server";
import { format } from "date-fns";
import { desc, eq } from "drizzle-orm";
@@ -20,7 +20,7 @@ import {
import { type Compose, findComposeById, updateCompose } from "./compose";
import { type Server, findServerById } from "./server";
import { execAsyncRemote } from "@/server/utils/process/execAsync";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
export type Deployment = typeof deployments.$inferSelect;

View File

@@ -1,5 +1,8 @@
import { db } from "@/server/db";
import { type apiCreateDestination, destinations } from "@/server/db/schema";
import { db } from "@dokploy/server/db";
import {
type apiCreateDestination,
destinations,
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { and, eq } from "drizzle-orm";

View File

@@ -1,4 +1,7 @@
import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync";
import {
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
export const getContainers = async (serverId?: string | null) => {
try {

View File

@@ -1,6 +1,6 @@
import { db } from "@/server/db";
import { generateRandomDomain } from "@/server/templates/utils";
import { manageDomain } from "@/server/utils/traefik/domain";
import { db } from "@dokploy/server/db";
import { generateRandomDomain } from "@dokploy/server/templates/utils";
import { manageDomain } from "@dokploy/server/utils/traefik/domain";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { type apiCreateDomain, domains } from "../db/schema";

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