mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
Merge branch 'canary' into copilot/fix-azure-openai-endpoint
This commit is contained in:
6
.github/workflows/pull-request.yml
vendored
6
.github/workflows/pull-request.yml
vendored
@@ -24,14 +24,14 @@ jobs:
|
||||
- name: Install Nixpacks
|
||||
if: matrix.job == 'test'
|
||||
run: |
|
||||
export NIXPACKS_VERSION=1.39.0
|
||||
export NIXPACKS_VERSION=1.41.0
|
||||
curl -sSL https://nixpacks.com/install.sh | bash
|
||||
echo "Nixpacks installed $NIXPACKS_VERSION"
|
||||
|
||||
|
||||
- name: Install Railpack
|
||||
if: matrix.job == 'test'
|
||||
run: |
|
||||
export RAILPACK_VERSION=0.15.0
|
||||
export RAILPACK_VERSION=0.15.4
|
||||
curl -sSL https://railpack.com/install.sh | bash
|
||||
echo "Railpack installed $RAILPACK_VERSION"
|
||||
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -43,4 +43,7 @@ yarn-error.log*
|
||||
*.pem
|
||||
|
||||
|
||||
.db
|
||||
.db
|
||||
|
||||
# Development environment
|
||||
.devcontainer
|
||||
@@ -148,7 +148,7 @@ curl -sSL https://railpack.com/install.sh | sh
|
||||
|
||||
```bash
|
||||
# Install Buildpacks
|
||||
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.35.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
||||
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.39.1/pack-v0.39.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
||||
```
|
||||
|
||||
## Pull Request
|
||||
|
||||
@@ -51,18 +51,18 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh --ver
|
||||
# Install Nixpacks and tsx
|
||||
# | VERBOSE=1 VERSION=1.21.0 bash
|
||||
|
||||
ARG NIXPACKS_VERSION=1.39.0
|
||||
ARG NIXPACKS_VERSION=1.41.0
|
||||
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
||||
&& chmod +x install.sh \
|
||||
&& ./install.sh \
|
||||
&& pnpm install -g tsx
|
||||
|
||||
# Install Railpack
|
||||
ARG RAILPACK_VERSION=0.2.2
|
||||
ARG RAILPACK_VERSION=0.15.4
|
||||
RUN curl -sSL https://railpack.com/install.sh | bash
|
||||
|
||||
# Install buildpacks
|
||||
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
|
||||
COPY --from=buildpacksio/pack:0.39.1 /usr/local/bin/pack /usr/local/bin/pack
|
||||
|
||||
EXPOSE 3000
|
||||
CMD [ "pnpm", "start" ]
|
||||
|
||||
@@ -60,4 +60,4 @@ RUN curl https://rclone.org/install.sh | bash
|
||||
RUN pnpm install -g tsx
|
||||
|
||||
EXPOSE 3000
|
||||
CMD [ "pnpm", "start" ]
|
||||
CMD [ "pnpm", "start" ]
|
||||
|
||||
@@ -35,4 +35,4 @@ COPY --from=build /prod/schedules/dist ./dist
|
||||
COPY --from=build /prod/schedules/package.json ./package.json
|
||||
COPY --from=build /prod/schedules/node_modules ./node_modules
|
||||
|
||||
CMD HOSTNAME=0.0.0.0 && pnpm start
|
||||
CMD HOSTNAME=0.0.0.0 && pnpm start
|
||||
|
||||
@@ -35,4 +35,4 @@ COPY --from=build /prod/api/dist ./dist
|
||||
COPY --from=build /prod/api/package.json ./package.json
|
||||
COPY --from=build /prod/api/node_modules ./node_modules
|
||||
|
||||
CMD HOSTNAME=0.0.0.0 && pnpm start
|
||||
CMD HOSTNAME=0.0.0.0 && pnpm start
|
||||
|
||||
60
README.md
60
README.md
@@ -68,53 +68,21 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
|
||||
[Github Sponsors](https://github.com/sponsors/Siumauricio)
|
||||
|
||||
<!-- Hero Sponsors 🎖 -->
|
||||
## Sponsors
|
||||
|
||||
<!-- Add Hero Sponsors here -->
|
||||
|
||||
### Hero Sponsors 🎖
|
||||
|
||||
<div>
|
||||
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy"><img src=".github/sponsors/hostinger.jpg" alt="Hostinger" width="300"/></a>
|
||||
<a href="https://www.lxaer.com/?ref=dokploy"><img src=".github/sponsors/lxaer.png" alt="LX Aer" width="100"/></a>
|
||||
<a href="https://www.lambdatest.com/?utm_source=dokploy&utm_medium=sponsor" target="_blank">
|
||||
<img src="https://www.lambdatest.com/blue-logo.png" width="450" height="100" />
|
||||
</a>
|
||||
<a href="https://awesome.tools/" target="_blank">
|
||||
<img src=".github/sponsors/awesome.png" width="200" height="150" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Premium Supporters 🥇 -->
|
||||
|
||||
<!-- Add Premium Supporters here -->
|
||||
|
||||
### Premium Supporters 🥇
|
||||
|
||||
<div>
|
||||
<a href="https://supafort.com/?ref=dokploy"><img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" width="300"/></a>
|
||||
<a href="https://agentdock.ai/?ref=dokploy"><img src=".github/sponsors/agentdock.png" alt="agentdock.ai" width="100"/></a>
|
||||
</div>
|
||||
|
||||
<!-- Elite Contributors 🥈 -->
|
||||
|
||||
<!-- Add Elite Contributors here -->
|
||||
|
||||
### Elite Contributors 🥈
|
||||
|
||||
<div>
|
||||
<a href="https://americancloud.com/?ref=dokploy"><img src=".github/sponsors/american-cloud.png" alt="AmericanCloud" width="300"/></a>
|
||||
<a href="https://tolgee.io/?utm_source=github_dokploy&utm_medium=banner&utm_campaign=dokploy"><img src="https://dokploy.com/tolgee-logo.png" alt="Tolgee" width="100"/></a>
|
||||
</div>
|
||||
|
||||
### Supporting Members 🥉
|
||||
|
||||
<div>
|
||||
|
||||
<a href="https://cloudblast.io/?ref=dokploy"><img src="https://cloudblast.io/img/logo-icon.193cf13e.svg" width="250px" alt="Cloudblast.io"/></a>
|
||||
|
||||
<a href="https://synexa.ai/?ref=dokploy"><img src=".github/sponsors/synexa.png" width="65px" alt="Synexa"/></a>
|
||||
</div>
|
||||
| Sponsor | Logo | Supporter Level |
|
||||
|---------|:----:|----------------|
|
||||
| [Hostinger](https://www.hostinger.com/vps-hosting?ref=dokploy) | <img src=".github/sponsors/hostinger.jpg" alt="Hostinger" width="200"/> | 🎖 Hero Sponsor |
|
||||
| [LX Aer](https://www.lxaer.com/?ref=dokploy) | <img src=".github/sponsors/lxaer.png" alt="LX Aer" width="100"/> | 🎖 Hero Sponsor |
|
||||
| [LinkDR](https://linkdr.com/?ref=dokploy) | <img src="https://dokploy.com/linkdr-logo.svg" alt="LinkDR" width="100"/> | 🎖 Hero Sponsor |
|
||||
| [LambdaTest](https://www.lambdatest.com/?utm_source=dokploy&utm_medium=sponsor) | <img src="https://www.lambdatest.com/blue-logo.png" alt="LambdaTest" width="200"/> | 🎖 Hero Sponsor |
|
||||
| [Awesome Tools](https://awesome.tools/) | <img src=".github/sponsors/awesome.png" alt="Awesome Tools" width="100"/> | 🎖 Hero Sponsor |
|
||||
| [Supafort](https://supafort.com/?ref=dokploy) | <img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" width="200"/> | 🥇 Premium Supporter |
|
||||
| [Agentdock](https://agentdock.ai/?ref=dokploy) | <img src=".github/sponsors/agentdock.png" alt="agentdock.ai" width="100"/> | 🥇 Premium Supporter |
|
||||
| [AmericanCloud](https://americancloud.com/?ref=dokploy) | <img src=".github/sponsors/american-cloud.png" alt="AmericanCloud" width="200"/> | 🥈 Elite Contributor |
|
||||
| [Tolgee](https://tolgee.io/?utm_source=github_dokploy&utm_medium=banner&utm_campaign=dokploy) | <img src="https://dokploy.com/tolgee-logo.png" alt="Tolgee" width="100"/> | 🥈 Elite Contributor |
|
||||
| [Cloudblast](https://cloudblast.io/?ref=dokploy) | <img src="https://cloudblast.io/img/logo-icon.193cf13e.svg" alt="Cloudblast.io" width="150"/> | 🥉 Supporting Member |
|
||||
| [Synexa](https://synexa.ai/?ref=dokploy) | <img src=".github/sponsors/synexa.png" alt="Synexa" width="100"/> | 🥉 Supporting Member |
|
||||
|
||||
### Community Backers 🤝
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"@dokploy/server": "workspace:*",
|
||||
"@hono/node-server": "^1.14.3",
|
||||
"@hono/zod-validator": "0.3.0",
|
||||
"@nerimity/mimiqueue": "1.2.3",
|
||||
"dotenv": "^16.4.5",
|
||||
"hono": "^4.7.10",
|
||||
"pino": "9.4.0",
|
||||
|
||||
@@ -25,7 +25,7 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
||||
titleLog: z.string().optional(),
|
||||
descriptionLog: z.string().optional(),
|
||||
server: z.boolean().optional(),
|
||||
type: z.enum(["deploy"]),
|
||||
type: z.enum(["deploy", "redeploy"]),
|
||||
applicationType: z.literal("application-preview"),
|
||||
serverId: z.string().min(1),
|
||||
}),
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
deployPreviewApplication,
|
||||
rebuildApplication,
|
||||
rebuildCompose,
|
||||
rebuildPreviewApplication,
|
||||
updateApplicationStatus,
|
||||
updateCompose,
|
||||
updatePreviewDeployment,
|
||||
@@ -54,7 +55,14 @@ export const deploy = async (job: DeployJob) => {
|
||||
previewStatus: "running",
|
||||
});
|
||||
if (job.server) {
|
||||
if (job.type === "deploy") {
|
||||
if (job.type === "redeploy") {
|
||||
await rebuildPreviewApplication({
|
||||
applicationId: job.applicationId,
|
||||
titleLog: job.titleLog || "Rebuild Preview Deployment",
|
||||
descriptionLog: job.descriptionLog || "",
|
||||
previewDeploymentId: job.previewDeploymentId,
|
||||
});
|
||||
} else if (job.type === "deploy") {
|
||||
await deployPreviewApplication({
|
||||
applicationId: job.applicationId,
|
||||
titleLog: job.titleLog || "Preview Deployment",
|
||||
|
||||
@@ -206,4 +206,38 @@ describe("getRegistryTag", () => {
|
||||
expect(result).toBe("docker.io/myuser/repo");
|
||||
});
|
||||
});
|
||||
|
||||
describe("special characters in username", () => {
|
||||
it("should handle Harbor robot account username with $ (e.g. robot$library+dokploy)", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "robot$library+dokploy",
|
||||
});
|
||||
const result = getRegistryTag(registry, "nginx");
|
||||
expect(result).toBe("docker.io/robot$library+dokploy/nginx");
|
||||
});
|
||||
|
||||
it("should handle username with $ and other special characters", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "robot$test+app",
|
||||
});
|
||||
const result = getRegistryTag(registry, "myapp:latest");
|
||||
expect(result).toBe("docker.io/robot$test+app/myapp:latest");
|
||||
});
|
||||
|
||||
it("should handle username with multiple $ symbols", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "user$name$test",
|
||||
});
|
||||
const result = getRegistryTag(registry, "app");
|
||||
expect(result).toBe("docker.io/user$name$test/app");
|
||||
});
|
||||
|
||||
it("should handle username with + and - symbols", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "robot+test-user",
|
||||
});
|
||||
const result = getRegistryTag(registry, "nginx:latest");
|
||||
expect(result).toBe("docker.io/robot+test-user/nginx:latest");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Domain } from "@dokploy/server";
|
||||
import { createDomainLabels } from "@dokploy/server";
|
||||
import { parse, stringify } from "yaml";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parse, stringify } from "yaml";
|
||||
|
||||
/**
|
||||
* Regression tests for Traefik Host rule label format.
|
||||
|
||||
@@ -25,7 +25,7 @@ if (typeof window === "undefined") {
|
||||
}
|
||||
|
||||
const baseApp: ApplicationNested = {
|
||||
railpackVersion: "0.2.2",
|
||||
railpackVersion: "0.15.4",
|
||||
applicationId: "",
|
||||
previewLabels: [],
|
||||
createEnvFile: true,
|
||||
|
||||
184
apps/dokploy/__test__/env/stack-environment.test.ts
vendored
Normal file
184
apps/dokploy/__test__/env/stack-environment.test.ts
vendored
Normal file
@@ -0,0 +1,184 @@
|
||||
import { getEnviromentVariablesObject } from "@dokploy/server/index";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const projectEnv = `
|
||||
ENVIRONMENT=staging
|
||||
DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db
|
||||
PORT=3000
|
||||
`;
|
||||
|
||||
const environmentEnv = `
|
||||
NODE_ENV=development
|
||||
API_URL=https://api.dev.example.com
|
||||
REDIS_URL=redis://localhost:6379
|
||||
DATABASE_NAME=dev_database
|
||||
SECRET_KEY=env-secret-123
|
||||
`;
|
||||
|
||||
describe("getEnviromentVariablesObject with environment variables (Stack compose)", () => {
|
||||
it("resolves environment variables correctly for Stack compose", () => {
|
||||
const serviceEnv = `
|
||||
FOO=\${{environment.NODE_ENV}}
|
||||
BAR=\${{environment.API_URL}}
|
||||
BAZ=test
|
||||
`;
|
||||
|
||||
const result = getEnviromentVariablesObject(
|
||||
serviceEnv,
|
||||
projectEnv,
|
||||
environmentEnv,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
FOO: "development",
|
||||
BAR: "https://api.dev.example.com",
|
||||
BAZ: "test",
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves both project and environment variables for Stack compose", () => {
|
||||
const serviceEnv = `
|
||||
ENVIRONMENT=\${{project.ENVIRONMENT}}
|
||||
NODE_ENV=\${{environment.NODE_ENV}}
|
||||
API_URL=\${{environment.API_URL}}
|
||||
DATABASE_URL=\${{project.DATABASE_URL}}
|
||||
SERVICE_PORT=4000
|
||||
`;
|
||||
|
||||
const result = getEnviromentVariablesObject(
|
||||
serviceEnv,
|
||||
projectEnv,
|
||||
environmentEnv,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
ENVIRONMENT: "staging",
|
||||
NODE_ENV: "development",
|
||||
API_URL: "https://api.dev.example.com",
|
||||
DATABASE_URL: "postgres://postgres:postgres@localhost:5432/project_db",
|
||||
SERVICE_PORT: "4000",
|
||||
});
|
||||
});
|
||||
|
||||
it("handles multiple environment references in single value for Stack compose", () => {
|
||||
const multiRefEnv = `
|
||||
HOST=localhost
|
||||
PORT=5432
|
||||
USERNAME=postgres
|
||||
PASSWORD=secret123
|
||||
`;
|
||||
|
||||
const serviceEnv = `
|
||||
DATABASE_URL=postgresql://\${{environment.USERNAME}}:\${{environment.PASSWORD}}@\${{environment.HOST}}:\${{environment.PORT}}/mydb
|
||||
`;
|
||||
|
||||
const result = getEnviromentVariablesObject(serviceEnv, "", multiRefEnv);
|
||||
|
||||
expect(result).toEqual({
|
||||
DATABASE_URL: "postgresql://postgres:secret123@localhost:5432/mydb",
|
||||
});
|
||||
});
|
||||
|
||||
it("throws error for undefined environment variables in Stack compose", () => {
|
||||
const serviceWithUndefined = `
|
||||
UNDEFINED_VAR=\${{environment.UNDEFINED_VAR}}
|
||||
`;
|
||||
|
||||
expect(() =>
|
||||
getEnviromentVariablesObject(serviceWithUndefined, "", environmentEnv),
|
||||
).toThrow("Invalid environment variable: environment.UNDEFINED_VAR");
|
||||
});
|
||||
|
||||
it("allows service variables to override environment variables in Stack compose", () => {
|
||||
const serviceOverrideEnv = `
|
||||
NODE_ENV=production
|
||||
API_URL=\${{environment.API_URL}}
|
||||
`;
|
||||
|
||||
const result = getEnviromentVariablesObject(
|
||||
serviceOverrideEnv,
|
||||
"",
|
||||
environmentEnv,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
NODE_ENV: "production",
|
||||
API_URL: "https://api.dev.example.com",
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves complex references with project, environment, and service variables for Stack compose", () => {
|
||||
const complexServiceEnv = `
|
||||
FULL_DATABASE_URL=\${{project.DATABASE_URL}}/\${{environment.DATABASE_NAME}}
|
||||
API_ENDPOINT=\${{environment.API_URL}}/\${{project.ENVIRONMENT}}/api
|
||||
SERVICE_NAME=my-service
|
||||
COMPLEX_VAR=\${{SERVICE_NAME}}-\${{environment.NODE_ENV}}-\${{project.ENVIRONMENT}}
|
||||
`;
|
||||
|
||||
const result = getEnviromentVariablesObject(
|
||||
complexServiceEnv,
|
||||
projectEnv,
|
||||
environmentEnv,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
FULL_DATABASE_URL:
|
||||
"postgres://postgres:postgres@localhost:5432/project_db/dev_database",
|
||||
API_ENDPOINT: "https://api.dev.example.com/staging/api",
|
||||
SERVICE_NAME: "my-service",
|
||||
COMPLEX_VAR: "my-service-development-staging",
|
||||
});
|
||||
});
|
||||
|
||||
it("maintains precedence: service > environment > project in Stack compose", () => {
|
||||
const conflictingProjectEnv = `
|
||||
NODE_ENV=production-project
|
||||
API_URL=https://project.api.com
|
||||
DATABASE_NAME=project_db
|
||||
`;
|
||||
|
||||
const conflictingEnvironmentEnv = `
|
||||
NODE_ENV=development-environment
|
||||
API_URL=https://environment.api.com
|
||||
DATABASE_NAME=env_db
|
||||
`;
|
||||
|
||||
const serviceWithConflicts = `
|
||||
NODE_ENV=service-override
|
||||
PROJECT_ENV=\${{project.NODE_ENV}}
|
||||
ENV_VAR=\${{environment.API_URL}}
|
||||
DB_NAME=\${{environment.DATABASE_NAME}}
|
||||
`;
|
||||
|
||||
const result = getEnviromentVariablesObject(
|
||||
serviceWithConflicts,
|
||||
conflictingProjectEnv,
|
||||
conflictingEnvironmentEnv,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
NODE_ENV: "service-override",
|
||||
PROJECT_ENV: "production-project",
|
||||
ENV_VAR: "https://environment.api.com",
|
||||
DB_NAME: "env_db",
|
||||
});
|
||||
});
|
||||
|
||||
it("handles empty environment variables in Stack compose", () => {
|
||||
const serviceWithEmpty = `
|
||||
SERVICE_VAR=test
|
||||
PROJECT_VAR=\${{project.ENVIRONMENT}}
|
||||
`;
|
||||
|
||||
const result = getEnviromentVariablesObject(
|
||||
serviceWithEmpty,
|
||||
projectEnv,
|
||||
"",
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
SERVICE_VAR: "test",
|
||||
PROJECT_VAR: "staging",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,21 +5,27 @@ vi.mock("node:fs", () => ({
|
||||
default: fs,
|
||||
}));
|
||||
|
||||
import type { FileConfig, User } from "@dokploy/server";
|
||||
import type { FileConfig } from "@dokploy/server";
|
||||
import {
|
||||
createDefaultServerTraefikConfig,
|
||||
loadOrCreateConfig,
|
||||
updateServerTraefik,
|
||||
} from "@dokploy/server";
|
||||
import type { webServerSettings } from "@dokploy/server/db/schema";
|
||||
import { beforeEach, expect, test, vi } from "vitest";
|
||||
|
||||
const baseAdmin: User = {
|
||||
type WebServerSettings = typeof webServerSettings.$inferSelect;
|
||||
|
||||
const baseSettings: WebServerSettings = {
|
||||
id: "",
|
||||
https: false,
|
||||
enablePaidFeatures: false,
|
||||
allowImpersonation: false,
|
||||
role: "user",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
certificateType: "none",
|
||||
host: null,
|
||||
serverIp: null,
|
||||
letsEncryptEmail: null,
|
||||
sshPrivateKey: null,
|
||||
enableDockerCleanup: false,
|
||||
logCleanupCron: null,
|
||||
metricsConfig: {
|
||||
containers: {
|
||||
refreshRate: 20,
|
||||
@@ -45,29 +51,8 @@ const baseAdmin: User = {
|
||||
cleanupCacheApplications: false,
|
||||
cleanupCacheOnCompose: false,
|
||||
cleanupCacheOnPreviews: false,
|
||||
createdAt: new Date(),
|
||||
serverIp: null,
|
||||
certificateType: "none",
|
||||
host: null,
|
||||
letsEncryptEmail: null,
|
||||
sshPrivateKey: null,
|
||||
enableDockerCleanup: false,
|
||||
logCleanupCron: null,
|
||||
serversQuantity: 0,
|
||||
stripeCustomerId: "",
|
||||
stripeSubscriptionId: "",
|
||||
banExpires: new Date(),
|
||||
banned: true,
|
||||
banReason: "",
|
||||
email: "",
|
||||
expirationDate: "",
|
||||
id: "",
|
||||
isRegistered: false,
|
||||
createdAt2: new Date().toISOString(),
|
||||
emailVerified: false,
|
||||
image: "",
|
||||
createdAt: null,
|
||||
updatedAt: new Date(),
|
||||
twoFactorEnabled: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -85,7 +70,7 @@ test("Should read the configuration file", () => {
|
||||
test("Should apply redirect-to-https", () => {
|
||||
updateServerTraefik(
|
||||
{
|
||||
...baseAdmin,
|
||||
...baseSettings,
|
||||
https: true,
|
||||
certificateType: "letsencrypt",
|
||||
},
|
||||
@@ -100,7 +85,7 @@ test("Should apply redirect-to-https", () => {
|
||||
});
|
||||
|
||||
test("Should change only host when no certificate", () => {
|
||||
updateServerTraefik(baseAdmin, "example.com");
|
||||
updateServerTraefik(baseSettings, "example.com");
|
||||
|
||||
const config: FileConfig = loadOrCreateConfig("dokploy");
|
||||
|
||||
@@ -110,7 +95,7 @@ test("Should change only host when no certificate", () => {
|
||||
test("Should not touch config without host", () => {
|
||||
const originalConfig: FileConfig = loadOrCreateConfig("dokploy");
|
||||
|
||||
updateServerTraefik(baseAdmin, null);
|
||||
updateServerTraefik(baseSettings, null);
|
||||
|
||||
const config: FileConfig = loadOrCreateConfig("dokploy");
|
||||
|
||||
@@ -119,11 +104,14 @@ test("Should not touch config without host", () => {
|
||||
|
||||
test("Should remove websecure if https rollback to http", () => {
|
||||
updateServerTraefik(
|
||||
{ ...baseAdmin, certificateType: "letsencrypt" },
|
||||
{ ...baseSettings, certificateType: "letsencrypt" },
|
||||
"example.com",
|
||||
);
|
||||
|
||||
updateServerTraefik({ ...baseAdmin, certificateType: "none" }, "example.com");
|
||||
updateServerTraefik(
|
||||
{ ...baseSettings, certificateType: "none" },
|
||||
"example.com",
|
||||
);
|
||||
|
||||
const config: FileConfig = loadOrCreateConfig("dokploy");
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createRouterConfig } from "@dokploy/server";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
const baseApp: ApplicationNested = {
|
||||
railpackVersion: "0.2.2",
|
||||
railpackVersion: "0.15.4",
|
||||
rollbackActive: false,
|
||||
applicationId: "",
|
||||
previewLabels: [],
|
||||
|
||||
@@ -21,7 +21,10 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
createConverter,
|
||||
NumberInputWithSteps,
|
||||
} from "@/components/ui/number-input";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -30,6 +33,23 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const CPU_STEP = 0.25;
|
||||
const MEMORY_STEP_MB = 256;
|
||||
|
||||
const formatNumber = (value: number, decimals = 2): string =>
|
||||
Number.isInteger(value) ? String(value) : value.toFixed(decimals);
|
||||
|
||||
const cpuConverter = createConverter(1_000_000_000, (cpu) =>
|
||||
cpu <= 0 ? "" : `${formatNumber(cpu)} CPU`,
|
||||
);
|
||||
|
||||
const memoryConverter = createConverter(1024 * 1024, (mb) => {
|
||||
if (mb <= 0) return "";
|
||||
return mb >= 1024
|
||||
? `${formatNumber(mb / 1024)} GB`
|
||||
: `${formatNumber(mb)} MB`;
|
||||
});
|
||||
|
||||
const addResourcesSchema = z.object({
|
||||
memoryReservation: z.string().optional(),
|
||||
cpuLimit: z.string().optional(),
|
||||
@@ -51,6 +71,7 @@ interface Props {
|
||||
}
|
||||
|
||||
type AddResources = z.infer<typeof addResourcesSchema>;
|
||||
|
||||
export const ShowResources = ({ id, type }: Props) => {
|
||||
const queryMap = {
|
||||
postgres: () =>
|
||||
@@ -163,16 +184,20 @@ export const ShowResources = ({ id, type }: Props) => {
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Memory hard limit in bytes. Example: 1GB =
|
||||
1073741824 bytes
|
||||
1073741824 bytes. Use +/- buttons to adjust by
|
||||
256 MB.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
<NumberInputWithSteps
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
placeholder="1073741824 (1GB in bytes)"
|
||||
{...field}
|
||||
step={MEMORY_STEP_MB}
|
||||
converter={memoryConverter}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -198,16 +223,20 @@ export const ShowResources = ({ id, type }: Props) => {
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Memory soft limit in bytes. Example: 256MB =
|
||||
268435456 bytes
|
||||
268435456 bytes. Use +/- buttons to adjust by 256
|
||||
MB.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
<NumberInputWithSteps
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
placeholder="268435456 (256MB in bytes)"
|
||||
{...field}
|
||||
step={MEMORY_STEP_MB}
|
||||
converter={memoryConverter}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -234,17 +263,20 @@ export const ShowResources = ({ id, type }: Props) => {
|
||||
<TooltipContent>
|
||||
<p>
|
||||
CPU quota in units of 10^-9 CPUs. Example: 2
|
||||
CPUs = 2000000000
|
||||
CPUs = 2000000000. Use +/- buttons to adjust by
|
||||
0.25 CPU.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
<NumberInputWithSteps
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
placeholder="2000000000 (2 CPUs)"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
step={CPU_STEP}
|
||||
converter={cpuConverter}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -271,14 +303,21 @@ export const ShowResources = ({ id, type }: Props) => {
|
||||
<TooltipContent>
|
||||
<p>
|
||||
CPU shares (relative weight). Example: 1 CPU =
|
||||
1000000000
|
||||
1000000000. Use +/- buttons to adjust by 0.25
|
||||
CPU.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input placeholder="1000000000 (1 CPU)" {...field} />
|
||||
<NumberInputWithSteps
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
placeholder="1000000000 (1 CPU)"
|
||||
step={CPU_STEP}
|
||||
converter={cpuConverter}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Cog } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -20,8 +20,39 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
// Railpack versions from https://github.com/railwayapp/railpack/releases
|
||||
export const RAILPACK_VERSIONS = [
|
||||
"0.15.4",
|
||||
"0.15.3",
|
||||
"0.15.2",
|
||||
"0.15.1",
|
||||
"0.15.0",
|
||||
"0.14.0",
|
||||
"0.13.0",
|
||||
"0.12.0",
|
||||
"0.11.0",
|
||||
"0.10.0",
|
||||
"0.9.2",
|
||||
"0.9.1",
|
||||
"0.9.0",
|
||||
"0.8.0",
|
||||
"0.7.0",
|
||||
"0.6.0",
|
||||
"0.5.0",
|
||||
"0.4.0",
|
||||
"0.3.0",
|
||||
"0.2.2",
|
||||
] as const;
|
||||
|
||||
export enum BuildType {
|
||||
dockerfile = "dockerfile",
|
||||
heroku_buildpacks = "heroku_buildpacks",
|
||||
@@ -65,7 +96,7 @@ const mySchema = z.discriminatedUnion("buildType", [
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal(BuildType.railpack),
|
||||
railpackVersion: z.string().nullable().default("0.2.2"),
|
||||
railpackVersion: z.string().nullable().default("0.15.4"),
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal(BuildType.static),
|
||||
@@ -152,6 +183,8 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
||||
});
|
||||
|
||||
const buildType = form.watch("buildType");
|
||||
const railpackVersion = form.watch("railpackVersion");
|
||||
const [isManualRailpackVersion, setIsManualRailpackVersion] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
@@ -163,6 +196,14 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
||||
};
|
||||
|
||||
form.reset(resetData(typedData));
|
||||
|
||||
// Check if railpack version is manual (not in the predefined list)
|
||||
if (
|
||||
data.railpackVersion &&
|
||||
!RAILPACK_VERSIONS.includes(data.railpackVersion as any)
|
||||
) {
|
||||
setIsManualRailpackVersion(true);
|
||||
}
|
||||
}
|
||||
}, [data, form]);
|
||||
|
||||
@@ -186,7 +227,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
||||
data.buildType === BuildType.static ? data.isStaticSpa : null,
|
||||
railpackVersion:
|
||||
data.buildType === BuildType.railpack
|
||||
? data.railpackVersion || "0.2.2"
|
||||
? data.railpackVersion || "0.15.4"
|
||||
: null,
|
||||
})
|
||||
.then(async () => {
|
||||
@@ -403,23 +444,88 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
||||
/>
|
||||
)}
|
||||
{buildType === BuildType.railpack && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="railpackVersion"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Railpack Version</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Railpack Version"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="railpackVersion"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Railpack Version</FormLabel>
|
||||
<FormControl>
|
||||
{isManualRailpackVersion ? (
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
placeholder="Enter custom version (e.g., 0.15.4)"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setIsManualRailpackVersion(false);
|
||||
field.onChange("0.15.4");
|
||||
}}
|
||||
>
|
||||
Use predefined versions
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
if (value === "manual") {
|
||||
setIsManualRailpackVersion(true);
|
||||
field.onChange("");
|
||||
} else {
|
||||
field.onChange(value);
|
||||
}
|
||||
}}
|
||||
value={field.value ?? "0.15.4"}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select Railpack version" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="manual">
|
||||
<span className="font-medium">
|
||||
✏️ Manual (Custom Version)
|
||||
</span>
|
||||
</SelectItem>
|
||||
{RAILPACK_VERSIONS.map((version) => (
|
||||
<SelectItem key={version} value={version}>
|
||||
v{version}
|
||||
{version === "0.15.4" && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="ml-2 px-1 text-xs"
|
||||
>
|
||||
Latest
|
||||
</Badge>
|
||||
)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Select a Railpack version or choose manual to enter a
|
||||
custom version.{" "}
|
||||
<a
|
||||
href="https://github.com/railwayapp/railpack/releases"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-primary underline underline-offset-4"
|
||||
>
|
||||
View releases
|
||||
</a>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="flex w-full justify-end">
|
||||
<Button isLoading={isLoading} type="submit">
|
||||
|
||||
@@ -256,9 +256,9 @@ export const ShowDeployments = ({
|
||||
return (
|
||||
<div
|
||||
key={deployment.deploymentId}
|
||||
className="flex items-center justify-between rounded-lg border p-4 gap-2"
|
||||
className="flex flex-col gap-4 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-1 flex-col min-w-0">
|
||||
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
||||
{index + 1}. {deployment.status}
|
||||
<StatusTooltip
|
||||
@@ -313,8 +313,8 @@ export const ShowDeployments = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2 max-w-[300px] w-full justify-start">
|
||||
<div className="text-sm capitalize text-muted-foreground flex items-center gap-2">
|
||||
<div className="flex w-full flex-col items-start gap-2 sm:w-auto sm:max-w-[300px] sm:items-end sm:justify-start">
|
||||
<div className="text-sm capitalize text-muted-foreground flex flex-wrap items-center gap-2">
|
||||
<DateTooltip date={deployment.createdAt} />
|
||||
{deployment.startedAt && deployment.finishedAt && (
|
||||
<Badge
|
||||
@@ -333,7 +333,7 @@ export const ShowDeployments = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-center sm:justify-end">
|
||||
{deployment.pid && deployment.status === "running" && (
|
||||
<DialogAction
|
||||
title="Kill Process"
|
||||
@@ -355,6 +355,7 @@ export const ShowDeployments = ({
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
isLoading={isKillingProcess}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Kill Process
|
||||
</Button>
|
||||
@@ -364,6 +365,7 @@ export const ShowDeployments = ({
|
||||
onClick={() => {
|
||||
setActiveLog(deployment);
|
||||
}}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
@@ -405,6 +407,7 @@ export const ShowDeployments = ({
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
isLoading={isRollingBack}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
<RefreshCcw className="size-4 text-primary group-hover:text-red-500" />
|
||||
Rollback
|
||||
|
||||
@@ -232,9 +232,9 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
<FormItem className="md:col-span-2 flex flex-col">
|
||||
<div className="flex items-center justify-between">
|
||||
<FormLabel>Repository</FormLabel>
|
||||
{field.value.owner && field.value.repo && (
|
||||
{field.value.gitlabPathNamespace && (
|
||||
<Link
|
||||
href={`${gitlabUrl}/${field.value.owner}/${field.value.repo}`}
|
||||
href={`${gitlabUrl}/${field.value.gitlabPathNamespace}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import {
|
||||
ExternalLink,
|
||||
FileText,
|
||||
GitPullRequest,
|
||||
Hammer,
|
||||
Loader2,
|
||||
PenSquare,
|
||||
RocketIcon,
|
||||
@@ -22,6 +24,12 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
|
||||
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
|
||||
@@ -38,6 +46,9 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
||||
const { mutateAsync: deletePreviewDeployment, isLoading } =
|
||||
api.previewDeployment.delete.useMutation();
|
||||
|
||||
const { mutateAsync: redeployPreviewDeployment } =
|
||||
api.previewDeployment.redeploy.useMutation();
|
||||
|
||||
const {
|
||||
data: previewDeployments,
|
||||
refetch: refetchPreviewDeployments,
|
||||
@@ -46,6 +57,8 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
||||
{ applicationId },
|
||||
{
|
||||
enabled: !!applicationId,
|
||||
refetchInterval: (data) =>
|
||||
data?.some((d) => d.previewStatus === "running") ? 2000 : false,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -193,6 +206,58 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
||||
</Button>
|
||||
</ShowDeploymentsModal>
|
||||
|
||||
<DialogAction
|
||||
title="Rebuild Preview Deployment"
|
||||
description="Are you sure you want to rebuild this preview deployment?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await redeployPreviewDeployment({
|
||||
previewDeploymentId:
|
||||
deployment.previewDeploymentId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"Preview deployment rebuild started",
|
||||
);
|
||||
refetchPreviewDeployments();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error rebuilding preview deployment",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
isLoading={status === "running"}
|
||||
className="gap-2"
|
||||
>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-2">
|
||||
<Hammer className="size-4" />
|
||||
Rebuild
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent
|
||||
sideOffset={5}
|
||||
className="z-[60]"
|
||||
>
|
||||
<p>
|
||||
Rebuild the preview deployment without
|
||||
downloading new code
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
|
||||
<AddPreviewDomain
|
||||
previewDeploymentId={`${deployment.previewDeploymentId}`}
|
||||
domainId={deployment.domain?.domainId}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -97,6 +97,16 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
const repository = form.watch("repository");
|
||||
const gitlabId = form.watch("gitlabId");
|
||||
|
||||
const gitlabUrl = useMemo(() => {
|
||||
const url = gitlabProviders?.find(
|
||||
(provider) => provider.gitlabId === gitlabId,
|
||||
)?.gitlabUrl;
|
||||
|
||||
const gitlabUrl = url?.replace(/\/$/, "");
|
||||
|
||||
return gitlabUrl || "https://gitlab.com";
|
||||
}, [gitlabId, gitlabProviders]);
|
||||
|
||||
const {
|
||||
data: repositories,
|
||||
isLoading: isLoadingRepositories,
|
||||
@@ -224,9 +234,9 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
<FormItem className="md:col-span-2 flex flex-col">
|
||||
<div className="flex items-center justify-between">
|
||||
<FormLabel>Repository</FormLabel>
|
||||
{field.value.owner && field.value.repo && (
|
||||
{field.value.gitlabPathNamespace && (
|
||||
<Link
|
||||
href={`https://gitlab.com/${field.value.owner}/${field.value.repo}`}
|
||||
href={`${gitlabUrl}/${field.value.gitlabPathNamespace}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||
|
||||
@@ -108,7 +108,8 @@ export const getLogType = (message: string): LogStyle => {
|
||||
/(?:might|may|could)\s+(?:not|cause|lead\s+to)/i.test(lowerMessage) ||
|
||||
/(?:!+\s*(?:warning|caution|attention)\s*!+)/i.test(lowerMessage) ||
|
||||
/\b(?:deprecated|obsolete)\b/i.test(lowerMessage) ||
|
||||
/\b(?:unstable|experimental)\b/i.test(lowerMessage)
|
||||
/\b(?:unstable|experimental)\b/i.test(lowerMessage) ||
|
||||
/⚠|⚠️/i.test(lowerMessage)
|
||||
) {
|
||||
return LOG_STYLES.warning;
|
||||
}
|
||||
|
||||
@@ -559,6 +559,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
type="password"
|
||||
placeholder="******************"
|
||||
autoComplete="one-time-code"
|
||||
enablePasswordGenerator={true}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -578,6 +579,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="******************"
|
||||
enablePasswordGenerator={true}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { CreditCard, FileText } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ShowInvoices } from "./show-invoices";
|
||||
|
||||
const navigationItems = [
|
||||
{
|
||||
name: "Subscription",
|
||||
href: "/dashboard/settings/billing",
|
||||
icon: CreditCard,
|
||||
},
|
||||
{
|
||||
name: "Invoices",
|
||||
href: "/dashboard/settings/invoices",
|
||||
icon: FileText,
|
||||
},
|
||||
];
|
||||
|
||||
export const ShowBillingInvoices = () => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Card className="bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||
<div className="rounded-xl bg-background shadow-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl flex flex-row gap-2">
|
||||
<CreditCard className="size-6 text-muted-foreground self-center" />
|
||||
Billing
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your subscription and invoices
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 py-4 border-t">
|
||||
<nav className="flex space-x-2 border-b">
|
||||
{navigationItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = router.pathname === item.href;
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-4 py-2 text-sm font-medium border-b-2 transition-colors",
|
||||
isActive
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-muted-foreground hover:text-primary hover:border-muted",
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
{item.name}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="mt-6">
|
||||
<ShowInvoices />
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -4,11 +4,13 @@ import {
|
||||
AlertTriangle,
|
||||
CheckIcon,
|
||||
CreditCard,
|
||||
FileText,
|
||||
Loader2,
|
||||
MinusIcon,
|
||||
PlusIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -37,7 +39,22 @@ export const calculatePrice = (count: number, isAnnual = false) => {
|
||||
if (count <= 1) return 4.5;
|
||||
return count * 3.5;
|
||||
};
|
||||
|
||||
const navigationItems = [
|
||||
{
|
||||
name: "Subscription",
|
||||
href: "/dashboard/settings/billing",
|
||||
icon: CreditCard,
|
||||
},
|
||||
{
|
||||
name: "Invoices",
|
||||
href: "/dashboard/settings/invoices",
|
||||
icon: FileText,
|
||||
},
|
||||
];
|
||||
|
||||
export const ShowBilling = () => {
|
||||
const router = useRouter();
|
||||
const { data: servers } = api.server.count.useQuery();
|
||||
const { data: admin } = api.user.get.useQuery();
|
||||
const { data, isLoading } = api.stripe.getProducts.useQuery();
|
||||
@@ -76,17 +93,41 @@ export const ShowBilling = () => {
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||
<div className="rounded-xl bg-background shadow-md ">
|
||||
<CardHeader className="">
|
||||
<Card className="bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||
<div className="rounded-xl bg-background shadow-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl flex flex-row gap-2">
|
||||
<CreditCard className="size-6 text-muted-foreground self-center" />
|
||||
Billing
|
||||
</CardTitle>
|
||||
<CardDescription>Manage your subscription</CardDescription>
|
||||
<CardDescription>
|
||||
Manage your subscription and invoices
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 py-8 border-t">
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<CardContent className="space-y-4 py-4 border-t">
|
||||
<nav className="flex space-x-2 border-b">
|
||||
{navigationItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = router.pathname === item.href;
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-4 py-2 text-sm font-medium border-b-2 transition-colors",
|
||||
isActive
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-muted-foreground hover:text-primary hover:border-muted",
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
{item.name}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="flex flex-col gap-4 w-full mt-6">
|
||||
<Tabs
|
||||
defaultValue="monthly"
|
||||
value={isAnnual ? "annual" : "monthly"}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { Download, ExternalLink, FileText, Loader2 } from "lucide-react";
|
||||
import type Stripe from "stripe";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const formatDate = (timestamp: number | null) => {
|
||||
if (!timestamp) return "-";
|
||||
return new Date(timestamp * 1000).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const formatAmount = (amount: number, currency: string) => {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: currency.toUpperCase(),
|
||||
}).format(amount / 100);
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: Stripe.Invoice.Status | null) => {
|
||||
const statusConfig: Record<
|
||||
Stripe.Invoice.Status,
|
||||
{ label: string; variant: "default" | "secondary" | "destructive" }
|
||||
> = {
|
||||
paid: { label: "Paid", variant: "default" },
|
||||
open: { label: "Open", variant: "secondary" },
|
||||
draft: { label: "Draft", variant: "secondary" },
|
||||
void: { label: "Void", variant: "destructive" },
|
||||
uncollectible: { label: "Uncollectible", variant: "destructive" },
|
||||
};
|
||||
|
||||
if (!status) {
|
||||
return <Badge variant="secondary">Unknown</Badge>;
|
||||
}
|
||||
|
||||
const config = statusConfig[status] || {
|
||||
label: status,
|
||||
variant: "secondary" as const,
|
||||
};
|
||||
|
||||
return <Badge variant={config.variant}>{config.label}</Badge>;
|
||||
};
|
||||
|
||||
export const ShowInvoices = () => {
|
||||
const { data: invoices, isLoading } = api.stripe.getInvoices.useQuery();
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center min-h-[20vh]">
|
||||
<span className="text-base text-muted-foreground flex flex-row gap-3 items-center">
|
||||
Loading invoices...
|
||||
<Loader2 className="animate-spin" />
|
||||
</span>
|
||||
</div>
|
||||
) : invoices && invoices.length > 0 ? (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Invoice</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead>Due Date</TableHead>
|
||||
<TableHead>Amount</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{invoices.map((invoice) => (
|
||||
<TableRow key={invoice.id}>
|
||||
<TableCell className="font-medium">
|
||||
{invoice.number || invoice.id.slice(0, 12)}
|
||||
</TableCell>
|
||||
<TableCell>{formatDate(invoice.created)}</TableCell>
|
||||
<TableCell>{formatDate(invoice.dueDate)}</TableCell>
|
||||
<TableCell>
|
||||
{formatAmount(invoice.amountDue, invoice.currency)}
|
||||
</TableCell>
|
||||
<TableCell>{getStatusBadge(invoice.status)}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
{invoice.hostedInvoiceUrl && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
window.open(
|
||||
invoice.hostedInvoiceUrl || "",
|
||||
"_blank",
|
||||
)
|
||||
}
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
{invoice.invoicePdf && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
window.open(invoice.invoicePdf || "", "_blank")
|
||||
}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center min-h-[20vh] gap-2">
|
||||
<FileText className="size-12 text-muted-foreground" />
|
||||
<p className="text-base text-muted-foreground">No invoices found</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Your invoices will appear here once you have a subscription
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Loader2, User } from "lucide-react";
|
||||
import { Loader2, Palette, User } from "lucide-react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { getAvatarType, isSolidColorAvatar } from "@/lib/avatar-utils";
|
||||
import { generateSHA256Hash, getFallbackAvatarInitials } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { Configure2FA } from "./configure-2fa";
|
||||
@@ -74,6 +75,7 @@ export const ProfileForm = () => {
|
||||
} = api.user.update.useMutation();
|
||||
const { t } = useTranslation("settings");
|
||||
const [gravatarHash, setGravatarHash] = useState<string | null>(null);
|
||||
const colorInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const availableAvatars = useMemo(() => {
|
||||
if (gravatarHash === null) return randomImages;
|
||||
@@ -274,16 +276,8 @@ export const ProfileForm = () => {
|
||||
onValueChange={(e) => {
|
||||
field.onChange(e);
|
||||
}}
|
||||
defaultValue={
|
||||
field.value?.startsWith("data:")
|
||||
? "upload"
|
||||
: field.value
|
||||
}
|
||||
value={
|
||||
field.value?.startsWith("data:")
|
||||
? "upload"
|
||||
: field.value
|
||||
}
|
||||
defaultValue={getAvatarType(field.value)}
|
||||
value={getAvatarType(field.value)}
|
||||
className="flex flex-row flex-wrap gap-2 max-xl:justify-center"
|
||||
>
|
||||
<FormItem key="no-avatar">
|
||||
@@ -370,6 +364,40 @@ export const ProfileForm = () => {
|
||||
/>
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem key="color-avatar">
|
||||
<FormLabel className="[&:has([data-state=checked])>.color-avatar]:border-primary [&:has([data-state=checked])>.color-avatar]:border-1 [&:has([data-state=checked])>.color-avatar]:p-px cursor-pointer relative">
|
||||
<FormControl>
|
||||
<RadioGroupItem
|
||||
value="color"
|
||||
className="sr-only"
|
||||
/>
|
||||
</FormControl>
|
||||
<div
|
||||
className="color-avatar h-12 w-12 rounded-full border hover:p-px hover:border-primary transition-colors flex items-center justify-center overflow-hidden cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: isSolidColorAvatar(
|
||||
field.value,
|
||||
)
|
||||
? field.value
|
||||
: undefined,
|
||||
}}
|
||||
onClick={() =>
|
||||
colorInputRef.current?.click()
|
||||
}
|
||||
>
|
||||
{!isSolidColorAvatar(field.value) && (
|
||||
<Palette className="h-5 w-5 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
ref={colorInputRef}
|
||||
type="color"
|
||||
className="absolute opacity-0 pointer-events-none w-12 h-12 top-0 left-0"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
{availableAvatars.map((image) => (
|
||||
<FormItem key={image}>
|
||||
<FormLabel className="[&:has([data-state=checked])>img]:border-primary [&:has([data-state=checked])>img]:border-1 [&:has([data-state=checked])>img]:p-px cursor-pointer">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Activity } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -7,7 +8,6 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { ShowStorageActions } from "./show-storage-actions";
|
||||
import { ShowTraefikActions } from "./show-traefik-actions";
|
||||
|
||||
@@ -7,9 +7,12 @@ interface Props {
|
||||
serverId?: string;
|
||||
}
|
||||
export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
||||
const { data, refetch } = api.user.get.useQuery(undefined, {
|
||||
enabled: !serverId,
|
||||
});
|
||||
const { data, refetch } = api.settings.getWebServerSettings.useQuery(
|
||||
undefined,
|
||||
{
|
||||
enabled: !serverId,
|
||||
},
|
||||
);
|
||||
|
||||
const { data: server, refetch: refetchServer } = api.server.one.useQuery(
|
||||
{
|
||||
@@ -22,7 +25,7 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
||||
|
||||
const enabled = serverId
|
||||
? server?.enableDockerCleanup
|
||||
: data?.user.enableDockerCleanup;
|
||||
: data?.enableDockerCleanup;
|
||||
|
||||
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();
|
||||
|
||||
@@ -30,7 +33,10 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
||||
try {
|
||||
await mutateAsync({
|
||||
enableDockerCleanup: checked,
|
||||
serverId: serverId,
|
||||
...(serverId && { serverId }),
|
||||
} as {
|
||||
enableDockerCleanup: boolean;
|
||||
serverId?: string;
|
||||
});
|
||||
if (serverId) {
|
||||
await refetchServer();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PlusIcon, Pencil } from "lucide-react";
|
||||
import { Pencil, PlusIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
@@ -80,7 +80,7 @@ const Schema = z.object({
|
||||
type Schema = z.infer<typeof Schema>;
|
||||
|
||||
export const SetupMonitoring = ({ serverId }: Props) => {
|
||||
const { data } = serverId
|
||||
const { data: serverData } = serverId
|
||||
? api.server.one.useQuery(
|
||||
{
|
||||
serverId: serverId || "",
|
||||
@@ -89,7 +89,14 @@ export const SetupMonitoring = ({ serverId }: Props) => {
|
||||
enabled: !!serverId,
|
||||
},
|
||||
)
|
||||
: api.user.getServerMetrics.useQuery();
|
||||
: { data: null };
|
||||
|
||||
const { data: webServerSettings } =
|
||||
api.settings.getWebServerSettings.useQuery(undefined, {
|
||||
enabled: !serverId,
|
||||
});
|
||||
|
||||
const data = serverId ? serverData : webServerSettings;
|
||||
|
||||
const url = useUrl();
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
@@ -89,15 +88,15 @@ export const SetupServer = ({ serverId, asButton = false }: Props) => {
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
) : (
|
||||
<DropdownMenuItem
|
||||
<Button
|
||||
className="w-full cursor-pointer "
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
>
|
||||
Setup Server
|
||||
</DropdownMenuItem>
|
||||
Setup Server <Settings className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
<DialogContent className="sm:max-w-4xl ">
|
||||
<DialogHeader>
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { format } from "date-fns";
|
||||
import {
|
||||
Clock,
|
||||
Key,
|
||||
KeyIcon,
|
||||
Loader2,
|
||||
MoreHorizontal,
|
||||
ServerIcon,
|
||||
Clock,
|
||||
User,
|
||||
Key,
|
||||
Network,
|
||||
ServerIcon,
|
||||
Terminal,
|
||||
Settings,
|
||||
Pencil,
|
||||
Trash2,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -31,9 +29,7 @@ import {
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
@@ -285,7 +281,32 @@ export const ShowServers = () => {
|
||||
|
||||
{/* Compact Actions */}
|
||||
{isActive && (
|
||||
<div className="flex items-center gap-2 pt-3 border-t mt-auto">
|
||||
<div className="flex items-center gap-2 pt-3 border-t mt-auto flex-wrap">
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SetupServer
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="max-w-xs"
|
||||
side="bottom"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<p className="font-semibold">
|
||||
Setup Server
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Configure and initialize your
|
||||
server with Docker, Traefik, and
|
||||
other essential services
|
||||
</p>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<TooltipProvider>
|
||||
{server.sshKeyId && (
|
||||
<Tooltip>
|
||||
@@ -311,20 +332,6 @@ export const ShowServers = () => {
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<SetupServer
|
||||
serverId={server.serverId}
|
||||
asButton={true}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Setup Server</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
|
||||
@@ -67,7 +67,7 @@ type AddServerDomain = z.infer<typeof addServerDomain>;
|
||||
|
||||
export const WebDomain = () => {
|
||||
const { t } = useTranslation("settings");
|
||||
const { data, refetch } = api.user.get.useQuery();
|
||||
const { data, refetch } = api.settings.getWebServerSettings.useQuery();
|
||||
const { mutateAsync, isLoading } =
|
||||
api.settings.assignDomainServer.useMutation();
|
||||
|
||||
@@ -82,15 +82,15 @@ export const WebDomain = () => {
|
||||
});
|
||||
const https = form.watch("https");
|
||||
const domain = form.watch("domain") || "";
|
||||
const host = data?.user?.host || "";
|
||||
const host = data?.host || "";
|
||||
const hasChanged = domain !== host;
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
domain: data?.user?.host || "",
|
||||
certificateType: data?.user?.certificateType,
|
||||
letsEncryptEmail: data?.user?.letsEncryptEmail || "",
|
||||
https: data?.user?.https || false,
|
||||
domain: data?.host || "",
|
||||
certificateType: data?.certificateType || "none",
|
||||
letsEncryptEmail: data?.letsEncryptEmail || "",
|
||||
https: data?.https || false,
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
@@ -16,7 +16,8 @@ import { UpdateServer } from "./web-server/update-server";
|
||||
|
||||
export const WebServer = () => {
|
||||
const { t } = useTranslation("settings");
|
||||
const { data } = api.user.get.useQuery();
|
||||
const { data: webServerSettings } =
|
||||
api.settings.getWebServerSettings.useQuery();
|
||||
|
||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||
|
||||
@@ -53,7 +54,7 @@ export const WebServer = () => {
|
||||
|
||||
<div className="flex items-center flex-wrap justify-between gap-4">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Server IP: {data?.user.serverIp}
|
||||
Server IP: {webServerSettings?.serverIp}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Version: {dokployVersion}
|
||||
|
||||
@@ -46,15 +46,15 @@ interface Props {
|
||||
export const UpdateServerIp = ({ children }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const { data } = api.user.get.useQuery();
|
||||
const { data, refetch } = api.settings.getWebServerSettings.useQuery();
|
||||
const { data: ip } = api.server.publicIp.useQuery();
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.user.update.useMutation();
|
||||
api.settings.updateServerIp.useMutation();
|
||||
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
serverIp: data?.user.serverIp || "",
|
||||
serverIp: data?.serverIp || "",
|
||||
},
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
@@ -62,13 +62,11 @@ export const UpdateServerIp = ({ children }: Props) => {
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
serverIp: data.user.serverIp || "",
|
||||
serverIp: data.serverIp || "",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
const setCurrentIp = () => {
|
||||
if (!ip) return;
|
||||
form.setValue("serverIp", ip);
|
||||
@@ -80,7 +78,7 @@ export const UpdateServerIp = ({ children }: Props) => {
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Server IP Updated");
|
||||
await utils.user.get.invalidate();
|
||||
await refetch();
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Fragment } from "react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
||||
@@ -10,7 +10,7 @@ export const ToggleVisibilityInput = ({ ...props }: InputProps) => {
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center space-x-2">
|
||||
<Input ref={inputRef} type={"password"} {...props} />
|
||||
<Input ref={inputRef} {...props} type="password" />
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||
import * as React from "react";
|
||||
|
||||
import { isSolidColorAvatar } from "@/lib/avatar-utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
@@ -20,14 +20,33 @@ Avatar.displayName = AvatarPrimitive.Root.displayName;
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> & {
|
||||
src?: string | null;
|
||||
}
|
||||
>(({ className, src, ...props }, ref) => {
|
||||
if (isSolidColorAvatar(src)) {
|
||||
return (
|
||||
<div
|
||||
key={`solid-${src}`}
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full rounded-full", className)}
|
||||
style={{
|
||||
backgroundColor: src,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
src={src ?? ""}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
|
||||
@@ -1,18 +1,75 @@
|
||||
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||
import { EyeIcon, EyeOffIcon, RefreshCcw } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { generateRandomPassword } from "@/lib/password-utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
errorMessage?: string;
|
||||
enablePasswordGenerator?: boolean;
|
||||
passwordGeneratorLength?: number;
|
||||
}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, errorMessage, type, ...props }, ref) => {
|
||||
(
|
||||
{
|
||||
className,
|
||||
errorMessage,
|
||||
type,
|
||||
enablePasswordGenerator = false,
|
||||
passwordGeneratorLength,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [showPassword, setShowPassword] = React.useState(false);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const isPassword = type === "password";
|
||||
const shouldShowGenerator =
|
||||
isPassword &&
|
||||
enablePasswordGenerator !== false &&
|
||||
!props.disabled &&
|
||||
!props.readOnly;
|
||||
const inputType = isPassword ? (showPassword ? "text" : "password") : type;
|
||||
|
||||
const setRefs = React.useCallback(
|
||||
(node: HTMLInputElement | null) => {
|
||||
// @ts-ignore
|
||||
inputRef.current = node;
|
||||
if (typeof ref === "function") {
|
||||
ref(node);
|
||||
} else if (ref) {
|
||||
ref.current = node;
|
||||
}
|
||||
},
|
||||
[ref],
|
||||
);
|
||||
|
||||
const handleGeneratePassword = () => {
|
||||
const nextValue =
|
||||
typeof passwordGeneratorLength === "number" &&
|
||||
passwordGeneratorLength > 0
|
||||
? generateRandomPassword(Math.floor(passwordGeneratorLength))
|
||||
: generateRandomPassword();
|
||||
|
||||
const input = inputRef.current;
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
const valueSetter = Object.getOwnPropertyDescriptor(
|
||||
HTMLInputElement.prototype,
|
||||
"value",
|
||||
)?.set;
|
||||
if (valueSetter) {
|
||||
valueSetter.call(input, nextValue);
|
||||
} else {
|
||||
input.value = nextValue;
|
||||
}
|
||||
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative w-full">
|
||||
@@ -21,25 +78,39 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
className={cn(
|
||||
// bg-gray
|
||||
"flex h-10 w-full rounded-md bg-input px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border disabled:cursor-not-allowed disabled:opacity-50",
|
||||
isPassword && "pr-10", // Add padding for the eye icon
|
||||
isPassword && (shouldShowGenerator ? "pr-16" : "pr-10"),
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
ref={setRefs}
|
||||
{...props}
|
||||
/>
|
||||
{isPassword && (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground focus:outline-none"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOffIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<EyeIcon className="h-4 w-4" />
|
||||
<div className="absolute inset-y-0 right-0 flex items-center gap-1 pr-3 text-muted-foreground">
|
||||
{shouldShowGenerator && (
|
||||
<button
|
||||
type="button"
|
||||
className="hover:text-foreground focus:outline-none"
|
||||
onClick={handleGeneratePassword}
|
||||
aria-label="Generate password"
|
||||
title="Generate password"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="hover:text-foreground focus:outline-none"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOffIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<EyeIcon className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{errorMessage && (
|
||||
|
||||
84
apps/dokploy/components/ui/number-input.tsx
Normal file
84
apps/dokploy/components/ui/number-input.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { MinusIcon, PlusIcon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
export interface UnitConverter {
|
||||
toValue: (raw: string | undefined) => number;
|
||||
fromValue: (value: number) => string;
|
||||
formatDisplay: (value: number) => string;
|
||||
}
|
||||
|
||||
export const createConverter = (
|
||||
multiplier: number,
|
||||
formatDisplay: (value: number) => string,
|
||||
): UnitConverter => ({
|
||||
toValue: (raw) => {
|
||||
if (!raw) return 0;
|
||||
const value = Number.parseInt(raw, 10);
|
||||
return Number.isNaN(value) ? 0 : value / multiplier;
|
||||
},
|
||||
fromValue: (value) =>
|
||||
value <= 0 ? "" : String(Math.round(value * multiplier)),
|
||||
formatDisplay,
|
||||
});
|
||||
|
||||
interface NumberInputWithStepsProps {
|
||||
value: string | undefined;
|
||||
onChange: (value: string) => void;
|
||||
placeholder: string;
|
||||
step: number;
|
||||
converter: UnitConverter;
|
||||
}
|
||||
|
||||
export const NumberInputWithSteps = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
step,
|
||||
converter,
|
||||
}: NumberInputWithStepsProps) => {
|
||||
const numericValue = converter.toValue(value);
|
||||
const displayValue = converter.formatDisplay(numericValue);
|
||||
|
||||
const handleIncrement = () =>
|
||||
onChange(converter.fromValue(numericValue + step));
|
||||
const handleDecrement = () =>
|
||||
onChange(converter.fromValue(Math.max(0, numericValue - step)));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-9 w-9 shrink-0"
|
||||
onClick={handleDecrement}
|
||||
disabled={numericValue <= 0}
|
||||
>
|
||||
<MinusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
value={value || ""}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="text-center"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-9 w-9 shrink-0"
|
||||
onClick={handleIncrement}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{displayValue && (
|
||||
<span className="text-xs text-muted-foreground text-center">
|
||||
{displayValue}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
114
apps/dokploy/drizzle/0133_striped_the_order.sql
Normal file
114
apps/dokploy/drizzle/0133_striped_the_order.sql
Normal file
@@ -0,0 +1,114 @@
|
||||
CREATE TABLE "webServerSettings" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"serverIp" text,
|
||||
"certificateType" "certificateType" DEFAULT 'none' NOT NULL,
|
||||
"https" boolean DEFAULT false NOT NULL,
|
||||
"host" text,
|
||||
"letsEncryptEmail" text,
|
||||
"sshPrivateKey" text,
|
||||
"enableDockerCleanup" boolean DEFAULT true NOT NULL,
|
||||
"logCleanupCron" text DEFAULT '0 0 * * *',
|
||||
"metricsConfig" jsonb DEFAULT '{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb NOT NULL,
|
||||
"cleanupCacheApplications" boolean DEFAULT false NOT NULL,
|
||||
"cleanupCacheOnPreviews" boolean DEFAULT false NOT NULL,
|
||||
"cleanupCacheOnCompose" boolean DEFAULT false NOT NULL,
|
||||
"created_at" timestamp DEFAULT now(),
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
-- Migrate data from user table to webServerSettings
|
||||
-- Get the owner user's data and insert into webServerSettings
|
||||
INSERT INTO "webServerSettings" (
|
||||
"id",
|
||||
"serverIp",
|
||||
"certificateType",
|
||||
"https",
|
||||
"host",
|
||||
"letsEncryptEmail",
|
||||
"sshPrivateKey",
|
||||
"enableDockerCleanup",
|
||||
"logCleanupCron",
|
||||
"metricsConfig",
|
||||
"cleanupCacheApplications",
|
||||
"cleanupCacheOnPreviews",
|
||||
"cleanupCacheOnCompose",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
)
|
||||
SELECT
|
||||
gen_random_uuid()::text as "id",
|
||||
u."serverIp",
|
||||
COALESCE(u."certificateType", 'none') as "certificateType",
|
||||
COALESCE(u."https", false) as "https",
|
||||
u."host",
|
||||
u."letsEncryptEmail",
|
||||
u."sshPrivateKey",
|
||||
COALESCE(u."enableDockerCleanup", true) as "enableDockerCleanup",
|
||||
COALESCE(u."logCleanupCron", '0 0 * * *') as "logCleanupCron",
|
||||
COALESCE(
|
||||
u."metricsConfig",
|
||||
'{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb
|
||||
) as "metricsConfig",
|
||||
COALESCE(u."cleanupCacheApplications", false) as "cleanupCacheApplications",
|
||||
COALESCE(u."cleanupCacheOnPreviews", false) as "cleanupCacheOnPreviews",
|
||||
COALESCE(u."cleanupCacheOnCompose", false) as "cleanupCacheOnCompose",
|
||||
NOW() as "created_at",
|
||||
NOW() as "updated_at"
|
||||
FROM "user" u
|
||||
INNER JOIN "member" m ON u."id" = m."user_id"
|
||||
WHERE m."role" = 'owner'
|
||||
ORDER BY m."created_at" ASC
|
||||
LIMIT 1;
|
||||
|
||||
-- If no owner found, create a default entry
|
||||
INSERT INTO "webServerSettings" (
|
||||
"id",
|
||||
"serverIp",
|
||||
"certificateType",
|
||||
"https",
|
||||
"host",
|
||||
"letsEncryptEmail",
|
||||
"sshPrivateKey",
|
||||
"enableDockerCleanup",
|
||||
"logCleanupCron",
|
||||
"metricsConfig",
|
||||
"cleanupCacheApplications",
|
||||
"cleanupCacheOnPreviews",
|
||||
"cleanupCacheOnCompose",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
)
|
||||
SELECT
|
||||
gen_random_uuid()::text as "id",
|
||||
NULL as "serverIp",
|
||||
'none' as "certificateType",
|
||||
false as "https",
|
||||
NULL as "host",
|
||||
NULL as "letsEncryptEmail",
|
||||
NULL as "sshPrivateKey",
|
||||
true as "enableDockerCleanup",
|
||||
'0 0 * * *' as "logCleanupCron",
|
||||
'{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb as "metricsConfig",
|
||||
false as "cleanupCacheApplications",
|
||||
false as "cleanupCacheOnPreviews",
|
||||
false as "cleanupCacheOnCompose",
|
||||
NOW() as "created_at",
|
||||
NOW() as "updated_at"
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM "webServerSettings"
|
||||
);
|
||||
|
||||
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "user" DROP COLUMN "serverIp";--> statement-breakpoint
|
||||
ALTER TABLE "user" DROP COLUMN "certificateType";--> statement-breakpoint
|
||||
ALTER TABLE "user" DROP COLUMN "https";--> statement-breakpoint
|
||||
ALTER TABLE "user" DROP COLUMN "host";--> statement-breakpoint
|
||||
ALTER TABLE "user" DROP COLUMN "letsEncryptEmail";--> statement-breakpoint
|
||||
ALTER TABLE "user" DROP COLUMN "sshPrivateKey";--> statement-breakpoint
|
||||
ALTER TABLE "user" DROP COLUMN "enableDockerCleanup";--> statement-breakpoint
|
||||
ALTER TABLE "user" DROP COLUMN "logCleanupCron";--> statement-breakpoint
|
||||
ALTER TABLE "user" DROP COLUMN "metricsConfig";--> statement-breakpoint
|
||||
ALTER TABLE "user" DROP COLUMN "cleanupCacheApplications";--> statement-breakpoint
|
||||
ALTER TABLE "user" DROP COLUMN "cleanupCacheOnPreviews";--> statement-breakpoint
|
||||
ALTER TABLE "user" DROP COLUMN "cleanupCacheOnCompose";
|
||||
1
apps/dokploy/drizzle/0134_strong_hercules.sql
Normal file
1
apps/dokploy/drizzle/0134_strong_hercules.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "application" ALTER COLUMN "railpackVersion" SET DEFAULT '0.15.4';
|
||||
6968
apps/dokploy/drizzle/meta/0133_snapshot.json
Normal file
6968
apps/dokploy/drizzle/meta/0133_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
6968
apps/dokploy/drizzle/meta/0134_snapshot.json
Normal file
6968
apps/dokploy/drizzle/meta/0134_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -932,6 +932,20 @@
|
||||
"when": 1765346573500,
|
||||
"tag": "0132_clean_layla_miller",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 133,
|
||||
"version": "7",
|
||||
"when": 1766301478005,
|
||||
"tag": "0133_striped_the_order",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 134,
|
||||
"version": "7",
|
||||
"when": 1767871040249,
|
||||
"tag": "0134_strong_hercules",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
30
apps/dokploy/lib/avatar-utils.ts
Normal file
30
apps/dokploy/lib/avatar-utils.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Checks if the given avatar value represents a solid color in hexadecimal format.
|
||||
*
|
||||
* @param value Avatar value to check.
|
||||
*
|
||||
* @return True if the avatar is a solid color, false otherwise.
|
||||
*/
|
||||
export function isSolidColorAvatar(value?: string | null) {
|
||||
return (
|
||||
(value?.startsWith("#") && /^#[0-9A-Fa-f]{6}$/.test(value)) ||
|
||||
value?.startsWith("color:") ||
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the avatar type for form selection (RadioGroup value).
|
||||
*
|
||||
* @param value Avatar value.
|
||||
*
|
||||
* @return "upload" for base64 images, "color" for solid colors, or the original value for other types.
|
||||
*/
|
||||
export function getAvatarType(value?: string | null) {
|
||||
if (!value) return "";
|
||||
|
||||
if (value.startsWith("data:")) return "upload";
|
||||
if (isSolidColorAvatar(value)) return "color";
|
||||
|
||||
return value;
|
||||
}
|
||||
38
apps/dokploy/lib/password-utils.ts
Normal file
38
apps/dokploy/lib/password-utils.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
const DEFAULT_PASSWORD_LENGTH = 20;
|
||||
const DEFAULT_PASSWORD_CHARSET =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
|
||||
export const generateRandomPassword = (
|
||||
length: number = DEFAULT_PASSWORD_LENGTH,
|
||||
charset: string = DEFAULT_PASSWORD_CHARSET,
|
||||
) => {
|
||||
const safeLength =
|
||||
Number.isFinite(length) && length > 0
|
||||
? Math.floor(length)
|
||||
: DEFAULT_PASSWORD_LENGTH;
|
||||
|
||||
if (safeLength <= 0 || charset.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const cryptoApi =
|
||||
typeof globalThis !== "undefined" ? globalThis.crypto : undefined;
|
||||
|
||||
if (!cryptoApi?.getRandomValues) {
|
||||
let fallback = "";
|
||||
for (let i = 0; i < safeLength; i += 1) {
|
||||
fallback += charset[Math.floor(Math.random() * charset.length)];
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const values = new Uint32Array(safeLength);
|
||||
cryptoApi.getRandomValues(values);
|
||||
|
||||
let result = "";
|
||||
for (const value of values) {
|
||||
result += charset[value % charset.length];
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.26.3",
|
||||
"version": "v0.26.5",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -33,7 +33,7 @@
|
||||
"docker:build:canary": "./docker/build.sh canary",
|
||||
"docker:push:canary": "./docker/push.sh canary",
|
||||
"version": "echo $(node -p \"require('./package.json').version\")",
|
||||
"test": "vitest --config __test__/vitest.config.ts volume-backups",
|
||||
"test": "vitest --config __test__/vitest.config.ts",
|
||||
"generate:openapi": "tsx -r dotenv/config scripts/generate-openapi.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -109,7 +109,6 @@
|
||||
"drizzle-orm": "^0.39.3",
|
||||
"drizzle-zod": "0.5.1",
|
||||
"fancy-ansi": "^0.1.3",
|
||||
"hi-base32": "^0.5.1",
|
||||
"i18next": "^23.16.8",
|
||||
"input-otp": "^1.4.2",
|
||||
"js-cookie": "^3.0.5",
|
||||
@@ -126,7 +125,6 @@
|
||||
"node-schedule": "2.1.1",
|
||||
"nodemailer": "6.9.14",
|
||||
"octokit": "3.1.2",
|
||||
"otpauth": "^9.4.0",
|
||||
"pino": "9.4.0",
|
||||
"pino-pretty": "11.2.2",
|
||||
"postgres": "3.4.4",
|
||||
@@ -140,7 +138,6 @@
|
||||
"react-i18next": "^15.5.2",
|
||||
"react-markdown": "^9.1.0",
|
||||
"recharts": "^2.15.3",
|
||||
"rotating-file-stream": "3.2.3",
|
||||
"slugify": "^1.6.6",
|
||||
"sonner": "^1.7.4",
|
||||
"ssh2": "1.15.0",
|
||||
@@ -156,9 +153,11 @@
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"yaml": "2.8.1",
|
||||
"zod": "^3.25.32",
|
||||
"zod-form-data": "^2.0.7"
|
||||
"zod-form-data": "^2.0.7",
|
||||
"semver": "7.7.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/bcrypt": "5.0.2",
|
||||
|
||||
@@ -909,7 +909,9 @@ const EnvironmentPage = (
|
||||
<ProjectEnvironment projectId={projectId}>
|
||||
<Button variant="outline">Project Environment</Button>
|
||||
</ProjectEnvironment>
|
||||
{(auth?.role === "owner" || auth?.canCreateServices) && (
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canCreateServices) && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>
|
||||
@@ -1032,6 +1034,7 @@ const EnvironmentPage = (
|
||||
</Button>
|
||||
</DialogAction>
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canDeleteServices) && (
|
||||
<>
|
||||
<DialogAction
|
||||
|
||||
@@ -192,7 +192,9 @@ const Service = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateApplication applicationId={applicationId} />
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canDeleteServices) && (
|
||||
<DeleteService id={applicationId} type="application" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -182,7 +182,9 @@ const Service = (
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateCompose composeId={composeId} />
|
||||
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canDeleteServices) && (
|
||||
<DeleteService id={composeId} type="compose" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -156,7 +156,9 @@ const Mariadb = (
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateMariadb mariadbId={mariadbId} />
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canDeleteServices) && (
|
||||
<DeleteService id={mariadbId} type="mariadb" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -155,7 +155,9 @@ const Mongo = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateMongo mongoId={mongoId} />
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canDeleteServices) && (
|
||||
<DeleteService id={mongoId} type="mongo" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -156,7 +156,9 @@ const MySql = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateMysql mysqlId={mysqlId} />
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canDeleteServices) && (
|
||||
<DeleteService id={mysqlId} type="mysql" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -154,7 +154,9 @@ const Postgresql = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdatePostgres postgresId={postgresId} />
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canDeleteServices) && (
|
||||
<DeleteService id={postgresId} type="postgres" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -154,7 +154,9 @@ const Redis = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateRedis redisId={redisId} />
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canDeleteServices) && (
|
||||
<DeleteService id={redisId} type="redis" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
63
apps/dokploy/pages/dashboard/settings/invoices.tsx
Normal file
63
apps/dokploy/pages/dashboard/settings/invoices.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
import { ShowBillingInvoices } from "@/components/dashboard/settings/billing/show-billing-invoices";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
|
||||
const Page = () => {
|
||||
return <ShowBillingInvoices />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
Page.getLayout = (page: ReactElement) => {
|
||||
return <DashboardLayout metaName="Invoices">{page}</DashboardLayout>;
|
||||
};
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
if (!IS_CLOUD) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user || user.role !== "owner") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
await helpers.user.get.prefetch();
|
||||
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
return {
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
findUserById,
|
||||
getWebServerSettings,
|
||||
IS_CLOUD,
|
||||
setupWebMonitoring,
|
||||
updateUser,
|
||||
updateWebServerSettings,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { apiUpdateWebServerMonitoring } from "@/server/db/schema";
|
||||
@@ -11,7 +11,7 @@ import { adminProcedure, createTRPCRouter } from "../trpc";
|
||||
export const adminRouter = createTRPCRouter({
|
||||
setupMonitoring: adminProcedure
|
||||
.input(apiUpdateWebServerMonitoring)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
if (IS_CLOUD) {
|
||||
throw new TRPCError({
|
||||
@@ -19,15 +19,8 @@ export const adminRouter = createTRPCRouter({
|
||||
message: "Feature disabled on cloud",
|
||||
});
|
||||
}
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
if (user.id !== ctx.user.ownerId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to setup the monitoring",
|
||||
});
|
||||
}
|
||||
|
||||
await updateUser(user.id, {
|
||||
await updateWebServerSettings({
|
||||
metricsConfig: {
|
||||
server: {
|
||||
type: "Dokploy",
|
||||
@@ -52,8 +45,9 @@ export const adminRouter = createTRPCRouter({
|
||||
},
|
||||
});
|
||||
|
||||
const currentServer = await setupWebMonitoring(user.id);
|
||||
return currentServer;
|
||||
await setupWebMonitoring();
|
||||
const settings = await getWebServerSettings();
|
||||
return settings;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -94,6 +94,40 @@ export const aiRouter = createTRPCRouter({
|
||||
{ headers },
|
||||
);
|
||||
break;
|
||||
case "perplexity":
|
||||
// Perplexity doesn't have a /models endpoint, return hardcoded list
|
||||
return [
|
||||
{
|
||||
id: "sonar-deep-research",
|
||||
object: "model",
|
||||
created: Date.now(),
|
||||
owned_by: "perplexity",
|
||||
},
|
||||
{
|
||||
id: "sonar-reasoning-pro",
|
||||
object: "model",
|
||||
created: Date.now(),
|
||||
owned_by: "perplexity",
|
||||
},
|
||||
{
|
||||
id: "sonar-reasoning",
|
||||
object: "model",
|
||||
created: Date.now(),
|
||||
owned_by: "perplexity",
|
||||
},
|
||||
{
|
||||
id: "sonar-pro",
|
||||
object: "model",
|
||||
created: Date.now(),
|
||||
owned_by: "perplexity",
|
||||
},
|
||||
{
|
||||
id: "sonar",
|
||||
object: "model",
|
||||
created: Date.now(),
|
||||
owned_by: "perplexity",
|
||||
},
|
||||
] as Model[];
|
||||
default:
|
||||
response = await fetch(`${apiUrl}/models`, { headers });
|
||||
}
|
||||
|
||||
@@ -285,6 +285,7 @@ export const backupRouter = createTRPCRouter({
|
||||
.mutation(async ({ input }) => {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
await runWebServerBackup(backup);
|
||||
await keepLatestNBackups(backup);
|
||||
return true;
|
||||
}),
|
||||
listBackupFiles: protectedProcedure
|
||||
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
findGitProviderById,
|
||||
findProjectById,
|
||||
findServerById,
|
||||
findUserById,
|
||||
getComposeContainer,
|
||||
getWebServerSettings,
|
||||
IS_CLOUD,
|
||||
loadServices,
|
||||
randomizeComposeFile,
|
||||
@@ -430,7 +430,11 @@ export const composeRouter = createTRPCRouter({
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
return { success: true, message: "Deployment queued" };
|
||||
return {
|
||||
success: true,
|
||||
message: "Deployment queued",
|
||||
composeId: compose.composeId,
|
||||
};
|
||||
}),
|
||||
redeploy: protectedProcedure
|
||||
.input(apiRedeployCompose)
|
||||
@@ -468,7 +472,11 @@ export const composeRouter = createTRPCRouter({
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
return { success: true, message: "Redeployment queued" };
|
||||
return {
|
||||
success: true,
|
||||
message: "Redeployment queued",
|
||||
composeId: compose.composeId,
|
||||
};
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
@@ -569,8 +577,7 @@ export const composeRouter = createTRPCRouter({
|
||||
|
||||
const template = await fetchTemplateFiles(input.id, input.baseUrl);
|
||||
|
||||
const admin = await findUserById(ctx.user.ownerId);
|
||||
let serverIp = admin.serverIp || "127.0.0.1";
|
||||
let serverIp = "127.0.0.1";
|
||||
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
@@ -579,6 +586,9 @@ export const composeRouter = createTRPCRouter({
|
||||
serverIp = server.ipAddress;
|
||||
} else if (process.env.NODE_ENV === "development") {
|
||||
serverIp = "127.0.0.1";
|
||||
} else {
|
||||
const settings = await getWebServerSettings();
|
||||
serverIp = settings?.serverIp || "127.0.0.1";
|
||||
}
|
||||
|
||||
const projectName = slugify(`${project.name} ${input.id}`);
|
||||
@@ -803,14 +813,16 @@ export const composeRouter = createTRPCRouter({
|
||||
const decodedData = Buffer.from(input.base64, "base64").toString(
|
||||
"utf-8",
|
||||
);
|
||||
const admin = await findUserById(ctx.user.ownerId);
|
||||
let serverIp = admin.serverIp || "127.0.0.1";
|
||||
let serverIp = "127.0.0.1";
|
||||
|
||||
if (compose.serverId) {
|
||||
const server = await findServerById(compose.serverId);
|
||||
serverIp = server.ipAddress;
|
||||
} else if (process.env.NODE_ENV === "development") {
|
||||
serverIp = "127.0.0.1";
|
||||
} else {
|
||||
const settings = await getWebServerSettings();
|
||||
serverIp = settings?.serverIp || "127.0.0.1";
|
||||
}
|
||||
const templateData = JSON.parse(decodedData);
|
||||
const config = parse(templateData.config) as CompleteTemplate;
|
||||
@@ -880,14 +892,16 @@ export const composeRouter = createTRPCRouter({
|
||||
await removeDomainById(domain.domainId);
|
||||
}
|
||||
|
||||
const admin = await findUserById(ctx.user.ownerId);
|
||||
let serverIp = admin.serverIp || "127.0.0.1";
|
||||
let serverIp = "127.0.0.1";
|
||||
|
||||
if (compose.serverId) {
|
||||
const server = await findServerById(compose.serverId);
|
||||
serverIp = server.ipAddress;
|
||||
} else if (process.env.NODE_ENV === "development") {
|
||||
serverIp = "127.0.0.1";
|
||||
} else {
|
||||
const settings = await getWebServerSettings();
|
||||
serverIp = settings?.serverIp || "127.0.0.1";
|
||||
}
|
||||
|
||||
const templateData = JSON.parse(decodedData);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
findPreviewDeploymentById,
|
||||
findServerById,
|
||||
generateTraefikMeDomain,
|
||||
getWebServerSettings,
|
||||
manageDomain,
|
||||
removeDomain,
|
||||
removeDomainById,
|
||||
@@ -107,16 +108,13 @@ export const domainRouter = createTRPCRouter({
|
||||
}),
|
||||
canGenerateTraefikMeDomains: protectedProcedure
|
||||
.input(z.object({ serverId: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
const organization = await findOrganizationById(
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
.query(async ({ input }) => {
|
||||
if (input.serverId) {
|
||||
const server = await findServerById(input.serverId);
|
||||
return server.ipAddress;
|
||||
}
|
||||
return organization?.owner.serverIp;
|
||||
const settings = await getWebServerSettings();
|
||||
return settings?.serverIp || "";
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
createSlackNotification,
|
||||
createTelegramNotification,
|
||||
findNotificationById,
|
||||
getWebServerSettings,
|
||||
IS_CLOUD,
|
||||
removeNotificationById,
|
||||
sendCustomNotification,
|
||||
@@ -66,7 +67,6 @@ import {
|
||||
apiUpdateTelegram,
|
||||
notifications,
|
||||
server,
|
||||
user,
|
||||
} from "@/server/db/schema";
|
||||
|
||||
export const notificationRouter = createTRPCRouter({
|
||||
@@ -364,21 +364,20 @@ export const notificationRouter = createTRPCRouter({
|
||||
let organizationId = "";
|
||||
let ServerName = "";
|
||||
if (input.ServerType === "Dokploy") {
|
||||
const result = await db
|
||||
.select()
|
||||
.from(user)
|
||||
.where(
|
||||
sql`${user.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`,
|
||||
);
|
||||
|
||||
if (!result?.[0]?.id) {
|
||||
const settings = await getWebServerSettings();
|
||||
if (
|
||||
!settings?.metricsConfig?.server?.token ||
|
||||
settings.metricsConfig.server.token !== input.Token
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Token not found",
|
||||
});
|
||||
}
|
||||
|
||||
organizationId = result?.[0]?.id;
|
||||
// For Dokploy server type, we don't have a specific organizationId
|
||||
// This might need to be adjusted based on your business logic
|
||||
organizationId = "";
|
||||
ServerName = "Dokploy";
|
||||
} else {
|
||||
const result = await db
|
||||
|
||||
@@ -2,11 +2,15 @@ import {
|
||||
findApplicationById,
|
||||
findPreviewDeploymentById,
|
||||
findPreviewDeploymentsByApplicationId,
|
||||
IS_CLOUD,
|
||||
removePreviewDeployment,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { apiFindAllByApplication } from "@/server/db/schema";
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { deploy } from "@/server/utils/deploy";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
export const previewDeploymentRouter = createTRPCRouter({
|
||||
@@ -60,4 +64,55 @@ export const previewDeploymentRouter = createTRPCRouter({
|
||||
}
|
||||
return previewDeployment;
|
||||
}),
|
||||
redeploy: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
previewDeploymentId: z.string(),
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const previewDeployment = await findPreviewDeploymentById(
|
||||
input.previewDeploymentId,
|
||||
);
|
||||
if (
|
||||
previewDeployment.application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to redeploy this preview deployment",
|
||||
});
|
||||
}
|
||||
const application = await findApplicationById(
|
||||
previewDeployment.applicationId,
|
||||
);
|
||||
const jobData: DeploymentJob = {
|
||||
applicationId: previewDeployment.applicationId,
|
||||
titleLog: input.title || "Rebuild Preview Deployment",
|
||||
descriptionLog: input.description || "",
|
||||
type: "redeploy",
|
||||
applicationType: "application-preview",
|
||||
previewDeploymentId: input.previewDeploymentId,
|
||||
server: !!application.serverId,
|
||||
};
|
||||
|
||||
if (IS_CLOUD && application.serverId) {
|
||||
jobData.serverId = application.serverId;
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
return true;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -12,11 +12,11 @@ import {
|
||||
DEFAULT_UPDATE_DATA,
|
||||
execAsync,
|
||||
findServerById,
|
||||
findUserById,
|
||||
getDokployImage,
|
||||
getDokployImageTag,
|
||||
getLogCleanupStatus,
|
||||
getUpdateData,
|
||||
getWebServerSettings,
|
||||
IS_CLOUD,
|
||||
parseRawConfig,
|
||||
paths,
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
updateLetsEncryptEmail,
|
||||
updateServerById,
|
||||
updateServerTraefik,
|
||||
updateUser,
|
||||
updateWebServerSettings,
|
||||
writeConfig,
|
||||
writeMainConfig,
|
||||
writeTraefikConfigInPath,
|
||||
@@ -77,11 +77,18 @@ import {
|
||||
} from "../trpc";
|
||||
|
||||
export const settingsRouter = createTRPCRouter({
|
||||
getWebServerSettings: protectedProcedure.query(async () => {
|
||||
if (IS_CLOUD) {
|
||||
return null;
|
||||
}
|
||||
const settings = await getWebServerSettings();
|
||||
return settings;
|
||||
}),
|
||||
reloadServer: adminProcedure.mutation(async () => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
await reloadDockerResource("dokploy");
|
||||
await reloadDockerResource("dokploy", undefined, packageInfo.version);
|
||||
return true;
|
||||
}),
|
||||
cleanRedis: adminProcedure.mutation(async () => {
|
||||
@@ -209,11 +216,11 @@ export const settingsRouter = createTRPCRouter({
|
||||
}),
|
||||
saveSSHPrivateKey: adminProcedure
|
||||
.input(apiSaveSSHKey)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
.mutation(async ({ input }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
await updateUser(ctx.user.ownerId, {
|
||||
await updateWebServerSettings({
|
||||
sshPrivateKey: input.sshPrivateKey,
|
||||
});
|
||||
|
||||
@@ -221,36 +228,36 @@ export const settingsRouter = createTRPCRouter({
|
||||
}),
|
||||
assignDomainServer: adminProcedure
|
||||
.input(apiAssignDomain)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
.mutation(async ({ input }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
const user = await updateUser(ctx.user.ownerId, {
|
||||
const settings = await updateWebServerSettings({
|
||||
host: input.host,
|
||||
letsEncryptEmail: input.letsEncryptEmail,
|
||||
certificateType: input.certificateType,
|
||||
https: input.https,
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
if (!settings) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "User not found",
|
||||
message: "Web server settings not found",
|
||||
});
|
||||
}
|
||||
|
||||
updateServerTraefik(user, input.host);
|
||||
updateServerTraefik(settings, input.host);
|
||||
if (input.letsEncryptEmail) {
|
||||
updateLetsEncryptEmail(input.letsEncryptEmail);
|
||||
}
|
||||
|
||||
return user;
|
||||
return settings;
|
||||
}),
|
||||
cleanSSHPrivateKey: adminProcedure.mutation(async ({ ctx }) => {
|
||||
cleanSSHPrivateKey: adminProcedure.mutation(async () => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
await updateUser(ctx.user.ownerId, {
|
||||
await updateWebServerSettings({
|
||||
sshPrivateKey: null,
|
||||
});
|
||||
return true;
|
||||
@@ -310,11 +317,11 @@ export const settingsRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
} else if (!IS_CLOUD) {
|
||||
const userUpdated = await updateUser(ctx.user.ownerId, {
|
||||
const settingsUpdated = await updateWebServerSettings({
|
||||
enableDockerCleanup: input.enableDockerCleanup,
|
||||
});
|
||||
|
||||
if (userUpdated?.enableDockerCleanup) {
|
||||
if (settingsUpdated?.enableDockerCleanup) {
|
||||
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
|
||||
console.log(
|
||||
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
||||
@@ -392,7 +399,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
|
||||
return await getUpdateData();
|
||||
return await getUpdateData(packageInfo.version);
|
||||
}),
|
||||
updateServer: adminProcedure.mutation(async () => {
|
||||
if (IS_CLOUD) {
|
||||
@@ -488,13 +495,28 @@ export const settingsRouter = createTRPCRouter({
|
||||
|
||||
return readConfigInPath(input.path, input.serverId);
|
||||
}),
|
||||
getIp: protectedProcedure.query(async ({ ctx }) => {
|
||||
getIp: protectedProcedure.query(async () => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
return "";
|
||||
}
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
return user.serverIp;
|
||||
const settings = await getWebServerSettings();
|
||||
return settings?.serverIp || "";
|
||||
}),
|
||||
updateServerIp: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
serverIp: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
const settings = await updateWebServerSettings({
|
||||
serverIp: input.serverIp,
|
||||
});
|
||||
return settings;
|
||||
}),
|
||||
|
||||
getOpenApiDocument: protectedProcedure.query(
|
||||
async ({ ctx }): Promise<unknown> => {
|
||||
|
||||
@@ -75,9 +75,9 @@ export const stripeRouter = createTRPCRouter({
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
mode: "subscription",
|
||||
line_items: items,
|
||||
...(stripeCustomerId && {
|
||||
customer: stripeCustomerId,
|
||||
}),
|
||||
...(stripeCustomerId
|
||||
? { customer: stripeCustomerId }
|
||||
: { customer_email: owner.email }),
|
||||
metadata: {
|
||||
adminId: owner.id,
|
||||
},
|
||||
@@ -128,4 +128,39 @@ export const stripeRouter = createTRPCRouter({
|
||||
|
||||
return servers.length < user.serversQuantity;
|
||||
}),
|
||||
|
||||
getInvoices: adminProcedure.query(async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
const stripeCustomerId = user.stripeCustomerId;
|
||||
|
||||
if (!stripeCustomerId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
||||
apiVersion: "2024-09-30.acacia",
|
||||
});
|
||||
|
||||
try {
|
||||
const invoices = await stripe.invoices.list({
|
||||
customer: stripeCustomerId,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
return invoices.data.map((invoice) => ({
|
||||
id: invoice.id,
|
||||
number: invoice.number,
|
||||
status: invoice.status,
|
||||
amountDue: invoice.amount_due,
|
||||
amountPaid: invoice.amount_paid,
|
||||
currency: invoice.currency,
|
||||
created: invoice.created,
|
||||
dueDate: invoice.due_date,
|
||||
hostedInvoiceUrl: invoice.hosted_invoice_url,
|
||||
invoicePdf: invoice.invoice_pdf,
|
||||
}));
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
findUserById,
|
||||
getDokployUrl,
|
||||
getUserByToken,
|
||||
getWebServerSettings,
|
||||
IS_CLOUD,
|
||||
removeUserById,
|
||||
sendEmailNotification,
|
||||
@@ -214,10 +215,11 @@ export const userRouter = createTRPCRouter({
|
||||
}),
|
||||
getMetricsToken: protectedProcedure.query(async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
const settings = await getWebServerSettings();
|
||||
return {
|
||||
serverIp: user.serverIp,
|
||||
serverIp: settings?.serverIp,
|
||||
enabledFeatures: user.enablePaidFeatures,
|
||||
metricsConfig: user?.metricsConfig,
|
||||
metricsConfig: settings?.metricsConfig,
|
||||
};
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
deployPreviewApplication,
|
||||
rebuildApplication,
|
||||
rebuildCompose,
|
||||
rebuildPreviewApplication,
|
||||
updateApplicationStatus,
|
||||
updateCompose,
|
||||
updatePreviewDeployment,
|
||||
@@ -54,7 +55,14 @@ export const deploymentWorker = new Worker(
|
||||
previewStatus: "running",
|
||||
});
|
||||
|
||||
if (job.data.type === "deploy") {
|
||||
if (job.data.type === "redeploy") {
|
||||
await rebuildPreviewApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
previewDeploymentId: job.data.previewDeploymentId,
|
||||
});
|
||||
} else if (job.data.type === "deploy") {
|
||||
await deployPreviewApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
|
||||
@@ -22,7 +22,7 @@ type DeployJob =
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
server?: boolean;
|
||||
type: "deploy";
|
||||
type: "deploy" | "redeploy";
|
||||
applicationType: "application-preview";
|
||||
previewDeploymentId: string;
|
||||
serverId?: string;
|
||||
|
||||
@@ -58,7 +58,7 @@ func (db *DB) GetLastNContainerMetrics(containerName string, limit int) ([]Conta
|
||||
WITH recent_metrics AS (
|
||||
SELECT metrics_json
|
||||
FROM container_metrics
|
||||
WHERE container_name LIKE ? || '%'
|
||||
WHERE container_name = ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ?
|
||||
)
|
||||
@@ -98,7 +98,7 @@ func (db *DB) GetAllMetricsContainer(containerName string) ([]ContainerMetric, e
|
||||
WITH recent_metrics AS (
|
||||
SELECT metrics_json
|
||||
FROM container_metrics
|
||||
WHERE container_name LIKE ? || '%'
|
||||
WHERE container_name = ?
|
||||
ORDER BY timestamp DESC
|
||||
)
|
||||
SELECT metrics_json FROM recent_metrics ORDER BY json_extract(metrics_json, '$.timestamp') ASC
|
||||
|
||||
@@ -57,7 +57,6 @@
|
||||
"drizzle-dbml-generator": "0.10.0",
|
||||
"drizzle-orm": "^0.39.3",
|
||||
"drizzle-zod": "0.5.1",
|
||||
"hi-base32": "^0.5.1",
|
||||
"yaml": "2.8.1",
|
||||
"lodash": "4.17.21",
|
||||
"micromatch": "4.0.8",
|
||||
@@ -67,7 +66,6 @@
|
||||
"node-schedule": "2.1.1",
|
||||
"nodemailer": "6.9.14",
|
||||
"octokit": "3.1.2",
|
||||
"otpauth": "^9.4.0",
|
||||
"pino": "9.4.0",
|
||||
"pino-pretty": "11.2.2",
|
||||
"postgres": "3.4.4",
|
||||
@@ -75,15 +73,16 @@
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"rotating-file-stream": "3.2.3",
|
||||
"shell-quote": "^1.8.1",
|
||||
"slugify": "^1.6.6",
|
||||
"ssh2": "1.15.0",
|
||||
"toml": "3.0.0",
|
||||
"ws": "8.16.0",
|
||||
"zod": "^3.25.32"
|
||||
"zod": "^3.25.32",
|
||||
"semver": "7.7.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/bcrypt": "5.0.2",
|
||||
"@types/dockerode": "3.3.23",
|
||||
@@ -112,4 +111,4 @@
|
||||
"node": "^20.16.0",
|
||||
"pnpm": ">=9.12.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,7 +277,7 @@ table application {
|
||||
replicas integer [not null, default: 1]
|
||||
applicationStatus applicationStatus [not null, default: 'idle']
|
||||
buildType buildType [not null, default: 'nixpacks']
|
||||
railpackVersion text [default: '0.2.2']
|
||||
railpackVersion text [default: '0.15.4']
|
||||
herokuVersion text [default: '24']
|
||||
publishDirectory text
|
||||
isStaticSpa boolean
|
||||
|
||||
@@ -177,7 +177,7 @@ export const applications = pgTable("application", {
|
||||
.notNull()
|
||||
.default("idle"),
|
||||
buildType: buildType("buildType").notNull().default("nixpacks"),
|
||||
railpackVersion: text("railpackVersion").default("0.2.2"),
|
||||
railpackVersion: text("railpackVersion").default("0.15.4"),
|
||||
herokuVersion: text("herokuVersion").default("24"),
|
||||
publishDirectory: text("publishDirectory"),
|
||||
isStaticSpa: boolean("isStaticSpa"),
|
||||
|
||||
@@ -35,3 +35,4 @@ export * from "./ssh-key";
|
||||
export * from "./user";
|
||||
export * from "./utils";
|
||||
export * from "./volume-backups";
|
||||
export * from "./web-server-settings";
|
||||
|
||||
@@ -3,7 +3,6 @@ import { relations } from "drizzle-orm";
|
||||
import {
|
||||
boolean,
|
||||
integer,
|
||||
jsonb,
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
@@ -15,7 +14,6 @@ import { account, apikey, organization } from "./account";
|
||||
import { backups } from "./backups";
|
||||
import { projects } from "./project";
|
||||
import { schedules } from "./schedule";
|
||||
import { certificateType } from "./shared";
|
||||
/**
|
||||
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
|
||||
* database instance for multiple projects.
|
||||
@@ -51,73 +49,10 @@ export const user = pgTable("user", {
|
||||
banExpires: timestamp("ban_expires"),
|
||||
updatedAt: timestamp("updated_at").notNull(),
|
||||
// Admin
|
||||
serverIp: text("serverIp"),
|
||||
certificateType: certificateType("certificateType").notNull().default("none"),
|
||||
https: boolean("https").notNull().default(false),
|
||||
host: text("host"),
|
||||
letsEncryptEmail: text("letsEncryptEmail"),
|
||||
sshPrivateKey: text("sshPrivateKey"),
|
||||
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(true),
|
||||
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
|
||||
role: text("role").notNull().default("user"),
|
||||
// Metrics
|
||||
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
|
||||
allowImpersonation: boolean("allowImpersonation").notNull().default(false),
|
||||
metricsConfig: jsonb("metricsConfig")
|
||||
.$type<{
|
||||
server: {
|
||||
type: "Dokploy" | "Remote";
|
||||
refreshRate: number;
|
||||
port: number;
|
||||
token: string;
|
||||
urlCallback: string;
|
||||
retentionDays: number;
|
||||
cronJob: string;
|
||||
thresholds: {
|
||||
cpu: number;
|
||||
memory: number;
|
||||
};
|
||||
};
|
||||
containers: {
|
||||
refreshRate: number;
|
||||
services: {
|
||||
include: string[];
|
||||
exclude: string[];
|
||||
};
|
||||
};
|
||||
}>()
|
||||
.notNull()
|
||||
.default({
|
||||
server: {
|
||||
type: "Dokploy",
|
||||
refreshRate: 60,
|
||||
port: 4500,
|
||||
token: "",
|
||||
retentionDays: 2,
|
||||
cronJob: "",
|
||||
urlCallback: "",
|
||||
thresholds: {
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
},
|
||||
},
|
||||
containers: {
|
||||
refreshRate: 60,
|
||||
services: {
|
||||
include: [],
|
||||
exclude: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
cleanupCacheApplications: boolean("cleanupCacheApplications")
|
||||
.notNull()
|
||||
.default(false),
|
||||
cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews")
|
||||
.notNull()
|
||||
.default(false),
|
||||
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
|
||||
.notNull()
|
||||
.default(false),
|
||||
stripeCustomerId: text("stripeCustomerId"),
|
||||
stripeSubscriptionId: text("stripeSubscriptionId"),
|
||||
serversQuantity: integer("serversQuantity").notNull().default(0),
|
||||
@@ -203,33 +138,6 @@ export const apiFindOneUserByAuth = createSchema
|
||||
// authId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiSaveSSHKey = createSchema
|
||||
.pick({
|
||||
sshPrivateKey: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiAssignDomain = createSchema
|
||||
.pick({
|
||||
host: true,
|
||||
certificateType: true,
|
||||
letsEncryptEmail: true,
|
||||
https: true,
|
||||
})
|
||||
.required()
|
||||
.partial({
|
||||
letsEncryptEmail: true,
|
||||
https: true,
|
||||
});
|
||||
|
||||
export const apiUpdateDockerCleanup = createSchema
|
||||
.pick({
|
||||
enableDockerCleanup: true,
|
||||
})
|
||||
.required()
|
||||
.extend({
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiTraefikConfig = z.object({
|
||||
traefikConfig: z.string().min(1),
|
||||
@@ -298,32 +206,6 @@ export const apiReadStatsLogs = z.object({
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const apiUpdateWebServerMonitoring = z.object({
|
||||
metricsConfig: z
|
||||
.object({
|
||||
server: z.object({
|
||||
refreshRate: z.number().min(2),
|
||||
port: z.number().min(1),
|
||||
token: z.string(),
|
||||
urlCallback: z.string().url(),
|
||||
retentionDays: z.number().min(1),
|
||||
cronJob: z.string().min(1),
|
||||
thresholds: z.object({
|
||||
cpu: z.number().min(0),
|
||||
memory: z.number().min(0),
|
||||
}),
|
||||
}),
|
||||
containers: z.object({
|
||||
refreshRate: z.number().min(2),
|
||||
services: z.object({
|
||||
include: z.array(z.string()).optional(),
|
||||
exclude: z.array(z.string()).optional(),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
|
||||
export const apiUpdateUser = createSchema.partial().extend({
|
||||
email: z
|
||||
.string()
|
||||
@@ -334,29 +216,4 @@ export const apiUpdateUser = createSchema.partial().extend({
|
||||
currentPassword: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
lastName: z.string().optional(),
|
||||
metricsConfig: z
|
||||
.object({
|
||||
server: z.object({
|
||||
type: z.enum(["Dokploy", "Remote"]),
|
||||
refreshRate: z.number(),
|
||||
port: z.number(),
|
||||
token: z.string(),
|
||||
urlCallback: z.string(),
|
||||
retentionDays: z.number(),
|
||||
cronJob: z.string(),
|
||||
thresholds: z.object({
|
||||
cpu: z.number(),
|
||||
memory: z.number(),
|
||||
}),
|
||||
}),
|
||||
containers: z.object({
|
||||
refreshRate: z.number(),
|
||||
services: z.object({
|
||||
include: z.array(z.string()),
|
||||
exclude: z.array(z.string()),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.optional(),
|
||||
logCleanupCron: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
178
packages/server/src/db/schema/web-server-settings.ts
Normal file
178
packages/server/src/db/schema/web-server-settings.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { boolean, jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { certificateType } from "./shared";
|
||||
|
||||
export const webServerSettings = pgTable("webServerSettings", {
|
||||
id: text("id")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
// Web Server Configuration
|
||||
serverIp: text("serverIp"),
|
||||
certificateType: certificateType("certificateType").notNull().default("none"),
|
||||
https: boolean("https").notNull().default(false),
|
||||
host: text("host"),
|
||||
letsEncryptEmail: text("letsEncryptEmail"),
|
||||
sshPrivateKey: text("sshPrivateKey"),
|
||||
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(true),
|
||||
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
|
||||
// Metrics Configuration
|
||||
metricsConfig: jsonb("metricsConfig")
|
||||
.$type<{
|
||||
server: {
|
||||
type: "Dokploy" | "Remote";
|
||||
refreshRate: number;
|
||||
port: number;
|
||||
token: string;
|
||||
urlCallback: string;
|
||||
retentionDays: number;
|
||||
cronJob: string;
|
||||
thresholds: {
|
||||
cpu: number;
|
||||
memory: number;
|
||||
};
|
||||
};
|
||||
containers: {
|
||||
refreshRate: number;
|
||||
services: {
|
||||
include: string[];
|
||||
exclude: string[];
|
||||
};
|
||||
};
|
||||
}>()
|
||||
.notNull()
|
||||
.default({
|
||||
server: {
|
||||
type: "Dokploy",
|
||||
refreshRate: 60,
|
||||
port: 4500,
|
||||
token: "",
|
||||
retentionDays: 2,
|
||||
cronJob: "",
|
||||
urlCallback: "",
|
||||
thresholds: {
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
},
|
||||
},
|
||||
containers: {
|
||||
refreshRate: 60,
|
||||
services: {
|
||||
include: [],
|
||||
exclude: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
// Cache Cleanup Configuration
|
||||
cleanupCacheApplications: boolean("cleanupCacheApplications")
|
||||
.notNull()
|
||||
.default(false),
|
||||
cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews")
|
||||
.notNull()
|
||||
.default(false),
|
||||
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
|
||||
.notNull()
|
||||
.default(false),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const webServerSettingsRelations = relations(
|
||||
webServerSettings,
|
||||
() => ({}),
|
||||
);
|
||||
|
||||
const createSchema = createInsertSchema(webServerSettings, {
|
||||
id: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiUpdateWebServerSettings = createSchema.partial().extend({
|
||||
serverIp: z.string().optional(),
|
||||
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
||||
https: z.boolean().optional(),
|
||||
host: z.string().optional(),
|
||||
letsEncryptEmail: z.string().email().optional().nullable(),
|
||||
sshPrivateKey: z.string().optional(),
|
||||
enableDockerCleanup: z.boolean().optional(),
|
||||
logCleanupCron: z.string().optional().nullable(),
|
||||
metricsConfig: z
|
||||
.object({
|
||||
server: z.object({
|
||||
type: z.enum(["Dokploy", "Remote"]),
|
||||
refreshRate: z.number(),
|
||||
port: z.number(),
|
||||
token: z.string(),
|
||||
urlCallback: z.string(),
|
||||
retentionDays: z.number(),
|
||||
cronJob: z.string(),
|
||||
thresholds: z.object({
|
||||
cpu: z.number(),
|
||||
memory: z.number(),
|
||||
}),
|
||||
}),
|
||||
containers: z.object({
|
||||
refreshRate: z.number(),
|
||||
services: z.object({
|
||||
include: z.array(z.string()),
|
||||
exclude: z.array(z.string()),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.optional(),
|
||||
cleanupCacheApplications: z.boolean().optional(),
|
||||
cleanupCacheOnPreviews: z.boolean().optional(),
|
||||
cleanupCacheOnCompose: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const apiAssignDomain = z
|
||||
.object({
|
||||
host: z.string(),
|
||||
certificateType: z.enum(["letsencrypt", "none", "custom"]),
|
||||
letsEncryptEmail: z.string().email().optional().nullable(),
|
||||
https: z.boolean().optional(),
|
||||
})
|
||||
.required()
|
||||
.partial({
|
||||
letsEncryptEmail: true,
|
||||
https: true,
|
||||
});
|
||||
|
||||
export const apiSaveSSHKey = z
|
||||
.object({
|
||||
sshPrivateKey: z.string(),
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateDockerCleanup = z.object({
|
||||
enableDockerCleanup: z.boolean(),
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiUpdateWebServerMonitoring = z.object({
|
||||
metricsConfig: z
|
||||
.object({
|
||||
server: z.object({
|
||||
refreshRate: z.number().min(2),
|
||||
port: z.number().min(1),
|
||||
token: z.string(),
|
||||
urlCallback: z.string().url(),
|
||||
retentionDays: z.number().min(1),
|
||||
cronJob: z.string().min(1),
|
||||
thresholds: z.object({
|
||||
cpu: z.number().min(0),
|
||||
memory: z.number().min(0),
|
||||
}),
|
||||
}),
|
||||
containers: z.object({
|
||||
refreshRate: z.number().min(2),
|
||||
services: z.object({
|
||||
include: z.array(z.string()).optional(),
|
||||
exclude: z.array(z.string()).optional(),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
@@ -41,6 +41,7 @@ export * from "./services/settings";
|
||||
export * from "./services/ssh-key";
|
||||
export * from "./services/user";
|
||||
export * from "./services/volume-backups";
|
||||
export * from "./services/web-server-settings";
|
||||
export * from "./setup/config-paths";
|
||||
export * from "./setup/monitoring-setup";
|
||||
export * from "./setup/postgres-setup";
|
||||
|
||||
@@ -9,7 +9,10 @@ import { IS_CLOUD } from "../constants";
|
||||
import { db } from "../db";
|
||||
import * as schema from "../db/schema";
|
||||
import { getUserByToken } from "../services/admin";
|
||||
import { updateUser } from "../services/user";
|
||||
import {
|
||||
getWebServerSettings,
|
||||
updateWebServerSettings,
|
||||
} from "../services/web-server-settings";
|
||||
import { getHubSpotUTK, submitToHubSpot } from "../utils/tracking/hubspot";
|
||||
import { sendEmail } from "../verification/send-verification-email";
|
||||
import { getPublicIpWithFallback } from "../wss/utils";
|
||||
@@ -35,22 +38,20 @@ const { handler, api } = betterAuth({
|
||||
},
|
||||
...(!IS_CLOUD && {
|
||||
async trustedOrigins() {
|
||||
const admin = await db.query.member.findFirst({
|
||||
where: eq(schema.member.role, "owner"),
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (admin?.user) {
|
||||
return [
|
||||
...(admin.user.serverIp
|
||||
? [`http://${admin.user.serverIp}:3000`]
|
||||
: []),
|
||||
...(admin.user.host ? [`https://${admin.user.host}`] : []),
|
||||
];
|
||||
const settings = await getWebServerSettings();
|
||||
if (!settings) {
|
||||
return [];
|
||||
}
|
||||
return [];
|
||||
return [
|
||||
...(settings?.serverIp ? [`http://${settings?.serverIp}:3000`] : []),
|
||||
...(settings?.host ? [`https://${settings?.host}`] : []),
|
||||
...(process.env.NODE_ENV === "development"
|
||||
? [
|
||||
"http://localhost:3000",
|
||||
"https://absolutely-handy-falcon.ngrok-free.app",
|
||||
]
|
||||
: []),
|
||||
];
|
||||
},
|
||||
}),
|
||||
emailVerification: {
|
||||
@@ -122,7 +123,7 @@ const { handler, api } = betterAuth({
|
||||
});
|
||||
|
||||
if (!IS_CLOUD) {
|
||||
await updateUser(user.id, {
|
||||
await updateWebServerSettings({
|
||||
serverIp: await getPublicIpWithFallback(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
import { getWebServerSettings } from "./web-server-settings";
|
||||
|
||||
export const findUserById = async (userId: string) => {
|
||||
const userResult = await db.query.user.findFirst({
|
||||
@@ -107,11 +108,11 @@ export const getDokployUrl = async () => {
|
||||
if (IS_CLOUD) {
|
||||
return "https://app.dokploy.com";
|
||||
}
|
||||
const owner = await findOwner();
|
||||
const settings = await getWebServerSettings();
|
||||
|
||||
if (owner.user.host) {
|
||||
const protocol = owner.user.https ? "https" : "http";
|
||||
return `${protocol}://${owner.user.host}`;
|
||||
if (settings?.host) {
|
||||
const protocol = settings?.https ? "https" : "http";
|
||||
return `${protocol}://${settings?.host}`;
|
||||
}
|
||||
return `http://${owner.user.serverIp}:${process.env.PORT}`;
|
||||
return `http://${settings?.serverIp}:${process.env.PORT}`;
|
||||
};
|
||||
|
||||
@@ -6,8 +6,8 @@ import { generateObject } from "ai";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
import { findOrganizationById } from "./admin";
|
||||
import { findServerById } from "./server";
|
||||
import { getWebServerSettings } from "./web-server-settings";
|
||||
|
||||
export const getAiSettingsByOrganizationId = async (organizationId: string) => {
|
||||
const aiSettings = await db.query.ai.findMany({
|
||||
@@ -79,8 +79,8 @@ export const suggestVariants = async ({
|
||||
|
||||
let ip = "";
|
||||
if (!IS_CLOUD) {
|
||||
const organization = await findOrganizationById(organizationId);
|
||||
ip = organization?.owner.serverIp || "";
|
||||
const settings = await getWebServerSettings();
|
||||
ip = settings?.serverIp || "";
|
||||
}
|
||||
|
||||
if (serverId) {
|
||||
|
||||
@@ -452,6 +452,137 @@ export const deployPreviewApplication = async ({
|
||||
return true;
|
||||
};
|
||||
|
||||
export const rebuildPreviewApplication = async ({
|
||||
applicationId,
|
||||
titleLog = "Rebuild Preview Deployment",
|
||||
descriptionLog = "",
|
||||
previewDeploymentId,
|
||||
}: {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
previewDeploymentId: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
const previewDeployment =
|
||||
await findPreviewDeploymentById(previewDeploymentId);
|
||||
|
||||
const deployment = await createDeploymentPreview({
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
previewDeploymentId: previewDeploymentId,
|
||||
});
|
||||
|
||||
const previewDomain = getDomainHost(previewDeployment?.domain as Domain);
|
||||
const issueParams = {
|
||||
owner: application?.owner || "",
|
||||
repository: application?.repository || "",
|
||||
issue_number: previewDeployment.pullRequestNumber,
|
||||
comment_id: Number.parseInt(previewDeployment.pullRequestCommentId),
|
||||
githubId: application?.githubId || "",
|
||||
};
|
||||
|
||||
try {
|
||||
const commentExists = await issueCommentExists({
|
||||
...issueParams,
|
||||
});
|
||||
if (!commentExists) {
|
||||
const result = await createPreviewDeploymentComment({
|
||||
...issueParams,
|
||||
previewDomain,
|
||||
appName: previewDeployment.appName,
|
||||
githubId: application?.githubId || "",
|
||||
previewDeploymentId,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Pull request comment not found",
|
||||
});
|
||||
}
|
||||
|
||||
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
|
||||
}
|
||||
|
||||
const buildingComment = getIssueComment(
|
||||
application.name,
|
||||
"running",
|
||||
previewDomain,
|
||||
);
|
||||
await updateIssueComment({
|
||||
...issueParams,
|
||||
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
||||
});
|
||||
|
||||
// Set application properties for preview deployment
|
||||
application.appName = previewDeployment.appName;
|
||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
application.rollbackActive = false;
|
||||
application.buildRegistry = null;
|
||||
application.rollbackRegistry = null;
|
||||
application.registry = null;
|
||||
|
||||
const serverId = application.serverId;
|
||||
let command = "set -e;";
|
||||
// Only rebuild, don't clone repository
|
||||
command += await getBuildCommand(application);
|
||||
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, commandWithLog);
|
||||
} else {
|
||||
await execAsync(commandWithLog);
|
||||
}
|
||||
await mechanizeDockerContainer(application);
|
||||
|
||||
const successComment = getIssueComment(
|
||||
application.name,
|
||||
"success",
|
||||
previewDomain,
|
||||
);
|
||||
await updateIssueComment({
|
||||
...issueParams,
|
||||
body: `### Dokploy Preview Deployment\n\n${successComment}`,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updatePreviewDeployment(previewDeploymentId, {
|
||||
previewStatus: "done",
|
||||
});
|
||||
} catch (error) {
|
||||
let command = "";
|
||||
|
||||
// Only log details for non-ExecError errors
|
||||
if (!(error instanceof ExecError)) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const encodedMessage = encodeBase64(message);
|
||||
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
|
||||
}
|
||||
|
||||
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
|
||||
const serverId = application.buildServerId || application.serverId;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
|
||||
const comment = getIssueComment(application.name, "error", previewDomain);
|
||||
await updateIssueComment({
|
||||
...issueParams,
|
||||
body: `### Dokploy Preview Deployment\n\n${comment}`,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updatePreviewDeployment(previewDeploymentId, {
|
||||
previewStatus: "error",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getApplicationStats = async (appName: string) => {
|
||||
if (appName === "dokploy") {
|
||||
return await getAdvancedStats(appName);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import dns from "node:dns";
|
||||
import { promisify } from "node:util";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
|
||||
import { generateRandomDomain } from "@dokploy/server/templates";
|
||||
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";
|
||||
import { findUserById } from "./admin";
|
||||
import { findApplicationById } from "./application";
|
||||
import { detectCDNProvider } from "./cdn";
|
||||
import { findServerById } from "./server";
|
||||
@@ -61,9 +61,9 @@ export const generateTraefikMeDomain = async (
|
||||
projectName: appName,
|
||||
});
|
||||
}
|
||||
const admin = await findUserById(userId);
|
||||
const settings = await getWebServerSettings();
|
||||
return generateRandomDomain({
|
||||
serverIp: admin?.serverIp || "",
|
||||
serverIp: settings?.serverIp || "",
|
||||
projectName: appName,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -13,11 +13,11 @@ import { removeDirectoryCode } from "../utils/filesystem/directory";
|
||||
import { authGithub } from "../utils/providers/github";
|
||||
import { removeTraefikConfig } from "../utils/traefik/application";
|
||||
import { manageDomain } from "../utils/traefik/domain";
|
||||
import { findUserById } from "./admin";
|
||||
import { findApplicationById } from "./application";
|
||||
import { removeDeploymentsByPreviewDeploymentId } from "./deployment";
|
||||
import { createDomain } from "./domain";
|
||||
import { type Github, getIssueComment } from "./github";
|
||||
import { getWebServerSettings } from "./web-server-settings";
|
||||
|
||||
export type PreviewDeployment = typeof previewDeployments.$inferSelect;
|
||||
|
||||
@@ -253,8 +253,8 @@ const generateWildcardDomain = async (
|
||||
}
|
||||
|
||||
if (!ip) {
|
||||
const admin = await findUserById(userId);
|
||||
ip = admin?.serverIp || "";
|
||||
const settings = await getWebServerSettings();
|
||||
ip = settings?.serverIp || "";
|
||||
}
|
||||
|
||||
const slugIp = ip.replaceAll(".", "-");
|
||||
|
||||
@@ -5,12 +5,12 @@ import {
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
import semver from "semver";
|
||||
import {
|
||||
initializeStandaloneTraefik,
|
||||
initializeTraefikService,
|
||||
type TraefikOptions,
|
||||
} from "../setup/traefik-setup";
|
||||
|
||||
export interface IUpdateData {
|
||||
latestVersion: string | null;
|
||||
updateAvailable: boolean;
|
||||
@@ -55,56 +55,95 @@ export const getServiceImageDigest = async () => {
|
||||
};
|
||||
|
||||
/** Returns latest version number and information whether server update is available by comparing current image's digest against digest for provided image tag via Docker hub API. */
|
||||
export const getUpdateData = async (): Promise<IUpdateData> => {
|
||||
let currentDigest: string;
|
||||
export const getUpdateData = async (
|
||||
currentVersion: string,
|
||||
): Promise<IUpdateData> => {
|
||||
try {
|
||||
currentDigest = await getServiceImageDigest();
|
||||
} catch (error) {
|
||||
// TODO: Docker versions 29.0.0 change the way to get the service image digest, so we need to update this in the future we upgrade to that version.
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
const baseUrl =
|
||||
"https://hub.docker.com/v2/repositories/dokploy/dokploy/tags";
|
||||
let url: string | null = `${baseUrl}?page_size=100`;
|
||||
let allResults: { digest: string; name: string }[] = [];
|
||||
|
||||
const baseUrl = "https://hub.docker.com/v2/repositories/dokploy/dokploy/tags";
|
||||
let url: string | null = `${baseUrl}?page_size=100`;
|
||||
let allResults: { digest: string; name: string }[] = [];
|
||||
while (url) {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
// Fetch all tags from Docker Hub
|
||||
while (url) {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
const data = (await response.json()) as {
|
||||
next: string | null;
|
||||
results: { digest: string; name: string }[];
|
||||
};
|
||||
const data = (await response.json()) as {
|
||||
next: string | null;
|
||||
results: { digest: string; name: string }[];
|
||||
};
|
||||
|
||||
allResults = allResults.concat(data.results);
|
||||
url = data?.next;
|
||||
}
|
||||
allResults = allResults.concat(data.results);
|
||||
url = data?.next;
|
||||
}
|
||||
|
||||
const imageTag = getDokployImageTag();
|
||||
const searchedDigest = allResults.find((t) => t.name === imageTag)?.digest;
|
||||
const currentImageTag = getDokployImageTag();
|
||||
|
||||
if (!searchedDigest) {
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
// Special handling for canary and feature branches
|
||||
// For development versions (canary/feature), don't perform update checks
|
||||
// These are unstable versions that change frequently, and users on these
|
||||
// branches are expected to manually manage updates
|
||||
if (currentImageTag === "canary" || currentImageTag === "feature") {
|
||||
const currentDigest = await getServiceImageDigest();
|
||||
const latestDigest = allResults.find(
|
||||
(t) => t.name === currentImageTag,
|
||||
)?.digest;
|
||||
if (!latestDigest) {
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
if (currentDigest !== latestDigest) {
|
||||
return {
|
||||
latestVersion: currentImageTag,
|
||||
updateAvailable: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
latestVersion: currentImageTag,
|
||||
updateAvailable: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (imageTag === "latest") {
|
||||
const versionedTag = allResults.find(
|
||||
(t) => t.digest === searchedDigest && t.name.startsWith("v"),
|
||||
);
|
||||
// For stable versions, use semver comparison
|
||||
// Find the "latest" tag and get its digest
|
||||
const latestTag = allResults.find((t) => t.name === "latest");
|
||||
|
||||
if (!versionedTag) {
|
||||
if (!latestTag) {
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
|
||||
const { name: latestVersion, digest } = versionedTag;
|
||||
const updateAvailable = digest !== currentDigest;
|
||||
// Find the versioned tag (v0.x.x) that has the same digest as "latest"
|
||||
const latestVersionTag = allResults.find(
|
||||
(t) => t.digest === latestTag.digest && t.name.startsWith("v"),
|
||||
);
|
||||
|
||||
return { latestVersion, updateAvailable };
|
||||
if (!latestVersionTag) {
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
|
||||
const latestVersion = latestVersionTag.name;
|
||||
|
||||
// Use semver to compare versions for stable releases
|
||||
const cleanedCurrent = semver.clean(currentVersion);
|
||||
const cleanedLatest = semver.clean(latestVersion);
|
||||
|
||||
if (!cleanedCurrent || !cleanedLatest) {
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
|
||||
// Check if the latest version is greater than the current version
|
||||
const updateAvailable = semver.gt(cleanedLatest, cleanedCurrent);
|
||||
|
||||
return {
|
||||
latestVersion,
|
||||
updateAvailable,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error fetching update data:", error);
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
const updateAvailable = searchedDigest !== currentDigest;
|
||||
return { latestVersion: imageTag, updateAvailable };
|
||||
};
|
||||
|
||||
interface TreeDataItem {
|
||||
@@ -254,11 +293,22 @@ fi`;
|
||||
export const reloadDockerResource = async (
|
||||
resourceName: string,
|
||||
serverId?: string,
|
||||
version?: string,
|
||||
) => {
|
||||
const resourceType = await getDockerResourceType(resourceName, serverId);
|
||||
let command = "";
|
||||
if (resourceType === "service") {
|
||||
command = `docker service update --force ${resourceName}`;
|
||||
if (resourceName === "dokploy") {
|
||||
const currentImageTag = getDokployImageTag();
|
||||
let imageTag = version;
|
||||
if (currentImageTag === "canary" || currentImageTag === "feature") {
|
||||
imageTag = currentImageTag;
|
||||
}
|
||||
|
||||
command = `docker service update --force --image dokploy/dokploy:${imageTag} ${resourceName}`;
|
||||
} else {
|
||||
command = `docker service update --force ${resourceName}`;
|
||||
}
|
||||
} else if (resourceType === "standalone") {
|
||||
command = `docker restart ${resourceName}`;
|
||||
} else {
|
||||
|
||||
44
packages/server/src/services/web-server-settings.ts
Normal file
44
packages/server/src/services/web-server-settings.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { webServerSettings } from "@dokploy/server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
/**
|
||||
* Get the web server settings (singleton - only one row should exist)
|
||||
*/
|
||||
export const getWebServerSettings = async () => {
|
||||
const settings = await db.query.webServerSettings.findFirst({
|
||||
orderBy: (settings, { asc }) => [asc(settings.createdAt)],
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
// Create default settings if none exist
|
||||
const [newSettings] = await db
|
||||
.insert(webServerSettings)
|
||||
.values({})
|
||||
.returning();
|
||||
|
||||
return newSettings;
|
||||
}
|
||||
|
||||
return settings;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update web server settings
|
||||
*/
|
||||
export const updateWebServerSettings = async (
|
||||
updates: Partial<typeof webServerSettings.$inferInsert>,
|
||||
) => {
|
||||
const current = await getWebServerSettings();
|
||||
|
||||
const [updated] = await db
|
||||
.update(webServerSettings)
|
||||
.set({
|
||||
...updates,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(webServerSettings.id, current?.id ?? ""))
|
||||
.returning();
|
||||
|
||||
return updated;
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { findServerById } from "@dokploy/server/services/server";
|
||||
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
|
||||
import type { ContainerCreateOptions } from "dockerode";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
import { findUserById } from "../services/admin";
|
||||
import { getDokployImageTag } from "../services/settings";
|
||||
import { pullImage, pullRemoteImage } from "../utils/docker/utils";
|
||||
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
|
||||
@@ -83,8 +83,8 @@ export const setupMonitoring = async (serverId: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const setupWebMonitoring = async (userId: string) => {
|
||||
const user = await findUserById(userId);
|
||||
export const setupWebMonitoring = async () => {
|
||||
const webServerSettings = await getWebServerSettings();
|
||||
|
||||
const containerName = "dokploy-monitoring";
|
||||
let imageName = "dokploy/monitoring:latest";
|
||||
@@ -99,7 +99,7 @@ export const setupWebMonitoring = async (userId: string) => {
|
||||
|
||||
const settings: ContainerCreateOptions = {
|
||||
name: containerName,
|
||||
Env: [`METRICS_CONFIG=${JSON.stringify(user?.metricsConfig)}`],
|
||||
Env: [`METRICS_CONFIG=${JSON.stringify(webServerSettings?.metricsConfig)}`],
|
||||
Image: imageName,
|
||||
HostConfig: {
|
||||
// Memory: 100 * 1024 * 1024, // 100MB en bytes
|
||||
@@ -110,9 +110,9 @@ export const setupWebMonitoring = async (userId: string) => {
|
||||
Name: "always",
|
||||
},
|
||||
PortBindings: {
|
||||
[`${user?.metricsConfig?.server?.port}/tcp`]: [
|
||||
[`${webServerSettings?.metricsConfig?.server?.port}/tcp`]: [
|
||||
{
|
||||
HostPort: user?.metricsConfig?.server?.port.toString(),
|
||||
HostPort: webServerSettings?.metricsConfig?.server?.port.toString(),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -126,7 +126,7 @@ export const setupWebMonitoring = async (userId: string) => {
|
||||
// NetworkMode: "host",
|
||||
},
|
||||
ExposedPorts: {
|
||||
[`${user?.metricsConfig?.server?.port}/tcp`]: {},
|
||||
[`${webServerSettings?.metricsConfig?.server?.port}/tcp`]: {},
|
||||
},
|
||||
};
|
||||
const docker = await getRemoteDocker();
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import path from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { IS_CLOUD, paths } from "@dokploy/server/constants";
|
||||
import { getDokployUrl } from "@dokploy/server/services/admin";
|
||||
import {
|
||||
createServerDeployment,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import { findServerById } from "@dokploy/server/services/server";
|
||||
import {
|
||||
findServerById,
|
||||
updateServerById,
|
||||
} from "@dokploy/server/services/server";
|
||||
import {
|
||||
getDefaultMiddlewares,
|
||||
getDefaultServerTraefikConfig,
|
||||
@@ -16,6 +20,15 @@ import {
|
||||
import slug from "slugify";
|
||||
import { Client } from "ssh2";
|
||||
import { recreateDirectory } from "../utils/filesystem/directory";
|
||||
import { setupMonitoring } from "./monitoring-setup";
|
||||
|
||||
const generateToken = () => {
|
||||
const array = new Uint8Array(64);
|
||||
crypto.getRandomValues(array);
|
||||
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
|
||||
"",
|
||||
);
|
||||
};
|
||||
|
||||
export const slugify = (text: string | undefined) => {
|
||||
if (!text) {
|
||||
@@ -59,6 +72,29 @@ export const serverSetup = async (
|
||||
);
|
||||
await installRequirements(serverId, onData);
|
||||
|
||||
if (IS_CLOUD) {
|
||||
onData?.("\nConfiguring Monitoring: 🔄\n");
|
||||
|
||||
const baseUrl = await getDokployUrl();
|
||||
const token = generateToken();
|
||||
const urlCallback = `${baseUrl}/api/trpc/notification.receiveNotification`;
|
||||
|
||||
// Update server with monitoring configuration
|
||||
await updateServerById(serverId, {
|
||||
metricsConfig: {
|
||||
server: {
|
||||
...server.metricsConfig.server,
|
||||
token: token,
|
||||
urlCallback: urlCallback,
|
||||
},
|
||||
containers: server.metricsConfig.containers,
|
||||
},
|
||||
});
|
||||
|
||||
await setupMonitoring(serverId);
|
||||
onData?.("\nMonitoring Configured: ✅\n");
|
||||
}
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
|
||||
onData?.("\nSetup Server: ✅\n");
|
||||
@@ -629,7 +665,7 @@ const installNixpacks = () => `
|
||||
if command_exists nixpacks; then
|
||||
echo "Nixpacks already installed ✅"
|
||||
else
|
||||
export NIXPACKS_VERSION=1.39.0
|
||||
export NIXPACKS_VERSION=1.41.0
|
||||
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
|
||||
echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
|
||||
fi
|
||||
@@ -639,7 +675,7 @@ const installRailpack = () => `
|
||||
if command_exists railpack; then
|
||||
echo "Railpack already installed ✅"
|
||||
else
|
||||
export RAILPACK_VERSION=0.2.2
|
||||
export RAILPACK_VERSION=0.15.4
|
||||
bash -c "$(curl -fsSL https://railpack.com/install.sh)"
|
||||
echo "Railpack version $RAILPACK_VERSION installed ✅"
|
||||
fi
|
||||
@@ -653,8 +689,8 @@ const installBuildpacks = () => `
|
||||
if command_exists pack; then
|
||||
echo "Buildpacks already installed ✅"
|
||||
else
|
||||
BUILDPACKS_VERSION=0.35.0
|
||||
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v$BUILDPACKS_VERSION-linux$SUFFIX.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
||||
BUILDPACKS_VERSION=0.39.1
|
||||
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.39.1/pack-v$BUILDPACKS_VERSION-linux$SUFFIX.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
||||
echo "Buildpacks version $BUILDPACKS_VERSION installed ✅"
|
||||
fi
|
||||
`;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { findOwner } from "@dokploy/server/services/admin";
|
||||
import { updateUser } from "@dokploy/server/services/user";
|
||||
import {
|
||||
getWebServerSettings,
|
||||
updateWebServerSettings,
|
||||
} from "@dokploy/server/services/web-server-settings";
|
||||
import { scheduledJobs, scheduleJob } from "node-schedule";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
|
||||
@@ -29,12 +31,9 @@ export const startLogCleanup = async (
|
||||
}
|
||||
});
|
||||
|
||||
const owner = await findOwner();
|
||||
if (owner) {
|
||||
await updateUser(owner.user.id, {
|
||||
logCleanupCron: cronExpression,
|
||||
});
|
||||
}
|
||||
await updateWebServerSettings({
|
||||
logCleanupCron: cronExpression,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -51,12 +50,9 @@ export const stopLogCleanup = async (): Promise<boolean> => {
|
||||
}
|
||||
|
||||
// Update database
|
||||
const owner = await findOwner();
|
||||
if (owner) {
|
||||
await updateUser(owner.user.id, {
|
||||
logCleanupCron: null,
|
||||
});
|
||||
}
|
||||
await updateWebServerSettings({
|
||||
logCleanupCron: null,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -69,8 +65,8 @@ export const getLogCleanupStatus = async (): Promise<{
|
||||
enabled: boolean;
|
||||
cronExpression: string | null;
|
||||
}> => {
|
||||
const owner = await findOwner();
|
||||
const cronExpression = owner?.user.logCleanupCron ?? null;
|
||||
const settings = await getWebServerSettings();
|
||||
const cronExpression = settings?.logCleanupCron ?? null;
|
||||
return {
|
||||
enabled: cronExpression !== null,
|
||||
cronExpression,
|
||||
|
||||
@@ -103,8 +103,9 @@ export function selectAIProvider(config: { apiUrl: string; apiKey: string }) {
|
||||
return createOpenAICompatible({
|
||||
name: "gemini",
|
||||
baseURL: config.apiUrl,
|
||||
queryParams: { key: config.apiKey },
|
||||
headers: {},
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
},
|
||||
});
|
||||
case "custom":
|
||||
return createOpenAICompatible({
|
||||
|
||||
@@ -2,6 +2,7 @@ import path from "node:path";
|
||||
import { member } from "@dokploy/server/db/schema";
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import { getAllServers } from "@dokploy/server/services/server";
|
||||
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { scheduleJob } from "node-schedule";
|
||||
import { db } from "../../db/index";
|
||||
@@ -25,7 +26,9 @@ export const initCronJobs = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (admin?.user?.enableDockerCleanup) {
|
||||
const webServerSettings = await getWebServerSettings();
|
||||
|
||||
if (webServerSettings?.enableDockerCleanup) {
|
||||
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
|
||||
console.log(
|
||||
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
|
||||
@@ -82,9 +85,12 @@ export const initCronJobs = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (admin?.user?.logCleanupCron) {
|
||||
console.log("Starting log requests cleanup", admin.user.logCleanupCron);
|
||||
await startLogCleanup(admin.user.logCleanupCron);
|
||||
if (webServerSettings?.logCleanupCron) {
|
||||
console.log(
|
||||
"Starting log requests cleanup",
|
||||
webServerSettings.logCleanupCron,
|
||||
);
|
||||
await startLogCleanup(webServerSettings.logCleanupCron);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ export const createCommand = (compose: ComposeNested) => {
|
||||
if (composeType === "docker-compose") {
|
||||
command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`;
|
||||
} else if (composeType === "stack") {
|
||||
command = `stack deploy -c ${path} ${appName} --prune`;
|
||||
command = `stack deploy -c ${path} ${appName} --prune --with-registry-auth`;
|
||||
}
|
||||
|
||||
return command;
|
||||
@@ -134,6 +134,7 @@ const getExportEnvCommand = (compose: ComposeNested) => {
|
||||
const envVars = getEnviromentVariablesObject(
|
||||
compose.env,
|
||||
compose.environment.project.env,
|
||||
compose.environment.env,
|
||||
);
|
||||
const exports = Object.entries(envVars)
|
||||
.map(([key, value]) => `${key}=${quote([value])}`)
|
||||
|
||||
@@ -117,7 +117,7 @@ const getRegistryCommands = (
|
||||
): string => {
|
||||
return `
|
||||
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" ;
|
||||
echo "${registry.password}" | docker login ${registry.registryUrl} -u ${registry.username} --password-stdin || {
|
||||
echo "${registry.password}" | docker login ${registry.registryUrl} -u '${registry.username}' --password-stdin || {
|
||||
echo "❌ DockerHub Failed" ;
|
||||
exit 1;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import type { User } from "@dokploy/server/services/user";
|
||||
import type { webServerSettings } from "@dokploy/server/db/schema/web-server-settings";
|
||||
import { parse, stringify } from "yaml";
|
||||
import {
|
||||
loadOrCreateConfig,
|
||||
@@ -12,10 +12,10 @@ import type { FileConfig } from "./file-types";
|
||||
import type { MainTraefikConfig } from "./types";
|
||||
|
||||
export const updateServerTraefik = (
|
||||
user: User | null,
|
||||
settings: typeof webServerSettings.$inferSelect | null,
|
||||
newHost: string | null,
|
||||
) => {
|
||||
const { https, certificateType } = user || {};
|
||||
const { https, certificateType } = settings || {};
|
||||
const appName = "dokploy";
|
||||
const config: FileConfig = loadOrCreateConfig(appName);
|
||||
|
||||
|
||||
99
pnpm-lock.yaml
generated
99
pnpm-lock.yaml
generated
@@ -51,9 +51,6 @@ importers:
|
||||
'@hono/zod-validator':
|
||||
specifier: 0.3.0
|
||||
version: 0.3.0(hono@4.7.10)(zod@3.25.32)
|
||||
'@nerimity/mimiqueue':
|
||||
specifier: 1.2.3
|
||||
version: 1.2.3(redis@4.7.0)
|
||||
dotenv:
|
||||
specifier: ^16.4.5
|
||||
version: 16.4.5
|
||||
@@ -313,9 +310,6 @@ importers:
|
||||
fancy-ansi:
|
||||
specifier: ^0.1.3
|
||||
version: 0.1.3
|
||||
hi-base32:
|
||||
specifier: ^0.5.1
|
||||
version: 0.5.1
|
||||
i18next:
|
||||
specifier: ^23.16.8
|
||||
version: 23.16.8
|
||||
@@ -364,9 +358,6 @@ importers:
|
||||
octokit:
|
||||
specifier: 3.1.2
|
||||
version: 3.1.2
|
||||
otpauth:
|
||||
specifier: ^9.4.0
|
||||
version: 9.4.0
|
||||
pino:
|
||||
specifier: 9.4.0
|
||||
version: 9.4.0
|
||||
@@ -406,9 +397,9 @@ importers:
|
||||
recharts:
|
||||
specifier: ^2.15.3
|
||||
version: 2.15.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
rotating-file-stream:
|
||||
specifier: 3.2.3
|
||||
version: 3.2.3
|
||||
semver:
|
||||
specifier: 7.7.3
|
||||
version: 7.7.3
|
||||
shell-quote:
|
||||
specifier: ^1.8.1
|
||||
version: 1.8.2
|
||||
@@ -494,6 +485,9 @@ importers:
|
||||
'@types/react-dom':
|
||||
specifier: 18.3.0
|
||||
version: 18.3.0
|
||||
'@types/semver':
|
||||
specifier: 7.7.1
|
||||
version: 7.7.1
|
||||
'@types/shell-quote':
|
||||
specifier: ^1.7.5
|
||||
version: 1.7.5
|
||||
@@ -681,9 +675,6 @@ importers:
|
||||
drizzle-zod:
|
||||
specifier: 0.5.1
|
||||
version: 0.5.1(drizzle-orm@0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.7)(postgres@3.4.4))(zod@3.25.32)
|
||||
hi-base32:
|
||||
specifier: ^0.5.1
|
||||
version: 0.5.1
|
||||
lodash:
|
||||
specifier: 4.17.21
|
||||
version: 4.17.21
|
||||
@@ -708,9 +699,6 @@ importers:
|
||||
octokit:
|
||||
specifier: 3.1.2
|
||||
version: 3.1.2
|
||||
otpauth:
|
||||
specifier: ^9.4.0
|
||||
version: 9.4.0
|
||||
pino:
|
||||
specifier: 9.4.0
|
||||
version: 9.4.0
|
||||
@@ -732,9 +720,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: 18.2.0
|
||||
version: 18.2.0(react@18.2.0)
|
||||
rotating-file-stream:
|
||||
specifier: 3.2.3
|
||||
version: 3.2.3
|
||||
semver:
|
||||
specifier: 7.7.3
|
||||
version: 7.7.3
|
||||
shell-quote:
|
||||
specifier: ^1.8.1
|
||||
version: 1.8.2
|
||||
@@ -790,6 +778,9 @@ importers:
|
||||
'@types/react-dom':
|
||||
specifier: 18.3.0
|
||||
version: 18.3.0
|
||||
'@types/semver':
|
||||
specifier: 7.7.1
|
||||
version: 7.7.1
|
||||
'@types/shell-quote':
|
||||
specifier: ^1.7.5
|
||||
version: 1.7.5
|
||||
@@ -1957,11 +1948,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@nerimity/mimiqueue@1.2.3':
|
||||
resolution: {integrity: sha512-WPoGe417P+S0FLfl3psRBI5adcAWXb917vCF1qD2yGZ1ggBEnMH6UrUK464gzJEOpAlGt8BBbIp0tgCEazZ47A==}
|
||||
peerDependencies:
|
||||
redis: ^4.7.0
|
||||
|
||||
'@next/env@16.0.10':
|
||||
resolution: {integrity: sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==}
|
||||
|
||||
@@ -4066,6 +4052,9 @@ packages:
|
||||
'@types/readable-stream@4.0.20':
|
||||
resolution: {integrity: sha512-eLgbR5KwUh8+6pngBDxS32MymdCsCHnGtwHTrC0GDorbc7NbcnkZAWptDLgZiRk9VRas+B6TyRgPDucq4zRs8g==}
|
||||
|
||||
'@types/semver@7.7.1':
|
||||
resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
|
||||
|
||||
'@types/shell-quote@1.7.5':
|
||||
resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==}
|
||||
|
||||
@@ -4308,9 +4297,6 @@ packages:
|
||||
assertion-error@1.1.0:
|
||||
resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
|
||||
|
||||
async-await-queue@2.1.4:
|
||||
resolution: {integrity: sha512-3DpDtxkKO0O/FPlWbk/CrbexjuSxWm1CH1bXlVNVyMBIkKHhT5D85gzHmGJokG3ibNGWQ7pHBmStxUW/z/0LYQ==}
|
||||
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
@@ -5395,9 +5381,6 @@ packages:
|
||||
help-me@5.0.0:
|
||||
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
|
||||
|
||||
hi-base32@0.5.1:
|
||||
resolution: {integrity: sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA==}
|
||||
|
||||
highlight.js@10.7.3:
|
||||
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
|
||||
|
||||
@@ -6432,9 +6415,6 @@ packages:
|
||||
openapi-types@12.1.3:
|
||||
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
|
||||
|
||||
otpauth@9.4.0:
|
||||
resolution: {integrity: sha512-fHIfzIG5RqCkK9cmV8WU+dPQr9/ebR5QOwGZn2JAr1RQF+lmAuLL2YdtdqvmBjNmgJlYk3KZ4a0XokaEhg1Jsw==}
|
||||
|
||||
p-cancelable@3.0.0:
|
||||
resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==}
|
||||
engines: {node: '>=12.20'}
|
||||
@@ -7064,10 +7044,6 @@ packages:
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
rotating-file-stream@3.2.3:
|
||||
resolution: {integrity: sha512-cfmm3tqdnbuYw2FBmRTPBDaohYEbMJ3211T35o6eZdr4d7v69+ZeK1Av84Br7FLj2dlzyeZSbN6qTuXXE6dawQ==}
|
||||
engines: {node: '>=14.0'}
|
||||
|
||||
rou3@0.5.1:
|
||||
resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==}
|
||||
|
||||
@@ -7097,11 +7073,6 @@ packages:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
|
||||
semver@7.7.2:
|
||||
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
semver@7.7.3:
|
||||
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -8129,7 +8100,7 @@ snapshots:
|
||||
'@commitlint/is-ignored@19.8.1':
|
||||
dependencies:
|
||||
'@commitlint/types': 19.8.1
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
|
||||
'@commitlint/lint@19.8.1':
|
||||
dependencies:
|
||||
@@ -8746,7 +8717,7 @@ snapshots:
|
||||
nopt: 5.0.0
|
||||
npmlog: 5.0.1
|
||||
rimraf: 3.0.2
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
tar: 6.2.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
@@ -8772,11 +8743,6 @@ snapshots:
|
||||
'@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3':
|
||||
optional: true
|
||||
|
||||
'@nerimity/mimiqueue@1.2.3(redis@4.7.0)':
|
||||
dependencies:
|
||||
async-await-queue: 2.1.4
|
||||
redis: 4.7.0
|
||||
|
||||
'@next/env@16.0.10': {}
|
||||
|
||||
'@next/swc-darwin-arm64@16.0.10':
|
||||
@@ -9337,7 +9303,7 @@ snapshots:
|
||||
'@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/semantic-conventions': 1.28.0
|
||||
forwarded-parse: 2.1.2
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -9538,7 +9504,7 @@ snapshots:
|
||||
'@types/shimmer': 1.2.0
|
||||
import-in-the-middle: 1.14.2
|
||||
require-in-the-middle: 7.5.2
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
shimmer: 1.2.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -9683,7 +9649,7 @@ snapshots:
|
||||
'@opentelemetry/propagator-b3': 1.30.1(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/propagator-jaeger': 1.30.1(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0)
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
|
||||
'@opentelemetry/semantic-conventions@1.28.0': {}
|
||||
|
||||
@@ -11431,6 +11397,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 20.17.51
|
||||
|
||||
'@types/semver@7.7.1': {}
|
||||
|
||||
'@types/shell-quote@1.7.5': {}
|
||||
|
||||
'@types/shimmer@1.2.0': {}
|
||||
@@ -11683,8 +11651,6 @@ snapshots:
|
||||
|
||||
assertion-error@1.1.0: {}
|
||||
|
||||
async-await-queue@2.1.4: {}
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
atomic-sleep@1.0.0: {}
|
||||
@@ -11830,7 +11796,7 @@ snapshots:
|
||||
lodash: 4.17.21
|
||||
msgpackr: 1.11.4
|
||||
node-abort-controller: 3.1.1
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
tslib: 2.8.1
|
||||
uuid: 9.0.1
|
||||
transitivePeerDependencies:
|
||||
@@ -12346,7 +12312,7 @@ snapshots:
|
||||
'@one-ini/wasm': 0.1.1
|
||||
commander: 10.0.1
|
||||
minimatch: 9.0.1
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
|
||||
electron-to-chromium@1.5.159: {}
|
||||
|
||||
@@ -12660,7 +12626,7 @@ snapshots:
|
||||
'@petamoriken/float16': 3.9.2
|
||||
debug: 4.4.1
|
||||
env-paths: 3.0.0
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
shell-quote: 1.8.2
|
||||
which: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
@@ -12834,8 +12800,6 @@ snapshots:
|
||||
|
||||
help-me@5.0.0: {}
|
||||
|
||||
hi-base32@0.5.1: {}
|
||||
|
||||
highlight.js@10.7.3: {}
|
||||
|
||||
highlightjs-vue@1.0.0: {}
|
||||
@@ -13148,7 +13112,7 @@ snapshots:
|
||||
lodash.isstring: 4.0.1
|
||||
lodash.once: 4.1.1
|
||||
ms: 2.1.3
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
|
||||
jss-plugin-camel-case@10.10.0:
|
||||
dependencies:
|
||||
@@ -13972,10 +13936,6 @@ snapshots:
|
||||
|
||||
openapi-types@12.1.3: {}
|
||||
|
||||
otpauth@9.4.0:
|
||||
dependencies:
|
||||
'@noble/hashes': 1.7.1
|
||||
|
||||
p-cancelable@3.0.0: {}
|
||||
|
||||
p-limit@2.3.0:
|
||||
@@ -14660,8 +14620,6 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc': 4.41.1
|
||||
fsevents: 2.3.3
|
||||
|
||||
rotating-file-stream@3.2.3: {}
|
||||
|
||||
rou3@0.5.1: {}
|
||||
|
||||
run-parallel@1.2.0:
|
||||
@@ -14686,10 +14644,7 @@ snapshots:
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
||||
semver@7.7.2: {}
|
||||
|
||||
semver@7.7.3:
|
||||
optional: true
|
||||
semver@7.7.3: {}
|
||||
|
||||
serialize-error-cjs@0.1.4: {}
|
||||
|
||||
|
||||
@@ -276,7 +276,7 @@ table application {
|
||||
replicas integer [not null, default: 1]
|
||||
applicationStatus applicationStatus [not null, default: 'idle']
|
||||
buildType buildType [not null, default: 'nixpacks']
|
||||
railpackVersion text [default: '0.2.2']
|
||||
railpackVersion text [default: '0.15.4']
|
||||
herokuVersion text [default: '24']
|
||||
publishDirectory text
|
||||
isStaticSpa boolean
|
||||
|
||||
Reference in New Issue
Block a user