mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-19 07:01:35 +00:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75448f358e | |||
| ba24ae6797 | |||
| 2f8af90da4 | |||
| 3f98208f03 | |||
| 5d231bf3e3 | |||
| 0124613516 | |||
| 3495fc89bf | |||
| 5ae289cae3 | |||
| a0162ab566 | |||
| 314a31dc27 | |||
| 108541c957 | |||
| 14be318502 | |||
| d6c2e38cb0 | |||
| 46c87a22b0 | |||
| 89c0e9ef67 | |||
| 8583b3c1a5 | |||
| 69a5615544 | |||
| a1230098c9 | |||
| eacb1440e0 | |||
| 7e38861985 | |||
| 056b6983db | |||
| f6b0700ea5 | |||
| 4325605578 | |||
| 5d11a045cc | |||
| d92de5a4a0 | |||
| 81051ca965 | |||
| 7b0bcee652 | |||
| 11e93dde38 | |||
| cbb3450b93 | |||
| a058d1f5c7 | |||
| 3835a6fe68 | |||
| 325c2a4180 | |||
| 51cb7d2287 | |||
| 081cf3fb80 | |||
| 2be9e1e3df | |||
| 3b0bdd8f74 | |||
| 6e79270423 | |||
| 9999f0a18e | |||
| 268ec1f397 | |||
| 018e56a7b4 | |||
| 1cf81e3802 | |||
| bb73c6e6dd | |||
| 15bc4b5dc5 | |||
| 30e8d9b86e | |||
| 5df820a740 | |||
| ba5af51347 | |||
| 6e1d020769 | |||
| deebf0f107 | |||
| 7047e4f199 | |||
| 069939e8f9 | |||
| b7e5fadf53 | |||
| 260af232e7 | |||
| 425d478a0b | |||
| 268fd7d771 | |||
| 4ba7972ffa | |||
| 2739690758 | |||
| 8f0702c4da | |||
| 92c5879438 | |||
| a94f8b96d3 | |||
| ae95485b74 | |||
| 2df02f596a | |||
| 7da96810a8 |
@@ -0,0 +1,34 @@
|
|||||||
|
name: Hotfix Cherry-Pick
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [closed, labeled]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: hotfix-to-main
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cherry-pick:
|
||||||
|
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'hotfix')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout main
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Cherry-pick fix to main
|
||||||
|
run: |
|
||||||
|
git config user.name "Dokploy Bot"
|
||||||
|
git config user.email "bot@dokploy.com"
|
||||||
|
SHA="${{ github.event.pull_request.merge_commit_sha }}"
|
||||||
|
if [ "$(git rev-list --parents -n1 "$SHA" | wc -w)" -gt 2 ]; then
|
||||||
|
git cherry-pick -x -m 1 "$SHA"
|
||||||
|
else
|
||||||
|
git cherry-pick -x "$SHA"
|
||||||
|
fi
|
||||||
|
git push origin main
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
name: Hotfix Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: hotfix-to-main
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout main
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
token: ${{ secrets.HOTFIX_PUSH_TOKEN }}
|
||||||
|
|
||||||
|
- name: Bump patch version and push
|
||||||
|
run: |
|
||||||
|
git config user.name "Dokploy Bot"
|
||||||
|
git config user.email "bot@dokploy.com"
|
||||||
|
CURRENT=$(node -p "require('./apps/dokploy/package.json').version")
|
||||||
|
NEW=$(echo "$CURRENT" | awk -F. -v OFS=. '{$NF++; print}')
|
||||||
|
sed -i "s/\"version\": \"$CURRENT\"/\"version\": \"$NEW\"/" apps/dokploy/package.json
|
||||||
|
git commit -am "chore: release ${NEW}"
|
||||||
|
git push origin main
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import type { ApplicationNested } from "@dokploy/server/utils/builders";
|
||||||
|
import { getRailpackCommand } from "@dokploy/server/utils/builders/railpack";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const createApplication = (
|
||||||
|
overrides: Partial<ApplicationNested> = {},
|
||||||
|
): ApplicationNested =>
|
||||||
|
({
|
||||||
|
appName: "test-app",
|
||||||
|
buildType: "railpack",
|
||||||
|
sourceType: "git",
|
||||||
|
buildPath: "/",
|
||||||
|
railpackVersion: "0.15.4",
|
||||||
|
env: "TEST_VAR=one",
|
||||||
|
cleanCache: false,
|
||||||
|
environment: {
|
||||||
|
project: {
|
||||||
|
env: "",
|
||||||
|
},
|
||||||
|
env: "",
|
||||||
|
},
|
||||||
|
...overrides,
|
||||||
|
}) as unknown as ApplicationNested;
|
||||||
|
|
||||||
|
const getSecretsHash = (command: string) => {
|
||||||
|
const match = command.match(/secrets-hash=([a-f0-9]{64})/);
|
||||||
|
if (!match?.[1]) {
|
||||||
|
throw new Error("secrets-hash build arg was not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return match[1];
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("getRailpackCommand", () => {
|
||||||
|
it("includes secrets-hash without clean cache", () => {
|
||||||
|
const command = getRailpackCommand(createApplication());
|
||||||
|
|
||||||
|
expect(command).toContain("--build-arg secrets-hash=");
|
||||||
|
expect(command).not.toContain("cache-key=");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes cache-key only when clean cache is enabled", () => {
|
||||||
|
const command = getRailpackCommand(
|
||||||
|
createApplication({
|
||||||
|
cleanCache: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(command).toContain("--build-arg secrets-hash=");
|
||||||
|
expect(command).toContain("--build-arg cache-key=");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("changes secrets-hash when an environment value changes", () => {
|
||||||
|
const firstCommand = getRailpackCommand(
|
||||||
|
createApplication({
|
||||||
|
env: "TEST_VAR=one",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const secondCommand = getRailpackCommand(
|
||||||
|
createApplication({
|
||||||
|
env: "TEST_VAR=two",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getSecretsHash(firstCommand)).not.toEqual(
|
||||||
|
getSecretsHash(secondCommand),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("changes secrets-hash when referenced project or environment values change", () => {
|
||||||
|
const firstCommand = getRailpackCommand(
|
||||||
|
createApplication({
|
||||||
|
env: [
|
||||||
|
"PROJECT_VALUE=${{project.SHARED_VALUE}}",
|
||||||
|
"ENVIRONMENT_VALUE=${{environment.SHARED_VALUE}}",
|
||||||
|
].join("\n"),
|
||||||
|
environment: {
|
||||||
|
project: {
|
||||||
|
env: "SHARED_VALUE=one",
|
||||||
|
},
|
||||||
|
env: "SHARED_VALUE=alpha",
|
||||||
|
},
|
||||||
|
} as Partial<ApplicationNested>),
|
||||||
|
);
|
||||||
|
const secondCommand = getRailpackCommand(
|
||||||
|
createApplication({
|
||||||
|
env: [
|
||||||
|
"PROJECT_VALUE=${{project.SHARED_VALUE}}",
|
||||||
|
"ENVIRONMENT_VALUE=${{environment.SHARED_VALUE}}",
|
||||||
|
].join("\n"),
|
||||||
|
environment: {
|
||||||
|
project: {
|
||||||
|
env: "SHARED_VALUE=two",
|
||||||
|
},
|
||||||
|
env: "SHARED_VALUE=beta",
|
||||||
|
},
|
||||||
|
} as Partial<ApplicationNested>),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getSecretsHash(firstCommand)).not.toEqual(
|
||||||
|
getSecretsHash(secondCommand),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { prepareEnvironmentVariables } from "@dokploy/server/index";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const projectEnv = `
|
||||||
|
ENVIRONMENT=staging
|
||||||
|
DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db
|
||||||
|
`;
|
||||||
|
|
||||||
|
const environmentEnv = `
|
||||||
|
NODE_ENV=production
|
||||||
|
POSTGRES_HOST=postgres.internal
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
REDIS_URL=redis://redis.internal:6379
|
||||||
|
`;
|
||||||
|
|
||||||
|
const serviceEnv = `
|
||||||
|
NODE_ENV=\${{environment.NODE_ENV}}
|
||||||
|
REDIS_URL=\${{environment.REDIS_URL}}
|
||||||
|
PORT=3000
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A rollback replays the snapshot stored in `rollbacks.fullContext`, which keeps
|
||||||
|
* the service env, the environment env and the project env captured at deploy time.
|
||||||
|
*/
|
||||||
|
const fullContext = {
|
||||||
|
env: serviceEnv,
|
||||||
|
environment: {
|
||||||
|
env: environmentEnv,
|
||||||
|
project: {
|
||||||
|
env: projectEnv,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("prepareEnvironmentVariables for application rollback", () => {
|
||||||
|
it("resolves environment variables from the rollback snapshot", () => {
|
||||||
|
const result = prepareEnvironmentVariables(
|
||||||
|
fullContext.env,
|
||||||
|
fullContext.environment.project.env,
|
||||||
|
fullContext.environment.env,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
"NODE_ENV=production",
|
||||||
|
"REDIS_URL=redis://redis.internal:6379",
|
||||||
|
"PORT=3000",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves project and environment variables together on rollback", () => {
|
||||||
|
const rollbackEnv = `
|
||||||
|
DATABASE_URL=\${{project.DATABASE_URL}}
|
||||||
|
POSTGRES_URL=postgres://\${{environment.POSTGRES_HOST}}:\${{environment.POSTGRES_PORT}}/app
|
||||||
|
ENVIRONMENT=\${{project.ENVIRONMENT}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = prepareEnvironmentVariables(
|
||||||
|
rollbackEnv,
|
||||||
|
projectEnv,
|
||||||
|
environmentEnv,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
"DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db",
|
||||||
|
"POSTGRES_URL=postgres://postgres.internal:5432/app",
|
||||||
|
"ENVIRONMENT=staging",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when the environment env of the snapshot is not passed", () => {
|
||||||
|
expect(() =>
|
||||||
|
prepareEnvironmentVariables(fullContext.env, projectEnv),
|
||||||
|
).toThrow("Invalid environment variable: environment.NODE_ENV");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maintains precedence: service > environment > project on rollback", () => {
|
||||||
|
const conflictingProjectEnv = `
|
||||||
|
NODE_ENV=project
|
||||||
|
API_URL=https://project.api.com
|
||||||
|
`;
|
||||||
|
|
||||||
|
const conflictingEnvironmentEnv = `
|
||||||
|
NODE_ENV=environment
|
||||||
|
API_URL=https://environment.api.com
|
||||||
|
`;
|
||||||
|
|
||||||
|
const rollbackEnv = `
|
||||||
|
NODE_ENV=service
|
||||||
|
PROJECT_API_URL=\${{project.API_URL}}
|
||||||
|
ENVIRONMENT_API_URL=\${{environment.API_URL}}
|
||||||
|
SELF_REFERENCE=\${{NODE_ENV}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = prepareEnvironmentVariables(
|
||||||
|
rollbackEnv,
|
||||||
|
conflictingProjectEnv,
|
||||||
|
conflictingEnvironmentEnv,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
"NODE_ENV=service",
|
||||||
|
"PROJECT_API_URL=https://project.api.com",
|
||||||
|
"ENVIRONMENT_API_URL=https://environment.api.com",
|
||||||
|
"SELF_REFERENCE=service",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
// cloneGithubRepository builds a shell command; the only thing under test here
|
||||||
|
// is which host ends up in the clone URL, so the app auth is stubbed out.
|
||||||
|
const mockFindGithubById = vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
|
vi.mock("@dokploy/server/services/github", () => ({
|
||||||
|
findGithubById: mockFindGithubById,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@octokit/auth-app", () => ({
|
||||||
|
createAppAuth: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("octokit", () => ({
|
||||||
|
Octokit: class {
|
||||||
|
auth = async () => ({ token: "gh-token" });
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { cloneGithubRepository } = await import(
|
||||||
|
"@dokploy/server/utils/providers/github"
|
||||||
|
);
|
||||||
|
|
||||||
|
const provider = (githubUrl: string) => ({
|
||||||
|
githubId: "gh-1",
|
||||||
|
githubUrl,
|
||||||
|
githubAppId: 1,
|
||||||
|
githubPrivateKey: "key",
|
||||||
|
githubInstallationId: "42",
|
||||||
|
});
|
||||||
|
|
||||||
|
const clone = async () => {
|
||||||
|
const command = await cloneGithubRepository({
|
||||||
|
appName: "my-app",
|
||||||
|
owner: "acme",
|
||||||
|
repository: "web",
|
||||||
|
branch: "main",
|
||||||
|
githubId: "gh-1",
|
||||||
|
enableSubmodules: false,
|
||||||
|
serverId: null,
|
||||||
|
});
|
||||||
|
return command.replace(/\\/g, "");
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("cloneGithubRepository host", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clones from github.com for a default provider", async () => {
|
||||||
|
mockFindGithubById.mockResolvedValue(provider("https://github.com"));
|
||||||
|
|
||||||
|
const command = await clone();
|
||||||
|
|
||||||
|
expect(command).toContain(
|
||||||
|
"https://oauth2:gh-token@github.com/acme/web.git",
|
||||||
|
);
|
||||||
|
expect(command).not.toContain("ghe.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clones from the Enterprise host, not github.com", async () => {
|
||||||
|
mockFindGithubById.mockResolvedValue(provider("https://acme.ghe.com"));
|
||||||
|
|
||||||
|
const command = await clone();
|
||||||
|
|
||||||
|
expect(command).toContain(
|
||||||
|
"https://oauth2:gh-token@acme.ghe.com/acme/web.git",
|
||||||
|
);
|
||||||
|
expect(command).not.toContain("github.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clones from a self-hosted Enterprise Server host", async () => {
|
||||||
|
mockFindGithubById.mockResolvedValue(
|
||||||
|
provider("https://github.corp.acme.com"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const command = await clone();
|
||||||
|
|
||||||
|
expect(command).toContain(
|
||||||
|
"https://oauth2:gh-token@github.corp.acme.com/acme/web.git",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps an explicit port in the clone host", async () => {
|
||||||
|
mockFindGithubById.mockResolvedValue(
|
||||||
|
provider("https://github.acme.com:8443"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const command = await clone();
|
||||||
|
|
||||||
|
expect(command).toContain(
|
||||||
|
"https://oauth2:gh-token@github.acme.com:8443/acme/web.git",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to github.com for a provider stored before this feature", async () => {
|
||||||
|
mockFindGithubById.mockResolvedValue(provider(""));
|
||||||
|
|
||||||
|
const command = await clone();
|
||||||
|
|
||||||
|
expect(command).toContain(
|
||||||
|
"https://oauth2:gh-token@github.com/acme/web.git",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import {
|
||||||
|
DEFAULT_GITHUB_API_URL,
|
||||||
|
DEFAULT_GITHUB_URL,
|
||||||
|
deriveGithubApiUrl,
|
||||||
|
normalizeGithubUrl,
|
||||||
|
parseGithubBaseUrl,
|
||||||
|
} from "@dokploy/server/utils/providers/github";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const urlOf = (result: ReturnType<typeof parseGithubBaseUrl>) =>
|
||||||
|
"url" in result ? result.url : null;
|
||||||
|
|
||||||
|
describe("normalizeGithubUrl", () => {
|
||||||
|
it("defaults to github.com when empty", () => {
|
||||||
|
expect(normalizeGithubUrl("")).toBe(DEFAULT_GITHUB_URL);
|
||||||
|
expect(normalizeGithubUrl(null)).toBe(DEFAULT_GITHUB_URL);
|
||||||
|
expect(normalizeGithubUrl(undefined)).toBe(DEFAULT_GITHUB_URL);
|
||||||
|
expect(normalizeGithubUrl(" ")).toBe(DEFAULT_GITHUB_URL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("assumes https when no scheme is given", () => {
|
||||||
|
expect(normalizeGithubUrl("acme.ghe.com")).toBe("https://acme.ghe.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips paths, queries and trailing slashes", () => {
|
||||||
|
expect(normalizeGithubUrl("https://acme.ghe.com/")).toBe(
|
||||||
|
"https://acme.ghe.com",
|
||||||
|
);
|
||||||
|
expect(normalizeGithubUrl("https://acme.ghe.com///")).toBe(
|
||||||
|
"https://acme.ghe.com",
|
||||||
|
);
|
||||||
|
expect(normalizeGithubUrl("https://github.acme.com/some/path?x=1")).toBe(
|
||||||
|
"https://github.acme.com",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps an explicit port", () => {
|
||||||
|
expect(normalizeGithubUrl("https://github.acme.com:8443")).toBe(
|
||||||
|
"https://github.acme.com:8443",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to github.com on unusable input", () => {
|
||||||
|
expect(normalizeGithubUrl("ftp://github.acme.com")).toBe(
|
||||||
|
DEFAULT_GITHUB_URL,
|
||||||
|
);
|
||||||
|
expect(normalizeGithubUrl("http://github.internal")).toBe(
|
||||||
|
DEFAULT_GITHUB_URL,
|
||||||
|
);
|
||||||
|
expect(normalizeGithubUrl("https://")).toBe(DEFAULT_GITHUB_URL);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("parseGithubBaseUrl", () => {
|
||||||
|
it("accepts github.com and Enterprise hosts", () => {
|
||||||
|
expect(urlOf(parseGithubBaseUrl("https://acme.ghe.com"))).toBe(
|
||||||
|
"https://acme.ghe.com",
|
||||||
|
);
|
||||||
|
// A self-hosted instance behind the corporate network is a valid target.
|
||||||
|
expect(urlOf(parseGithubBaseUrl("https://github.corp.acme.com"))).toBe(
|
||||||
|
"https://github.corp.acme.com",
|
||||||
|
);
|
||||||
|
expect(urlOf(parseGithubBaseUrl("https://github.acme.com:8443"))).toBe(
|
||||||
|
"https://github.acme.com:8443",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats an absent value as github.com", () => {
|
||||||
|
// Not specified is different from specified wrong.
|
||||||
|
expect(urlOf(parseGithubBaseUrl(undefined))).toBe(DEFAULT_GITHUB_URL);
|
||||||
|
expect(urlOf(parseGithubBaseUrl(null))).toBe(DEFAULT_GITHUB_URL);
|
||||||
|
expect(urlOf(parseGithubBaseUrl(" "))).toBe(DEFAULT_GITHUB_URL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects plaintext http", () => {
|
||||||
|
expect(parseGithubBaseUrl("http://acme.ghe.com")).toHaveProperty("error");
|
||||||
|
expect(parseGithubBaseUrl("http://localhost:2375")).toHaveProperty("error");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects dotless hostnames", () => {
|
||||||
|
expect(parseGithubBaseUrl("https://metadata")).toHaveProperty("error");
|
||||||
|
expect(parseGithubBaseUrl("https://localhost")).toHaveProperty("error");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a dotless hostname hidden behind the DNS root label", () => {
|
||||||
|
// "metadata." resolves like "metadata" but the trailing dot would satisfy
|
||||||
|
// a naive `includes(".")` check.
|
||||||
|
expect(parseGithubBaseUrl("https://metadata.")).toHaveProperty("error");
|
||||||
|
expect(parseGithubBaseUrl("https://localhost.")).toHaveProperty("error");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("never falls back to github.com on a typo", () => {
|
||||||
|
// The symptom that opened the ticket: a provider silently pointing at
|
||||||
|
// github.com and reporting that it cannot find the repositories.
|
||||||
|
for (const typo of [
|
||||||
|
"htps://acme.ghe.com",
|
||||||
|
"ftp://acme.ghe.com",
|
||||||
|
"https://",
|
||||||
|
"acme .ghe.com",
|
||||||
|
]) {
|
||||||
|
const result = parseGithubBaseUrl(typo);
|
||||||
|
expect(result, typo).toHaveProperty("error");
|
||||||
|
expect(urlOf(result), typo).not.toBe(DEFAULT_GITHUB_URL);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("deriveGithubApiUrl", () => {
|
||||||
|
it("maps github.com to api.github.com", () => {
|
||||||
|
expect(deriveGithubApiUrl("https://github.com")).toBe(
|
||||||
|
DEFAULT_GITHUB_API_URL,
|
||||||
|
);
|
||||||
|
expect(deriveGithubApiUrl("https://www.github.com")).toBe(
|
||||||
|
DEFAULT_GITHUB_API_URL,
|
||||||
|
);
|
||||||
|
expect(deriveGithubApiUrl(undefined)).toBe(DEFAULT_GITHUB_API_URL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefixes api. for data residency tenants", () => {
|
||||||
|
expect(deriveGithubApiUrl("https://acme.ghe.com")).toBe(
|
||||||
|
"https://api.acme.ghe.com",
|
||||||
|
);
|
||||||
|
expect(deriveGithubApiUrl("americancreditacceptance.ghe.com")).toBe(
|
||||||
|
"https://api.americancreditacceptance.ghe.com",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses /api/v3 for Enterprise Server", () => {
|
||||||
|
expect(deriveGithubApiUrl("https://github.acme.com")).toBe(
|
||||||
|
"https://github.acme.com/api/v3",
|
||||||
|
);
|
||||||
|
expect(deriveGithubApiUrl("https://github.acme.com:8443")).toBe(
|
||||||
|
"https://github.acme.com:8443/api/v3",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not treat a lookalike host as data residency", () => {
|
||||||
|
// Must not match the .ghe.com branch just because the string contains it.
|
||||||
|
expect(deriveGithubApiUrl("https://ghe.com.acme.io")).toBe(
|
||||||
|
"https://ghe.com.acme.io/api/v3",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still detects data residency behind the DNS root label", () => {
|
||||||
|
// "acme.ghe.com." would otherwise miss endsWith(".ghe.com") and fall
|
||||||
|
// through to the /api/v3 branch.
|
||||||
|
expect(deriveGithubApiUrl("https://acme.ghe.com.")).toBe(
|
||||||
|
"https://api.acme.ghe.com",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps www.github.com, which a user may well type", () => {
|
||||||
|
// Not dead weight: GitHub never redirects a manifest there, but the value
|
||||||
|
// comes from a text field. Without this it would derive
|
||||||
|
// https://www.github.com/api/v3.
|
||||||
|
expect(deriveGithubApiUrl("https://www.github.com")).toBe(
|
||||||
|
DEFAULT_GITHUB_API_URL,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("providers created before Enterprise support", () => {
|
||||||
|
it("keeps pointing at github.com", () => {
|
||||||
|
// The column defaults to https://github.com, but a null must not break it.
|
||||||
|
expect(deriveGithubApiUrl(null)).toBe(DEFAULT_GITHUB_API_URL);
|
||||||
|
expect(new URL(normalizeGithubUrl(null)).host).toBe("github.com");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
// The gh_init branch runs on a GET the user can be linked into, so a rejected
|
||||||
|
// host must produce a 400 *before* any outbound request is made.
|
||||||
|
const mockValidateRequest = vi.hoisted(() => vi.fn());
|
||||||
|
const mockHasPermission = vi.hoisted(() => vi.fn());
|
||||||
|
const mockCreateGithub = vi.hoisted(() => vi.fn());
|
||||||
|
const mockOctokitRequest = vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
|
vi.mock("@dokploy/server", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("@dokploy/server")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
validateRequest: mockValidateRequest,
|
||||||
|
createGithub: mockCreateGithub,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("@dokploy/server/services/permission", () => ({
|
||||||
|
hasPermission: mockHasPermission,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("octokit", () => ({
|
||||||
|
Octokit: class {
|
||||||
|
request = mockOctokitRequest;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { default: handler } = await import("@/pages/api/providers/github/setup");
|
||||||
|
|
||||||
|
const ORG = "org-1";
|
||||||
|
const USER = "user-1";
|
||||||
|
|
||||||
|
const buildRes = () => {
|
||||||
|
const res = {
|
||||||
|
statusCode: 0,
|
||||||
|
body: undefined as unknown,
|
||||||
|
redirectedTo: undefined as string | undefined,
|
||||||
|
status(code: number) {
|
||||||
|
res.statusCode = code;
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
json(payload: unknown) {
|
||||||
|
res.body = payload;
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
redirect(_code: number, url: string) {
|
||||||
|
res.redirectedTo = url;
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const call = async (githubUrl?: string | string[]) => {
|
||||||
|
const res = buildRes();
|
||||||
|
const req = {
|
||||||
|
query: {
|
||||||
|
code: "manifest-code",
|
||||||
|
state: `gh_init:${ORG}:${USER}`,
|
||||||
|
...(githubUrl === undefined ? {} : { githubUrl }),
|
||||||
|
},
|
||||||
|
headers: {},
|
||||||
|
} as unknown as Parameters<typeof handler>[0];
|
||||||
|
|
||||||
|
await handler(req, res as unknown as Parameters<typeof handler>[1]);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("github setup handler — host validation", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockValidateRequest.mockResolvedValue({
|
||||||
|
user: { id: USER },
|
||||||
|
session: { activeOrganizationId: ORG },
|
||||||
|
});
|
||||||
|
mockHasPermission.mockResolvedValue(true);
|
||||||
|
mockOctokitRequest.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
name: "Dokploy",
|
||||||
|
html_url: "https://acme.ghe.com/apps/dokploy",
|
||||||
|
id: 1,
|
||||||
|
client_id: "cid",
|
||||||
|
client_secret: "csecret",
|
||||||
|
webhook_secret: "wsecret",
|
||||||
|
pem: "key",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mockCreateGithub.mockResolvedValue(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["http://acme.ghe.com", "plaintext http"],
|
||||||
|
["http://localhost:2375", "internal service over http"],
|
||||||
|
["https://metadata", "dotless hostname"],
|
||||||
|
["htps://acme.ghe.com", "scheme typo"],
|
||||||
|
])("rejects %s (%s) with 400 and no outbound request", async (githubUrl) => {
|
||||||
|
const res = await call(githubUrl);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(mockOctokitRequest).not.toHaveBeenCalled();
|
||||||
|
expect(mockCreateGithub).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws on a repeated parameter instead of silently picking one", async () => {
|
||||||
|
// ?githubUrl=a&githubUrl=b reaches .trim() on an array.
|
||||||
|
await expect(
|
||||||
|
call(["https://acme.ghe.com", "https://evil.com"]),
|
||||||
|
).rejects.toThrow();
|
||||||
|
|
||||||
|
expect(mockCreateGithub).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts a data residency tenant", async () => {
|
||||||
|
await call("https://acme.ghe.com");
|
||||||
|
|
||||||
|
expect(mockCreateGithub).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ githubUrl: "https://acme.ghe.com" }),
|
||||||
|
ORG,
|
||||||
|
USER,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts a self-hosted Enterprise Server host", async () => {
|
||||||
|
await call("https://github.corp.acme.com");
|
||||||
|
|
||||||
|
expect(mockCreateGithub).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ githubUrl: "https://github.corp.acme.com" }),
|
||||||
|
ORG,
|
||||||
|
USER,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats an absent parameter as github.com", async () => {
|
||||||
|
await call(undefined);
|
||||||
|
|
||||||
|
expect(mockCreateGithub).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ githubUrl: "https://github.com" }),
|
||||||
|
ORG,
|
||||||
|
USER,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { parseGithubBaseUrl } from "@dokploy/server/utils/providers/github";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { DEFAULT_GITHUB_URL, resolveGithubBaseUrl } from "@/utils/github-utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The client cannot import from @dokploy/server (node-only modules), so
|
||||||
|
* resolveGithubBaseUrl duplicates parseGithubBaseUrl. Nothing but this file
|
||||||
|
* stops the two from drifting apart — and they already did once, when the
|
||||||
|
* client stripped trailing slashes but not paths and the form action disagreed
|
||||||
|
* with the persisted row.
|
||||||
|
*
|
||||||
|
* The invariant: for the same input, both must make the same accept/reject
|
||||||
|
* decision, and agree on the URL when they accept.
|
||||||
|
*/
|
||||||
|
const INPUTS = [
|
||||||
|
// Accepted
|
||||||
|
"https://github.com",
|
||||||
|
"github.com",
|
||||||
|
"https://acme.ghe.com",
|
||||||
|
"acme.ghe.com",
|
||||||
|
"https://github.corp.acme.com",
|
||||||
|
"https://github.acme.com:8443",
|
||||||
|
"https://acme.ghe.com/",
|
||||||
|
"https://acme.ghe.com///",
|
||||||
|
"https://acme.ghe.com/enterprises/foo",
|
||||||
|
"https://acme.ghe.com/some/path?x=1",
|
||||||
|
" acme.ghe.com ",
|
||||||
|
"https://acme.ghe.com.",
|
||||||
|
"https://169.254.169.254",
|
||||||
|
"https://127.0.0.1",
|
||||||
|
"https://2130706433",
|
||||||
|
"https://0x7f.1",
|
||||||
|
"",
|
||||||
|
" ",
|
||||||
|
// Rejected
|
||||||
|
"http://acme.ghe.com",
|
||||||
|
"http://localhost:2375",
|
||||||
|
"https://[::1]",
|
||||||
|
"https://[::ffff:127.0.0.1]",
|
||||||
|
"https://metadata",
|
||||||
|
"https://metadata.",
|
||||||
|
"https://localhost",
|
||||||
|
"https://localhost.",
|
||||||
|
"htps://acme.ghe.com",
|
||||||
|
"ftp://acme.ghe.com",
|
||||||
|
"https://",
|
||||||
|
"acme .ghe.com",
|
||||||
|
"esto no es una url",
|
||||||
|
];
|
||||||
|
|
||||||
|
describe("client and server agree on GitHub base URLs", () => {
|
||||||
|
it.each(INPUTS)("%j", (input) => {
|
||||||
|
const client = resolveGithubBaseUrl(input);
|
||||||
|
const server = parseGithubBaseUrl(input);
|
||||||
|
|
||||||
|
const clientAccepted = !client.error;
|
||||||
|
const serverAccepted = "url" in server;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
clientAccepted,
|
||||||
|
`accept/reject differs for ${JSON.stringify(input)}`,
|
||||||
|
).toBe(serverAccepted);
|
||||||
|
|
||||||
|
if (clientAccepted && "url" in server) {
|
||||||
|
expect(client.baseUrl, `resolved URL differs for ${input}`).toBe(
|
||||||
|
server.url,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("both treat an empty value as github.com", () => {
|
||||||
|
expect(resolveGithubBaseUrl("").baseUrl).toBe(DEFAULT_GITHUB_URL);
|
||||||
|
expect(parseGithubBaseUrl("")).toEqual({ url: DEFAULT_GITHUB_URL });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -55,6 +55,28 @@ describe("processLogs", () => {
|
|||||||
expect(result.data).toHaveLength(2);
|
expect(result.data).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not throw when filtering by hostname and an entry has no RequestHost", () => {
|
||||||
|
const entryWithoutRequestHost = sampleLogEntry.replace(
|
||||||
|
/"RequestHost":"[^"]*",/,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
const mixedEntries = `${sampleLogEntry}\n${entryWithoutRequestHost}`;
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
parseRawConfig(mixedEntries, undefined, undefined, "traefik.me"),
|
||||||
|
).not.toThrow();
|
||||||
|
|
||||||
|
const result = parseRawConfig(
|
||||||
|
mixedEntries,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
"traefik.me",
|
||||||
|
);
|
||||||
|
expect(result.totalCount).toBe(1);
|
||||||
|
expect(result.data[0]?.RequestHost).toBe("s222-umami-c381af.traefik.me");
|
||||||
|
});
|
||||||
|
|
||||||
it("should filter out Dokploy dashboard requests", () => {
|
it("should filter out Dokploy dashboard requests", () => {
|
||||||
const dokployDashboardEntry = `{"ClientAddr":"172.71.187.131:9485","ClientHost":"172.71.187.131","ClientPort":"9485","ClientUsername":"-","DownstreamContentSize":14550,"DownstreamStatus":200,"Duration":57681682,"OriginContentSize":14550,"OriginDuration":57612242,"OriginStatus":200,"Overhead":69440,"RequestAddr":"hostinger.dokploy.com","RequestContentSize":0,"RequestCount":20142,"RequestHost":"hostinger.dokploy.com","RequestMethod":"GET","RequestPath":"/_next/data/cb_zzI4Rp9G7Q7djrFKh0/en/dashboard/traefik.json","RequestPort":"-","RequestProtocol":"HTTP/2.0","RequestScheme":"https","RetryAttempts":0,"RouterName":"dokploy-router-app-secure@file","ServiceAddr":"dokploy:3000","ServiceName":"dokploy-service-app@file","ServiceURL":"http://dokploy:3000","StartLocal":"2025-12-10T05:10:41.957755949Z","StartUTC":"2025-12-10T05:10:41.957755949Z","TLSCipher":"TLS_AES_128_GCM_SHA256","TLSVersion":"1.3","entryPointName":"websecure","level":"info","msg":"","time":"2025-12-10T05:10:42Z"}`;
|
const dokployDashboardEntry = `{"ClientAddr":"172.71.187.131:9485","ClientHost":"172.71.187.131","ClientPort":"9485","ClientUsername":"-","DownstreamContentSize":14550,"DownstreamStatus":200,"Duration":57681682,"OriginContentSize":14550,"OriginDuration":57612242,"OriginStatus":200,"Overhead":69440,"RequestAddr":"hostinger.dokploy.com","RequestContentSize":0,"RequestCount":20142,"RequestHost":"hostinger.dokploy.com","RequestMethod":"GET","RequestPath":"/_next/data/cb_zzI4Rp9G7Q7djrFKh0/en/dashboard/traefik.json","RequestPort":"-","RequestProtocol":"HTTP/2.0","RequestScheme":"https","RetryAttempts":0,"RouterName":"dokploy-router-app-secure@file","ServiceAddr":"dokploy:3000","ServiceName":"dokploy-service-app@file","ServiceURL":"http://dokploy:3000","StartLocal":"2025-12-10T05:10:41.957755949Z","StartUTC":"2025-12-10T05:10:41.957755949Z","TLSCipher":"TLS_AES_128_GCM_SHA256","TLSVersion":"1.3","entryPointName":"websecure","level":"info","msg":"","time":"2025-12-10T05:10:42Z"}`;
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ describe("VALID_HOSTNAME_REGEX", () => {
|
|||||||
"a.b.c.example.co",
|
"a.b.c.example.co",
|
||||||
"xn--80ak6aa92e.com",
|
"xn--80ak6aa92e.com",
|
||||||
"123.example.com",
|
"123.example.com",
|
||||||
|
"example",
|
||||||
|
"dokploy-server",
|
||||||
|
"localhost",
|
||||||
])("accepts valid hostname %s", (host) => {
|
])("accepts valid hostname %s", (host) => {
|
||||||
expect(VALID_HOSTNAME_REGEX.test(host)).toBe(true);
|
expect(VALID_HOSTNAME_REGEX.test(host)).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -17,7 +20,6 @@ describe("VALID_HOSTNAME_REGEX", () => {
|
|||||||
"bbn_client.example.com",
|
"bbn_client.example.com",
|
||||||
"-example.com",
|
"-example.com",
|
||||||
"example-.com",
|
"example-.com",
|
||||||
"example",
|
|
||||||
"exa mple.com",
|
"exa mple.com",
|
||||||
"example..com",
|
"example..com",
|
||||||
"",
|
"",
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ export const ShowDeployments = ({
|
|||||||
const [activeLog, setActiveLog] = useState<
|
const [activeLog, setActiveLog] = useState<
|
||||||
RouterOutputs["deployment"]["all"][number] | null
|
RouterOutputs["deployment"]["all"][number] | null
|
||||||
>(null);
|
>(null);
|
||||||
|
const [removingDeploymentIds, setRemovingDeploymentIds] = useState<
|
||||||
|
Set<string>
|
||||||
|
>(new Set());
|
||||||
const { data: deployments, isPending: isLoadingDeployments } =
|
const { data: deployments, isPending: isLoadingDeployments } =
|
||||||
api.deployment.allByType.useQuery(
|
api.deployment.allByType.useQuery(
|
||||||
{
|
{
|
||||||
@@ -81,7 +84,7 @@ export const ShowDeployments = ({
|
|||||||
api.rollback.rollback.useMutation();
|
api.rollback.rollback.useMutation();
|
||||||
const { mutateAsync: killProcess, isPending: isKillingProcess } =
|
const { mutateAsync: killProcess, isPending: isKillingProcess } =
|
||||||
api.deployment.killProcess.useMutation();
|
api.deployment.killProcess.useMutation();
|
||||||
const { mutateAsync: removeDeployment, isPending: isRemovingDeployment } =
|
const { mutateAsync: removeDeployment } =
|
||||||
api.deployment.removeDeployment.useMutation();
|
api.deployment.removeDeployment.useMutation();
|
||||||
|
|
||||||
// Cancel deployment mutations
|
// Cancel deployment mutations
|
||||||
@@ -408,6 +411,11 @@ export const ShowDeployments = ({
|
|||||||
description="Are you sure you want to delete this deployment? This action cannot be undone."
|
description="Are you sure you want to delete this deployment? This action cannot be undone."
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
setRemovingDeploymentIds((deploymentIds) => {
|
||||||
|
const nextDeploymentIds = new Set(deploymentIds);
|
||||||
|
nextDeploymentIds.add(deployment.deploymentId);
|
||||||
|
return nextDeploymentIds;
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
await removeDeployment({
|
await removeDeployment({
|
||||||
deploymentId: deployment.deploymentId,
|
deploymentId: deployment.deploymentId,
|
||||||
@@ -415,13 +423,25 @@ export const ShowDeployments = ({
|
|||||||
toast.success("Deployment deleted successfully");
|
toast.success("Deployment deleted successfully");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Error deleting deployment");
|
toast.error("Error deleting deployment");
|
||||||
|
} finally {
|
||||||
|
setRemovingDeploymentIds((deploymentIds) => {
|
||||||
|
const nextDeploymentIds = new Set(
|
||||||
|
deploymentIds,
|
||||||
|
);
|
||||||
|
nextDeploymentIds.delete(
|
||||||
|
deployment.deploymentId,
|
||||||
|
);
|
||||||
|
return nextDeploymentIds;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
size="sm"
|
size="sm"
|
||||||
isLoading={isRemovingDeployment}
|
isLoading={removingDeploymentIds.has(
|
||||||
|
deployment.deploymentId,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
<Trash2 className="size-4" />
|
<Trash2 className="size-4" />
|
||||||
|
|||||||
@@ -209,7 +209,9 @@ export const createColumns = ({
|
|||||||
</Badge>
|
</Badge>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="max-w-xs">
|
<TooltipContent className="max-w-xs">
|
||||||
{validationState?.error ? (
|
{validationState?.isValid && validationState?.message ? (
|
||||||
|
<p>{validationState.message}</p>
|
||||||
|
) : validationState?.error ? (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<p className="font-medium text-red-500">Error:</p>
|
<p className="font-medium text-red-500">Error:</p>
|
||||||
<p>{validationState.error}</p>
|
<p>{validationState.error}</p>
|
||||||
|
|||||||
@@ -626,7 +626,10 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="max-w-xs">
|
<TooltipContent className="max-w-xs">
|
||||||
{validationState?.error ? (
|
{validationState?.isValid &&
|
||||||
|
validationState?.message ? (
|
||||||
|
<p>{validationState.message}</p>
|
||||||
|
) : validationState?.error ? (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<p className="font-medium text-red-500">
|
<p className="font-medium text-red-500">
|
||||||
Error:
|
Error:
|
||||||
|
|||||||
+9
-5
@@ -447,14 +447,18 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
{field.value?.map((path, index) => (
|
{field.value?.map((path, index) => (
|
||||||
<Badge key={index} variant="secondary">
|
<Badge key={index} variant="secondary">
|
||||||
{path}
|
{path}
|
||||||
<X
|
<button
|
||||||
className="ml-1 size-3 cursor-pointer"
|
type="button"
|
||||||
|
aria-label="Remove watch path"
|
||||||
|
className="inline-flex items-center focus-visible:ring-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newPaths = [...(field.value || [])];
|
const newPaths = [...(field.value || [])];
|
||||||
newPaths.splice(index, 1);
|
newPaths.splice(index, 1);
|
||||||
form.setValue("watchPaths", newPaths);
|
form.setValue("watchPaths", newPaths);
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<X className="ml-1 size-3 cursor-pointer" />
|
||||||
|
</button>
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -502,14 +506,14 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="enableSubmodules"
|
name="enableSubmodules"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
<FormLabel>Enable Submodules</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -242,14 +242,18 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
{field.value?.map((path, index) => (
|
{field.value?.map((path, index) => (
|
||||||
<Badge key={index} variant="secondary">
|
<Badge key={index} variant="secondary">
|
||||||
{path}
|
{path}
|
||||||
<X
|
<button
|
||||||
className="ml-1 size-3 cursor-pointer"
|
type="button"
|
||||||
|
aria-label="Remove watch path"
|
||||||
|
className="inline-flex items-center focus-visible:ring-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newPaths = [...(field.value || [])];
|
const newPaths = [...(field.value || [])];
|
||||||
newPaths.splice(index, 1);
|
newPaths.splice(index, 1);
|
||||||
form.setValue("watchPaths", newPaths);
|
form.setValue("watchPaths", newPaths);
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<X className="ml-1 size-3 cursor-pointer" />
|
||||||
|
</button>
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -298,14 +302,14 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="enableSubmodules"
|
name="enableSubmodules"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
<FormLabel>Enable Submodules</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+9
-5
@@ -477,14 +477,18 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
|||||||
className="flex items-center gap-1"
|
className="flex items-center gap-1"
|
||||||
>
|
>
|
||||||
{path}
|
{path}
|
||||||
<X
|
<button
|
||||||
className="size-3 cursor-pointer hover:text-destructive"
|
type="button"
|
||||||
|
aria-label="Remove watch path"
|
||||||
|
className="inline-flex items-center focus-visible:ring-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newPaths = [...(field.value || [])];
|
const newPaths = [...(field.value || [])];
|
||||||
newPaths.splice(index, 1);
|
newPaths.splice(index, 1);
|
||||||
field.onChange(newPaths);
|
field.onChange(newPaths);
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<X className="size-3 cursor-pointer hover:text-destructive" />
|
||||||
|
</button>
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -531,14 +535,14 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="enableSubmodules"
|
name="enableSubmodules"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
<FormLabel>Enable Submodules</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+22
-8
@@ -47,6 +47,7 @@ import {
|
|||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { DEFAULT_GITHUB_URL } from "@/utils/github-utils";
|
||||||
|
|
||||||
const GithubProviderSchema = z.object({
|
const GithubProviderSchema = z.object({
|
||||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||||
@@ -96,6 +97,11 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
|
|
||||||
const repository = form.watch("repository");
|
const repository = form.watch("repository");
|
||||||
const githubId = form.watch("githubId");
|
const githubId = form.watch("githubId");
|
||||||
|
|
||||||
|
// Enterprise repositories do not live on github.com.
|
||||||
|
const providerUrl =
|
||||||
|
githubProviders?.find((provider) => provider.githubId === githubId)
|
||||||
|
?.githubUrl ?? DEFAULT_GITHUB_URL;
|
||||||
const triggerType = form.watch("triggerType");
|
const triggerType = form.watch("triggerType");
|
||||||
|
|
||||||
const { data: repositories, isPending: isLoadingRepositories } =
|
const { data: repositories, isPending: isLoadingRepositories } =
|
||||||
@@ -227,7 +233,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
<FormLabel>Repository</FormLabel>
|
<FormLabel>Repository</FormLabel>
|
||||||
{field.value.owner && field.value.repo && (
|
{field.value.owner && field.value.repo && (
|
||||||
<Link
|
<Link
|
||||||
href={`https://github.com/${field.value.owner}/${field.value.repo}`}
|
href={`${providerUrl}/${field.value.owner}/${field.value.repo}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||||
@@ -438,8 +444,12 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={(value) => {
|
||||||
defaultValue={field.value}
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
field.onChange(value);
|
||||||
|
}}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -487,14 +497,18 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
className="flex items-center gap-1"
|
className="flex items-center gap-1"
|
||||||
>
|
>
|
||||||
{path}
|
{path}
|
||||||
<X
|
<button
|
||||||
className="size-3 cursor-pointer hover:text-destructive"
|
type="button"
|
||||||
|
aria-label="Remove watch path"
|
||||||
|
className="inline-flex items-center focus-visible:ring-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newPaths = [...(field.value || [])];
|
const newPaths = [...(field.value || [])];
|
||||||
newPaths.splice(index, 1);
|
newPaths.splice(index, 1);
|
||||||
field.onChange(newPaths);
|
field.onChange(newPaths);
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<X className="size-3 cursor-pointer hover:text-destructive" />
|
||||||
|
</button>
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -543,14 +557,14 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="enableSubmodules"
|
name="enableSubmodules"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
<FormLabel>Enable Submodules</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+9
-5
@@ -467,14 +467,18 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
className="flex items-center gap-1"
|
className="flex items-center gap-1"
|
||||||
>
|
>
|
||||||
{path}
|
{path}
|
||||||
<X
|
<button
|
||||||
className="size-3 cursor-pointer hover:text-destructive"
|
type="button"
|
||||||
|
aria-label="Remove watch path"
|
||||||
|
className="inline-flex items-center focus-visible:ring-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newPaths = [...(field.value || [])];
|
const newPaths = [...(field.value || [])];
|
||||||
newPaths.splice(index, 1);
|
newPaths.splice(index, 1);
|
||||||
field.onChange(newPaths);
|
field.onChange(newPaths);
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<X className="size-3 cursor-pointer hover:text-destructive" />
|
||||||
|
</button>
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -521,14 +525,14 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="enableSubmodules"
|
name="enableSubmodules"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
<FormLabel>Enable Submodules</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
import { GlobeIcon, Pencil, Search, X } from "lucide-react";
|
import { CircuitBoard, GlobeIcon, Pencil, Search, X } from "lucide-react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -16,7 +16,8 @@ import { type BundledIcon, bundledIcons } from "@/lib/bundled-icons";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
interface ShowIconSettingsProps {
|
interface ShowIconSettingsProps {
|
||||||
applicationId: string;
|
serviceId: string;
|
||||||
|
serviceType: "application" | "compose";
|
||||||
icon?: string | null;
|
icon?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +27,8 @@ const svgToDataUrl = (icon: BundledIcon): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ShowIconSettings = ({
|
export const ShowIconSettings = ({
|
||||||
applicationId,
|
serviceId,
|
||||||
|
serviceType,
|
||||||
icon,
|
icon,
|
||||||
}: ShowIconSettingsProps) => {
|
}: ShowIconSettingsProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -48,6 +50,17 @@ export const ShowIconSettings = ({
|
|||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutateAsync: updateApplication } =
|
const { mutateAsync: updateApplication } =
|
||||||
api.application.update.useMutation();
|
api.application.update.useMutation();
|
||||||
|
const { mutateAsync: updateCompose } = api.compose.update.useMutation();
|
||||||
|
|
||||||
|
const updateIcon = async (newIcon: string | null) => {
|
||||||
|
if (serviceType === "compose") {
|
||||||
|
await updateCompose({ composeId: serviceId, icon: newIcon });
|
||||||
|
await utils.compose.one.invalidate({ composeId: serviceId });
|
||||||
|
} else {
|
||||||
|
await updateApplication({ applicationId: serviceId, icon: newIcon });
|
||||||
|
await utils.application.one.invalidate({ applicationId: serviceId });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
@@ -59,12 +72,8 @@ export const ShowIconSettings = ({
|
|||||||
const handleIconSelect = async (selectedIcon: BundledIcon) => {
|
const handleIconSelect = async (selectedIcon: BundledIcon) => {
|
||||||
try {
|
try {
|
||||||
const dataUrl = svgToDataUrl(selectedIcon);
|
const dataUrl = svgToDataUrl(selectedIcon);
|
||||||
await updateApplication({
|
await updateIcon(dataUrl);
|
||||||
applicationId,
|
|
||||||
icon: dataUrl,
|
|
||||||
});
|
|
||||||
toast.success("Icon saved successfully");
|
toast.success("Icon saved successfully");
|
||||||
await utils.application.one.invalidate({ applicationId });
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
toast.error("Error saving icon");
|
toast.error("Error saving icon");
|
||||||
@@ -73,12 +82,8 @@ export const ShowIconSettings = ({
|
|||||||
|
|
||||||
const handleRemoveIcon = async () => {
|
const handleRemoveIcon = async () => {
|
||||||
try {
|
try {
|
||||||
await updateApplication({
|
await updateIcon(null);
|
||||||
applicationId,
|
|
||||||
icon: null,
|
|
||||||
});
|
|
||||||
toast.success("Icon removed");
|
toast.success("Icon removed");
|
||||||
await utils.application.one.invalidate({ applicationId });
|
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
toast.error("Error removing icon");
|
toast.error("Error removing icon");
|
||||||
}
|
}
|
||||||
@@ -130,12 +135,8 @@ export const ShowIconSettings = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await updateApplication({
|
await updateIcon(sanitizedDataUrl);
|
||||||
applicationId,
|
|
||||||
icon: sanitizedDataUrl,
|
|
||||||
});
|
|
||||||
toast.success("Icon saved!");
|
toast.success("Icon saved!");
|
||||||
await utils.application.one.invalidate({ applicationId });
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
toast.error("Error saving icon");
|
toast.error("Error saving icon");
|
||||||
@@ -147,12 +148,8 @@ export const ShowIconSettings = ({
|
|||||||
reader.onload = async (event) => {
|
reader.onload = async (event) => {
|
||||||
const result = event.target?.result as string;
|
const result = event.target?.result as string;
|
||||||
try {
|
try {
|
||||||
await updateApplication({
|
await updateIcon(result);
|
||||||
applicationId,
|
|
||||||
icon: result,
|
|
||||||
});
|
|
||||||
toast.success("Icon saved!");
|
toast.success("Icon saved!");
|
||||||
await utils.application.one.invalidate({ applicationId });
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
toast.error("Error saving icon");
|
toast.error("Error saving icon");
|
||||||
@@ -172,9 +169,11 @@ export const ShowIconSettings = ({
|
|||||||
// biome-ignore lint/performance/noImgElement: icon is data URL or base64
|
// biome-ignore lint/performance/noImgElement: icon is data URL or base64
|
||||||
<img
|
<img
|
||||||
src={icon}
|
src={icon}
|
||||||
alt="Application icon"
|
alt="Service icon"
|
||||||
className="h-8 w-8 object-contain"
|
className="h-8 w-8 object-contain"
|
||||||
/>
|
/>
|
||||||
|
) : serviceType === "compose" ? (
|
||||||
|
<CircuitBoard className="h-6 w-6 text-muted-foreground" />
|
||||||
) : (
|
) : (
|
||||||
<GlobeIcon className="h-6 w-6 text-muted-foreground" />
|
<GlobeIcon className="h-6 w-6 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -58,8 +58,12 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
const handleRunManually = async (scheduleId: string) => {
|
const handleRunManually = async (scheduleId: string) => {
|
||||||
setRunningSchedules((prev) => new Set(prev).add(scheduleId));
|
setRunningSchedules((prev) => new Set(prev).add(scheduleId));
|
||||||
try {
|
try {
|
||||||
await runManually({ scheduleId });
|
const result = await runManually({ scheduleId });
|
||||||
toast.success("Schedule run successfully");
|
if (result.status === "error") {
|
||||||
|
toast.error("Schedule run failed, check the deployment logs");
|
||||||
|
} else {
|
||||||
|
toast.success("Schedule run successfully");
|
||||||
|
}
|
||||||
await refetchSchedules();
|
await refetchSchedules();
|
||||||
} catch {
|
} catch {
|
||||||
toast.error("Error running schedule");
|
toast.error("Error running schedule");
|
||||||
|
|||||||
+9
-5
@@ -451,14 +451,18 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
{field.value?.map((path, index) => (
|
{field.value?.map((path, index) => (
|
||||||
<Badge key={index} variant="secondary">
|
<Badge key={index} variant="secondary">
|
||||||
{path}
|
{path}
|
||||||
<X
|
<button
|
||||||
className="ml-1 size-3 cursor-pointer"
|
type="button"
|
||||||
|
aria-label="Remove watch path"
|
||||||
|
className="inline-flex items-center focus-visible:ring-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newPaths = [...(field.value || [])];
|
const newPaths = [...(field.value || [])];
|
||||||
newPaths.splice(index, 1);
|
newPaths.splice(index, 1);
|
||||||
form.setValue("watchPaths", newPaths);
|
form.setValue("watchPaths", newPaths);
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<X className="ml-1 size-3 cursor-pointer" />
|
||||||
|
</button>
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -506,14 +510,14 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="enableSubmodules"
|
name="enableSubmodules"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
<FormLabel>Enable Submodules</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+9
-5
@@ -251,14 +251,18 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
|||||||
{field.value?.map((path, index) => (
|
{field.value?.map((path, index) => (
|
||||||
<Badge key={index} variant="secondary">
|
<Badge key={index} variant="secondary">
|
||||||
{path}
|
{path}
|
||||||
<X
|
<button
|
||||||
className="ml-1 size-3 cursor-pointer"
|
type="button"
|
||||||
|
aria-label="Remove watch path"
|
||||||
|
className="inline-flex items-center focus-visible:ring-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newPaths = [...(field.value || [])];
|
const newPaths = [...(field.value || [])];
|
||||||
newPaths.splice(index, 1);
|
newPaths.splice(index, 1);
|
||||||
form.setValue("watchPaths", newPaths);
|
form.setValue("watchPaths", newPaths);
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<X className="ml-1 size-3 cursor-pointer" />
|
||||||
|
</button>
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -306,14 +310,14 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="enableSubmodules"
|
name="enableSubmodules"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
<FormLabel>Enable Submodules</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+9
-5
@@ -442,14 +442,18 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
|||||||
{field.value?.map((path, index) => (
|
{field.value?.map((path, index) => (
|
||||||
<Badge key={index} variant="secondary">
|
<Badge key={index} variant="secondary">
|
||||||
{path}
|
{path}
|
||||||
<X
|
<button
|
||||||
className="ml-1 size-3 cursor-pointer"
|
type="button"
|
||||||
|
aria-label="Remove watch path"
|
||||||
|
className="inline-flex items-center focus-visible:ring-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newPaths = [...(field.value || [])];
|
const newPaths = [...(field.value || [])];
|
||||||
newPaths.splice(index, 1);
|
newPaths.splice(index, 1);
|
||||||
form.setValue("watchPaths", newPaths);
|
form.setValue("watchPaths", newPaths);
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<X className="ml-1 size-3 cursor-pointer" />
|
||||||
|
</button>
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -497,14 +501,14 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="enableSubmodules"
|
name="enableSubmodules"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
<FormLabel>Enable Submodules</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+22
-8
@@ -47,6 +47,7 @@ import {
|
|||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { DEFAULT_GITHUB_URL } from "@/utils/github-utils";
|
||||||
|
|
||||||
const GithubProviderSchema = z.object({
|
const GithubProviderSchema = z.object({
|
||||||
composePath: z.string().min(1),
|
composePath: z.string().min(1),
|
||||||
@@ -98,6 +99,11 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
const repository = form.watch("repository");
|
const repository = form.watch("repository");
|
||||||
const githubId = form.watch("githubId");
|
const githubId = form.watch("githubId");
|
||||||
const triggerType = form.watch("triggerType");
|
const triggerType = form.watch("triggerType");
|
||||||
|
|
||||||
|
// Enterprise repositories do not live on github.com.
|
||||||
|
const providerUrl =
|
||||||
|
githubProviders?.find((provider) => provider.githubId === githubId)
|
||||||
|
?.githubUrl ?? DEFAULT_GITHUB_URL;
|
||||||
const { data: repositories, isPending: isLoadingRepositories } =
|
const { data: repositories, isPending: isLoadingRepositories } =
|
||||||
api.github.getGithubRepositories.useQuery(
|
api.github.getGithubRepositories.useQuery(
|
||||||
{
|
{
|
||||||
@@ -220,7 +226,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
<FormLabel>Repository</FormLabel>
|
<FormLabel>Repository</FormLabel>
|
||||||
{field.value.owner && field.value.repo && (
|
{field.value.owner && field.value.repo && (
|
||||||
<Link
|
<Link
|
||||||
href={`https://github.com/${field.value.owner}/${field.value.repo}`}
|
href={`${providerUrl}/${field.value.owner}/${field.value.repo}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||||
@@ -432,8 +438,12 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={(value) => {
|
||||||
defaultValue={field.value}
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
field.onChange(value);
|
||||||
|
}}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -479,14 +489,18 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
{field.value?.map((path, index) => (
|
{field.value?.map((path, index) => (
|
||||||
<Badge key={index} variant="secondary">
|
<Badge key={index} variant="secondary">
|
||||||
{path}
|
{path}
|
||||||
<X
|
<button
|
||||||
className="ml-1 size-3 cursor-pointer"
|
type="button"
|
||||||
|
aria-label="Remove watch path"
|
||||||
|
className="inline-flex items-center focus-visible:ring-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newPaths = [...(field.value || [])];
|
const newPaths = [...(field.value || [])];
|
||||||
newPaths.splice(index, 1);
|
newPaths.splice(index, 1);
|
||||||
form.setValue("watchPaths", newPaths);
|
form.setValue("watchPaths", newPaths);
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<X className="ml-1 size-3 cursor-pointer" />
|
||||||
|
</button>
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -538,14 +552,14 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="enableSubmodules"
|
name="enableSubmodules"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
<FormLabel>Enable Submodules</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+9
-5
@@ -468,14 +468,18 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
{field.value?.map((path, index) => (
|
{field.value?.map((path, index) => (
|
||||||
<Badge key={index} variant="secondary">
|
<Badge key={index} variant="secondary">
|
||||||
{path}
|
{path}
|
||||||
<X
|
<button
|
||||||
className="ml-1 size-3 cursor-pointer"
|
type="button"
|
||||||
|
aria-label="Remove watch path"
|
||||||
|
className="inline-flex items-center focus-visible:ring-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newPaths = [...(field.value || [])];
|
const newPaths = [...(field.value || [])];
|
||||||
newPaths.splice(index, 1);
|
newPaths.splice(index, 1);
|
||||||
form.setValue("watchPaths", newPaths);
|
form.setValue("watchPaths", newPaths);
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<X className="ml-1 size-3 cursor-pointer" />
|
||||||
|
</button>
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -523,14 +527,14 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="enableSubmodules"
|
name="enableSubmodules"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
<FormLabel>Enable Submodules</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ export const RestoreBackup = ({
|
|||||||
Restore Backup
|
Restore Backup
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-lg">
|
<DialogContent className="sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center">
|
<DialogTitle className="flex items-center">
|
||||||
<RotateCcw className="mr-2 size-4" />
|
<RotateCcw className="mr-2 size-4" />
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ const networkFormSchema = z
|
|||||||
attachable: z.boolean(),
|
attachable: z.boolean(),
|
||||||
enableIPv4: z.boolean(),
|
enableIPv4: z.boolean(),
|
||||||
enableIPv6: z.boolean(),
|
enableIPv6: z.boolean(),
|
||||||
|
mtu: z
|
||||||
|
.string()
|
||||||
|
.refine(
|
||||||
|
(value) =>
|
||||||
|
value === "" ||
|
||||||
|
(/^\d+$/.test(value) && +value >= 68 && +value <= 65535),
|
||||||
|
{ message: "MTU must be a number between 68 and 65535" },
|
||||||
|
),
|
||||||
ipamDriver: z.string().optional(),
|
ipamDriver: z.string().optional(),
|
||||||
ipamConfig: z.array(ipamConfigEntrySchema),
|
ipamConfig: z.array(ipamConfigEntrySchema),
|
||||||
})
|
})
|
||||||
@@ -85,6 +93,7 @@ const defaultValues: NetworkFormValues = {
|
|||||||
attachable: false,
|
attachable: false,
|
||||||
enableIPv4: true,
|
enableIPv4: true,
|
||||||
enableIPv6: false,
|
enableIPv6: false,
|
||||||
|
mtu: "",
|
||||||
ipamDriver: "",
|
ipamDriver: "",
|
||||||
ipamConfig: [],
|
ipamConfig: [],
|
||||||
};
|
};
|
||||||
@@ -147,6 +156,7 @@ export const HandleNetwork = ({ serverId, children }: HandleNetworkProps) => {
|
|||||||
attachable: data.attachable,
|
attachable: data.attachable,
|
||||||
enableIPv4: data.enableIPv4,
|
enableIPv4: data.enableIPv4,
|
||||||
enableIPv6: data.enableIPv6,
|
enableIPv6: data.enableIPv6,
|
||||||
|
mtu: data.mtu ? Number(data.mtu) : undefined,
|
||||||
ipam: {
|
ipam: {
|
||||||
driver: data.ipamDriver || undefined,
|
driver: data.ipamDriver || undefined,
|
||||||
config: data.ipamConfig,
|
config: data.ipamConfig,
|
||||||
@@ -232,6 +242,27 @@ export const HandleNetwork = ({ serverId, children }: HandleNetworkProps) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="mtu"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>MTU (optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="1500"
|
||||||
|
inputMode="numeric"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription className="text-muted-foreground">
|
||||||
|
Maximum transmission unit. Leave empty to use Docker's
|
||||||
|
default.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
{toggleOptions.map((option) => (
|
{toggleOptions.map((option) => (
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { DEFAULT_GITHUB_URL, resolveGithubBaseUrl } from "@/utils/github-utils";
|
||||||
|
|
||||||
export const AddGithubProvider = () => {
|
export const AddGithubProvider = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
@@ -23,6 +24,9 @@ export const AddGithubProvider = () => {
|
|||||||
const [manifest, setManifest] = useState("");
|
const [manifest, setManifest] = useState("");
|
||||||
const [isOrganization, setIsOrganization] = useState(false);
|
const [isOrganization, setIsOrganization] = useState(false);
|
||||||
const [organizationName, setOrganization] = useState("");
|
const [organizationName, setOrganization] = useState("");
|
||||||
|
const [githubUrl, setGithubUrl] = useState(DEFAULT_GITHUB_URL);
|
||||||
|
|
||||||
|
const { baseUrl, error: githubUrlError } = resolveGithubBaseUrl(githubUrl);
|
||||||
|
|
||||||
const randomString = () => Math.random().toString(36).slice(2, 8);
|
const randomString = () => Math.random().toString(36).slice(2, 8);
|
||||||
|
|
||||||
@@ -30,7 +34,7 @@ export const AddGithubProvider = () => {
|
|||||||
const url = document.location.origin;
|
const url = document.location.origin;
|
||||||
const manifest = JSON.stringify(
|
const manifest = JSON.stringify(
|
||||||
{
|
{
|
||||||
redirect_url: `${origin}/api/providers/github/setup?organizationId=${activeOrganization?.id ?? ""}&userId=${session?.user?.id ?? ""}`,
|
redirect_url: `${origin}/api/providers/github/setup?organizationId=${activeOrganization?.id ?? ""}&userId=${session?.user?.id ?? ""}&githubUrl=${encodeURIComponent(baseUrl)}`,
|
||||||
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}-${randomString()}`,
|
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}-${randomString()}`,
|
||||||
url: origin,
|
url: origin,
|
||||||
hook_attributes: {
|
hook_attributes: {
|
||||||
@@ -52,7 +56,7 @@ export const AddGithubProvider = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
setManifest(manifest);
|
setManifest(manifest);
|
||||||
}, [activeOrganization?.id, session?.user?.id]);
|
}, [activeOrganization?.id, session?.user?.id, baseUrl]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
@@ -79,6 +83,25 @@ export const AddGithubProvider = () => {
|
|||||||
below to get started.
|
below to get started.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4 flex flex-col gap-4">
|
<div className="mt-4 flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<span className="text-sm">GitHub URL</span>
|
||||||
|
<Input
|
||||||
|
placeholder={DEFAULT_GITHUB_URL}
|
||||||
|
value={githubUrl}
|
||||||
|
onChange={(e) => setGithubUrl(e.target.value)}
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground text-xs">
|
||||||
|
Leave as is for github.com. For GitHub Enterprise, use your
|
||||||
|
instance URL (e.g. https://acme.ghe.com or
|
||||||
|
https://github.acme.com).
|
||||||
|
</span>
|
||||||
|
{githubUrlError && (
|
||||||
|
<span className="text-destructive text-xs">
|
||||||
|
{githubUrlError}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<span>Organization?</span>
|
<span>Organization?</span>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -98,8 +121,8 @@ export const AddGithubProvider = () => {
|
|||||||
<form
|
<form
|
||||||
action={
|
action={
|
||||||
isOrganization
|
isOrganization
|
||||||
? `https://github.com/organizations/${organizationName}/settings/apps/new?state=gh_init:${activeOrganization?.id}:${session?.user?.id ?? ""}`
|
? `${baseUrl}/organizations/${organizationName}/settings/apps/new?state=gh_init:${activeOrganization?.id}:${session?.user?.id ?? ""}`
|
||||||
: `https://github.com/settings/apps/new?state=gh_init:${activeOrganization?.id}:${session?.user?.id ?? ""}`
|
: `${baseUrl}/settings/apps/new?state=gh_init:${activeOrganization?.id}:${session?.user?.id ?? ""}`
|
||||||
}
|
}
|
||||||
method="post"
|
method="post"
|
||||||
>
|
>
|
||||||
@@ -116,8 +139,8 @@ export const AddGithubProvider = () => {
|
|||||||
<a
|
<a
|
||||||
href={
|
href={
|
||||||
isOrganization && organizationName
|
isOrganization && organizationName
|
||||||
? `https://github.com/organizations/${organizationName}/settings/installations`
|
? `${baseUrl}/organizations/${organizationName}/settings/installations`
|
||||||
: "https://github.com/settings/installations"
|
: `${baseUrl}/settings/installations`
|
||||||
}
|
}
|
||||||
className={`text-muted-foreground text-sm hover:underline duration-300
|
className={`text-muted-foreground text-sm hover:underline duration-300
|
||||||
${
|
${
|
||||||
@@ -131,7 +154,10 @@ export const AddGithubProvider = () => {
|
|||||||
Unsure if you already have an app?
|
Unsure if you already have an app?
|
||||||
</a>
|
</a>
|
||||||
<Button
|
<Button
|
||||||
disabled={isOrganization && organizationName.length < 1}
|
disabled={
|
||||||
|
!!githubUrlError ||
|
||||||
|
(isOrganization && organizationName.length < 1)
|
||||||
|
}
|
||||||
type="submit"
|
type="submit"
|
||||||
className="self-end"
|
className="self-end"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -147,6 +147,15 @@ export const EditGithubProvider = ({ githubId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<span className="text-sm font-medium">GitHub URL</span>
|
||||||
|
<Input value={github?.githubUrl ?? ""} readOnly />
|
||||||
|
<span className="text-muted-foreground text-xs">
|
||||||
|
Set when the app was created and not editable, the app
|
||||||
|
credentials belong to this instance.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full justify-between gap-4 mt-4">
|
<div className="flex w-full justify-between gap-4 mt-4">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { DEFAULT_GITHUB_URL } from "@/utils/github-utils";
|
||||||
import { useUrl } from "@/utils/hooks/use-url";
|
import { useUrl } from "@/utils/hooks/use-url";
|
||||||
import { AddBitbucketProvider } from "./bitbucket/add-bitbucket-provider";
|
import { AddBitbucketProvider } from "./bitbucket/add-bitbucket-provider";
|
||||||
import { EditBitbucketProvider } from "./bitbucket/edit-bitbucket-provider";
|
import { EditBitbucketProvider } from "./bitbucket/edit-bitbucket-provider";
|
||||||
@@ -161,6 +162,14 @@ export const ShowGitProviders = () => {
|
|||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{gitProvider.name}
|
{gitProvider.name}
|
||||||
</span>
|
</span>
|
||||||
|
{isGithub &&
|
||||||
|
gitProvider.github?.githubUrl &&
|
||||||
|
gitProvider.github.githubUrl !==
|
||||||
|
DEFAULT_GITHUB_URL && (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{gitProvider.github.githubUrl}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{formatDate(
|
{formatDate(
|
||||||
gitProvider.createdAt,
|
gitProvider.createdAt,
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import { Fingerprint, Loader2, Plus, Trash2 } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
export const ManagePasskeys = () => {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
const { data: passkeys, isLoading } = api.user.listPasskeys.useQuery(
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
enabled: isDialogOpen,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleAddPasskey = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsAdding(true);
|
||||||
|
try {
|
||||||
|
const result = await authClient.passkey.addPasskey({
|
||||||
|
name: name.trim() || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
toast.error(result.error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("Passkey added successfully");
|
||||||
|
setName("");
|
||||||
|
utils.user.listPasskeys.invalidate();
|
||||||
|
} catch {
|
||||||
|
toast.error("Failed to add passkey");
|
||||||
|
} finally {
|
||||||
|
setIsAdding(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeletePasskey = async (id: string) => {
|
||||||
|
setDeletingId(id);
|
||||||
|
try {
|
||||||
|
const result = await authClient.passkey.deletePasskey({ id });
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
toast.error(result.error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("Passkey removed");
|
||||||
|
utils.user.listPasskeys.invalidate();
|
||||||
|
} catch {
|
||||||
|
toast.error("Failed to remove passkey");
|
||||||
|
} finally {
|
||||||
|
setDeletingId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="secondary">
|
||||||
|
<Fingerprint className="size-4 text-muted-foreground" />
|
||||||
|
Passkeys
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Passkeys</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Sign in without a password using your device's biometrics, security
|
||||||
|
key, or password manager.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[10vh]">
|
||||||
|
<span>Loading...</span>
|
||||||
|
<Loader2 className="animate-spin size-4" />
|
||||||
|
</div>
|
||||||
|
) : passkeys && passkeys.length > 0 ? (
|
||||||
|
<div className="grid gap-3">
|
||||||
|
{passkeys.map((passkey) => (
|
||||||
|
<div
|
||||||
|
key={passkey.id}
|
||||||
|
className="flex items-center justify-between gap-2 p-4 border rounded-lg"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-1 min-w-0">
|
||||||
|
<span className="font-medium flex items-center gap-2">
|
||||||
|
<Fingerprint className="size-4 text-muted-foreground shrink-0" />
|
||||||
|
<span className="truncate">
|
||||||
|
{passkey.name || "Unnamed passkey"}
|
||||||
|
</span>
|
||||||
|
<Badge variant="outline">
|
||||||
|
{passkey.deviceType === "singleDevice"
|
||||||
|
? "Device"
|
||||||
|
: "Synced"}
|
||||||
|
</Badge>
|
||||||
|
</span>
|
||||||
|
{passkey.createdAt && (
|
||||||
|
<DateTooltip
|
||||||
|
date={passkey.createdAt.toString()}
|
||||||
|
className="text-sm"
|
||||||
|
>
|
||||||
|
Added
|
||||||
|
</DateTooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<DialogAction
|
||||||
|
title="Remove this passkey?"
|
||||||
|
description="You will no longer be able to sign in with it. This action cannot be undone."
|
||||||
|
onClick={() => handleDeletePasskey(passkey.id)}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
isLoading={deletingId === passkey.id}
|
||||||
|
className="text-muted-foreground hover:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center gap-2 py-6 text-sm text-muted-foreground border rounded-lg">
|
||||||
|
<Fingerprint className="size-6" />
|
||||||
|
<span>No passkeys registered yet</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleAddPasskey} className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="passkey-name">Passkey Name</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
id="passkey-name"
|
||||||
|
placeholder="e.g. MacBook Touch ID"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button type="submit" isLoading={isAdding}>
|
||||||
|
<Plus className="size-4" />
|
||||||
|
Add Passkey
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -31,6 +31,7 @@ import { generateSHA256Hash, getFallbackAvatarInitials } from "@/lib/utils";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Configure2FA } from "./configure-2fa";
|
import { Configure2FA } from "./configure-2fa";
|
||||||
import { Enable2FA } from "./enable-2fa";
|
import { Enable2FA } from "./enable-2fa";
|
||||||
|
import { ManagePasskeys } from "./manage-passkeys";
|
||||||
|
|
||||||
const profileSchema = z.object({
|
const profileSchema = z.object({
|
||||||
email: z
|
email: z
|
||||||
@@ -162,7 +163,10 @@ export const ProfileForm = () => {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!data?.user.twoFactorEnabled ? <Enable2FA /> : <Configure2FA />}
|
<div className="flex flex-row gap-2 flex-wrap">
|
||||||
|
<ManagePasskeys />
|
||||||
|
{!data?.user.twoFactorEnabled ? <Enable2FA /> : <Configure2FA />}
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="space-y-2 py-8 border-t">
|
<CardContent className="space-y-2 py-8 border-t">
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ export const ShowDokployActions = () => {
|
|||||||
const { mutateAsync: reloadServer, isPending } =
|
const { mutateAsync: reloadServer, isPending } =
|
||||||
api.settings.reloadServer.useMutation();
|
api.settings.reloadServer.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: cleanRedis } = api.settings.cleanRedis.useMutation();
|
|
||||||
const { mutateAsync: reloadRedis } = api.settings.reloadRedis.useMutation();
|
|
||||||
const { mutateAsync: cleanAllDeploymentQueue } =
|
const { mutateAsync: cleanAllDeploymentQueue } =
|
||||||
api.settings.cleanAllDeploymentQueue.useMutation();
|
api.settings.cleanAllDeploymentQueue.useMutation();
|
||||||
|
|
||||||
@@ -70,21 +68,6 @@ export const ShowDokployActions = () => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</UpdateServerIp>
|
</UpdateServerIp>
|
||||||
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={async () => {
|
|
||||||
await cleanRedis()
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Redis cleaned");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error cleaning Redis");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Clean Redis
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@@ -99,21 +82,6 @@ export const ShowDokployActions = () => {
|
|||||||
>
|
>
|
||||||
Clean all deployment queue
|
Clean all deployment queue
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={async () => {
|
|
||||||
await reloadRedis()
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Redis reloaded");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error reloading Redis");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Reload Redis
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ type ServiceStatus = {
|
|||||||
|
|
||||||
type HealthResult = {
|
type HealthResult = {
|
||||||
postgres: ServiceStatus;
|
postgres: ServiceStatus;
|
||||||
redis: ServiceStatus;
|
|
||||||
traefik: ServiceStatus;
|
traefik: ServiceStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,7 +88,6 @@ export const UpdateWebServer = ({
|
|||||||
const allHealthy =
|
const allHealthy =
|
||||||
healthResult &&
|
healthResult &&
|
||||||
healthResult.postgres.status === "healthy" &&
|
healthResult.postgres.status === "healthy" &&
|
||||||
healthResult.redis.status === "healthy" &&
|
|
||||||
healthResult.traefik.status === "healthy";
|
healthResult.traefik.status === "healthy";
|
||||||
|
|
||||||
const checkIsUpdateFinished = async () => {
|
const checkIsUpdateFinished = async () => {
|
||||||
@@ -179,7 +177,7 @@ export const UpdateWebServer = ({
|
|||||||
{modalState === "checking" && (
|
{modalState === "checking" && (
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<Loader2 className="animate-spin h-4 w-4" />
|
<Loader2 className="animate-spin h-4 w-4" />
|
||||||
Checking PostgreSQL, Redis and Traefik...
|
Checking PostgreSQL and Traefik...
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -190,10 +188,6 @@ export const UpdateWebServer = ({
|
|||||||
name="PostgreSQL"
|
name="PostgreSQL"
|
||||||
service={healthResult.postgres}
|
service={healthResult.postgres}
|
||||||
/>
|
/>
|
||||||
<ServiceStatusItem
|
|
||||||
name="Redis"
|
|
||||||
service={healthResult.redis}
|
|
||||||
/>
|
|
||||||
<ServiceStatusItem
|
<ServiceStatusItem
|
||||||
name="Traefik"
|
name="Traefik"
|
||||||
service={healthResult.traefik}
|
service={healthResult.traefik}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type * as React from "react";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
"group/badge inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2.5 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
"group/badge inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2.5 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg:not(.cursor-pointer)]:pointer-events-none [&>svg]:size-3!",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -1,21 +1,37 @@
|
|||||||
import { CheckIcon, ChevronRightIcon } from "lucide-react";
|
import { CheckIcon, ChevronRightIcon } from "lucide-react";
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
||||||
import type * as React from "react";
|
import * as React from "react";
|
||||||
import { markNestedPopupClosed } from "@/components/ui/nested-popup-context";
|
import { markNestedPopupClosed } from "@/components/ui/nested-popup-context";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function DropdownMenu({
|
function DropdownMenu({
|
||||||
|
open,
|
||||||
|
defaultOpen,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
|
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(
|
||||||
|
defaultOpen ?? false,
|
||||||
|
);
|
||||||
|
const isControlled = open !== undefined;
|
||||||
|
const isOpen = isControlled ? open : uncontrolledOpen;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Root
|
<DropdownMenuPrimitive.Root
|
||||||
data-slot="dropdown-menu"
|
data-slot="dropdown-menu"
|
||||||
onOpenChange={(open) => {
|
open={isOpen}
|
||||||
if (!open) {
|
onOpenChange={(nextOpen) => {
|
||||||
|
// Radix closes menus on window blur, unmounting any dialog rendered inside
|
||||||
|
if (!nextOpen && !document.hasFocus()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isControlled) {
|
||||||
|
setUncontrolledOpen(nextOpen);
|
||||||
|
}
|
||||||
|
if (!nextOpen) {
|
||||||
markNestedPopupClosed();
|
markNestedPopupClosed();
|
||||||
}
|
}
|
||||||
onOpenChange?.(open);
|
onOpenChange?.(nextOpen);
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -464,7 +464,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sidebarMenuButtonVariants = cva(
|
const sidebarMenuButtonVariants = cva(
|
||||||
"peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate",
|
"peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
@@ -473,8 +473,8 @@ const sidebarMenuButtonVariants = cva(
|
|||||||
"bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]",
|
"bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-8 text-sm",
|
default: "h-8 text-sm group-data-[collapsible=icon]:p-2!",
|
||||||
sm: "h-7 text-xs",
|
sm: "h-7 text-xs group-data-[collapsible=icon]:p-2!",
|
||||||
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
|
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "compose" ADD COLUMN "icon" text;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
CREATE TABLE "passkey" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"name" text,
|
||||||
|
"public_key" text NOT NULL,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"credential_id" text NOT NULL,
|
||||||
|
"counter" integer NOT NULL,
|
||||||
|
"device_type" text NOT NULL,
|
||||||
|
"backed_up" boolean NOT NULL,
|
||||||
|
"transports" text,
|
||||||
|
"created_at" timestamp,
|
||||||
|
"aaguid" text
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "passkey" ADD CONSTRAINT "passkey_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE INDEX "passkey_userId_idx" ON "passkey" USING btree ("user_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "passkey_credentialID_idx" ON "passkey" USING btree ("credential_id");
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "network" ADD COLUMN "mtu" integer;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "github" ADD COLUMN "githubUrl" text DEFAULT 'https://github.com' NOT NULL;
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1233,6 +1233,34 @@
|
|||||||
"when": 1784965709630,
|
"when": 1784965709630,
|
||||||
"tag": "0175_fantastic_peter_quill",
|
"tag": "0175_fantastic_peter_quill",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 176,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1785274179336,
|
||||||
|
"tag": "0176_slimy_marten_broadcloak",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 177,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1785709848209,
|
||||||
|
"tag": "0177_glossy_brother_voodoo",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 178,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1785751631838,
|
||||||
|
"tag": "0178_silky_randall",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 179,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1785800152143,
|
||||||
|
"tag": "0179_foamy_marauders",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { apiKeyClient } from "@better-auth/api-key/client";
|
import { apiKeyClient } from "@better-auth/api-key/client";
|
||||||
|
import { passkeyClient } from "@better-auth/passkey/client";
|
||||||
import { ssoClient } from "@better-auth/sso/client";
|
import { ssoClient } from "@better-auth/sso/client";
|
||||||
import {
|
import {
|
||||||
adminClient,
|
adminClient,
|
||||||
@@ -13,6 +14,7 @@ export const authClient = createAuthClient({
|
|||||||
plugins: [
|
plugins: [
|
||||||
organizationClient(),
|
organizationClient(),
|
||||||
twoFactorClient(),
|
twoFactorClient(),
|
||||||
|
passkeyClient(),
|
||||||
apiKeyClient(),
|
apiKeyClient(),
|
||||||
ssoClient(),
|
ssoClient(),
|
||||||
adminClient(),
|
adminClient(),
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"@ai-sdk/openai": "^3.0.29",
|
"@ai-sdk/openai": "^3.0.29",
|
||||||
"@ai-sdk/openai-compatible": "^2.0.30",
|
"@ai-sdk/openai-compatible": "^2.0.30",
|
||||||
"@better-auth/api-key": "1.6.23",
|
"@better-auth/api-key": "1.6.23",
|
||||||
|
"@better-auth/passkey": "1.6.23",
|
||||||
"@better-auth/scim": "1.6.23",
|
"@better-auth/scim": "1.6.23",
|
||||||
"@better-auth/sso": "1.6.23",
|
"@better-auth/sso": "1.6.23",
|
||||||
"@codemirror/autocomplete": "^6.18.6",
|
"@codemirror/autocomplete": "^6.18.6",
|
||||||
@@ -86,7 +87,6 @@
|
|||||||
"better-auth": "1.6.23",
|
"better-auth": "1.6.23",
|
||||||
"bl": "6.0.11",
|
"bl": "6.0.11",
|
||||||
"boxen": "^7.1.1",
|
"boxen": "^7.1.1",
|
||||||
"bullmq": "5.67.3",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^0.2.1",
|
"cmdk": "^0.2.1",
|
||||||
|
|||||||
@@ -5,12 +5,11 @@ import { buttonVariants } from "@/components/ui/button";
|
|||||||
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
statusCode: number;
|
statusCode?: number;
|
||||||
error?: Error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Custom404({ statusCode, error }: Props) {
|
export default function ErrorPage({ statusCode }: Props) {
|
||||||
const displayStatusCode = statusCode || 400;
|
const displayStatusCode = statusCode || 500;
|
||||||
const { config: whitelabeling } = useWhitelabelingPublic();
|
const { config: whitelabeling } = useWhitelabelingPublic();
|
||||||
const appName = whitelabeling?.appName || "Dokploy";
|
const appName = whitelabeling?.appName || "Dokploy";
|
||||||
const logoUrl = whitelabeling?.logoUrl || undefined;
|
const logoUrl = whitelabeling?.logoUrl || undefined;
|
||||||
@@ -45,12 +44,6 @@ export default function Custom404({ statusCode, error }: Props) {
|
|||||||
{errorDescription}
|
{errorDescription}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{error && (
|
|
||||||
<div className="mt-3 text-red-500">
|
|
||||||
<p>{error.message}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-5 flex flex-col justify-center items-center gap-2 sm:flex-row sm:gap-3">
|
<div className="mt-5 flex flex-col justify-center items-center gap-2 sm:flex-row sm:gap-3">
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/home"
|
href="/dashboard/home"
|
||||||
@@ -101,8 +94,7 @@ export default function Custom404({ statusCode, error }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
ErrorPage.getInitialProps = ({ res, err }: NextPageContext) => {
|
||||||
Error.getInitialProps = ({ res, err }: NextPageContext) => {
|
const statusCode = res ? res.statusCode : err ? (err.statusCode ?? 500) : 404;
|
||||||
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
|
return { statusCode };
|
||||||
return { statusCode, error: err };
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -119,9 +119,11 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
// If webhook doesn't provide image info, we'll use the configured image (old behavior)
|
// If webhook doesn't provide image info, we'll use the configured image (old behavior)
|
||||||
} else if (sourceType === "github") {
|
} else if (sourceType === "github") {
|
||||||
const normalizedCommits = req.body?.commits?.flatMap(
|
const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
|
|
||||||
const shouldDeployPaths = shouldDeploy(
|
const shouldDeployPaths = shouldDeploy(
|
||||||
application.watchPaths,
|
application.watchPaths,
|
||||||
@@ -150,21 +152,29 @@ export default async function handler(
|
|||||||
let normalizedCommits: string[] = [];
|
let normalizedCommits: string[] = [];
|
||||||
|
|
||||||
if (provider === "github") {
|
if (provider === "github") {
|
||||||
normalizedCommits = req.body?.commits?.flatMap(
|
normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
} else if (provider === "gitlab") {
|
} else if (provider === "gitlab") {
|
||||||
normalizedCommits = req.body?.commits?.flatMap(
|
normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
} else if (provider === "gitea") {
|
} else if (provider === "gitea") {
|
||||||
normalizedCommits = req.body?.commits?.flatMap(
|
normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
} else if (provider === "soft-serve") {
|
} else if (provider === "soft-serve") {
|
||||||
normalizedCommits = req.body?.commits?.flatMap(
|
normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldDeployPaths = shouldDeploy(
|
const shouldDeployPaths = shouldDeploy(
|
||||||
@@ -179,9 +189,11 @@ export default async function handler(
|
|||||||
} else if (sourceType === "gitlab") {
|
} else if (sourceType === "gitlab") {
|
||||||
const branchName = extractBranchName(req.headers, req.body);
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
|
|
||||||
const normalizedCommits = req.body?.commits?.flatMap(
|
const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
|
|
||||||
const shouldDeployPaths = shouldDeploy(
|
const shouldDeployPaths = shouldDeploy(
|
||||||
application.watchPaths,
|
application.watchPaths,
|
||||||
@@ -225,9 +237,11 @@ export default async function handler(
|
|||||||
} else if (sourceType === "gitea") {
|
} else if (sourceType === "gitea") {
|
||||||
const branchName = extractBranchName(req.headers, req.body);
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
|
|
||||||
const normalizedCommits = req.body?.commits?.flatMap(
|
const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
|
|
||||||
const shouldDeployPaths = shouldDeploy(
|
const shouldDeployPaths = shouldDeploy(
|
||||||
application.watchPaths,
|
application.watchPaths,
|
||||||
|
|||||||
@@ -54,9 +54,11 @@ export default async function handler(
|
|||||||
|
|
||||||
if (sourceType === "github") {
|
if (sourceType === "github") {
|
||||||
const branchName = extractBranchName(req.headers, req.body);
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
const normalizedCommits = req.body?.commits?.flatMap(
|
const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
|
|
||||||
const shouldDeployPaths = shouldDeploy(
|
const shouldDeployPaths = shouldDeploy(
|
||||||
composeResult.watchPaths,
|
composeResult.watchPaths,
|
||||||
@@ -74,9 +76,11 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
} else if (sourceType === "gitlab") {
|
} else if (sourceType === "gitlab") {
|
||||||
const branchName = extractBranchName(req.headers, req.body);
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
const normalizedCommits = req.body?.commits?.flatMap(
|
const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
|
|
||||||
const shouldDeployPaths = shouldDeploy(
|
const shouldDeployPaths = shouldDeploy(
|
||||||
composeResult.watchPaths,
|
composeResult.watchPaths,
|
||||||
@@ -125,17 +129,23 @@ export default async function handler(
|
|||||||
let normalizedCommits: string[] = [];
|
let normalizedCommits: string[] = [];
|
||||||
|
|
||||||
if (provider === "github") {
|
if (provider === "github") {
|
||||||
normalizedCommits = req.body?.commits?.flatMap(
|
normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
} else if (provider === "gitlab") {
|
} else if (provider === "gitlab") {
|
||||||
normalizedCommits = req.body?.commits?.flatMap(
|
normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
} else if (provider === "gitea") {
|
} else if (provider === "gitea") {
|
||||||
normalizedCommits = req.body?.commits?.flatMap(
|
normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldDeployPaths = shouldDeploy(
|
const shouldDeployPaths = shouldDeploy(
|
||||||
@@ -150,9 +160,11 @@ export default async function handler(
|
|||||||
} else if (sourceType === "gitea") {
|
} else if (sourceType === "gitea") {
|
||||||
const branchName = extractBranchName(req.headers, req.body);
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
|
|
||||||
const normalizedCommits = req.body?.commits?.flatMap(
|
const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
|
|
||||||
const shouldDeployPaths = shouldDeploy(
|
const shouldDeployPaths = shouldDeploy(
|
||||||
composeResult.watchPaths,
|
composeResult.watchPaths,
|
||||||
|
|||||||
@@ -223,9 +223,11 @@ export default async function handler(
|
|||||||
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
||||||
const deploymentHash = extractHash(req.headers, req.body);
|
const deploymentHash = extractHash(req.headers, req.body);
|
||||||
const owner = getGithubRepositoryOwner(githubBody);
|
const owner = getGithubRepositoryOwner(githubBody);
|
||||||
const normalizedCommits = githubBody?.commits?.flatMap(
|
const normalizedCommits = githubBody?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
|
|
||||||
const apps = await db.query.applications.findMany({
|
const apps = await db.query.applications.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { createGithub, validateRequest } from "@dokploy/server";
|
import {
|
||||||
|
createGithub,
|
||||||
|
deriveGithubApiUrl,
|
||||||
|
parseGithubBaseUrl,
|
||||||
|
validateRequest,
|
||||||
|
} from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { hasPermission } from "@dokploy/server/services/permission";
|
import { hasPermission } from "@dokploy/server/services/permission";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
@@ -11,13 +16,14 @@ type Query = {
|
|||||||
state: string;
|
state: string;
|
||||||
installation_id: string;
|
installation_id: string;
|
||||||
setup_action: string;
|
setup_action: string;
|
||||||
|
githubUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse,
|
res: NextApiResponse,
|
||||||
) {
|
) {
|
||||||
const { code, state, installation_id }: Query = req.query as Query;
|
const { code, state, installation_id, githubUrl }: Query = req.query as Query;
|
||||||
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return res.status(400).json({ error: "Missing code parameter" });
|
return res.status(400).json({ error: "Missing code parameter" });
|
||||||
@@ -38,7 +44,15 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (action === "gh_init") {
|
if (action === "gh_init") {
|
||||||
const octokit = new Octokit({});
|
// Reject before any outbound request: this runs on a GET the user can be
|
||||||
|
// linked into, so the host is not trusted.
|
||||||
|
const parsed = parseGithubBaseUrl(githubUrl);
|
||||||
|
if ("error" in parsed) {
|
||||||
|
return res.status(400).json({ error: parsed.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = parsed.url;
|
||||||
|
const octokit = new Octokit({ baseUrl: deriveGithubApiUrl(baseUrl) });
|
||||||
const { data } = await octokit.request(
|
const { data } = await octokit.request(
|
||||||
"POST /app-manifests/{code}/conversions",
|
"POST /app-manifests/{code}/conversions",
|
||||||
{
|
{
|
||||||
@@ -55,6 +69,7 @@ export default async function handler(
|
|||||||
githubClientSecret: data.client_secret,
|
githubClientSecret: data.client_secret,
|
||||||
githubWebhookSecret: data.webhook_secret,
|
githubWebhookSecret: data.webhook_secret,
|
||||||
githubPrivateKey: data.pem,
|
githubPrivateKey: data.pem,
|
||||||
|
githubUrl: baseUrl,
|
||||||
},
|
},
|
||||||
session.activeOrganizationId,
|
session.activeOrganizationId,
|
||||||
user.id,
|
user.id,
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ export const extractServicesFromEnvironment = (
|
|||||||
serverId: item.serverId,
|
serverId: item.serverId,
|
||||||
serverName: item?.server?.name || null,
|
serverName: item?.server?.name || null,
|
||||||
lastDeployDate,
|
lastDeployDate,
|
||||||
|
icon: item.icon || null,
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
@@ -1701,9 +1702,17 @@ const EnvironmentPage = (
|
|||||||
) : (
|
) : (
|
||||||
<GlobeIcon className="h-6 w-6" />
|
<GlobeIcon className="h-6 w-6" />
|
||||||
))}
|
))}
|
||||||
{service.type === "compose" && (
|
{service.type === "compose" &&
|
||||||
<CircuitBoard className="h-6 w-6" />
|
(service.icon ? (
|
||||||
)}
|
// biome-ignore lint/performance/noImgElement: compose icon is data URL
|
||||||
|
<img
|
||||||
|
src={service.icon}
|
||||||
|
alt={service.name}
|
||||||
|
className="size-7 object-contain rounded-sm"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CircuitBoard className="h-6 w-6" />
|
||||||
|
))}
|
||||||
{service.type === "libsql" && (
|
{service.type === "libsql" && (
|
||||||
<LibsqlIcon className="h-6 w-6" />
|
<LibsqlIcon className="h-6 w-6" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
+2
-1
@@ -127,7 +127,8 @@ const Service = (
|
|||||||
<CardTitle className="text-xl flex flex-row gap-2 items-center">
|
<CardTitle className="text-xl flex flex-row gap-2 items-center">
|
||||||
<div className="relative flex flex-row gap-4 items-center">
|
<div className="relative flex flex-row gap-4 items-center">
|
||||||
<ShowIconSettings
|
<ShowIconSettings
|
||||||
applicationId={applicationId}
|
serviceId={applicationId}
|
||||||
|
serviceType="application"
|
||||||
icon={data?.icon}
|
icon={data?.icon}
|
||||||
/>
|
/>
|
||||||
<div className="absolute -right-1 -top-2 z-10">
|
<div className="absolute -right-1 -top-2 z-10">
|
||||||
|
|||||||
+10
-6
@@ -1,7 +1,7 @@
|
|||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
import { CircuitBoard, HelpCircle, ServerOff } from "lucide-react";
|
import { HelpCircle, ServerOff } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
GetServerSidePropsContext,
|
GetServerSidePropsContext,
|
||||||
InferGetServerSidePropsType,
|
InferGetServerSidePropsType,
|
||||||
@@ -17,6 +17,7 @@ import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes
|
|||||||
import { ShowDeployments } from "@/components/dashboard/application/deployments/show-deployments";
|
import { ShowDeployments } from "@/components/dashboard/application/deployments/show-deployments";
|
||||||
import { ShowDomains } from "@/components/dashboard/application/domains/show-domains";
|
import { ShowDomains } from "@/components/dashboard/application/domains/show-domains";
|
||||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
|
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
|
||||||
|
import { ShowIconSettings } from "@/components/dashboard/application/icon/show-icon-settings";
|
||||||
import { ShowPatches } from "@/components/dashboard/application/patches/show-patches";
|
import { ShowPatches } from "@/components/dashboard/application/patches/show-patches";
|
||||||
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
||||||
import { ShowVolumeBackups } from "@/components/dashboard/application/volume-backups/show-volume-backups";
|
import { ShowVolumeBackups } from "@/components/dashboard/application/volume-backups/show-volume-backups";
|
||||||
@@ -113,13 +114,16 @@ const Service = (
|
|||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<CardHeader className="flex flex-row justify-between items-center">
|
<CardHeader className="flex flex-row justify-between items-center">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<CardTitle className="text-xl flex flex-row gap-2">
|
<CardTitle className="text-xl flex flex-row gap-2 items-center">
|
||||||
<div className="relative flex flex-row gap-4">
|
<div className="relative flex flex-row gap-4 items-center">
|
||||||
<div className="absolute -right-1 -top-2">
|
<ShowIconSettings
|
||||||
|
serviceId={composeId}
|
||||||
|
serviceType="compose"
|
||||||
|
icon={data?.icon}
|
||||||
|
/>
|
||||||
|
<div className="absolute -right-1 -top-2 z-10">
|
||||||
<StatusTooltip status={data?.composeStatus} />
|
<StatusTooltip status={data?.composeStatus} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CircuitBoard className="h-6 w-6 text-muted-foreground" />
|
|
||||||
</div>
|
</div>
|
||||||
{data?.name}
|
{data?.name}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||||
import { REGEXP_ONLY_DIGITS } from "input-otp";
|
import { REGEXP_ONLY_DIGITS } from "input-otp";
|
||||||
|
import { Fingerprint } from "lucide-react";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -68,6 +69,7 @@ export default function Home({ IS_CLOUD, enforceSSO }: Props) {
|
|||||||
const { config: whitelabeling } = useWhitelabelingPublic();
|
const { config: whitelabeling } = useWhitelabelingPublic();
|
||||||
const { data: showSignInWithSSO } = api.sso.showSignInWithSSO.useQuery();
|
const { data: showSignInWithSSO } = api.sso.showSignInWithSSO.useQuery();
|
||||||
const [isLoginLoading, setIsLoginLoading] = useState(false);
|
const [isLoginLoading, setIsLoginLoading] = useState(false);
|
||||||
|
const [isPasskeyLoading, setIsPasskeyLoading] = useState(false);
|
||||||
const [isTwoFactorLoading, setIsTwoFactorLoading] = useState(false);
|
const [isTwoFactorLoading, setIsTwoFactorLoading] = useState(false);
|
||||||
const [isBackupCodeLoading, setIsBackupCodeLoading] = useState(false);
|
const [isBackupCodeLoading, setIsBackupCodeLoading] = useState(false);
|
||||||
const [isTwoFactor, setIsTwoFactor] = useState(false);
|
const [isTwoFactor, setIsTwoFactor] = useState(false);
|
||||||
@@ -123,6 +125,34 @@ export default function Home({ IS_CLOUD, enforceSSO }: Props) {
|
|||||||
setIsLoginLoading(false);
|
setIsLoginLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const onPasskeySignIn = async () => {
|
||||||
|
setIsPasskeyLoading(true);
|
||||||
|
try {
|
||||||
|
const { data, error } = await authClient.signIn.passkey();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
const errorCode = "code" in error ? error.code : undefined;
|
||||||
|
if (
|
||||||
|
errorCode !== "AUTH_CANCELLED" &&
|
||||||
|
errorCode !== "ERROR_CEREMONY_ABORTED"
|
||||||
|
) {
|
||||||
|
toast.error(error.message || "Failed to sign in with passkey");
|
||||||
|
setError(error.message || "Failed to sign in with passkey");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
toast.success("Logged in successfully");
|
||||||
|
router.push("/dashboard/home");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error("An error occurred while signing in with passkey");
|
||||||
|
} finally {
|
||||||
|
setIsPasskeyLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onTwoFactorSubmit = async (e: React.FormEvent) => {
|
const onTwoFactorSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (twoFactorCode.length !== 6) {
|
if (twoFactorCode.length !== 6) {
|
||||||
@@ -227,6 +257,16 @@ export default function Home({ IS_CLOUD, enforceSSO }: Props) {
|
|||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full mt-4"
|
||||||
|
type="button"
|
||||||
|
onClick={onPasskeySignIn}
|
||||||
|
isLoading={isPasskeyLoading}
|
||||||
|
>
|
||||||
|
<Fingerprint className="size-4" />
|
||||||
|
Sign in with Passkey
|
||||||
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
deleteAllMiddlewares,
|
deleteAllMiddlewares,
|
||||||
findApplicationById,
|
findApplicationById,
|
||||||
findEnvironmentById,
|
findEnvironmentById,
|
||||||
|
findPreviewDeploymentsByApplicationId,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
getAccessibleServerIds,
|
getAccessibleServerIds,
|
||||||
getApplicationStats,
|
getApplicationStats,
|
||||||
@@ -16,6 +17,7 @@ import {
|
|||||||
removeDeployments,
|
removeDeployments,
|
||||||
removeDirectoryCode,
|
removeDirectoryCode,
|
||||||
removeMonitoringDirectory,
|
removeMonitoringDirectory,
|
||||||
|
removePreviewDeployment,
|
||||||
removeService,
|
removeService,
|
||||||
removeTraefikConfig,
|
removeTraefikConfig,
|
||||||
startService,
|
startService,
|
||||||
@@ -234,6 +236,15 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const previewDeploymentsList =
|
||||||
|
await findPreviewDeploymentsByApplicationId(input.applicationId);
|
||||||
|
|
||||||
|
for (const previewDeployment of previewDeploymentsList) {
|
||||||
|
try {
|
||||||
|
await removePreviewDeployment(previewDeployment.previewDeploymentId);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
const result = await db
|
const result = await db
|
||||||
.delete(applications)
|
.delete(applications)
|
||||||
.where(eq(applications.applicationId, input.applicationId))
|
.where(eq(applications.applicationId, input.applicationId))
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
type CompleteTemplate,
|
type CompleteTemplate,
|
||||||
fetchTemplateFiles,
|
fetchTemplateFiles,
|
||||||
|
fetchTemplateLogo,
|
||||||
fetchTemplatesList,
|
fetchTemplatesList,
|
||||||
} from "@dokploy/server/templates/github";
|
} from "@dokploy/server/templates/github";
|
||||||
import { processTemplate } from "@dokploy/server/templates/processors";
|
import { processTemplate } from "@dokploy/server/templates/processors";
|
||||||
@@ -601,7 +602,10 @@ export const composeRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const template = await fetchTemplateFiles(input.id, input.baseUrl);
|
const [template, templateLogo] = await Promise.all([
|
||||||
|
fetchTemplateFiles(input.id, input.baseUrl),
|
||||||
|
fetchTemplateLogo(input.id, input.baseUrl),
|
||||||
|
]);
|
||||||
|
|
||||||
let serverIp = "127.0.0.1";
|
let serverIp = "127.0.0.1";
|
||||||
|
|
||||||
@@ -639,6 +643,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
name: input.id,
|
name: input.id,
|
||||||
sourceType: "raw",
|
sourceType: "raw",
|
||||||
appName: appName,
|
appName: appName,
|
||||||
|
icon: templateLogo,
|
||||||
});
|
});
|
||||||
|
|
||||||
await addNewService(ctx, compose.composeId);
|
await addNewService(ctx, compose.composeId);
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export const gitProviderRouter = createTRPCRouter({
|
|||||||
githubAppName: r.github.githubAppName,
|
githubAppName: r.github.githubAppName,
|
||||||
githubAppId: r.github.githubAppId,
|
githubAppId: r.github.githubAppId,
|
||||||
githubInstallationId: r.github.githubInstallationId,
|
githubInstallationId: r.github.githubInstallationId,
|
||||||
|
githubUrl: r.github.githubUrl,
|
||||||
isConfigured: !!(
|
isConfigured: !!(
|
||||||
r.github.githubPrivateKey &&
|
r.github.githubPrivateKey &&
|
||||||
r.github.githubAppId &&
|
r.github.githubAppId &&
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export const githubRouter = createTRPCRouter({
|
|||||||
.map((provider) => {
|
.map((provider) => {
|
||||||
return {
|
return {
|
||||||
githubId: provider.githubId,
|
githubId: provider.githubId,
|
||||||
|
githubUrl: provider.githubUrl,
|
||||||
gitProvider: {
|
gitProvider: {
|
||||||
...provider.gitProvider,
|
...provider.gitProvider,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -329,13 +329,17 @@ export const scheduleRouter = createTRPCRouter({
|
|||||||
await checkPermission(ctx, { schedule: ["create"] });
|
await checkPermission(ctx, { schedule: ["create"] });
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await runCommand(input.scheduleId);
|
const deployment = await runCommand(input.scheduleId);
|
||||||
await audit(ctx, {
|
await audit(ctx, {
|
||||||
action: "run",
|
action: "run",
|
||||||
resourceType: "schedule",
|
resourceType: "schedule",
|
||||||
resourceId: input.scheduleId,
|
resourceId: input.scheduleId,
|
||||||
});
|
});
|
||||||
return true;
|
return {
|
||||||
|
status: deployment.status,
|
||||||
|
deploymentId: deployment.deploymentId,
|
||||||
|
logPath: deployment.logPath,
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
checkGPUStatus,
|
checkGPUStatus,
|
||||||
checkPortInUse,
|
checkPortInUse,
|
||||||
checkPostgresHealth,
|
checkPostgresHealth,
|
||||||
checkRedisHealth,
|
|
||||||
checkTraefikHealth,
|
checkTraefikHealth,
|
||||||
cleanupAll,
|
cleanupAll,
|
||||||
cleanupAllBackground,
|
cleanupAllBackground,
|
||||||
@@ -13,7 +12,6 @@ import {
|
|||||||
cleanupSystem,
|
cleanupSystem,
|
||||||
cleanupVolumes,
|
cleanupVolumes,
|
||||||
DEFAULT_UPDATE_DATA,
|
DEFAULT_UPDATE_DATA,
|
||||||
execAsync,
|
|
||||||
findServerById,
|
findServerById,
|
||||||
getDockerDiskUsage,
|
getDockerDiskUsage,
|
||||||
getDokployImageTag,
|
getDokployImageTag,
|
||||||
@@ -103,41 +101,6 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
cleanRedis: adminProcedure.mutation(async ({ ctx }) => {
|
|
||||||
if (IS_CLOUD) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { stdout: containerId } = await execAsync(
|
|
||||||
`docker ps --filter "name=dokploy-redis" --filter "status=running" -q | head -n 1`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!containerId) {
|
|
||||||
throw new Error("Redis container not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const redisContainerId = containerId.trim();
|
|
||||||
|
|
||||||
await execAsync(`docker exec -i ${redisContainerId} redis-cli flushall`);
|
|
||||||
await audit(ctx, {
|
|
||||||
action: "update",
|
|
||||||
resourceType: "settings",
|
|
||||||
resourceName: "clean-redis",
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
reloadRedis: adminProcedure.mutation(async ({ ctx }) => {
|
|
||||||
if (IS_CLOUD) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
await reloadDockerResource("dokploy-redis");
|
|
||||||
await audit(ctx, {
|
|
||||||
action: "reload",
|
|
||||||
resourceType: "settings",
|
|
||||||
resourceName: "dokploy-redis",
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
cleanAllDeploymentQueue: adminProcedure.mutation(async ({ ctx }) => {
|
cleanAllDeploymentQueue: adminProcedure.mutation(async ({ ctx }) => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return true;
|
return true;
|
||||||
@@ -959,18 +922,16 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return {
|
return {
|
||||||
postgres: { status: "healthy" as const },
|
postgres: { status: "healthy" as const },
|
||||||
redis: { status: "healthy" as const },
|
|
||||||
traefik: { status: "healthy" as const },
|
traefik: { status: "healthy" as const },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const [postgres, redis, traefik] = await Promise.all([
|
const [postgres, traefik] = await Promise.all([
|
||||||
checkPostgresHealth(),
|
checkPostgresHealth(),
|
||||||
checkRedisHealth(),
|
|
||||||
checkTraefikHealth(),
|
checkTraefikHealth(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { postgres, redis, traefik };
|
return { postgres, traefik };
|
||||||
}),
|
}),
|
||||||
setupGPU: adminProcedure
|
setupGPU: adminProcedure
|
||||||
.input(
|
.input(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
createOrganizationUserWithCredentials,
|
createOrganizationUserWithCredentials,
|
||||||
findNotificationById,
|
findNotificationById,
|
||||||
findOrganizationById,
|
findOrganizationById,
|
||||||
|
findPasskeysByUserId,
|
||||||
findUserById,
|
findUserById,
|
||||||
getDokployUrl,
|
getDokployUrl,
|
||||||
getUserByToken,
|
getUserByToken,
|
||||||
@@ -173,6 +174,9 @@ export const userRouter = createTRPCRouter({
|
|||||||
getPermissions: protectedProcedure.query(async ({ ctx }) => {
|
getPermissions: protectedProcedure.query(async ({ ctx }) => {
|
||||||
return resolvePermissions(ctx);
|
return resolvePermissions(ctx);
|
||||||
}),
|
}),
|
||||||
|
listPasskeys: protectedProcedure.query(async ({ ctx }) => {
|
||||||
|
return findPasskeysByUserId(ctx.user.id);
|
||||||
|
}),
|
||||||
haveRootAccess: protectedProcedure.query(async ({ ctx }) => {
|
haveRootAccess: protectedProcedure.query(async ({ ctx }) => {
|
||||||
if (!IS_CLOUD) {
|
if (!IS_CLOUD) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import type { ConnectionOptions } from "bullmq";
|
|
||||||
|
|
||||||
export const redisConfig: ConnectionOptions = {
|
|
||||||
host:
|
|
||||||
process.env.NODE_ENV === "production"
|
|
||||||
? process.env.REDIS_HOST || "dokploy-redis"
|
|
||||||
: "127.0.0.1",
|
|
||||||
};
|
|
||||||
@@ -6,7 +6,6 @@ const execAsync = promisify(exec);
|
|||||||
|
|
||||||
import { setupDirectories } from "@dokploy/server/setup/config-paths";
|
import { setupDirectories } from "@dokploy/server/setup/config-paths";
|
||||||
import { initializePostgres } from "@dokploy/server/setup/postgres-setup";
|
import { initializePostgres } from "@dokploy/server/setup/postgres-setup";
|
||||||
import { initializeRedis } from "@dokploy/server/setup/redis-setup";
|
|
||||||
import {
|
import {
|
||||||
initializeNetwork,
|
initializeNetwork,
|
||||||
initializeSwarm,
|
initializeSwarm,
|
||||||
@@ -29,7 +28,6 @@ import {
|
|||||||
createDefaultServerTraefikConfig();
|
createDefaultServerTraefikConfig();
|
||||||
await execAsync(`docker pull traefik:v${TRAEFIK_VERSION}`);
|
await execAsync(`docker pull traefik:v${TRAEFIK_VERSION}`);
|
||||||
await initializeStandaloneTraefik();
|
await initializeStandaloneTraefik();
|
||||||
await initializeRedis();
|
|
||||||
await initializePostgres();
|
await initializePostgres();
|
||||||
console.log("Dokploy setup completed");
|
console.log("Dokploy setup completed");
|
||||||
exit(0);
|
exit(0);
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// utils/github-utils.ts
|
||||||
|
// This file contains client-safe utilities for GitHub integration.
|
||||||
|
// Duplicates parseGithubBaseUrl from @dokploy/server, which cannot be imported
|
||||||
|
// here (node-only modules). Kept in step by github-url-parity.test.ts.
|
||||||
|
|
||||||
|
export const DEFAULT_GITHUB_URL = "https://github.com";
|
||||||
|
|
||||||
|
interface ResolvedGithubBaseUrl {
|
||||||
|
baseUrl: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resolveGithubBaseUrl = (value: string): ResolvedGithubBaseUrl => {
|
||||||
|
const raw = value.trim() || DEFAULT_GITHUB_URL;
|
||||||
|
const withScheme = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
|
||||||
|
|
||||||
|
let parsed: URL;
|
||||||
|
try {
|
||||||
|
parsed = new URL(withScheme);
|
||||||
|
} catch {
|
||||||
|
// Keep the raw value rather than defaulting: the failure stays visible.
|
||||||
|
return { baseUrl: withScheme, error: `"${raw}" is not a valid URL` };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.protocol !== "https:") {
|
||||||
|
return {
|
||||||
|
baseUrl: parsed.origin,
|
||||||
|
error:
|
||||||
|
"Only https is supported. Creating the app over http would leave it orphaned on your instance.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// "acme.ghe.com." resolves the same as "acme.ghe.com", but the trailing dot
|
||||||
|
// would slip past the checks below.
|
||||||
|
if (parsed.hostname.endsWith(".")) {
|
||||||
|
parsed.hostname = parsed.hostname.replace(/\.+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { hostname } = parsed;
|
||||||
|
|
||||||
|
if (!hostname.includes(".")) {
|
||||||
|
return {
|
||||||
|
baseUrl: parsed.origin,
|
||||||
|
error: `"${hostname}" is not a fully qualified hostname`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { baseUrl: parsed.origin };
|
||||||
|
};
|
||||||
@@ -180,6 +180,9 @@ export const initializeJobs = async () => {
|
|||||||
where: eq(schedules.enabled, true),
|
where: eq(schedules.enabled, true),
|
||||||
with: {
|
with: {
|
||||||
application: {
|
application: {
|
||||||
|
columns: {
|
||||||
|
applicationId: true,
|
||||||
|
},
|
||||||
with: {
|
with: {
|
||||||
server: true,
|
server: true,
|
||||||
},
|
},
|
||||||
@@ -227,6 +230,9 @@ export const initializeJobs = async () => {
|
|||||||
where: eq(volumeBackups.enabled, true),
|
where: eq(volumeBackups.enabled, true),
|
||||||
with: {
|
with: {
|
||||||
application: {
|
application: {
|
||||||
|
columns: {
|
||||||
|
applicationId: true,
|
||||||
|
},
|
||||||
with: {
|
with: {
|
||||||
server: true,
|
server: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"@ai-sdk/openai": "^3.0.29",
|
"@ai-sdk/openai": "^3.0.29",
|
||||||
"@ai-sdk/openai-compatible": "^2.0.30",
|
"@ai-sdk/openai-compatible": "^2.0.30",
|
||||||
"@better-auth/api-key": "1.6.23",
|
"@better-auth/api-key": "1.6.23",
|
||||||
|
"@better-auth/passkey": "1.6.23",
|
||||||
"@better-auth/scim": "1.6.23",
|
"@better-auth/scim": "1.6.23",
|
||||||
"@better-auth/sso": "1.6.23",
|
"@better-auth/sso": "1.6.23",
|
||||||
"@better-auth/utils": "0.4.2",
|
"@better-auth/utils": "0.4.2",
|
||||||
|
|||||||
@@ -223,6 +223,36 @@ export const twoFactor = pgTable("two_factor", {
|
|||||||
lockedUntil: timestamp("locked_until"),
|
lockedUntil: timestamp("locked_until"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const passkey = pgTable(
|
||||||
|
"passkey",
|
||||||
|
{
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
name: text("name"),
|
||||||
|
publicKey: text("public_key").notNull(),
|
||||||
|
userId: text("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
|
credentialID: text("credential_id").notNull(),
|
||||||
|
counter: integer("counter").notNull(),
|
||||||
|
deviceType: text("device_type").notNull(),
|
||||||
|
backedUp: boolean("backed_up").notNull(),
|
||||||
|
transports: text("transports"),
|
||||||
|
createdAt: timestamp("created_at"),
|
||||||
|
aaguid: text("aaguid"),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index("passkey_userId_idx").on(table.userId),
|
||||||
|
index("passkey_credentialID_idx").on(table.credentialID),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const passkeyRelations = relations(passkey, ({ one }) => ({
|
||||||
|
user: one(user, {
|
||||||
|
fields: [passkey.userId],
|
||||||
|
references: [user.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
export const apikey = pgTable("apikey", {
|
export const apikey = pgTable("apikey", {
|
||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
name: text("name"),
|
name: text("name"),
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export const compose = pgTable("compose", {
|
|||||||
.default(false),
|
.default(false),
|
||||||
triggerType: triggerType("triggerType").default("push"),
|
triggerType: triggerType("triggerType").default("push"),
|
||||||
composeStatus: applicationStatus("composeStatus").notNull().default("idle"),
|
composeStatus: applicationStatus("composeStatus").notNull().default("idle"),
|
||||||
|
icon: text("icon"),
|
||||||
environmentId: text("environmentId")
|
environmentId: text("environmentId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => environments.environmentId, { onDelete: "cascade" }),
|
.references(() => environments.environmentId, { onDelete: "cascade" }),
|
||||||
@@ -190,6 +191,11 @@ const createSchema = createInsertSchema(compose, {
|
|||||||
.optional(),
|
.optional(),
|
||||||
triggerType: z.enum(["push", "tag"]).optional(),
|
triggerType: z.enum(["push", "tag"]).optional(),
|
||||||
composeStatus: z.enum(["idle", "running", "done", "error"]).optional(),
|
composeStatus: z.enum(["idle", "running", "done", "error"]).optional(),
|
||||||
|
icon: z
|
||||||
|
.string()
|
||||||
|
.max(2 * 1024 * 1024, "Icon must be less than 2MB")
|
||||||
|
.nullable()
|
||||||
|
.optional(),
|
||||||
serviceNetworks: z
|
serviceNetworks: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const github = pgTable("github", {
|
|||||||
githubInstallationId: text("githubInstallationId"),
|
githubInstallationId: text("githubInstallationId"),
|
||||||
githubPrivateKey: text("githubPrivateKey"),
|
githubPrivateKey: text("githubPrivateKey"),
|
||||||
githubWebhookSecret: text("githubWebhookSecret"),
|
githubWebhookSecret: text("githubWebhookSecret"),
|
||||||
|
githubUrl: text("githubUrl").default("https://github.com").notNull(),
|
||||||
gitProviderId: text("gitProviderId")
|
gitProviderId: text("gitProviderId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => gitProvider.gitProviderId, { onDelete: "cascade" }),
|
.references(() => gitProvider.gitProviderId, { onDelete: "cascade" }),
|
||||||
@@ -36,6 +37,10 @@ export const apiCreateGithub = z.object({
|
|||||||
githubInstallationId: z.string().optional(),
|
githubInstallationId: z.string().optional(),
|
||||||
githubPrivateKey: z.string().optional(),
|
githubPrivateKey: z.string().optional(),
|
||||||
githubWebhookSecret: z.string().nullable(),
|
githubWebhookSecret: z.string().nullable(),
|
||||||
|
githubUrl: z
|
||||||
|
.url()
|
||||||
|
.startsWith("https://", "Only https is supported for GitHub instances")
|
||||||
|
.optional(),
|
||||||
gitProviderId: z.string().optional(),
|
gitProviderId: z.string().optional(),
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
import { boolean, jsonb, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
import {
|
||||||
|
boolean,
|
||||||
|
integer,
|
||||||
|
jsonb,
|
||||||
|
pgEnum,
|
||||||
|
pgTable,
|
||||||
|
text,
|
||||||
|
} from "drizzle-orm/pg-core";
|
||||||
import { createInsertSchema } from "drizzle-zod";
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -19,6 +26,7 @@ export const network = pgTable("network", {
|
|||||||
attachable: boolean("attachable").notNull().default(false),
|
attachable: boolean("attachable").notNull().default(false),
|
||||||
enableIPv4: boolean("enableIPv4").notNull().default(true),
|
enableIPv4: boolean("enableIPv4").notNull().default(true),
|
||||||
enableIPv6: boolean("enableIPv6").notNull().default(false),
|
enableIPv6: boolean("enableIPv6").notNull().default(false),
|
||||||
|
mtu: integer("mtu"),
|
||||||
ipam: jsonb("ipam")
|
ipam: jsonb("ipam")
|
||||||
.$type<{
|
.$type<{
|
||||||
driver?: string;
|
driver?: string;
|
||||||
@@ -55,6 +63,7 @@ const createSchema = createInsertSchema(network, {
|
|||||||
attachable: z.boolean().optional(),
|
attachable: z.boolean().optional(),
|
||||||
enableIPv4: z.boolean().optional(),
|
enableIPv4: z.boolean().optional(),
|
||||||
enableIPv6: z.boolean().optional(),
|
enableIPv6: z.boolean().optional(),
|
||||||
|
mtu: z.number().int().min(68).max(65535).optional().nullable(),
|
||||||
ipam: z
|
ipam: z
|
||||||
.object({
|
.object({
|
||||||
driver: z.string().optional(),
|
driver: z.string().optional(),
|
||||||
@@ -109,6 +118,7 @@ export const apiCreateNetwork = createSchema
|
|||||||
attachable: true,
|
attachable: true,
|
||||||
enableIPv4: true,
|
enableIPv4: true,
|
||||||
enableIPv6: true,
|
enableIPv6: true,
|
||||||
|
mtu: true,
|
||||||
ipam: true,
|
ipam: true,
|
||||||
serverId: true,
|
serverId: true,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Application } from "@dokploy/server/services/application";
|
import type { Application } from "@dokploy/server/services/application";
|
||||||
|
import type { Environment } from "@dokploy/server/services/environment";
|
||||||
import type { Mount } from "@dokploy/server/services/mount";
|
import type { Mount } from "@dokploy/server/services/mount";
|
||||||
import type { Port } from "@dokploy/server/services/port";
|
import type { Port } from "@dokploy/server/services/port";
|
||||||
import type { Project } from "@dokploy/server/services/project";
|
import type { Project } from "@dokploy/server/services/project";
|
||||||
@@ -27,7 +28,7 @@ export const rollbacks = pgTable("rollback", {
|
|||||||
.$defaultFn(() => new Date().toISOString()),
|
.$defaultFn(() => new Date().toISOString()),
|
||||||
fullContext: jsonb("fullContext").$type<
|
fullContext: jsonb("fullContext").$type<
|
||||||
Application & {
|
Application & {
|
||||||
environment: {
|
environment: Environment & {
|
||||||
project: Project;
|
project: Project;
|
||||||
};
|
};
|
||||||
mounts: Mount[];
|
mounts: Mount[];
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ export * from "./setup/config-paths";
|
|||||||
export * from "./setup/forward-auth-setup";
|
export * from "./setup/forward-auth-setup";
|
||||||
export * from "./setup/monitoring-setup";
|
export * from "./setup/monitoring-setup";
|
||||||
export * from "./setup/postgres-setup";
|
export * from "./setup/postgres-setup";
|
||||||
export * from "./setup/redis-setup";
|
|
||||||
export * from "./setup/server-audit";
|
export * from "./setup/server-audit";
|
||||||
export * from "./setup/server-setup";
|
export * from "./setup/server-setup";
|
||||||
export * from "./setup/server-validate";
|
export * from "./setup/server-validate";
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { apiKey } from "@better-auth/api-key";
|
import { apiKey } from "@better-auth/api-key";
|
||||||
|
import { passkey } from "@better-auth/passkey";
|
||||||
import { scim } from "@better-auth/scim";
|
import { scim } from "@better-auth/scim";
|
||||||
import { sso } from "@better-auth/sso";
|
import { sso } from "@better-auth/sso";
|
||||||
import { betterAuth } from "better-auth";
|
import { betterAuth } from "better-auth";
|
||||||
@@ -33,6 +34,7 @@ export const auth = betterAuth({
|
|||||||
apiKey({ enableMetadata: true, references: "user" }),
|
apiKey({ enableMetadata: true, references: "user" }),
|
||||||
sso(),
|
sso(),
|
||||||
twoFactor(),
|
twoFactor(),
|
||||||
|
passkey(),
|
||||||
organization({
|
organization({
|
||||||
ac,
|
ac,
|
||||||
roles: { owner: ownerRole, admin: adminRole, member: memberRole },
|
roles: { owner: ownerRole, admin: adminRole, member: memberRole },
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { IncomingMessage } from "node:http";
|
import type { IncomingMessage } from "node:http";
|
||||||
import { apiKey } from "@better-auth/api-key";
|
import { apiKey } from "@better-auth/api-key";
|
||||||
|
import { passkey } from "@better-auth/passkey";
|
||||||
import { scim } from "@better-auth/scim";
|
import { scim } from "@better-auth/scim";
|
||||||
import { sso } from "@better-auth/sso";
|
import { sso } from "@better-auth/sso";
|
||||||
import * as bcrypt from "bcrypt";
|
import * as bcrypt from "bcrypt";
|
||||||
@@ -435,6 +436,7 @@ const createBetterAuth = () =>
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
twoFactor(),
|
twoFactor(),
|
||||||
|
passkey(),
|
||||||
organization({
|
organization({
|
||||||
ac,
|
ac,
|
||||||
roles: {
|
roles: {
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ export const findEnvironmentById = async (environmentId: string) => {
|
|||||||
composeStatus: true,
|
composeStatus: true,
|
||||||
description: true,
|
description: true,
|
||||||
serverId: true,
|
serverId: true,
|
||||||
|
icon: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
libsql: {
|
libsql: {
|
||||||
@@ -239,7 +240,12 @@ export const findEnvironmentsByProjectId = async (projectId: string) => {
|
|||||||
with: { server: { columns: { name: true } } },
|
with: { server: { columns: { name: true } } },
|
||||||
},
|
},
|
||||||
compose: {
|
compose: {
|
||||||
columns: { ...serviceColumns, composeId: true, composeStatus: true },
|
columns: {
|
||||||
|
...serviceColumns,
|
||||||
|
composeId: true,
|
||||||
|
composeStatus: true,
|
||||||
|
icon: true,
|
||||||
|
},
|
||||||
with: { server: { columns: { name: true } } },
|
with: { server: { columns: { name: true } } },
|
||||||
},
|
},
|
||||||
libsql: {
|
libsql: {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type DockerNetworkInfo = {
|
|||||||
Ingress?: boolean;
|
Ingress?: boolean;
|
||||||
EnableIPv4?: boolean;
|
EnableIPv4?: boolean;
|
||||||
EnableIPv6?: boolean;
|
EnableIPv6?: boolean;
|
||||||
|
Options?: Record<string, string> | null;
|
||||||
IPAM?: {
|
IPAM?: {
|
||||||
Driver?: string;
|
Driver?: string;
|
||||||
Config?: Array<{
|
Config?: Array<{
|
||||||
@@ -34,6 +35,11 @@ type DockerNetworkInfo = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const parseMtu = (value: string | undefined) => {
|
||||||
|
const mtu = Number.parseInt(value ?? "", 10);
|
||||||
|
return Number.isNaN(mtu) ? null : mtu;
|
||||||
|
};
|
||||||
|
|
||||||
const isImportableDockerNetwork = (dockerNetwork: DockerNetworkInfo) =>
|
const isImportableDockerNetwork = (dockerNetwork: DockerNetworkInfo) =>
|
||||||
!RESERVED_NETWORKS.includes(dockerNetwork.Name) &&
|
!RESERVED_NETWORKS.includes(dockerNetwork.Name) &&
|
||||||
!dockerNetwork.Ingress &&
|
!dockerNetwork.Ingress &&
|
||||||
@@ -51,6 +57,7 @@ const mapDockerNetworkToRow = (
|
|||||||
// Older daemons don't report EnableIPv4; IPv4 is always on there
|
// Older daemons don't report EnableIPv4; IPv4 is always on there
|
||||||
enableIPv4: dockerNetwork.EnableIPv4 ?? true,
|
enableIPv4: dockerNetwork.EnableIPv4 ?? true,
|
||||||
enableIPv6: dockerNetwork.EnableIPv6 ?? false,
|
enableIPv6: dockerNetwork.EnableIPv6 ?? false,
|
||||||
|
mtu: parseMtu(dockerNetwork.Options?.["com.docker.network.driver.mtu"]),
|
||||||
ipam: {
|
ipam: {
|
||||||
driver: dockerNetwork.IPAM?.Driver,
|
driver: dockerNetwork.IPAM?.Driver,
|
||||||
config: (dockerNetwork.IPAM?.Config ?? []).map((c) => ({
|
config: (dockerNetwork.IPAM?.Config ?? []).map((c) => ({
|
||||||
@@ -255,6 +262,9 @@ const createDockerNetworkFromRow = async (row: typeof network.$inferSelect) => {
|
|||||||
// the daemon (API >= 1.47); the body is sent as-is
|
// the daemon (API >= 1.47); the body is sent as-is
|
||||||
EnableIPv4: row.enableIPv4,
|
EnableIPv4: row.enableIPv4,
|
||||||
EnableIPv6: row.enableIPv6,
|
EnableIPv6: row.enableIPv6,
|
||||||
|
Options: row.mtu
|
||||||
|
? { "com.docker.network.driver.mtu": String(row.mtu) }
|
||||||
|
: undefined,
|
||||||
IPAM: {
|
IPAM: {
|
||||||
Driver: ipam.driver || "default",
|
Driver: ipam.driver || "default",
|
||||||
Config: ipamConfig.length > 0 ? ipamConfig : undefined,
|
Config: ipamConfig.length > 0 ? ipamConfig : undefined,
|
||||||
|
|||||||
@@ -30,12 +30,8 @@ export const finPortById = async (portId: string) => {
|
|||||||
where: eq(ports.portId, portId),
|
where: eq(ports.portId, portId),
|
||||||
with: {
|
with: {
|
||||||
application: {
|
application: {
|
||||||
with: {
|
columns: {
|
||||||
environment: {
|
applicationId: true,
|
||||||
with: {
|
|
||||||
project: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { manageDomain } from "../utils/traefik/domain";
|
|||||||
import { findApplicationById } from "./application";
|
import { findApplicationById } from "./application";
|
||||||
import { removeDeploymentsByPreviewDeploymentId } from "./deployment";
|
import { removeDeploymentsByPreviewDeploymentId } from "./deployment";
|
||||||
import { createDomain } from "./domain";
|
import { createDomain } from "./domain";
|
||||||
import { type Github, getIssueComment } from "./github";
|
import { findGithubById, getIssueComment } from "./github";
|
||||||
import { getWebServerSettings } from "./web-server-settings";
|
import { getWebServerSettings } from "./web-server-settings";
|
||||||
|
|
||||||
export type PreviewDeployment = typeof previewDeployments.$inferSelect;
|
export type PreviewDeployment = typeof previewDeployments.$inferSelect;
|
||||||
@@ -142,7 +142,17 @@ export const createPreviewDeployment = async (
|
|||||||
org?.ownerId || "",
|
org?.ownerId || "",
|
||||||
);
|
);
|
||||||
|
|
||||||
const octokit = authGithub(application?.github as Github);
|
if (!application.githubId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Github Account not configured correctly",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// `findApplicationById` redacts `githubPrivateKey` from the `github`
|
||||||
|
// relation, so the provider must be refetched to authenticate.
|
||||||
|
const githubProvider = await findGithubById(application.githubId);
|
||||||
|
const octokit = authGithub(githubProvider);
|
||||||
|
|
||||||
const runningComment = getIssueComment(
|
const runningComment = getIssueComment(
|
||||||
application.name,
|
application.name,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
|
|||||||
import { getRemoteDocker } from "../utils/servers/remote-docker";
|
import { getRemoteDocker } from "../utils/servers/remote-docker";
|
||||||
import { type Application, findApplicationById } from "./application";
|
import { type Application, findApplicationById } from "./application";
|
||||||
import { findDeploymentById } from "./deployment";
|
import { findDeploymentById } from "./deployment";
|
||||||
|
import type { Environment } from "./environment";
|
||||||
import type { Mount } from "./mount";
|
import type { Mount } from "./mount";
|
||||||
import { resolveServiceNetworks } from "./network";
|
import { resolveServiceNetworks } from "./network";
|
||||||
import type { Port } from "./port";
|
import type { Port } from "./port";
|
||||||
@@ -106,19 +107,7 @@ export const findRollbackById = async (rollbackId: string) => {
|
|||||||
const result = await db.query.rollbacks.findFirst({
|
const result = await db.query.rollbacks.findFirst({
|
||||||
where: eq(rollbacks.rollbackId, rollbackId),
|
where: eq(rollbacks.rollbackId, rollbackId),
|
||||||
with: {
|
with: {
|
||||||
deployment: {
|
deployment: true,
|
||||||
with: {
|
|
||||||
application: {
|
|
||||||
with: {
|
|
||||||
environment: {
|
|
||||||
with: {
|
|
||||||
project: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -214,7 +203,7 @@ const rollbackApplication = async (
|
|||||||
image: string,
|
image: string,
|
||||||
serverId?: string | null,
|
serverId?: string | null,
|
||||||
fullContext?: Application & {
|
fullContext?: Application & {
|
||||||
environment: {
|
environment: Environment & {
|
||||||
project: Project;
|
project: Project;
|
||||||
};
|
};
|
||||||
mounts: Mount[];
|
mounts: Mount[];
|
||||||
@@ -279,6 +268,7 @@ const rollbackApplication = async (
|
|||||||
const envVariables = prepareEnvironmentVariables(
|
const envVariables = prepareEnvironmentVariables(
|
||||||
env,
|
env,
|
||||||
fullContext.environment.project.env,
|
fullContext.environment.project.env,
|
||||||
|
fullContext.environment.env,
|
||||||
);
|
);
|
||||||
|
|
||||||
let rollbackImage = image;
|
let rollbackImage = image;
|
||||||
|
|||||||
@@ -81,6 +81,11 @@ export const findScheduleById = async (scheduleId: string) => {
|
|||||||
where: eq(schedules.scheduleId, scheduleId),
|
where: eq(schedules.scheduleId, scheduleId),
|
||||||
with: {
|
with: {
|
||||||
application: {
|
application: {
|
||||||
|
columns: {
|
||||||
|
applicationId: true,
|
||||||
|
appName: true,
|
||||||
|
serverId: true,
|
||||||
|
},
|
||||||
with: {
|
with: {
|
||||||
environment: {
|
environment: {
|
||||||
with: {
|
with: {
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import {
|
|||||||
apikey,
|
apikey,
|
||||||
invitation,
|
invitation,
|
||||||
member,
|
member,
|
||||||
|
passkey,
|
||||||
user,
|
user,
|
||||||
} from "@dokploy/server/db/schema";
|
} from "@dokploy/server/db/schema";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import * as bcrypt from "bcrypt";
|
import * as bcrypt from "bcrypt";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, desc, eq } from "drizzle-orm";
|
||||||
import { auth } from "../lib/auth";
|
import { auth } from "../lib/auth";
|
||||||
|
|
||||||
export type User = typeof user.$inferSelect;
|
export type User = typeof user.$inferSelect;
|
||||||
@@ -396,6 +397,21 @@ export const findMemberById = async (
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const findPasskeysByUserId = async (userId: string) => {
|
||||||
|
return db.query.passkey.findMany({
|
||||||
|
where: eq(passkey.userId, userId),
|
||||||
|
columns: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
deviceType: true,
|
||||||
|
backedUp: true,
|
||||||
|
createdAt: true,
|
||||||
|
aaguid: true,
|
||||||
|
},
|
||||||
|
orderBy: [desc(passkey.createdAt)],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const createOrganizationUserWithCredentials = async ({
|
export const createOrganizationUserWithCredentials = async ({
|
||||||
organizationId,
|
organizationId,
|
||||||
email,
|
email,
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ export const findVolumeBackupById = async (volumeBackupId: string) => {
|
|||||||
where: eq(volumeBackups.volumeBackupId, volumeBackupId),
|
where: eq(volumeBackups.volumeBackupId, volumeBackupId),
|
||||||
with: {
|
with: {
|
||||||
application: {
|
application: {
|
||||||
|
columns: {
|
||||||
|
applicationId: true,
|
||||||
|
appName: true,
|
||||||
|
serverId: true,
|
||||||
|
},
|
||||||
with: {
|
with: {
|
||||||
environment: {
|
environment: {
|
||||||
with: {
|
with: {
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
import type { CreateServiceOptions } from "dockerode";
|
|
||||||
import { docker } from "../constants";
|
|
||||||
import { pullImage } from "../utils/docker/utils";
|
|
||||||
|
|
||||||
export const initializeRedis = async () => {
|
|
||||||
const imageName = "redis:8";
|
|
||||||
const containerName = "dokploy-redis";
|
|
||||||
|
|
||||||
const settings: CreateServiceOptions = {
|
|
||||||
Name: containerName,
|
|
||||||
TaskTemplate: {
|
|
||||||
ContainerSpec: {
|
|
||||||
Image: imageName,
|
|
||||||
Mounts: [
|
|
||||||
{
|
|
||||||
Type: "volume",
|
|
||||||
Source: "dokploy-redis",
|
|
||||||
Target: "/data",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
Networks: [{ Target: "dokploy-network" }],
|
|
||||||
Placement: {
|
|
||||||
Constraints: ["node.role==manager"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Mode: {
|
|
||||||
Replicated: {
|
|
||||||
Replicas: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...(process.env.NODE_ENV === "development" && {
|
|
||||||
EndpointSpec: {
|
|
||||||
Ports: [
|
|
||||||
{
|
|
||||||
TargetPort: 6379,
|
|
||||||
PublishedPort: 6379,
|
|
||||||
Protocol: "tcp",
|
|
||||||
PublishMode: "host",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
await pullImage(imageName);
|
|
||||||
|
|
||||||
const service = docker.getService(containerName);
|
|
||||||
const inspect = await service.inspect();
|
|
||||||
await service.update({
|
|
||||||
version: Number.parseInt(inspect.Version.Index),
|
|
||||||
...settings,
|
|
||||||
});
|
|
||||||
console.log("Redis Started ✅");
|
|
||||||
} catch (_) {
|
|
||||||
try {
|
|
||||||
await docker.createService(settings);
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error?.statusCode !== 409) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
console.log("Redis service already exists, continuing...");
|
|
||||||
}
|
|
||||||
console.log("Redis Not Found: Starting ✅");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -74,6 +74,49 @@ export async function fetchTemplatesList(
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LOGO_MIME_TYPES: Record<string, string> = {
|
||||||
|
png: "image/png",
|
||||||
|
jpg: "image/jpeg",
|
||||||
|
jpeg: "image/jpeg",
|
||||||
|
svg: "image/svg+xml",
|
||||||
|
webp: "image/webp",
|
||||||
|
gif: "image/gif",
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function fetchTemplateLogo(
|
||||||
|
templateId: string,
|
||||||
|
baseUrl = "https://templates.dokploy.com",
|
||||||
|
): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const templates = await fetchTemplatesList(baseUrl);
|
||||||
|
const template = templates.find((t) => t.id === templateId);
|
||||||
|
if (!template?.logo) return null;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${baseUrl}/blueprints/${templateId}/${template.logo}`,
|
||||||
|
{ signal: AbortSignal.timeout(10000) },
|
||||||
|
);
|
||||||
|
if (!response.ok) return null;
|
||||||
|
|
||||||
|
const buffer = Buffer.from(await response.arrayBuffer());
|
||||||
|
if (buffer.length === 0) return null;
|
||||||
|
|
||||||
|
const contentType = response.headers.get("content-type")?.split(";")[0];
|
||||||
|
const extension = template.logo.split(".").pop()?.toLowerCase() ?? "";
|
||||||
|
const mimeType = contentType?.startsWith("image/")
|
||||||
|
? contentType
|
||||||
|
: LOGO_MIME_TYPES[extension];
|
||||||
|
if (!mimeType) return null;
|
||||||
|
|
||||||
|
const dataUrl = `data:${mimeType};base64,${buffer.toString("base64")}`;
|
||||||
|
if (dataUrl.length > 2 * 1024 * 1024) return null;
|
||||||
|
|
||||||
|
return dataUrl;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches a specific template's files
|
* Fetches a specific template's files
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export function parseRawConfig(
|
|||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
parsedLogs = parsedLogs.filter((log) =>
|
parsedLogs = parsedLogs.filter((log) =>
|
||||||
log.RequestHost.toLowerCase().includes(search.toLowerCase()),
|
(log.RequestHost ?? "").toLowerCase().includes(search.toLowerCase()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,14 +53,9 @@ export const getRailpackCommand = (application: ApplicationNested) => {
|
|||||||
"build",
|
"build",
|
||||||
"--builder",
|
"--builder",
|
||||||
builderName,
|
builderName,
|
||||||
...(cacheKey
|
"--build-arg",
|
||||||
? [
|
`secrets-hash=${secretsHash}`,
|
||||||
"--build-arg",
|
...(cacheKey ? ["--build-arg", `cache-key=${cacheKey}`] : []),
|
||||||
`secrets-hash=${secretsHash}`,
|
|
||||||
"--build-arg",
|
|
||||||
`cache-key=${cacheKey}`,
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
"--build-arg",
|
"--build-arg",
|
||||||
`BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v${application.railpackVersion}`,
|
`BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v${application.railpackVersion}`,
|
||||||
"-f",
|
"-f",
|
||||||
|
|||||||
@@ -892,50 +892,6 @@ export const checkPostgresHealth = async (): Promise<ServiceHealthStatus> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkRedisHealth = async (): Promise<ServiceHealthStatus> => {
|
|
||||||
const serviceCheck = await checkSwarmServiceRunning("dokploy-redis");
|
|
||||||
if (serviceCheck.status === "unhealthy") {
|
|
||||||
return serviceCheck;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify Redis actually responds to PING
|
|
||||||
const containerId = await getSwarmServiceContainerId("dokploy-redis");
|
|
||||||
if (!containerId) {
|
|
||||||
return { status: "unhealthy", message: "Could not find running container" };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const exec = await docker.getContainer(containerId).exec({
|
|
||||||
Cmd: ["redis-cli", "ping"],
|
|
||||||
AttachStdout: true,
|
|
||||||
AttachStderr: true,
|
|
||||||
});
|
|
||||||
const stream = await exec.start({});
|
|
||||||
|
|
||||||
const output = await new Promise<string>((resolve) => {
|
|
||||||
let data = "";
|
|
||||||
stream.on("data", (chunk: Buffer) => {
|
|
||||||
data += chunk.toString();
|
|
||||||
});
|
|
||||||
stream.on("end", () => resolve(data));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!output.includes("PONG")) {
|
|
||||||
return {
|
|
||||||
status: "unhealthy",
|
|
||||||
message: `Redis did not respond with PONG: ${output.trim()}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { status: "healthy" };
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
status: "unhealthy",
|
|
||||||
message: error instanceof Error ? error.message : "Failed to check Redis",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const checkTraefikHealth = async (): Promise<ServiceHealthStatus> => {
|
export const checkTraefikHealth = async (): Promise<ServiceHealthStatus> => {
|
||||||
// Traefik can run as a standalone container or a swarm service
|
// Traefik can run as a standalone container or a swarm service
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// Valid hostname per RFC 1123: labels of letters, digits and hyphens
|
// Valid hostname per RFC 1123: labels of letters, digits and hyphens
|
||||||
// (no leading/trailing hyphen), separated by dots. Underscores are rejected
|
// (no leading/trailing hyphen), optionally separated by dots. Underscores are rejected
|
||||||
// because Let's Encrypt refuses to issue certificates for them.
|
// because Let's Encrypt refuses to issue certificates for them.
|
||||||
export const VALID_HOSTNAME_REGEX =
|
export const VALID_HOSTNAME_REGEX =
|
||||||
/^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
|
/^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
|
||||||
|
|
||||||
export const INVALID_HOSTNAME_MESSAGE =
|
export const INVALID_HOSTNAME_MESSAGE =
|
||||||
"Invalid domain name. Use only letters, numbers, hyphens and dots (e.g. example.com). Underscores are not allowed.";
|
"Invalid domain name. Use only letters, numbers, hyphens and dots (e.g. example.com). Underscores are not allowed.";
|
||||||
|
|||||||
@@ -9,6 +9,68 @@ import { Octokit } from "octokit";
|
|||||||
import { quote } from "shell-quote";
|
import { quote } from "shell-quote";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
|
|
||||||
|
export const DEFAULT_GITHUB_URL = "https://github.com";
|
||||||
|
export const DEFAULT_GITHUB_API_URL = "https://api.github.com";
|
||||||
|
|
||||||
|
export const parseGithubBaseUrl = (
|
||||||
|
githubUrl?: string | null,
|
||||||
|
): { url: string } | { error: string } => {
|
||||||
|
const raw = githubUrl?.trim();
|
||||||
|
if (!raw) {
|
||||||
|
return { url: DEFAULT_GITHUB_URL };
|
||||||
|
}
|
||||||
|
|
||||||
|
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw)
|
||||||
|
? raw
|
||||||
|
: `https://${raw}`;
|
||||||
|
|
||||||
|
let parsed: URL;
|
||||||
|
try {
|
||||||
|
parsed = new URL(withScheme);
|
||||||
|
} catch {
|
||||||
|
return { error: `"${raw}" is not a valid URL` };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.protocol !== "https:") {
|
||||||
|
return { error: "Only https is supported for GitHub instances" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// "acme.ghe.com." resolves the same as "acme.ghe.com", but the trailing dot
|
||||||
|
// would slip past the checks below.
|
||||||
|
if (parsed.hostname.endsWith(".")) {
|
||||||
|
parsed.hostname = parsed.hostname.replace(/\.+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { hostname } = parsed;
|
||||||
|
|
||||||
|
if (!hostname.includes(".")) {
|
||||||
|
return { error: `"${hostname}" is not a fully qualified hostname` };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { url: `${parsed.protocol}//${parsed.host}` };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeGithubUrl = (githubUrl?: string | null): string => {
|
||||||
|
const result = parseGithubBaseUrl(githubUrl);
|
||||||
|
return "url" in result ? result.url : DEFAULT_GITHUB_URL;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deriveGithubApiUrl = (githubUrl?: string | null): string => {
|
||||||
|
const normalized = normalizeGithubUrl(githubUrl);
|
||||||
|
const { protocol, host } = new URL(normalized);
|
||||||
|
|
||||||
|
if (host === "github.com" || host === "www.github.com") {
|
||||||
|
return DEFAULT_GITHUB_API_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data residency tenants keep the api. prefix; GHES does not have one.
|
||||||
|
if (host === "ghe.com" || host.endsWith(".ghe.com")) {
|
||||||
|
return `${protocol}//api.${host}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${protocol}//${host}/api/v3`;
|
||||||
|
};
|
||||||
|
|
||||||
export const authGithub = (githubProvider: Github): Octokit => {
|
export const authGithub = (githubProvider: Github): Octokit => {
|
||||||
if (!haveGithubRequirements(githubProvider)) {
|
if (!haveGithubRequirements(githubProvider)) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -24,6 +86,7 @@ export const authGithub = (githubProvider: Github): Octokit => {
|
|||||||
privateKey: githubProvider?.githubPrivateKey || "",
|
privateKey: githubProvider?.githubPrivateKey || "",
|
||||||
installationId: githubProvider?.githubInstallationId,
|
installationId: githubProvider?.githubInstallationId,
|
||||||
},
|
},
|
||||||
|
baseUrl: deriveGithubApiUrl(githubProvider?.githubUrl),
|
||||||
});
|
});
|
||||||
|
|
||||||
return octokit;
|
return octokit;
|
||||||
@@ -162,10 +225,11 @@ export const cloneGithubRepository = async ({
|
|||||||
const outputPath = outputPathOverride ?? join(basePath, appName, "code");
|
const outputPath = outputPathOverride ?? join(basePath, appName, "code");
|
||||||
const octokit = authGithub(githubProvider);
|
const octokit = authGithub(githubProvider);
|
||||||
const token = await getGithubToken(octokit);
|
const token = await getGithubToken(octokit);
|
||||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
const cloneBase = new URL(normalizeGithubUrl(githubProvider.githubUrl));
|
||||||
|
const repoclone = `${cloneBase.host}/${owner}/${repository}.git`;
|
||||||
command += `rm -rf ${outputPath};`;
|
command += `rm -rf ${outputPath};`;
|
||||||
command += `mkdir -p ${outputPath};`;
|
command += `mkdir -p ${outputPath};`;
|
||||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
const cloneUrl = `${cloneBase.protocol}//oauth2:${token}@${repoclone}`;
|
||||||
|
|
||||||
command += `echo ${quote([`Cloning Repo ${repoclone} to ${outputPath}: ✅`])};`;
|
command += `echo ${quote([`Cloning Repo ${repoclone} to ${outputPath}: ✅`])};`;
|
||||||
command += `git clone --branch ${quote([String(branch ?? "")])} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${quote([String(cloneUrl ?? "")])} ${quote([String(outputPath ?? "")])} --progress;`;
|
command += `git clone --branch ${quote([String(branch ?? "")])} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${quote([String(cloneUrl ?? "")])} ${quote([String(outputPath ?? "")])} --progress;`;
|
||||||
@@ -187,6 +251,7 @@ export const getGithubRepositories = async (githubId?: string) => {
|
|||||||
privateKey: githubProvider.githubPrivateKey,
|
privateKey: githubProvider.githubPrivateKey,
|
||||||
installationId: githubProvider.githubInstallationId,
|
installationId: githubProvider.githubInstallationId,
|
||||||
},
|
},
|
||||||
|
baseUrl: deriveGithubApiUrl(githubProvider.githubUrl),
|
||||||
});
|
});
|
||||||
|
|
||||||
const repositories = (await octokit.paginate(
|
const repositories = (await octokit.paginate(
|
||||||
@@ -213,6 +278,7 @@ export const getGithubBranches = async (
|
|||||||
privateKey: githubProvider.githubPrivateKey,
|
privateKey: githubProvider.githubPrivateKey,
|
||||||
installationId: githubProvider.githubInstallationId,
|
installationId: githubProvider.githubInstallationId,
|
||||||
},
|
},
|
||||||
|
baseUrl: deriveGithubApiUrl(githubProvider.githubUrl),
|
||||||
});
|
});
|
||||||
|
|
||||||
const branches = (await octokit.paginate(octokit.rest.repos.listBranches, {
|
const branches = (await octokit.paginate(octokit.rest.repos.listBranches, {
|
||||||
|
|||||||
@@ -55,25 +55,46 @@ export const runCommand = async (scheduleId: string) => {
|
|||||||
description: "Schedule",
|
description: "Schedule",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (scheduleType === "application" || scheduleType === "compose") {
|
try {
|
||||||
let containerId = "";
|
if (scheduleType === "application" || scheduleType === "compose") {
|
||||||
let serverId = "";
|
let containerId = "";
|
||||||
if (scheduleType === "application" && application) {
|
let serverId = "";
|
||||||
const container = await getServiceContainer(
|
if (scheduleType === "application" && application) {
|
||||||
application.appName,
|
const container = await getServiceContainer(
|
||||||
application.serverId,
|
application.appName,
|
||||||
);
|
application.serverId,
|
||||||
containerId = container?.Id || "";
|
);
|
||||||
serverId = application.serverId || "";
|
containerId = container?.Id || "";
|
||||||
}
|
serverId = application.serverId || "";
|
||||||
if (scheduleType === "compose" && compose) {
|
}
|
||||||
const container = await getComposeContainer(compose, serviceName || "");
|
if (scheduleType === "compose" && compose) {
|
||||||
containerId = container?.Id || "";
|
const container = await getComposeContainer(compose, serviceName || "");
|
||||||
serverId = compose.serverId || "";
|
containerId = container?.Id || "";
|
||||||
}
|
serverId = compose.serverId || "";
|
||||||
|
}
|
||||||
|
|
||||||
if (serverId) {
|
if (!containerId) {
|
||||||
try {
|
const target =
|
||||||
|
scheduleType === "compose"
|
||||||
|
? `service '${serviceName}' of compose '${compose?.name}'`
|
||||||
|
: `application '${application?.appName}'`;
|
||||||
|
const message = `Container not found for ${target}, make sure the service is running`;
|
||||||
|
if (serverId) {
|
||||||
|
await execAsyncRemote(
|
||||||
|
serverId,
|
||||||
|
`echo ${quote([`❌ ${message}`])} >> ${quote([deployment.logPath])}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const writeStream = createWriteStream(deployment.logPath, {
|
||||||
|
flags: "a",
|
||||||
|
});
|
||||||
|
writeStream.write(`❌ ${message}\n`);
|
||||||
|
writeStream.end();
|
||||||
|
}
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
await execAsyncRemote(
|
await execAsyncRemote(
|
||||||
serverId,
|
serverId,
|
||||||
`
|
`
|
||||||
@@ -86,47 +107,44 @@ export const runCommand = async (scheduleId: string) => {
|
|||||||
echo "✅ Command executed successfully" >> ${quote([deployment.logPath])};
|
echo "✅ Command executed successfully" >> ${quote([deployment.logPath])};
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} else {
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
const writeStream = createWriteStream(deployment.logPath, {
|
||||||
throw error;
|
flags: "a",
|
||||||
}
|
});
|
||||||
} else {
|
|
||||||
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
|
writeStream.write(
|
||||||
|
"This feature is not available in the cloud version.",
|
||||||
|
);
|
||||||
|
writeStream.end();
|
||||||
|
return { ...deployment, status: "running" as const };
|
||||||
|
}
|
||||||
writeStream.write(
|
writeStream.write(
|
||||||
"This feature is not available in the cloud version.",
|
`docker exec ${containerId} ${shellType} -c ${command}\n`,
|
||||||
|
);
|
||||||
|
await spawnAsync(
|
||||||
|
"docker",
|
||||||
|
["exec", containerId, shellType, "-c", command],
|
||||||
|
(data) => {
|
||||||
|
if (writeStream.writable) {
|
||||||
|
writeStream.write(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
writeStream.write("✅ Command executed successfully\n");
|
||||||
|
writeStream.end();
|
||||||
|
} catch (error) {
|
||||||
|
writeStream.write("❌ Command failed\n");
|
||||||
|
writeStream.write(
|
||||||
|
error instanceof Error ? error.message : "Unknown error",
|
||||||
);
|
);
|
||||||
writeStream.end();
|
writeStream.end();
|
||||||
return;
|
throw error;
|
||||||
}
|
}
|
||||||
writeStream.write(
|
|
||||||
`docker exec ${containerId} ${shellType} -c ${command}\n`,
|
|
||||||
);
|
|
||||||
await spawnAsync(
|
|
||||||
"docker",
|
|
||||||
["exec", containerId, shellType, "-c", command],
|
|
||||||
(data) => {
|
|
||||||
if (writeStream.writable) {
|
|
||||||
writeStream.write(data);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
writeStream.write("✅ Command executed successfully\n");
|
|
||||||
} catch (error) {
|
|
||||||
writeStream.write("❌ Command failed\n");
|
|
||||||
writeStream.write(
|
|
||||||
error instanceof Error ? error.message : "Unknown error",
|
|
||||||
);
|
|
||||||
writeStream.end();
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
} else if (scheduleType === "dokploy-server") {
|
||||||
} else if (scheduleType === "dokploy-server") {
|
|
||||||
try {
|
|
||||||
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
|
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
|
||||||
const { SCHEDULES_PATH } = paths();
|
const { SCHEDULES_PATH } = paths();
|
||||||
const fullPath = path.join(SCHEDULES_PATH, appName || "");
|
const fullPath = path.join(SCHEDULES_PATH, appName || "");
|
||||||
@@ -151,18 +169,13 @@ export const runCommand = async (scheduleId: string) => {
|
|||||||
cwd: fullPath,
|
cwd: fullPath,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} else if (scheduleType === "server") {
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
} else if (scheduleType === "server") {
|
|
||||||
try {
|
|
||||||
const { SCHEDULES_PATH } = paths(true);
|
const { SCHEDULES_PATH } = paths(true);
|
||||||
const fullPath = path.join(SCHEDULES_PATH, appName || "");
|
const fullPath = path.join(SCHEDULES_PATH, appName || "");
|
||||||
const command = `
|
const command = `
|
||||||
set -e
|
set -e
|
||||||
echo "Running script" >> ${deployment.logPath};
|
echo "Running script" >> ${deployment.logPath};
|
||||||
bash -c ${fullPath}/script.sh 2>&1 | tee -a ${deployment.logPath} || {
|
bash -c ${fullPath}/script.sh 2>&1 | tee -a ${deployment.logPath} || {
|
||||||
echo "❌ Command failed" >> ${deployment.logPath};
|
echo "❌ Command failed" >> ${deployment.logPath};
|
||||||
exit 1;
|
exit 1;
|
||||||
}
|
}
|
||||||
@@ -177,10 +190,11 @@ export const runCommand = async (scheduleId: string) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||||
|
return { ...deployment, status: "done" as const };
|
||||||
|
} catch {
|
||||||
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
|
return { ...deployment, status: "error" as const };
|
||||||
}
|
}
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "./src/**/*"],
|
"include": ["next-env.d.ts", "./src/**/*"],
|
||||||
"exclude": ["**/dist", "tsup.ts"],
|
"exclude": ["**/dist", "tsup.ts", "src/lib/auth-cli.ts"],
|
||||||
"tsc-alias": {
|
"tsc-alias": {
|
||||||
"resolveFullPaths": true,
|
"resolveFullPaths": true,
|
||||||
"verbose": false
|
"verbose": false
|
||||||
|
|||||||
Generated
+251
-3
@@ -118,6 +118,9 @@ importers:
|
|||||||
'@better-auth/api-key':
|
'@better-auth/api-key':
|
||||||
specifier: 1.6.23
|
specifier: 1.6.23
|
||||||
version: 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.1.3)(kysely@0.29.3)(nanostores@1.1.1))(@better-auth/utils@0.4.2)(better-auth@1.6.23(c1a003db19823d196c7d6468bf307df5))(better-call@1.3.7(zod@4.3.6))
|
version: 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.1.3)(kysely@0.29.3)(nanostores@1.1.1))(@better-auth/utils@0.4.2)(better-auth@1.6.23(c1a003db19823d196c7d6468bf307df5))(better-call@1.3.7(zod@4.3.6))
|
||||||
|
'@better-auth/passkey':
|
||||||
|
specifier: 1.6.23
|
||||||
|
version: 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.1.3)(kysely@0.29.3)(nanostores@1.1.1))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.23(c1a003db19823d196c7d6468bf307df5))(better-call@1.3.7(zod@4.3.6))(nanostores@1.1.1)
|
||||||
'@better-auth/scim':
|
'@better-auth/scim':
|
||||||
specifier: 1.6.23
|
specifier: 1.6.23
|
||||||
version: 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.1.3)(kysely@0.29.3)(nanostores@1.1.1))(@better-auth/utils@0.4.2)(better-auth@1.6.23(c1a003db19823d196c7d6468bf307df5))(better-call@1.3.7(zod@4.3.6))
|
version: 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.1.3)(kysely@0.29.3)(nanostores@1.1.1))(@better-auth/utils@0.4.2)(better-auth@1.6.23(c1a003db19823d196c7d6468bf307df5))(better-call@1.3.7(zod@4.3.6))
|
||||||
@@ -232,9 +235,6 @@ importers:
|
|||||||
boxen:
|
boxen:
|
||||||
specifier: ^7.1.1
|
specifier: ^7.1.1
|
||||||
version: 7.1.1
|
version: 7.1.1
|
||||||
bullmq:
|
|
||||||
specifier: 5.67.3
|
|
||||||
version: 5.67.3
|
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.1
|
specifier: ^0.7.1
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
@@ -585,6 +585,9 @@ importers:
|
|||||||
'@better-auth/api-key':
|
'@better-auth/api-key':
|
||||||
specifier: 1.6.23
|
specifier: 1.6.23
|
||||||
version: 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.1.3)(kysely@0.29.3)(nanostores@1.1.1))(@better-auth/utils@0.4.2)(better-auth@1.6.23(1ed75fb9bf08f6ba6496d8d3414d7193))(better-call@1.3.7(zod@4.3.6))
|
version: 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.1.3)(kysely@0.29.3)(nanostores@1.1.1))(@better-auth/utils@0.4.2)(better-auth@1.6.23(1ed75fb9bf08f6ba6496d8d3414d7193))(better-call@1.3.7(zod@4.3.6))
|
||||||
|
'@better-auth/passkey':
|
||||||
|
specifier: 1.6.23
|
||||||
|
version: 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.1.3)(kysely@0.29.3)(nanostores@1.1.1))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.23(1ed75fb9bf08f6ba6496d8d3414d7193))(better-call@1.3.7(zod@4.3.6))(nanostores@1.1.1)
|
||||||
'@better-auth/scim':
|
'@better-auth/scim':
|
||||||
specifier: 1.6.23
|
specifier: 1.6.23
|
||||||
version: 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.1.3)(kysely@0.29.3)(nanostores@1.1.1))(@better-auth/utils@0.4.2)(better-auth@1.6.23(1ed75fb9bf08f6ba6496d8d3414d7193))(better-call@1.3.7(zod@4.3.6))
|
version: 1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.1.3)(kysely@0.29.3)(nanostores@1.1.1))(@better-auth/utils@0.4.2)(better-auth@1.6.23(1ed75fb9bf08f6ba6496d8d3414d7193))(better-call@1.3.7(zod@4.3.6))
|
||||||
@@ -1069,6 +1072,16 @@ packages:
|
|||||||
mongodb:
|
mongodb:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@better-auth/passkey@1.6.23':
|
||||||
|
resolution: {integrity: sha512-nr5tKaNd/huUwTYX4DUm4HcXgBkixj0lXrRHdy9azY4fFjF49Tyif4sPyRMWnQJYxlRJ1HVEMJq2nGyr5CLXQg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@better-auth/core': ^1.6.23
|
||||||
|
'@better-auth/utils': 0.4.2
|
||||||
|
'@better-fetch/fetch': 1.3.1
|
||||||
|
better-auth: ^1.6.23
|
||||||
|
better-call: 1.3.7
|
||||||
|
nanostores: ^1.0.1
|
||||||
|
|
||||||
'@better-auth/prisma-adapter@1.6.23':
|
'@better-auth/prisma-adapter@1.6.23':
|
||||||
resolution: {integrity: sha512-2qSdzidq4tkb1eS5TTqb4Nzg0mdZWm3Qky9SYeXeb8PpVQbC2sxqJhEM5mK7y12uU6I8hc64wO9f7AFVNL+6UQ==}
|
resolution: {integrity: sha512-2qSdzidq4tkb1eS5TTqb4Nzg0mdZWm3Qky9SYeXeb8PpVQbC2sxqJhEM5mK7y12uU6I8hc64wO9f7AFVNL+6UQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1433,6 +1446,9 @@ packages:
|
|||||||
'@hapi/bourne@3.0.0':
|
'@hapi/bourne@3.0.0':
|
||||||
resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==}
|
resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==}
|
||||||
|
|
||||||
|
'@hexagon/base64@1.1.28':
|
||||||
|
resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==}
|
||||||
|
|
||||||
'@hono/node-server@1.19.9':
|
'@hono/node-server@1.19.9':
|
||||||
resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==}
|
resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==}
|
||||||
engines: {node: '>=18.14.1'}
|
engines: {node: '>=18.14.1'}
|
||||||
@@ -1741,6 +1757,9 @@ packages:
|
|||||||
'@leichtgewicht/ip-codec@2.0.5':
|
'@leichtgewicht/ip-codec@2.0.5':
|
||||||
resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==}
|
resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==}
|
||||||
|
|
||||||
|
'@levischuck/tiny-cbor@0.2.11':
|
||||||
|
resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==}
|
||||||
|
|
||||||
'@lezer/common@1.5.2':
|
'@lezer/common@1.5.2':
|
||||||
resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==}
|
resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==}
|
||||||
|
|
||||||
@@ -2520,6 +2539,46 @@ packages:
|
|||||||
'@oslojs/encoding@1.1.0':
|
'@oslojs/encoding@1.1.0':
|
||||||
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
|
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
|
||||||
|
|
||||||
|
'@peculiar/asn1-android@2.8.0':
|
||||||
|
resolution: {integrity: sha512-skLbS+IOGv1lUgDqtChr8xvtvEr3HMse/JGBaL2r1J1o/n7a8wqOrovMtlRq/UXLhxvmLaONP67hwtshgzwfzA==}
|
||||||
|
|
||||||
|
'@peculiar/asn1-cms@2.8.0':
|
||||||
|
resolution: {integrity: sha512-NgekZOrSJFSBFLFoLfwePguAWAx7z1+f2TEsWFUMyiqqfntZ4+S/S5hzqME3q4pCA0iOsFKdwiQ35dwY24eVqA==}
|
||||||
|
|
||||||
|
'@peculiar/asn1-csr@2.8.0':
|
||||||
|
resolution: {integrity: sha512-akbF8+uvleHs8sejNPQxwmVFuInAg6FMNHOwMILXfP518YfFJwdR3jr6oNUPOaEJfuEhn/vkNOCIT6ASUd4mbg==}
|
||||||
|
|
||||||
|
'@peculiar/asn1-ecc@2.8.0':
|
||||||
|
resolution: {integrity: sha512-ohwlk+u9Rv2NOAY1c6MfHj45ATVF8R1DUN/WCgABiRtLi2ZftlZWZX7KvpAbU8v9xPcmoILfELeEABj/rn18AQ==}
|
||||||
|
|
||||||
|
'@peculiar/asn1-pfx@2.8.0':
|
||||||
|
resolution: {integrity: sha512-5yof1ytoB++RQtaFbqSUJ8pxDJtZT6vbVqZ8XoJ61ph7UjNVvfFwAilnCodqkNsAodpy13gDhoxZXw00pghnyg==}
|
||||||
|
|
||||||
|
'@peculiar/asn1-pkcs8@2.8.0':
|
||||||
|
resolution: {integrity: sha512-qAKXtLpBEw9LqhKpjw3ajZSXlBur+ipW+y2ivVBQAG6F6qRx94yO+1ZR4mvw+YaCfKSaOzLeYEzsPaBp4SJELA==}
|
||||||
|
|
||||||
|
'@peculiar/asn1-pkcs9@2.8.0':
|
||||||
|
resolution: {integrity: sha512-b5nDWCnkV60+cQ141D6sVVwK9nz64R5n3zSVnklGd+ECdkW2Ol3U1a6yYFlalpSOaD557yuJB64A+q42jG7lUQ==}
|
||||||
|
|
||||||
|
'@peculiar/asn1-rsa@2.8.0':
|
||||||
|
resolution: {integrity: sha512-zHEUlCqB2mk7x2lxDwHHJy7hWZOPdGHVlsmITWKB5/PbQo61atbu9PJ/0r9dQNMwFzbKPXZ8uK8/91eUhRznSg==}
|
||||||
|
|
||||||
|
'@peculiar/asn1-schema@2.8.0':
|
||||||
|
resolution: {integrity: sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==}
|
||||||
|
|
||||||
|
'@peculiar/asn1-x509-attr@2.8.0':
|
||||||
|
resolution: {integrity: sha512-tHjkfS/qhMnmrlB2J9NhflQlQ7In3khO3CfmVrriOlpTeErY9ZIKOso1hQ5JQiyrJ7ShvqVPk7E5fQmbclkSKA==}
|
||||||
|
|
||||||
|
'@peculiar/asn1-x509@2.8.0':
|
||||||
|
resolution: {integrity: sha512-N0CMuhWUzsWEVq6F1q9X6+VKUnWzSW+cSVg+aPaGGwDdbFoFWTYgin5MHwXgpWd6y9COMBxnfy/Qc+Xc7F0Zwg==}
|
||||||
|
|
||||||
|
'@peculiar/utils@2.0.3':
|
||||||
|
resolution: {integrity: sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==}
|
||||||
|
|
||||||
|
'@peculiar/x509@1.14.3':
|
||||||
|
resolution: {integrity: sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@prisma/client@5.22.0':
|
'@prisma/client@5.22.0':
|
||||||
resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==}
|
resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==}
|
||||||
engines: {node: '>=16.13'}
|
engines: {node: '>=16.13'}
|
||||||
@@ -3713,6 +3772,13 @@ packages:
|
|||||||
'@selderee/plugin-htmlparser2@0.11.0':
|
'@selderee/plugin-htmlparser2@0.11.0':
|
||||||
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
||||||
|
|
||||||
|
'@simplewebauthn/browser@13.3.0':
|
||||||
|
resolution: {integrity: sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ==}
|
||||||
|
|
||||||
|
'@simplewebauthn/server@13.3.2':
|
||||||
|
resolution: {integrity: sha512-KEDhfcGP1PAKRVSDjA3npTQFqS2b/srm+ipoNBNHdkzrHAlaRQUTE+a5f4ywsx6thxAw1NU2rYcLEY1949RGbQ==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@sindresorhus/is@5.6.0':
|
'@sindresorhus/is@5.6.0':
|
||||||
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
|
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
|
||||||
engines: {node: '>=14.16'}
|
engines: {node: '>=14.16'}
|
||||||
@@ -4455,6 +4521,10 @@ packages:
|
|||||||
asn1@0.2.6:
|
asn1@0.2.6:
|
||||||
resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==}
|
resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==}
|
||||||
|
|
||||||
|
asn1js@3.0.10:
|
||||||
|
resolution: {integrity: sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
assertion-error@2.0.1:
|
assertion-error@2.0.1:
|
||||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -7429,6 +7499,13 @@ packages:
|
|||||||
pure-rand@6.1.0:
|
pure-rand@6.1.0:
|
||||||
resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==}
|
resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==}
|
||||||
|
|
||||||
|
pvtsutils@1.3.6:
|
||||||
|
resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==}
|
||||||
|
|
||||||
|
pvutils@1.1.5:
|
||||||
|
resolution: {integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
qrcode@1.5.4:
|
qrcode@1.5.4:
|
||||||
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
|
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
@@ -7693,6 +7770,9 @@ packages:
|
|||||||
redux@5.0.1:
|
redux@5.0.1:
|
||||||
resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
|
resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
|
||||||
|
|
||||||
|
reflect-metadata@0.2.2:
|
||||||
|
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
|
||||||
|
|
||||||
refractor@5.0.0:
|
refractor@5.0.0:
|
||||||
resolution: {integrity: sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==}
|
resolution: {integrity: sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==}
|
||||||
|
|
||||||
@@ -8333,6 +8413,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==}
|
resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
tslib@1.14.1:
|
||||||
|
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
|
||||||
|
|
||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
@@ -8341,6 +8424,10 @@ packages:
|
|||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
tsyringe@4.10.0:
|
||||||
|
resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==}
|
||||||
|
engines: {node: '>= 6.0.0'}
|
||||||
|
|
||||||
tunnel-agent@0.6.0:
|
tunnel-agent@0.6.0:
|
||||||
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
||||||
|
|
||||||
@@ -9136,6 +9223,30 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
mongodb: 7.1.0(socks@2.8.8)
|
mongodb: 7.1.0(socks@2.8.8)
|
||||||
|
|
||||||
|
'@better-auth/passkey@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.1.3)(kysely@0.29.3)(nanostores@1.1.1))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.23(1ed75fb9bf08f6ba6496d8d3414d7193))(better-call@1.3.7(zod@4.3.6))(nanostores@1.1.1)':
|
||||||
|
dependencies:
|
||||||
|
'@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.1.3)(kysely@0.29.3)(nanostores@1.1.1)
|
||||||
|
'@better-auth/utils': 0.4.2
|
||||||
|
'@better-fetch/fetch': 1.3.1
|
||||||
|
'@simplewebauthn/browser': 13.3.0
|
||||||
|
'@simplewebauthn/server': 13.3.2
|
||||||
|
better-auth: 1.6.23(1ed75fb9bf08f6ba6496d8d3414d7193)
|
||||||
|
better-call: 1.3.7(zod@4.3.6)
|
||||||
|
nanostores: 1.1.1
|
||||||
|
zod: 4.3.6
|
||||||
|
|
||||||
|
'@better-auth/passkey@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.1.3)(kysely@0.29.3)(nanostores@1.1.1))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.23(c1a003db19823d196c7d6468bf307df5))(better-call@1.3.7(zod@4.3.6))(nanostores@1.1.1)':
|
||||||
|
dependencies:
|
||||||
|
'@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.1.3)(kysely@0.29.3)(nanostores@1.1.1)
|
||||||
|
'@better-auth/utils': 0.4.2
|
||||||
|
'@better-fetch/fetch': 1.3.1
|
||||||
|
'@simplewebauthn/browser': 13.3.0
|
||||||
|
'@simplewebauthn/server': 13.3.2
|
||||||
|
better-auth: 1.6.23(c1a003db19823d196c7d6468bf307df5)
|
||||||
|
better-call: 1.3.7(zod@4.3.6)
|
||||||
|
nanostores: 1.1.1
|
||||||
|
zod: 4.3.6
|
||||||
|
|
||||||
'@better-auth/prisma-adapter@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.1.3)(kysely@0.29.3)(nanostores@1.1.1))(@better-auth/utils@0.4.2)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3)))(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3))':
|
'@better-auth/prisma-adapter@1.6.23(@better-auth/core@1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.1.3)(kysely@0.29.3)(nanostores@1.1.1))(@better-auth/utils@0.4.2)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3)))(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.1.3)(kysely@0.29.3)(nanostores@1.1.1)
|
'@better-auth/core': 1.6.23(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.0)(better-call@1.3.7(zod@4.3.6))(jose@6.1.3)(kysely@0.29.3)(nanostores@1.1.1)
|
||||||
@@ -9502,6 +9613,8 @@ snapshots:
|
|||||||
|
|
||||||
'@hapi/bourne@3.0.0': {}
|
'@hapi/bourne@3.0.0': {}
|
||||||
|
|
||||||
|
'@hexagon/base64@1.1.28': {}
|
||||||
|
|
||||||
'@hono/node-server@1.19.9(hono@4.11.4)':
|
'@hono/node-server@1.19.9(hono@4.11.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
hono: 4.11.4
|
hono: 4.11.4
|
||||||
@@ -9779,6 +9892,8 @@ snapshots:
|
|||||||
|
|
||||||
'@leichtgewicht/ip-codec@2.0.5': {}
|
'@leichtgewicht/ip-codec@2.0.5': {}
|
||||||
|
|
||||||
|
'@levischuck/tiny-cbor@0.2.11': {}
|
||||||
|
|
||||||
'@lezer/common@1.5.2': {}
|
'@lezer/common@1.5.2': {}
|
||||||
|
|
||||||
'@lezer/css@1.3.1':
|
'@lezer/css@1.3.1':
|
||||||
@@ -10813,6 +10928,106 @@ snapshots:
|
|||||||
|
|
||||||
'@oslojs/encoding@1.1.0': {}
|
'@oslojs/encoding@1.1.0': {}
|
||||||
|
|
||||||
|
'@peculiar/asn1-android@2.8.0':
|
||||||
|
dependencies:
|
||||||
|
'@peculiar/asn1-schema': 2.8.0
|
||||||
|
asn1js: 3.0.10
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@peculiar/asn1-cms@2.8.0':
|
||||||
|
dependencies:
|
||||||
|
'@peculiar/asn1-schema': 2.8.0
|
||||||
|
'@peculiar/asn1-x509': 2.8.0
|
||||||
|
'@peculiar/asn1-x509-attr': 2.8.0
|
||||||
|
asn1js: 3.0.10
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@peculiar/asn1-csr@2.8.0':
|
||||||
|
dependencies:
|
||||||
|
'@peculiar/asn1-schema': 2.8.0
|
||||||
|
'@peculiar/asn1-x509': 2.8.0
|
||||||
|
asn1js: 3.0.10
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@peculiar/asn1-ecc@2.8.0':
|
||||||
|
dependencies:
|
||||||
|
'@peculiar/asn1-schema': 2.8.0
|
||||||
|
'@peculiar/asn1-x509': 2.8.0
|
||||||
|
asn1js: 3.0.10
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@peculiar/asn1-pfx@2.8.0':
|
||||||
|
dependencies:
|
||||||
|
'@peculiar/asn1-cms': 2.8.0
|
||||||
|
'@peculiar/asn1-pkcs8': 2.8.0
|
||||||
|
'@peculiar/asn1-rsa': 2.8.0
|
||||||
|
'@peculiar/asn1-schema': 2.8.0
|
||||||
|
asn1js: 3.0.10
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@peculiar/asn1-pkcs8@2.8.0':
|
||||||
|
dependencies:
|
||||||
|
'@peculiar/asn1-schema': 2.8.0
|
||||||
|
'@peculiar/asn1-x509': 2.8.0
|
||||||
|
asn1js: 3.0.10
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@peculiar/asn1-pkcs9@2.8.0':
|
||||||
|
dependencies:
|
||||||
|
'@peculiar/asn1-cms': 2.8.0
|
||||||
|
'@peculiar/asn1-pfx': 2.8.0
|
||||||
|
'@peculiar/asn1-pkcs8': 2.8.0
|
||||||
|
'@peculiar/asn1-schema': 2.8.0
|
||||||
|
'@peculiar/asn1-x509': 2.8.0
|
||||||
|
'@peculiar/asn1-x509-attr': 2.8.0
|
||||||
|
asn1js: 3.0.10
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@peculiar/asn1-rsa@2.8.0':
|
||||||
|
dependencies:
|
||||||
|
'@peculiar/asn1-schema': 2.8.0
|
||||||
|
'@peculiar/asn1-x509': 2.8.0
|
||||||
|
asn1js: 3.0.10
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@peculiar/asn1-schema@2.8.0':
|
||||||
|
dependencies:
|
||||||
|
'@peculiar/utils': 2.0.3
|
||||||
|
asn1js: 3.0.10
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@peculiar/asn1-x509-attr@2.8.0':
|
||||||
|
dependencies:
|
||||||
|
'@peculiar/asn1-schema': 2.8.0
|
||||||
|
'@peculiar/asn1-x509': 2.8.0
|
||||||
|
asn1js: 3.0.10
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@peculiar/asn1-x509@2.8.0':
|
||||||
|
dependencies:
|
||||||
|
'@peculiar/asn1-schema': 2.8.0
|
||||||
|
'@peculiar/utils': 2.0.3
|
||||||
|
asn1js: 3.0.10
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@peculiar/utils@2.0.3':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@peculiar/x509@1.14.3':
|
||||||
|
dependencies:
|
||||||
|
'@peculiar/asn1-cms': 2.8.0
|
||||||
|
'@peculiar/asn1-csr': 2.8.0
|
||||||
|
'@peculiar/asn1-ecc': 2.8.0
|
||||||
|
'@peculiar/asn1-pkcs9': 2.8.0
|
||||||
|
'@peculiar/asn1-rsa': 2.8.0
|
||||||
|
'@peculiar/asn1-schema': 2.8.0
|
||||||
|
'@peculiar/asn1-x509': 2.8.0
|
||||||
|
pvtsutils: 1.3.6
|
||||||
|
reflect-metadata: 0.2.2
|
||||||
|
tslib: 2.8.1
|
||||||
|
tsyringe: 4.10.0
|
||||||
|
|
||||||
'@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3))':
|
'@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3))':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
prisma: 7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3)
|
prisma: 7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3)
|
||||||
@@ -12030,6 +12245,19 @@ snapshots:
|
|||||||
domhandler: 5.0.3
|
domhandler: 5.0.3
|
||||||
selderee: 0.11.0
|
selderee: 0.11.0
|
||||||
|
|
||||||
|
'@simplewebauthn/browser@13.3.0': {}
|
||||||
|
|
||||||
|
'@simplewebauthn/server@13.3.2':
|
||||||
|
dependencies:
|
||||||
|
'@hexagon/base64': 1.1.28
|
||||||
|
'@levischuck/tiny-cbor': 0.2.11
|
||||||
|
'@peculiar/asn1-android': 2.8.0
|
||||||
|
'@peculiar/asn1-ecc': 2.8.0
|
||||||
|
'@peculiar/asn1-rsa': 2.8.0
|
||||||
|
'@peculiar/asn1-schema': 2.8.0
|
||||||
|
'@peculiar/asn1-x509': 2.8.0
|
||||||
|
'@peculiar/x509': 1.14.3
|
||||||
|
|
||||||
'@sindresorhus/is@5.6.0': {}
|
'@sindresorhus/is@5.6.0': {}
|
||||||
|
|
||||||
'@sindresorhus/merge-streams@4.0.0': {}
|
'@sindresorhus/merge-streams@4.0.0': {}
|
||||||
@@ -13075,6 +13303,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer: 2.1.2
|
safer-buffer: 2.1.2
|
||||||
|
|
||||||
|
asn1js@3.0.10:
|
||||||
|
dependencies:
|
||||||
|
pvtsutils: 1.3.6
|
||||||
|
pvutils: 1.1.5
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
assertion-error@2.0.1: {}
|
assertion-error@2.0.1: {}
|
||||||
|
|
||||||
ast-types@0.16.1:
|
ast-types@0.16.1:
|
||||||
@@ -16047,6 +16281,12 @@ snapshots:
|
|||||||
pure-rand@6.1.0:
|
pure-rand@6.1.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
pvtsutils@1.3.6:
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
pvutils@1.1.5: {}
|
||||||
|
|
||||||
qrcode@1.5.4:
|
qrcode@1.5.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
dijkstrajs: 1.0.3
|
dijkstrajs: 1.0.3
|
||||||
@@ -16387,6 +16627,8 @@ snapshots:
|
|||||||
|
|
||||||
redux@5.0.1: {}
|
redux@5.0.1: {}
|
||||||
|
|
||||||
|
reflect-metadata@0.2.2: {}
|
||||||
|
|
||||||
refractor@5.0.0:
|
refractor@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/hast': 3.0.4
|
'@types/hast': 3.0.4
|
||||||
@@ -17209,6 +17451,8 @@ snapshots:
|
|||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
strip-bom: 3.0.0
|
strip-bom: 3.0.0
|
||||||
|
|
||||||
|
tslib@1.14.1: {}
|
||||||
|
|
||||||
tslib@2.8.1: {}
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
tsx@4.22.4:
|
tsx@4.22.4:
|
||||||
@@ -17217,6 +17461,10 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
tsyringe@4.10.0:
|
||||||
|
dependencies:
|
||||||
|
tslib: 1.14.1
|
||||||
|
|
||||||
tunnel-agent@0.6.0:
|
tunnel-agent@0.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
|
|||||||
Reference in New Issue
Block a user