mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-18 14:41:38 +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);
|
||||
});
|
||||
|
||||
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", () => {
|
||||
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",
|
||||
"xn--80ak6aa92e.com",
|
||||
"123.example.com",
|
||||
"example",
|
||||
"dokploy-server",
|
||||
"localhost",
|
||||
])("accepts valid hostname %s", (host) => {
|
||||
expect(VALID_HOSTNAME_REGEX.test(host)).toBe(true);
|
||||
});
|
||||
@@ -17,7 +20,6 @@ describe("VALID_HOSTNAME_REGEX", () => {
|
||||
"bbn_client.example.com",
|
||||
"-example.com",
|
||||
"example-.com",
|
||||
"example",
|
||||
"exa mple.com",
|
||||
"example..com",
|
||||
"",
|
||||
|
||||
@@ -63,6 +63,9 @@ export const ShowDeployments = ({
|
||||
const [activeLog, setActiveLog] = useState<
|
||||
RouterOutputs["deployment"]["all"][number] | null
|
||||
>(null);
|
||||
const [removingDeploymentIds, setRemovingDeploymentIds] = useState<
|
||||
Set<string>
|
||||
>(new Set());
|
||||
const { data: deployments, isPending: isLoadingDeployments } =
|
||||
api.deployment.allByType.useQuery(
|
||||
{
|
||||
@@ -81,7 +84,7 @@ export const ShowDeployments = ({
|
||||
api.rollback.rollback.useMutation();
|
||||
const { mutateAsync: killProcess, isPending: isKillingProcess } =
|
||||
api.deployment.killProcess.useMutation();
|
||||
const { mutateAsync: removeDeployment, isPending: isRemovingDeployment } =
|
||||
const { mutateAsync: removeDeployment } =
|
||||
api.deployment.removeDeployment.useMutation();
|
||||
|
||||
// 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."
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
setRemovingDeploymentIds((deploymentIds) => {
|
||||
const nextDeploymentIds = new Set(deploymentIds);
|
||||
nextDeploymentIds.add(deployment.deploymentId);
|
||||
return nextDeploymentIds;
|
||||
});
|
||||
try {
|
||||
await removeDeployment({
|
||||
deploymentId: deployment.deploymentId,
|
||||
@@ -415,13 +423,25 @@ export const ShowDeployments = ({
|
||||
toast.success("Deployment deleted successfully");
|
||||
} catch (error) {
|
||||
toast.error("Error deleting deployment");
|
||||
} finally {
|
||||
setRemovingDeploymentIds((deploymentIds) => {
|
||||
const nextDeploymentIds = new Set(
|
||||
deploymentIds,
|
||||
);
|
||||
nextDeploymentIds.delete(
|
||||
deployment.deploymentId,
|
||||
);
|
||||
return nextDeploymentIds;
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
isLoading={isRemovingDeployment}
|
||||
isLoading={removingDeploymentIds.has(
|
||||
deployment.deploymentId,
|
||||
)}
|
||||
>
|
||||
Delete
|
||||
<Trash2 className="size-4" />
|
||||
|
||||
@@ -209,7 +209,9 @@ export const createColumns = ({
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
{validationState?.error ? (
|
||||
{validationState?.isValid && validationState?.message ? (
|
||||
<p>{validationState.message}</p>
|
||||
) : validationState?.error ? (
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="font-medium text-red-500">Error:</p>
|
||||
<p>{validationState.error}</p>
|
||||
|
||||
@@ -626,7 +626,10 @@ export const ShowDomains = ({ id, type }: Props) => {
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
{validationState?.error ? (
|
||||
{validationState?.isValid &&
|
||||
validationState?.message ? (
|
||||
<p>{validationState.message}</p>
|
||||
) : validationState?.error ? (
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="font-medium text-red-500">
|
||||
Error:
|
||||
|
||||
+9
-5
@@ -447,14 +447,18 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
{field.value?.map((path, index) => (
|
||||
<Badge key={index} variant="secondary">
|
||||
{path}
|
||||
<X
|
||||
className="ml-1 size-3 cursor-pointer"
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove watch path"
|
||||
className="inline-flex items-center focus-visible:ring-2"
|
||||
onClick={() => {
|
||||
const newPaths = [...(field.value || [])];
|
||||
newPaths.splice(index, 1);
|
||||
form.setValue("watchPaths", newPaths);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<X className="ml-1 size-3 cursor-pointer" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -502,14 +506,14 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="enableSubmodules"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
||||
<FormLabel>Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -242,14 +242,18 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
||||
{field.value?.map((path, index) => (
|
||||
<Badge key={index} variant="secondary">
|
||||
{path}
|
||||
<X
|
||||
className="ml-1 size-3 cursor-pointer"
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove watch path"
|
||||
className="inline-flex items-center focus-visible:ring-2"
|
||||
onClick={() => {
|
||||
const newPaths = [...(field.value || [])];
|
||||
newPaths.splice(index, 1);
|
||||
form.setValue("watchPaths", newPaths);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<X className="ml-1 size-3 cursor-pointer" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -298,14 +302,14 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="enableSubmodules"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
||||
<FormLabel>Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
+9
-5
@@ -477,14 +477,18 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
{path}
|
||||
<X
|
||||
className="size-3 cursor-pointer hover:text-destructive"
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove watch path"
|
||||
className="inline-flex items-center focus-visible:ring-2"
|
||||
onClick={() => {
|
||||
const newPaths = [...(field.value || [])];
|
||||
newPaths.splice(index, 1);
|
||||
field.onChange(newPaths);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<X className="size-3 cursor-pointer hover:text-destructive" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -531,14 +535,14 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="enableSubmodules"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
||||
<FormLabel>Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
+22
-8
@@ -47,6 +47,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { DEFAULT_GITHUB_URL } from "@/utils/github-utils";
|
||||
|
||||
const GithubProviderSchema = z.object({
|
||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||
@@ -96,6 +97,11 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
|
||||
const repository = form.watch("repository");
|
||||
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 { data: repositories, isPending: isLoadingRepositories } =
|
||||
@@ -227,7 +233,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
<FormLabel>Repository</FormLabel>
|
||||
{field.value.owner && field.value.repo && (
|
||||
<Link
|
||||
href={`https://github.com/${field.value.owner}/${field.value.repo}`}
|
||||
href={`${providerUrl}/${field.value.owner}/${field.value.repo}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||
@@ -438,8 +444,12 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
onValueChange={(value) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
field.onChange(value);
|
||||
}}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
@@ -487,14 +497,18 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
{path}
|
||||
<X
|
||||
className="size-3 cursor-pointer hover:text-destructive"
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove watch path"
|
||||
className="inline-flex items-center focus-visible:ring-2"
|
||||
onClick={() => {
|
||||
const newPaths = [...(field.value || [])];
|
||||
newPaths.splice(index, 1);
|
||||
field.onChange(newPaths);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<X className="size-3 cursor-pointer hover:text-destructive" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -543,14 +557,14 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="enableSubmodules"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
||||
<FormLabel>Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
+9
-5
@@ -467,14 +467,18 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
{path}
|
||||
<X
|
||||
className="size-3 cursor-pointer hover:text-destructive"
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove watch path"
|
||||
className="inline-flex items-center focus-visible:ring-2"
|
||||
onClick={() => {
|
||||
const newPaths = [...(field.value || [])];
|
||||
newPaths.splice(index, 1);
|
||||
field.onChange(newPaths);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<X className="size-3 cursor-pointer hover:text-destructive" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -521,14 +525,14 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="enableSubmodules"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
||||
<FormLabel>Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -16,7 +16,8 @@ import { type BundledIcon, bundledIcons } from "@/lib/bundled-icons";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
interface ShowIconSettingsProps {
|
||||
applicationId: string;
|
||||
serviceId: string;
|
||||
serviceType: "application" | "compose";
|
||||
icon?: string | null;
|
||||
}
|
||||
|
||||
@@ -26,7 +27,8 @@ const svgToDataUrl = (icon: BundledIcon): string => {
|
||||
};
|
||||
|
||||
export const ShowIconSettings = ({
|
||||
applicationId,
|
||||
serviceId,
|
||||
serviceType,
|
||||
icon,
|
||||
}: ShowIconSettingsProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -48,6 +50,17 @@ export const ShowIconSettings = ({
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync: updateApplication } =
|
||||
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(() => {
|
||||
if (open) {
|
||||
@@ -59,12 +72,8 @@ export const ShowIconSettings = ({
|
||||
const handleIconSelect = async (selectedIcon: BundledIcon) => {
|
||||
try {
|
||||
const dataUrl = svgToDataUrl(selectedIcon);
|
||||
await updateApplication({
|
||||
applicationId,
|
||||
icon: dataUrl,
|
||||
});
|
||||
await updateIcon(dataUrl);
|
||||
toast.success("Icon saved successfully");
|
||||
await utils.application.one.invalidate({ applicationId });
|
||||
setOpen(false);
|
||||
} catch (_error) {
|
||||
toast.error("Error saving icon");
|
||||
@@ -73,12 +82,8 @@ export const ShowIconSettings = ({
|
||||
|
||||
const handleRemoveIcon = async () => {
|
||||
try {
|
||||
await updateApplication({
|
||||
applicationId,
|
||||
icon: null,
|
||||
});
|
||||
await updateIcon(null);
|
||||
toast.success("Icon removed");
|
||||
await utils.application.one.invalidate({ applicationId });
|
||||
} catch (_error) {
|
||||
toast.error("Error removing icon");
|
||||
}
|
||||
@@ -130,12 +135,8 @@ export const ShowIconSettings = ({
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await updateApplication({
|
||||
applicationId,
|
||||
icon: sanitizedDataUrl,
|
||||
});
|
||||
await updateIcon(sanitizedDataUrl);
|
||||
toast.success("Icon saved!");
|
||||
await utils.application.one.invalidate({ applicationId });
|
||||
setOpen(false);
|
||||
} catch (_error) {
|
||||
toast.error("Error saving icon");
|
||||
@@ -147,12 +148,8 @@ export const ShowIconSettings = ({
|
||||
reader.onload = async (event) => {
|
||||
const result = event.target?.result as string;
|
||||
try {
|
||||
await updateApplication({
|
||||
applicationId,
|
||||
icon: result,
|
||||
});
|
||||
await updateIcon(result);
|
||||
toast.success("Icon saved!");
|
||||
await utils.application.one.invalidate({ applicationId });
|
||||
setOpen(false);
|
||||
} catch (_error) {
|
||||
toast.error("Error saving icon");
|
||||
@@ -172,9 +169,11 @@ export const ShowIconSettings = ({
|
||||
// biome-ignore lint/performance/noImgElement: icon is data URL or base64
|
||||
<img
|
||||
src={icon}
|
||||
alt="Application icon"
|
||||
alt="Service icon"
|
||||
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" />
|
||||
)}
|
||||
|
||||
@@ -58,8 +58,12 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
const handleRunManually = async (scheduleId: string) => {
|
||||
setRunningSchedules((prev) => new Set(prev).add(scheduleId));
|
||||
try {
|
||||
await runManually({ scheduleId });
|
||||
toast.success("Schedule run successfully");
|
||||
const result = await runManually({ scheduleId });
|
||||
if (result.status === "error") {
|
||||
toast.error("Schedule run failed, check the deployment logs");
|
||||
} else {
|
||||
toast.success("Schedule run successfully");
|
||||
}
|
||||
await refetchSchedules();
|
||||
} catch {
|
||||
toast.error("Error running schedule");
|
||||
|
||||
+9
-5
@@ -451,14 +451,18 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
{field.value?.map((path, index) => (
|
||||
<Badge key={index} variant="secondary">
|
||||
{path}
|
||||
<X
|
||||
className="ml-1 size-3 cursor-pointer"
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove watch path"
|
||||
className="inline-flex items-center focus-visible:ring-2"
|
||||
onClick={() => {
|
||||
const newPaths = [...(field.value || [])];
|
||||
newPaths.splice(index, 1);
|
||||
form.setValue("watchPaths", newPaths);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<X className="ml-1 size-3 cursor-pointer" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -506,14 +510,14 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
control={form.control}
|
||||
name="enableSubmodules"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
||||
<FormLabel>Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
+9
-5
@@ -251,14 +251,18 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
||||
{field.value?.map((path, index) => (
|
||||
<Badge key={index} variant="secondary">
|
||||
{path}
|
||||
<X
|
||||
className="ml-1 size-3 cursor-pointer"
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove watch path"
|
||||
className="inline-flex items-center focus-visible:ring-2"
|
||||
onClick={() => {
|
||||
const newPaths = [...(field.value || [])];
|
||||
newPaths.splice(index, 1);
|
||||
form.setValue("watchPaths", newPaths);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<X className="ml-1 size-3 cursor-pointer" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -306,14 +310,14 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
||||
control={form.control}
|
||||
name="enableSubmodules"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
||||
<FormLabel>Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
+9
-5
@@ -442,14 +442,18 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
||||
{field.value?.map((path, index) => (
|
||||
<Badge key={index} variant="secondary">
|
||||
{path}
|
||||
<X
|
||||
className="ml-1 size-3 cursor-pointer"
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove watch path"
|
||||
className="inline-flex items-center focus-visible:ring-2"
|
||||
onClick={() => {
|
||||
const newPaths = [...(field.value || [])];
|
||||
newPaths.splice(index, 1);
|
||||
form.setValue("watchPaths", newPaths);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<X className="ml-1 size-3 cursor-pointer" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -497,14 +501,14 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
||||
control={form.control}
|
||||
name="enableSubmodules"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
||||
<FormLabel>Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
+22
-8
@@ -47,6 +47,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { DEFAULT_GITHUB_URL } from "@/utils/github-utils";
|
||||
|
||||
const GithubProviderSchema = z.object({
|
||||
composePath: z.string().min(1),
|
||||
@@ -98,6 +99,11 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
const repository = form.watch("repository");
|
||||
const githubId = form.watch("githubId");
|
||||
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 } =
|
||||
api.github.getGithubRepositories.useQuery(
|
||||
{
|
||||
@@ -220,7 +226,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
<FormLabel>Repository</FormLabel>
|
||||
{field.value.owner && field.value.repo && (
|
||||
<Link
|
||||
href={`https://github.com/${field.value.owner}/${field.value.repo}`}
|
||||
href={`${providerUrl}/${field.value.owner}/${field.value.repo}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||
@@ -432,8 +438,12 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
onValueChange={(value) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
field.onChange(value);
|
||||
}}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
@@ -479,14 +489,18 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
{field.value?.map((path, index) => (
|
||||
<Badge key={index} variant="secondary">
|
||||
{path}
|
||||
<X
|
||||
className="ml-1 size-3 cursor-pointer"
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove watch path"
|
||||
className="inline-flex items-center focus-visible:ring-2"
|
||||
onClick={() => {
|
||||
const newPaths = [...(field.value || [])];
|
||||
newPaths.splice(index, 1);
|
||||
form.setValue("watchPaths", newPaths);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<X className="ml-1 size-3 cursor-pointer" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -538,14 +552,14 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
control={form.control}
|
||||
name="enableSubmodules"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
||||
<FormLabel>Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
+9
-5
@@ -468,14 +468,18 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
{field.value?.map((path, index) => (
|
||||
<Badge key={index} variant="secondary">
|
||||
{path}
|
||||
<X
|
||||
className="ml-1 size-3 cursor-pointer"
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove watch path"
|
||||
className="inline-flex items-center focus-visible:ring-2"
|
||||
onClick={() => {
|
||||
const newPaths = [...(field.value || [])];
|
||||
newPaths.splice(index, 1);
|
||||
form.setValue("watchPaths", newPaths);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<X className="ml-1 size-3 cursor-pointer" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -523,14 +527,14 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
control={form.control}
|
||||
name="enableSubmodules"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
||||
<FormLabel>Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -316,7 +316,7 @@ export const RestoreBackup = ({
|
||||
Restore Backup
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center">
|
||||
<RotateCcw className="mr-2 size-4" />
|
||||
|
||||
@@ -54,6 +54,14 @@ const networkFormSchema = z
|
||||
attachable: z.boolean(),
|
||||
enableIPv4: 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(),
|
||||
ipamConfig: z.array(ipamConfigEntrySchema),
|
||||
})
|
||||
@@ -85,6 +93,7 @@ const defaultValues: NetworkFormValues = {
|
||||
attachable: false,
|
||||
enableIPv4: true,
|
||||
enableIPv6: false,
|
||||
mtu: "",
|
||||
ipamDriver: "",
|
||||
ipamConfig: [],
|
||||
};
|
||||
@@ -147,6 +156,7 @@ export const HandleNetwork = ({ serverId, children }: HandleNetworkProps) => {
|
||||
attachable: data.attachable,
|
||||
enableIPv4: data.enableIPv4,
|
||||
enableIPv6: data.enableIPv6,
|
||||
mtu: data.mtu ? Number(data.mtu) : undefined,
|
||||
ipam: {
|
||||
driver: data.ipamDriver || undefined,
|
||||
config: data.ipamConfig,
|
||||
@@ -232,6 +242,27 @@ export const HandleNetwork = ({ serverId, children }: HandleNetworkProps) => {
|
||||
</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 className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{toggleOptions.map((option) => (
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { DEFAULT_GITHUB_URL, resolveGithubBaseUrl } from "@/utils/github-utils";
|
||||
|
||||
export const AddGithubProvider = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -23,6 +24,9 @@ export const AddGithubProvider = () => {
|
||||
const [manifest, setManifest] = useState("");
|
||||
const [isOrganization, setIsOrganization] = useState(false);
|
||||
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);
|
||||
|
||||
@@ -30,7 +34,7 @@ export const AddGithubProvider = () => {
|
||||
const url = document.location.origin;
|
||||
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()}`,
|
||||
url: origin,
|
||||
hook_attributes: {
|
||||
@@ -52,7 +56,7 @@ export const AddGithubProvider = () => {
|
||||
);
|
||||
|
||||
setManifest(manifest);
|
||||
}, [activeOrganization?.id, session?.user?.id]);
|
||||
}, [activeOrganization?.id, session?.user?.id, baseUrl]);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
@@ -79,6 +83,25 @@ export const AddGithubProvider = () => {
|
||||
below to get started.
|
||||
</p>
|
||||
<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">
|
||||
<span>Organization?</span>
|
||||
<Switch
|
||||
@@ -98,8 +121,8 @@ export const AddGithubProvider = () => {
|
||||
<form
|
||||
action={
|
||||
isOrganization
|
||||
? `https://github.com/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}/organizations/${organizationName}/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"
|
||||
>
|
||||
@@ -116,8 +139,8 @@ export const AddGithubProvider = () => {
|
||||
<a
|
||||
href={
|
||||
isOrganization && organizationName
|
||||
? `https://github.com/organizations/${organizationName}/settings/installations`
|
||||
: "https://github.com/settings/installations"
|
||||
? `${baseUrl}/organizations/${organizationName}/settings/installations`
|
||||
: `${baseUrl}/settings/installations`
|
||||
}
|
||||
className={`text-muted-foreground text-sm hover:underline duration-300
|
||||
${
|
||||
@@ -131,7 +154,10 @@ export const AddGithubProvider = () => {
|
||||
Unsure if you already have an app?
|
||||
</a>
|
||||
<Button
|
||||
disabled={isOrganization && organizationName.length < 1}
|
||||
disabled={
|
||||
!!githubUrlError ||
|
||||
(isOrganization && organizationName.length < 1)
|
||||
}
|
||||
type="submit"
|
||||
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">
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import { DEFAULT_GITHUB_URL } from "@/utils/github-utils";
|
||||
import { useUrl } from "@/utils/hooks/use-url";
|
||||
import { AddBitbucketProvider } from "./bitbucket/add-bitbucket-provider";
|
||||
import { EditBitbucketProvider } from "./bitbucket/edit-bitbucket-provider";
|
||||
@@ -161,6 +162,14 @@ export const ShowGitProviders = () => {
|
||||
<span className="text-sm font-medium">
|
||||
{gitProvider.name}
|
||||
</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">
|
||||
{formatDate(
|
||||
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 { Configure2FA } from "./configure-2fa";
|
||||
import { Enable2FA } from "./enable-2fa";
|
||||
import { ManagePasskeys } from "./manage-passkeys";
|
||||
|
||||
const profileSchema = z.object({
|
||||
email: z
|
||||
@@ -162,7 +163,10 @@ export const ProfileForm = () => {
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
{!data?.user.twoFactorEnabled ? <Enable2FA /> : <Configure2FA />}
|
||||
<div className="flex flex-row gap-2 flex-wrap">
|
||||
<ManagePasskeys />
|
||||
{!data?.user.twoFactorEnabled ? <Enable2FA /> : <Configure2FA />}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-2 py-8 border-t">
|
||||
|
||||
@@ -19,8 +19,6 @@ export const ShowDokployActions = () => {
|
||||
const { mutateAsync: reloadServer, isPending } =
|
||||
api.settings.reloadServer.useMutation();
|
||||
|
||||
const { mutateAsync: cleanRedis } = api.settings.cleanRedis.useMutation();
|
||||
const { mutateAsync: reloadRedis } = api.settings.reloadRedis.useMutation();
|
||||
const { mutateAsync: cleanAllDeploymentQueue } =
|
||||
api.settings.cleanAllDeploymentQueue.useMutation();
|
||||
|
||||
@@ -70,21 +68,6 @@ export const ShowDokployActions = () => {
|
||||
</DropdownMenuItem>
|
||||
</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
|
||||
className="cursor-pointer"
|
||||
onClick={async () => {
|
||||
@@ -99,21 +82,6 @@ export const ShowDokployActions = () => {
|
||||
>
|
||||
Clean all deployment queue
|
||||
</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>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -30,7 +30,6 @@ type ServiceStatus = {
|
||||
|
||||
type HealthResult = {
|
||||
postgres: ServiceStatus;
|
||||
redis: ServiceStatus;
|
||||
traefik: ServiceStatus;
|
||||
};
|
||||
|
||||
@@ -89,7 +88,6 @@ export const UpdateWebServer = ({
|
||||
const allHealthy =
|
||||
healthResult &&
|
||||
healthResult.postgres.status === "healthy" &&
|
||||
healthResult.redis.status === "healthy" &&
|
||||
healthResult.traefik.status === "healthy";
|
||||
|
||||
const checkIsUpdateFinished = async () => {
|
||||
@@ -179,7 +177,7 @@ export const UpdateWebServer = ({
|
||||
{modalState === "checking" && (
|
||||
<span className="flex items-center gap-2">
|
||||
<Loader2 className="animate-spin h-4 w-4" />
|
||||
Checking PostgreSQL, Redis and Traefik...
|
||||
Checking PostgreSQL and Traefik...
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -190,10 +188,6 @@ export const UpdateWebServer = ({
|
||||
name="PostgreSQL"
|
||||
service={healthResult.postgres}
|
||||
/>
|
||||
<ServiceStatusItem
|
||||
name="Redis"
|
||||
service={healthResult.redis}
|
||||
/>
|
||||
<ServiceStatusItem
|
||||
name="Traefik"
|
||||
service={healthResult.traefik}
|
||||
|
||||
@@ -5,7 +5,7 @@ import type * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
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: {
|
||||
variant: {
|
||||
|
||||
@@ -1,21 +1,37 @@
|
||||
import { CheckIcon, ChevronRightIcon } from "lucide-react";
|
||||
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 { cn } from "@/lib/utils";
|
||||
|
||||
function DropdownMenu({
|
||||
open,
|
||||
defaultOpen,
|
||||
onOpenChange,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(
|
||||
defaultOpen ?? false,
|
||||
);
|
||||
const isControlled = open !== undefined;
|
||||
const isOpen = isControlled ? open : uncontrolledOpen;
|
||||
|
||||
return (
|
||||
<DropdownMenuPrimitive.Root
|
||||
data-slot="dropdown-menu"
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
open={isOpen}
|
||||
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();
|
||||
}
|
||||
onOpenChange?.(open);
|
||||
onOpenChange?.(nextOpen);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -464,7 +464,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
}
|
||||
|
||||
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: {
|
||||
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)]",
|
||||
},
|
||||
size: {
|
||||
default: "h-8 text-sm",
|
||||
sm: "h-7 text-xs",
|
||||
default: "h-8 text-sm group-data-[collapsible=icon]:p-2!",
|
||||
sm: "h-7 text-xs group-data-[collapsible=icon]:p-2!",
|
||||
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,
|
||||
"tag": "0175_fantastic_peter_quill",
|
||||
"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 { passkeyClient } from "@better-auth/passkey/client";
|
||||
import { ssoClient } from "@better-auth/sso/client";
|
||||
import {
|
||||
adminClient,
|
||||
@@ -13,6 +14,7 @@ export const authClient = createAuthClient({
|
||||
plugins: [
|
||||
organizationClient(),
|
||||
twoFactorClient(),
|
||||
passkeyClient(),
|
||||
apiKeyClient(),
|
||||
ssoClient(),
|
||||
adminClient(),
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"@ai-sdk/openai": "^3.0.29",
|
||||
"@ai-sdk/openai-compatible": "^2.0.30",
|
||||
"@better-auth/api-key": "1.6.23",
|
||||
"@better-auth/passkey": "1.6.23",
|
||||
"@better-auth/scim": "1.6.23",
|
||||
"@better-auth/sso": "1.6.23",
|
||||
"@codemirror/autocomplete": "^6.18.6",
|
||||
@@ -86,7 +87,6 @@
|
||||
"better-auth": "1.6.23",
|
||||
"bl": "6.0.11",
|
||||
"boxen": "^7.1.1",
|
||||
"bullmq": "5.67.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^0.2.1",
|
||||
|
||||
@@ -5,12 +5,11 @@ import { buttonVariants } from "@/components/ui/button";
|
||||
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||
|
||||
interface Props {
|
||||
statusCode: number;
|
||||
error?: Error;
|
||||
statusCode?: number;
|
||||
}
|
||||
|
||||
export default function Custom404({ statusCode, error }: Props) {
|
||||
const displayStatusCode = statusCode || 400;
|
||||
export default function ErrorPage({ statusCode }: Props) {
|
||||
const displayStatusCode = statusCode || 500;
|
||||
const { config: whitelabeling } = useWhitelabelingPublic();
|
||||
const appName = whitelabeling?.appName || "Dokploy";
|
||||
const logoUrl = whitelabeling?.logoUrl || undefined;
|
||||
@@ -45,12 +44,6 @@ export default function Custom404({ statusCode, error }: Props) {
|
||||
{errorDescription}
|
||||
</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">
|
||||
<Link
|
||||
href="/dashboard/home"
|
||||
@@ -101,8 +94,7 @@ export default function Custom404({ statusCode, error }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
Error.getInitialProps = ({ res, err }: NextPageContext) => {
|
||||
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
|
||||
return { statusCode, error: err };
|
||||
ErrorPage.getInitialProps = ({ res, err }: NextPageContext) => {
|
||||
const statusCode = res ? res.statusCode : err ? (err.statusCode ?? 500) : 404;
|
||||
return { statusCode };
|
||||
};
|
||||
|
||||
@@ -119,9 +119,11 @@ export default async function handler(
|
||||
}
|
||||
// If webhook doesn't provide image info, we'll use the configured image (old behavior)
|
||||
} else if (sourceType === "github") {
|
||||
const normalizedCommits = req.body?.commits?.flatMap(
|
||||
(commit: any) => commit.modified,
|
||||
);
|
||||
const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||
...(commit.added || []),
|
||||
...(commit.modified || []),
|
||||
...(commit.removed || []),
|
||||
]);
|
||||
|
||||
const shouldDeployPaths = shouldDeploy(
|
||||
application.watchPaths,
|
||||
@@ -150,21 +152,29 @@ export default async function handler(
|
||||
let normalizedCommits: string[] = [];
|
||||
|
||||
if (provider === "github") {
|
||||
normalizedCommits = req.body?.commits?.flatMap(
|
||||
(commit: any) => commit.modified,
|
||||
);
|
||||
normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||
...(commit.added || []),
|
||||
...(commit.modified || []),
|
||||
...(commit.removed || []),
|
||||
]);
|
||||
} else if (provider === "gitlab") {
|
||||
normalizedCommits = req.body?.commits?.flatMap(
|
||||
(commit: any) => commit.modified,
|
||||
);
|
||||
normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||
...(commit.added || []),
|
||||
...(commit.modified || []),
|
||||
...(commit.removed || []),
|
||||
]);
|
||||
} else if (provider === "gitea") {
|
||||
normalizedCommits = req.body?.commits?.flatMap(
|
||||
(commit: any) => commit.modified,
|
||||
);
|
||||
normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||
...(commit.added || []),
|
||||
...(commit.modified || []),
|
||||
...(commit.removed || []),
|
||||
]);
|
||||
} else if (provider === "soft-serve") {
|
||||
normalizedCommits = req.body?.commits?.flatMap(
|
||||
(commit: any) => commit.modified,
|
||||
);
|
||||
normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||
...(commit.added || []),
|
||||
...(commit.modified || []),
|
||||
...(commit.removed || []),
|
||||
]);
|
||||
}
|
||||
|
||||
const shouldDeployPaths = shouldDeploy(
|
||||
@@ -179,9 +189,11 @@ export default async function handler(
|
||||
} else if (sourceType === "gitlab") {
|
||||
const branchName = extractBranchName(req.headers, req.body);
|
||||
|
||||
const normalizedCommits = req.body?.commits?.flatMap(
|
||||
(commit: any) => commit.modified,
|
||||
);
|
||||
const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||
...(commit.added || []),
|
||||
...(commit.modified || []),
|
||||
...(commit.removed || []),
|
||||
]);
|
||||
|
||||
const shouldDeployPaths = shouldDeploy(
|
||||
application.watchPaths,
|
||||
@@ -225,9 +237,11 @@ export default async function handler(
|
||||
} else if (sourceType === "gitea") {
|
||||
const branchName = extractBranchName(req.headers, req.body);
|
||||
|
||||
const normalizedCommits = req.body?.commits?.flatMap(
|
||||
(commit: any) => commit.modified,
|
||||
);
|
||||
const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||
...(commit.added || []),
|
||||
...(commit.modified || []),
|
||||
...(commit.removed || []),
|
||||
]);
|
||||
|
||||
const shouldDeployPaths = shouldDeploy(
|
||||
application.watchPaths,
|
||||
|
||||
@@ -54,9 +54,11 @@ export default async function handler(
|
||||
|
||||
if (sourceType === "github") {
|
||||
const branchName = extractBranchName(req.headers, req.body);
|
||||
const normalizedCommits = req.body?.commits?.flatMap(
|
||||
(commit: any) => commit.modified,
|
||||
);
|
||||
const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||
...(commit.added || []),
|
||||
...(commit.modified || []),
|
||||
...(commit.removed || []),
|
||||
]);
|
||||
|
||||
const shouldDeployPaths = shouldDeploy(
|
||||
composeResult.watchPaths,
|
||||
@@ -74,9 +76,11 @@ export default async function handler(
|
||||
}
|
||||
} else if (sourceType === "gitlab") {
|
||||
const branchName = extractBranchName(req.headers, req.body);
|
||||
const normalizedCommits = req.body?.commits?.flatMap(
|
||||
(commit: any) => commit.modified,
|
||||
);
|
||||
const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||
...(commit.added || []),
|
||||
...(commit.modified || []),
|
||||
...(commit.removed || []),
|
||||
]);
|
||||
|
||||
const shouldDeployPaths = shouldDeploy(
|
||||
composeResult.watchPaths,
|
||||
@@ -125,17 +129,23 @@ export default async function handler(
|
||||
let normalizedCommits: string[] = [];
|
||||
|
||||
if (provider === "github") {
|
||||
normalizedCommits = req.body?.commits?.flatMap(
|
||||
(commit: any) => commit.modified,
|
||||
);
|
||||
normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||
...(commit.added || []),
|
||||
...(commit.modified || []),
|
||||
...(commit.removed || []),
|
||||
]);
|
||||
} else if (provider === "gitlab") {
|
||||
normalizedCommits = req.body?.commits?.flatMap(
|
||||
(commit: any) => commit.modified,
|
||||
);
|
||||
normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||
...(commit.added || []),
|
||||
...(commit.modified || []),
|
||||
...(commit.removed || []),
|
||||
]);
|
||||
} else if (provider === "gitea") {
|
||||
normalizedCommits = req.body?.commits?.flatMap(
|
||||
(commit: any) => commit.modified,
|
||||
);
|
||||
normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||
...(commit.added || []),
|
||||
...(commit.modified || []),
|
||||
...(commit.removed || []),
|
||||
]);
|
||||
}
|
||||
|
||||
const shouldDeployPaths = shouldDeploy(
|
||||
@@ -150,9 +160,11 @@ export default async function handler(
|
||||
} else if (sourceType === "gitea") {
|
||||
const branchName = extractBranchName(req.headers, req.body);
|
||||
|
||||
const normalizedCommits = req.body?.commits?.flatMap(
|
||||
(commit: any) => commit.modified,
|
||||
);
|
||||
const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||
...(commit.added || []),
|
||||
...(commit.modified || []),
|
||||
...(commit.removed || []),
|
||||
]);
|
||||
|
||||
const shouldDeployPaths = shouldDeploy(
|
||||
composeResult.watchPaths,
|
||||
|
||||
@@ -223,9 +223,11 @@ export default async function handler(
|
||||
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
||||
const deploymentHash = extractHash(req.headers, req.body);
|
||||
const owner = getGithubRepositoryOwner(githubBody);
|
||||
const normalizedCommits = githubBody?.commits?.flatMap(
|
||||
(commit: any) => commit.modified,
|
||||
);
|
||||
const normalizedCommits = githubBody?.commits?.flatMap((commit: any) => [
|
||||
...(commit.added || []),
|
||||
...(commit.modified || []),
|
||||
...(commit.removed || []),
|
||||
]);
|
||||
|
||||
const apps = await db.query.applications.findMany({
|
||||
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 { hasPermission } from "@dokploy/server/services/permission";
|
||||
import { eq } from "drizzle-orm";
|
||||
@@ -11,13 +16,14 @@ type Query = {
|
||||
state: string;
|
||||
installation_id: string;
|
||||
setup_action: string;
|
||||
githubUrl?: string;
|
||||
};
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
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) {
|
||||
return res.status(400).json({ error: "Missing code parameter" });
|
||||
@@ -38,7 +44,15 @@ export default async function handler(
|
||||
}
|
||||
|
||||
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(
|
||||
"POST /app-manifests/{code}/conversions",
|
||||
{
|
||||
@@ -55,6 +69,7 @@ export default async function handler(
|
||||
githubClientSecret: data.client_secret,
|
||||
githubWebhookSecret: data.webhook_secret,
|
||||
githubPrivateKey: data.pem,
|
||||
githubUrl: baseUrl,
|
||||
},
|
||||
session.activeOrganizationId,
|
||||
user.id,
|
||||
|
||||
@@ -259,6 +259,7 @@ export const extractServicesFromEnvironment = (
|
||||
serverId: item.serverId,
|
||||
serverName: item?.server?.name || null,
|
||||
lastDeployDate,
|
||||
icon: item.icon || null,
|
||||
};
|
||||
}) || [];
|
||||
|
||||
@@ -1701,9 +1702,17 @@ const EnvironmentPage = (
|
||||
) : (
|
||||
<GlobeIcon className="h-6 w-6" />
|
||||
))}
|
||||
{service.type === "compose" && (
|
||||
<CircuitBoard className="h-6 w-6" />
|
||||
)}
|
||||
{service.type === "compose" &&
|
||||
(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" && (
|
||||
<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">
|
||||
<div className="relative flex flex-row gap-4 items-center">
|
||||
<ShowIconSettings
|
||||
applicationId={applicationId}
|
||||
serviceId={applicationId}
|
||||
serviceType="application"
|
||||
icon={data?.icon}
|
||||
/>
|
||||
<div className="absolute -right-1 -top-2 z-10">
|
||||
|
||||
+10
-6
@@ -1,7 +1,7 @@
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { CircuitBoard, HelpCircle, ServerOff } from "lucide-react";
|
||||
import { HelpCircle, ServerOff } from "lucide-react";
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
InferGetServerSidePropsType,
|
||||
@@ -17,6 +17,7 @@ import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes
|
||||
import { ShowDeployments } from "@/components/dashboard/application/deployments/show-deployments";
|
||||
import { ShowDomains } from "@/components/dashboard/application/domains/show-domains";
|
||||
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 { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
||||
import { ShowVolumeBackups } from "@/components/dashboard/application/volume-backups/show-volume-backups";
|
||||
@@ -113,13 +114,16 @@ const Service = (
|
||||
<div className="flex flex-col gap-4">
|
||||
<CardHeader className="flex flex-row justify-between items-center">
|
||||
<div className="flex flex-col">
|
||||
<CardTitle className="text-xl flex flex-row gap-2">
|
||||
<div className="relative flex flex-row gap-4">
|
||||
<div className="absolute -right-1 -top-2">
|
||||
<CardTitle className="text-xl flex flex-row gap-2 items-center">
|
||||
<div className="relative flex flex-row gap-4 items-center">
|
||||
<ShowIconSettings
|
||||
serviceId={composeId}
|
||||
serviceType="compose"
|
||||
icon={data?.icon}
|
||||
/>
|
||||
<div className="absolute -right-1 -top-2 z-10">
|
||||
<StatusTooltip status={data?.composeStatus} />
|
||||
</div>
|
||||
|
||||
<CircuitBoard className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
{data?.name}
|
||||
</CardTitle>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { REGEXP_ONLY_DIGITS } from "input-otp";
|
||||
import { Fingerprint } from "lucide-react";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -68,6 +69,7 @@ export default function Home({ IS_CLOUD, enforceSSO }: Props) {
|
||||
const { config: whitelabeling } = useWhitelabelingPublic();
|
||||
const { data: showSignInWithSSO } = api.sso.showSignInWithSSO.useQuery();
|
||||
const [isLoginLoading, setIsLoginLoading] = useState(false);
|
||||
const [isPasskeyLoading, setIsPasskeyLoading] = useState(false);
|
||||
const [isTwoFactorLoading, setIsTwoFactorLoading] = useState(false);
|
||||
const [isBackupCodeLoading, setIsBackupCodeLoading] = useState(false);
|
||||
const [isTwoFactor, setIsTwoFactor] = useState(false);
|
||||
@@ -123,6 +125,34 @@ export default function Home({ IS_CLOUD, enforceSSO }: Props) {
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
if (twoFactorCode.length !== 6) {
|
||||
@@ -227,6 +257,16 @@ export default function Home({ IS_CLOUD, enforceSSO }: Props) {
|
||||
</Button>
|
||||
</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,
|
||||
findApplicationById,
|
||||
findEnvironmentById,
|
||||
findPreviewDeploymentsByApplicationId,
|
||||
findProjectById,
|
||||
getAccessibleServerIds,
|
||||
getApplicationStats,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
removeDeployments,
|
||||
removeDirectoryCode,
|
||||
removeMonitoringDirectory,
|
||||
removePreviewDeployment,
|
||||
removeService,
|
||||
removeTraefikConfig,
|
||||
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
|
||||
.delete(applications)
|
||||
.where(eq(applications.applicationId, input.applicationId))
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
import {
|
||||
type CompleteTemplate,
|
||||
fetchTemplateFiles,
|
||||
fetchTemplateLogo,
|
||||
fetchTemplatesList,
|
||||
} from "@dokploy/server/templates/github";
|
||||
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";
|
||||
|
||||
@@ -639,6 +643,7 @@ export const composeRouter = createTRPCRouter({
|
||||
name: input.id,
|
||||
sourceType: "raw",
|
||||
appName: appName,
|
||||
icon: templateLogo,
|
||||
});
|
||||
|
||||
await addNewService(ctx, compose.composeId);
|
||||
|
||||
@@ -49,6 +49,7 @@ export const gitProviderRouter = createTRPCRouter({
|
||||
githubAppName: r.github.githubAppName,
|
||||
githubAppId: r.github.githubAppId,
|
||||
githubInstallationId: r.github.githubInstallationId,
|
||||
githubUrl: r.github.githubUrl,
|
||||
isConfigured: !!(
|
||||
r.github.githubPrivateKey &&
|
||||
r.github.githubAppId &&
|
||||
|
||||
@@ -67,6 +67,7 @@ export const githubRouter = createTRPCRouter({
|
||||
.map((provider) => {
|
||||
return {
|
||||
githubId: provider.githubId,
|
||||
githubUrl: provider.githubUrl,
|
||||
gitProvider: {
|
||||
...provider.gitProvider,
|
||||
},
|
||||
|
||||
@@ -329,13 +329,17 @@ export const scheduleRouter = createTRPCRouter({
|
||||
await checkPermission(ctx, { schedule: ["create"] });
|
||||
}
|
||||
try {
|
||||
await runCommand(input.scheduleId);
|
||||
const deployment = await runCommand(input.scheduleId);
|
||||
await audit(ctx, {
|
||||
action: "run",
|
||||
resourceType: "schedule",
|
||||
resourceId: input.scheduleId,
|
||||
});
|
||||
return true;
|
||||
return {
|
||||
status: deployment.status,
|
||||
deploymentId: deployment.deploymentId,
|
||||
logPath: deployment.logPath,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
checkGPUStatus,
|
||||
checkPortInUse,
|
||||
checkPostgresHealth,
|
||||
checkRedisHealth,
|
||||
checkTraefikHealth,
|
||||
cleanupAll,
|
||||
cleanupAllBackground,
|
||||
@@ -13,7 +12,6 @@ import {
|
||||
cleanupSystem,
|
||||
cleanupVolumes,
|
||||
DEFAULT_UPDATE_DATA,
|
||||
execAsync,
|
||||
findServerById,
|
||||
getDockerDiskUsage,
|
||||
getDokployImageTag,
|
||||
@@ -103,41 +101,6 @@ export const settingsRouter = createTRPCRouter({
|
||||
});
|
||||
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 }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
@@ -959,18 +922,16 @@ export const settingsRouter = createTRPCRouter({
|
||||
if (IS_CLOUD) {
|
||||
return {
|
||||
postgres: { status: "healthy" as const },
|
||||
redis: { status: "healthy" as const },
|
||||
traefik: { status: "healthy" as const },
|
||||
};
|
||||
}
|
||||
|
||||
const [postgres, redis, traefik] = await Promise.all([
|
||||
const [postgres, traefik] = await Promise.all([
|
||||
checkPostgresHealth(),
|
||||
checkRedisHealth(),
|
||||
checkTraefikHealth(),
|
||||
]);
|
||||
|
||||
return { postgres, redis, traefik };
|
||||
return { postgres, traefik };
|
||||
}),
|
||||
setupGPU: adminProcedure
|
||||
.input(
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
createOrganizationUserWithCredentials,
|
||||
findNotificationById,
|
||||
findOrganizationById,
|
||||
findPasskeysByUserId,
|
||||
findUserById,
|
||||
getDokployUrl,
|
||||
getUserByToken,
|
||||
@@ -173,6 +174,9 @@ export const userRouter = createTRPCRouter({
|
||||
getPermissions: protectedProcedure.query(async ({ ctx }) => {
|
||||
return resolvePermissions(ctx);
|
||||
}),
|
||||
listPasskeys: protectedProcedure.query(async ({ ctx }) => {
|
||||
return findPasskeysByUserId(ctx.user.id);
|
||||
}),
|
||||
haveRootAccess: protectedProcedure.query(async ({ ctx }) => {
|
||||
if (!IS_CLOUD) {
|
||||
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 { initializePostgres } from "@dokploy/server/setup/postgres-setup";
|
||||
import { initializeRedis } from "@dokploy/server/setup/redis-setup";
|
||||
import {
|
||||
initializeNetwork,
|
||||
initializeSwarm,
|
||||
@@ -29,7 +28,6 @@ import {
|
||||
createDefaultServerTraefikConfig();
|
||||
await execAsync(`docker pull traefik:v${TRAEFIK_VERSION}`);
|
||||
await initializeStandaloneTraefik();
|
||||
await initializeRedis();
|
||||
await initializePostgres();
|
||||
console.log("Dokploy setup completed");
|
||||
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),
|
||||
with: {
|
||||
application: {
|
||||
columns: {
|
||||
applicationId: true,
|
||||
},
|
||||
with: {
|
||||
server: true,
|
||||
},
|
||||
@@ -227,6 +230,9 @@ export const initializeJobs = async () => {
|
||||
where: eq(volumeBackups.enabled, true),
|
||||
with: {
|
||||
application: {
|
||||
columns: {
|
||||
applicationId: true,
|
||||
},
|
||||
with: {
|
||||
server: true,
|
||||
},
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"@ai-sdk/openai": "^3.0.29",
|
||||
"@ai-sdk/openai-compatible": "^2.0.30",
|
||||
"@better-auth/api-key": "1.6.23",
|
||||
"@better-auth/passkey": "1.6.23",
|
||||
"@better-auth/scim": "1.6.23",
|
||||
"@better-auth/sso": "1.6.23",
|
||||
"@better-auth/utils": "0.4.2",
|
||||
|
||||
@@ -223,6 +223,36 @@ export const twoFactor = pgTable("two_factor", {
|
||||
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", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name"),
|
||||
|
||||
@@ -98,6 +98,7 @@ export const compose = pgTable("compose", {
|
||||
.default(false),
|
||||
triggerType: triggerType("triggerType").default("push"),
|
||||
composeStatus: applicationStatus("composeStatus").notNull().default("idle"),
|
||||
icon: text("icon"),
|
||||
environmentId: text("environmentId")
|
||||
.notNull()
|
||||
.references(() => environments.environmentId, { onDelete: "cascade" }),
|
||||
@@ -190,6 +191,11 @@ const createSchema = createInsertSchema(compose, {
|
||||
.optional(),
|
||||
triggerType: z.enum(["push", "tag"]).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
|
||||
.array(
|
||||
z.object({
|
||||
|
||||
@@ -16,6 +16,7 @@ export const github = pgTable("github", {
|
||||
githubInstallationId: text("githubInstallationId"),
|
||||
githubPrivateKey: text("githubPrivateKey"),
|
||||
githubWebhookSecret: text("githubWebhookSecret"),
|
||||
githubUrl: text("githubUrl").default("https://github.com").notNull(),
|
||||
gitProviderId: text("gitProviderId")
|
||||
.notNull()
|
||||
.references(() => gitProvider.gitProviderId, { onDelete: "cascade" }),
|
||||
@@ -36,6 +37,10 @@ export const apiCreateGithub = z.object({
|
||||
githubInstallationId: z.string().optional(),
|
||||
githubPrivateKey: z.string().optional(),
|
||||
githubWebhookSecret: z.string().nullable(),
|
||||
githubUrl: z
|
||||
.url()
|
||||
.startsWith("https://", "Only https is supported for GitHub instances")
|
||||
.optional(),
|
||||
gitProviderId: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
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 { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
@@ -19,6 +26,7 @@ export const network = pgTable("network", {
|
||||
attachable: boolean("attachable").notNull().default(false),
|
||||
enableIPv4: boolean("enableIPv4").notNull().default(true),
|
||||
enableIPv6: boolean("enableIPv6").notNull().default(false),
|
||||
mtu: integer("mtu"),
|
||||
ipam: jsonb("ipam")
|
||||
.$type<{
|
||||
driver?: string;
|
||||
@@ -55,6 +63,7 @@ const createSchema = createInsertSchema(network, {
|
||||
attachable: z.boolean().optional(),
|
||||
enableIPv4: z.boolean().optional(),
|
||||
enableIPv6: z.boolean().optional(),
|
||||
mtu: z.number().int().min(68).max(65535).optional().nullable(),
|
||||
ipam: z
|
||||
.object({
|
||||
driver: z.string().optional(),
|
||||
@@ -109,6 +118,7 @@ export const apiCreateNetwork = createSchema
|
||||
attachable: true,
|
||||
enableIPv4: true,
|
||||
enableIPv6: true,
|
||||
mtu: true,
|
||||
ipam: true,
|
||||
serverId: true,
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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 { Port } from "@dokploy/server/services/port";
|
||||
import type { Project } from "@dokploy/server/services/project";
|
||||
@@ -27,7 +28,7 @@ export const rollbacks = pgTable("rollback", {
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
fullContext: jsonb("fullContext").$type<
|
||||
Application & {
|
||||
environment: {
|
||||
environment: Environment & {
|
||||
project: Project;
|
||||
};
|
||||
mounts: Mount[];
|
||||
|
||||
@@ -56,7 +56,6 @@ export * from "./setup/config-paths";
|
||||
export * from "./setup/forward-auth-setup";
|
||||
export * from "./setup/monitoring-setup";
|
||||
export * from "./setup/postgres-setup";
|
||||
export * from "./setup/redis-setup";
|
||||
export * from "./setup/server-audit";
|
||||
export * from "./setup/server-setup";
|
||||
export * from "./setup/server-validate";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { apiKey } from "@better-auth/api-key";
|
||||
import { passkey } from "@better-auth/passkey";
|
||||
import { scim } from "@better-auth/scim";
|
||||
import { sso } from "@better-auth/sso";
|
||||
import { betterAuth } from "better-auth";
|
||||
@@ -33,6 +34,7 @@ export const auth = betterAuth({
|
||||
apiKey({ enableMetadata: true, references: "user" }),
|
||||
sso(),
|
||||
twoFactor(),
|
||||
passkey(),
|
||||
organization({
|
||||
ac,
|
||||
roles: { owner: ownerRole, admin: adminRole, member: memberRole },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { IncomingMessage } from "node:http";
|
||||
import { apiKey } from "@better-auth/api-key";
|
||||
import { passkey } from "@better-auth/passkey";
|
||||
import { scim } from "@better-auth/scim";
|
||||
import { sso } from "@better-auth/sso";
|
||||
import * as bcrypt from "bcrypt";
|
||||
@@ -435,6 +436,7 @@ const createBetterAuth = () =>
|
||||
},
|
||||
}),
|
||||
twoFactor(),
|
||||
passkey(),
|
||||
organization({
|
||||
ac,
|
||||
roles: {
|
||||
|
||||
@@ -168,6 +168,7 @@ export const findEnvironmentById = async (environmentId: string) => {
|
||||
composeStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
icon: true,
|
||||
},
|
||||
},
|
||||
libsql: {
|
||||
@@ -239,7 +240,12 @@ export const findEnvironmentsByProjectId = async (projectId: string) => {
|
||||
with: { server: { columns: { name: true } } },
|
||||
},
|
||||
compose: {
|
||||
columns: { ...serviceColumns, composeId: true, composeStatus: true },
|
||||
columns: {
|
||||
...serviceColumns,
|
||||
composeId: true,
|
||||
composeStatus: true,
|
||||
icon: true,
|
||||
},
|
||||
with: { server: { columns: { name: true } } },
|
||||
},
|
||||
libsql: {
|
||||
|
||||
@@ -24,6 +24,7 @@ type DockerNetworkInfo = {
|
||||
Ingress?: boolean;
|
||||
EnableIPv4?: boolean;
|
||||
EnableIPv6?: boolean;
|
||||
Options?: Record<string, string> | null;
|
||||
IPAM?: {
|
||||
Driver?: string;
|
||||
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) =>
|
||||
!RESERVED_NETWORKS.includes(dockerNetwork.Name) &&
|
||||
!dockerNetwork.Ingress &&
|
||||
@@ -51,6 +57,7 @@ const mapDockerNetworkToRow = (
|
||||
// Older daemons don't report EnableIPv4; IPv4 is always on there
|
||||
enableIPv4: dockerNetwork.EnableIPv4 ?? true,
|
||||
enableIPv6: dockerNetwork.EnableIPv6 ?? false,
|
||||
mtu: parseMtu(dockerNetwork.Options?.["com.docker.network.driver.mtu"]),
|
||||
ipam: {
|
||||
driver: dockerNetwork.IPAM?.Driver,
|
||||
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
|
||||
EnableIPv4: row.enableIPv4,
|
||||
EnableIPv6: row.enableIPv6,
|
||||
Options: row.mtu
|
||||
? { "com.docker.network.driver.mtu": String(row.mtu) }
|
||||
: undefined,
|
||||
IPAM: {
|
||||
Driver: ipam.driver || "default",
|
||||
Config: ipamConfig.length > 0 ? ipamConfig : undefined,
|
||||
|
||||
@@ -30,12 +30,8 @@ export const finPortById = async (portId: string) => {
|
||||
where: eq(ports.portId, portId),
|
||||
with: {
|
||||
application: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
applicationId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ import { manageDomain } from "../utils/traefik/domain";
|
||||
import { findApplicationById } from "./application";
|
||||
import { removeDeploymentsByPreviewDeploymentId } from "./deployment";
|
||||
import { createDomain } from "./domain";
|
||||
import { type Github, getIssueComment } from "./github";
|
||||
import { findGithubById, getIssueComment } from "./github";
|
||||
import { getWebServerSettings } from "./web-server-settings";
|
||||
|
||||
export type PreviewDeployment = typeof previewDeployments.$inferSelect;
|
||||
@@ -142,7 +142,17 @@ export const createPreviewDeployment = async (
|
||||
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(
|
||||
application.name,
|
||||
|
||||
@@ -19,6 +19,7 @@ import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
|
||||
import { getRemoteDocker } from "../utils/servers/remote-docker";
|
||||
import { type Application, findApplicationById } from "./application";
|
||||
import { findDeploymentById } from "./deployment";
|
||||
import type { Environment } from "./environment";
|
||||
import type { Mount } from "./mount";
|
||||
import { resolveServiceNetworks } from "./network";
|
||||
import type { Port } from "./port";
|
||||
@@ -106,19 +107,7 @@ export const findRollbackById = async (rollbackId: string) => {
|
||||
const result = await db.query.rollbacks.findFirst({
|
||||
where: eq(rollbacks.rollbackId, rollbackId),
|
||||
with: {
|
||||
deployment: {
|
||||
with: {
|
||||
application: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
deployment: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -214,7 +203,7 @@ const rollbackApplication = async (
|
||||
image: string,
|
||||
serverId?: string | null,
|
||||
fullContext?: Application & {
|
||||
environment: {
|
||||
environment: Environment & {
|
||||
project: Project;
|
||||
};
|
||||
mounts: Mount[];
|
||||
@@ -279,6 +268,7 @@ const rollbackApplication = async (
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
fullContext.environment.project.env,
|
||||
fullContext.environment.env,
|
||||
);
|
||||
|
||||
let rollbackImage = image;
|
||||
|
||||
@@ -81,6 +81,11 @@ export const findScheduleById = async (scheduleId: string) => {
|
||||
where: eq(schedules.scheduleId, scheduleId),
|
||||
with: {
|
||||
application: {
|
||||
columns: {
|
||||
applicationId: true,
|
||||
appName: true,
|
||||
serverId: true,
|
||||
},
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
|
||||
@@ -4,11 +4,12 @@ import {
|
||||
apikey,
|
||||
invitation,
|
||||
member,
|
||||
passkey,
|
||||
user,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import * as bcrypt from "bcrypt";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
import { auth } from "../lib/auth";
|
||||
|
||||
export type User = typeof user.$inferSelect;
|
||||
@@ -396,6 +397,21 @@ export const findMemberById = async (
|
||||
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 ({
|
||||
organizationId,
|
||||
email,
|
||||
|
||||
@@ -13,6 +13,11 @@ export const findVolumeBackupById = async (volumeBackupId: string) => {
|
||||
where: eq(volumeBackups.volumeBackupId, volumeBackupId),
|
||||
with: {
|
||||
application: {
|
||||
columns: {
|
||||
applicationId: true,
|
||||
appName: true,
|
||||
serverId: true,
|
||||
},
|
||||
with: {
|
||||
environment: {
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -120,7 +120,7 @@ export function parseRawConfig(
|
||||
|
||||
if (search) {
|
||||
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",
|
||||
"--builder",
|
||||
builderName,
|
||||
...(cacheKey
|
||||
? [
|
||||
"--build-arg",
|
||||
`secrets-hash=${secretsHash}`,
|
||||
"--build-arg",
|
||||
`cache-key=${cacheKey}`,
|
||||
]
|
||||
: []),
|
||||
"--build-arg",
|
||||
`secrets-hash=${secretsHash}`,
|
||||
...(cacheKey ? ["--build-arg", `cache-key=${cacheKey}`] : []),
|
||||
"--build-arg",
|
||||
`BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v${application.railpackVersion}`,
|
||||
"-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> => {
|
||||
// Traefik can run as a standalone container or a swarm service
|
||||
try {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// 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.
|
||||
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 =
|
||||
"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 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 => {
|
||||
if (!haveGithubRequirements(githubProvider)) {
|
||||
throw new TRPCError({
|
||||
@@ -24,6 +86,7 @@ export const authGithub = (githubProvider: Github): Octokit => {
|
||||
privateKey: githubProvider?.githubPrivateKey || "",
|
||||
installationId: githubProvider?.githubInstallationId,
|
||||
},
|
||||
baseUrl: deriveGithubApiUrl(githubProvider?.githubUrl),
|
||||
});
|
||||
|
||||
return octokit;
|
||||
@@ -162,10 +225,11 @@ export const cloneGithubRepository = async ({
|
||||
const outputPath = outputPathOverride ?? join(basePath, appName, "code");
|
||||
const octokit = authGithub(githubProvider);
|
||||
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 += `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 += `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,
|
||||
installationId: githubProvider.githubInstallationId,
|
||||
},
|
||||
baseUrl: deriveGithubApiUrl(githubProvider.githubUrl),
|
||||
});
|
||||
|
||||
const repositories = (await octokit.paginate(
|
||||
@@ -213,6 +278,7 @@ export const getGithubBranches = async (
|
||||
privateKey: githubProvider.githubPrivateKey,
|
||||
installationId: githubProvider.githubInstallationId,
|
||||
},
|
||||
baseUrl: deriveGithubApiUrl(githubProvider.githubUrl),
|
||||
});
|
||||
|
||||
const branches = (await octokit.paginate(octokit.rest.repos.listBranches, {
|
||||
|
||||
@@ -55,25 +55,46 @@ export const runCommand = async (scheduleId: string) => {
|
||||
description: "Schedule",
|
||||
});
|
||||
|
||||
if (scheduleType === "application" || scheduleType === "compose") {
|
||||
let containerId = "";
|
||||
let serverId = "";
|
||||
if (scheduleType === "application" && application) {
|
||||
const container = await getServiceContainer(
|
||||
application.appName,
|
||||
application.serverId,
|
||||
);
|
||||
containerId = container?.Id || "";
|
||||
serverId = application.serverId || "";
|
||||
}
|
||||
if (scheduleType === "compose" && compose) {
|
||||
const container = await getComposeContainer(compose, serviceName || "");
|
||||
containerId = container?.Id || "";
|
||||
serverId = compose.serverId || "";
|
||||
}
|
||||
try {
|
||||
if (scheduleType === "application" || scheduleType === "compose") {
|
||||
let containerId = "";
|
||||
let serverId = "";
|
||||
if (scheduleType === "application" && application) {
|
||||
const container = await getServiceContainer(
|
||||
application.appName,
|
||||
application.serverId,
|
||||
);
|
||||
containerId = container?.Id || "";
|
||||
serverId = application.serverId || "";
|
||||
}
|
||||
if (scheduleType === "compose" && compose) {
|
||||
const container = await getComposeContainer(compose, serviceName || "");
|
||||
containerId = container?.Id || "";
|
||||
serverId = compose.serverId || "";
|
||||
}
|
||||
|
||||
if (serverId) {
|
||||
try {
|
||||
if (!containerId) {
|
||||
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(
|
||||
serverId,
|
||||
`
|
||||
@@ -86,47 +107,44 @@ export const runCommand = async (scheduleId: string) => {
|
||||
echo "✅ Command executed successfully" >> ${quote([deployment.logPath])};
|
||||
`,
|
||||
);
|
||||
} catch (error) {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
|
||||
} else {
|
||||
const writeStream = createWriteStream(deployment.logPath, {
|
||||
flags: "a",
|
||||
});
|
||||
|
||||
try {
|
||||
if (IS_CLOUD) {
|
||||
try {
|
||||
if (IS_CLOUD) {
|
||||
writeStream.write(
|
||||
"This feature is not available in the cloud version.",
|
||||
);
|
||||
writeStream.end();
|
||||
return { ...deployment, status: "running" as const };
|
||||
}
|
||||
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();
|
||||
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") {
|
||||
try {
|
||||
} else if (scheduleType === "dokploy-server") {
|
||||
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
|
||||
const { SCHEDULES_PATH } = paths();
|
||||
const fullPath = path.join(SCHEDULES_PATH, appName || "");
|
||||
@@ -151,18 +169,13 @@ export const runCommand = async (scheduleId: string) => {
|
||||
cwd: fullPath,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
throw error;
|
||||
}
|
||||
} else if (scheduleType === "server") {
|
||||
try {
|
||||
} else if (scheduleType === "server") {
|
||||
const { SCHEDULES_PATH } = paths(true);
|
||||
const fullPath = path.join(SCHEDULES_PATH, appName || "");
|
||||
const command = `
|
||||
set -e
|
||||
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};
|
||||
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/**/*"],
|
||||
"exclude": ["**/dist", "tsup.ts"],
|
||||
"exclude": ["**/dist", "tsup.ts", "src/lib/auth-cli.ts"],
|
||||
"tsc-alias": {
|
||||
"resolveFullPaths": true,
|
||||
"verbose": false
|
||||
|
||||
Generated
+251
-3
@@ -118,6 +118,9 @@ importers:
|
||||
'@better-auth/api-key':
|
||||
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))
|
||||
'@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':
|
||||
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))
|
||||
@@ -232,9 +235,6 @@ importers:
|
||||
boxen:
|
||||
specifier: ^7.1.1
|
||||
version: 7.1.1
|
||||
bullmq:
|
||||
specifier: 5.67.3
|
||||
version: 5.67.3
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
@@ -585,6 +585,9 @@ importers:
|
||||
'@better-auth/api-key':
|
||||
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))
|
||||
'@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':
|
||||
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))
|
||||
@@ -1069,6 +1072,16 @@ packages:
|
||||
mongodb:
|
||||
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':
|
||||
resolution: {integrity: sha512-2qSdzidq4tkb1eS5TTqb4Nzg0mdZWm3Qky9SYeXeb8PpVQbC2sxqJhEM5mK7y12uU6I8hc64wO9f7AFVNL+6UQ==}
|
||||
peerDependencies:
|
||||
@@ -1433,6 +1446,9 @@ packages:
|
||||
'@hapi/bourne@3.0.0':
|
||||
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':
|
||||
resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==}
|
||||
engines: {node: '>=18.14.1'}
|
||||
@@ -1741,6 +1757,9 @@ packages:
|
||||
'@leichtgewicht/ip-codec@2.0.5':
|
||||
resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==}
|
||||
|
||||
'@levischuck/tiny-cbor@0.2.11':
|
||||
resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==}
|
||||
|
||||
'@lezer/common@1.5.2':
|
||||
resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==}
|
||||
|
||||
@@ -2520,6 +2539,46 @@ packages:
|
||||
'@oslojs/encoding@1.1.0':
|
||||
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':
|
||||
resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==}
|
||||
engines: {node: '>=16.13'}
|
||||
@@ -3713,6 +3772,13 @@ packages:
|
||||
'@selderee/plugin-htmlparser2@0.11.0':
|
||||
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':
|
||||
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
|
||||
engines: {node: '>=14.16'}
|
||||
@@ -4455,6 +4521,10 @@ packages:
|
||||
asn1@0.2.6:
|
||||
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:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -7429,6 +7499,13 @@ packages:
|
||||
pure-rand@6.1.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
@@ -7693,6 +7770,9 @@ packages:
|
||||
redux@5.0.1:
|
||||
resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
|
||||
|
||||
reflect-metadata@0.2.2:
|
||||
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
|
||||
|
||||
refractor@5.0.0:
|
||||
resolution: {integrity: sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==}
|
||||
|
||||
@@ -8333,6 +8413,9 @@ packages:
|
||||
resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tslib@1.14.1:
|
||||
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
@@ -8341,6 +8424,10 @@ packages:
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
tsyringe@4.10.0:
|
||||
resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
|
||||
tunnel-agent@0.6.0:
|
||||
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
||||
|
||||
@@ -9136,6 +9223,30 @@ snapshots:
|
||||
optionalDependencies:
|
||||
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))':
|
||||
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)
|
||||
@@ -9502,6 +9613,8 @@ snapshots:
|
||||
|
||||
'@hapi/bourne@3.0.0': {}
|
||||
|
||||
'@hexagon/base64@1.1.28': {}
|
||||
|
||||
'@hono/node-server@1.19.9(hono@4.11.4)':
|
||||
dependencies:
|
||||
hono: 4.11.4
|
||||
@@ -9779,6 +9892,8 @@ snapshots:
|
||||
|
||||
'@leichtgewicht/ip-codec@2.0.5': {}
|
||||
|
||||
'@levischuck/tiny-cbor@0.2.11': {}
|
||||
|
||||
'@lezer/common@1.5.2': {}
|
||||
|
||||
'@lezer/css@1.3.1':
|
||||
@@ -10813,6 +10928,106 @@ snapshots:
|
||||
|
||||
'@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))':
|
||||
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)
|
||||
@@ -12030,6 +12245,19 @@ snapshots:
|
||||
domhandler: 5.0.3
|
||||
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/merge-streams@4.0.0': {}
|
||||
@@ -13075,6 +13303,12 @@ snapshots:
|
||||
dependencies:
|
||||
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: {}
|
||||
|
||||
ast-types@0.16.1:
|
||||
@@ -16047,6 +16281,12 @@ snapshots:
|
||||
pure-rand@6.1.0:
|
||||
optional: true
|
||||
|
||||
pvtsutils@1.3.6:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
pvutils@1.1.5: {}
|
||||
|
||||
qrcode@1.5.4:
|
||||
dependencies:
|
||||
dijkstrajs: 1.0.3
|
||||
@@ -16387,6 +16627,8 @@ snapshots:
|
||||
|
||||
redux@5.0.1: {}
|
||||
|
||||
reflect-metadata@0.2.2: {}
|
||||
|
||||
refractor@5.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
@@ -17209,6 +17451,8 @@ snapshots:
|
||||
minimist: 1.2.8
|
||||
strip-bom: 3.0.0
|
||||
|
||||
tslib@1.14.1: {}
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
tsx@4.22.4:
|
||||
@@ -17217,6 +17461,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
tsyringe@4.10.0:
|
||||
dependencies:
|
||||
tslib: 1.14.1
|
||||
|
||||
tunnel-agent@0.6.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
Reference in New Issue
Block a user