mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-17 20:05:24 +02:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df3965a581 | ||
|
|
bddc0c3d15 | ||
|
|
9626c162cc | ||
|
|
c4596ffa76 | ||
|
|
de62aff0fb | ||
|
|
d5dd35c8f8 | ||
|
|
4631ede015 | ||
|
|
bc22d05f8d | ||
|
|
7ba9818894 | ||
|
|
f577778667 | ||
|
|
12d3f1871c |
26
apps/dokploy/__test__/api/api-key-name.test.ts
Normal file
26
apps/dokploy/__test__/api/api-key-name.test.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { API_KEY_NAME_MAX_LENGTH, apiKeyNameSchema } from "@/lib/api-keys";
|
||||||
|
|
||||||
|
describe("apiKeyNameSchema", () => {
|
||||||
|
it("rejects an empty name", () => {
|
||||||
|
const result = apiKeyNameSchema.safeParse("");
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts a name at the maximum length", () => {
|
||||||
|
const name = "a".repeat(API_KEY_NAME_MAX_LENGTH);
|
||||||
|
const result = apiKeyNameSchema.safeParse(name);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a name over the maximum length instead of passing it to better-auth", () => {
|
||||||
|
const name = "a".repeat(API_KEY_NAME_MAX_LENGTH + 1);
|
||||||
|
const result = apiKeyNameSchema.safeParse(name);
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
if (!result.success) {
|
||||||
|
expect(result.error.issues[0]?.message).toBe(
|
||||||
|
`Name must be at most ${API_KEY_NAME_MAX_LENGTH} characters`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -42,6 +42,39 @@ test("Add suffix to volumes declared directly in services", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const composeFileAccessMode = `
|
||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: nginx:alpine
|
||||||
|
volumes:
|
||||||
|
- web_config:/etc/nginx/conf.d:ro
|
||||||
|
- certs/sub:/etc/certs:Z
|
||||||
|
`;
|
||||||
|
|
||||||
|
test("Add suffix to volumes preserves access mode (:ro, :z, :Z)", () => {
|
||||||
|
const composeData = parse(composeFileAccessMode) as ComposeSpecification;
|
||||||
|
|
||||||
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
|
if (!composeData.services) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedComposeData = addSuffixToVolumesInServices(
|
||||||
|
composeData.services,
|
||||||
|
suffix,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(updatedComposeData.web?.volumes).toContain(
|
||||||
|
`web_config-${suffix}:/etc/nginx/conf.d:ro`,
|
||||||
|
);
|
||||||
|
expect(updatedComposeData.web?.volumes).toContain(
|
||||||
|
`certs-${suffix}/sub:/etc/certs:Z`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const composeFileTypeVolume = `
|
const composeFileTypeVolume = `
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
|
|||||||
@@ -256,14 +256,19 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
: isLoadingRepositories
|
: isLoadingRepositories
|
||||||
? "Loading...."
|
? "Loading...."
|
||||||
: (repositories?.find(
|
: (repositories?.find(
|
||||||
(repo) => repo.name === field.value.repo,
|
(repo) =>
|
||||||
|
repo.name === field.value.repo &&
|
||||||
|
repo.owner.username === field.value.owner,
|
||||||
)?.name ?? "Select repository")}
|
)?.name ?? "Select repository")}
|
||||||
|
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0" align="start">
|
<PopoverContent
|
||||||
|
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search repository..."
|
placeholder="Search repository..."
|
||||||
@@ -283,7 +288,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{repositories?.map((repo) => (
|
{repositories?.map((repo) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={repo.name}
|
value={`${repo.owner.username}/${repo.name}`}
|
||||||
key={repo.url}
|
key={repo.url}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
form.setValue("repository", {
|
form.setValue("repository", {
|
||||||
@@ -294,8 +299,8 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
form.setValue("branch", "");
|
form.setValue("branch", "");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex min-w-0 items-center gap-2">
|
||||||
<span>{repo.name}</span>
|
<span className="truncate">{repo.name}</span>
|
||||||
<span className="text-muted-foreground text-xs">
|
<span className="text-muted-foreground text-xs">
|
||||||
{repo.owner.username}
|
{repo.owner.username}
|
||||||
</span>
|
</span>
|
||||||
@@ -303,7 +308,8 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
repo.name === field.value.repo
|
repo.name === field.value.repo &&
|
||||||
|
repo.owner.username === field.value.owner
|
||||||
? "opacity-100"
|
? "opacity-100"
|
||||||
: "opacity-0",
|
: "opacity-0",
|
||||||
)}
|
)}
|
||||||
@@ -350,7 +356,10 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0" align="start">
|
<PopoverContent
|
||||||
|
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search branch..."
|
placeholder="Search branch..."
|
||||||
@@ -378,7 +387,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
form.setValue("branch", branch.name);
|
form.setValue("branch", branch.name);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{branch.name}
|
<span className="truncate">{branch.name}</span>
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
|
|||||||
@@ -270,14 +270,18 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
|||||||
? "Loading...."
|
? "Loading...."
|
||||||
: (repositories?.find(
|
: (repositories?.find(
|
||||||
(repo: GiteaRepository) =>
|
(repo: GiteaRepository) =>
|
||||||
repo.name === field.value.repo,
|
repo.name === field.value.repo &&
|
||||||
|
repo.owner.username === field.value.owner,
|
||||||
)?.name ?? "Select repository")}
|
)?.name ?? "Select repository")}
|
||||||
|
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0" align="start">
|
<PopoverContent
|
||||||
|
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search repository..."
|
placeholder="Search repository..."
|
||||||
@@ -303,7 +307,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
|||||||
{repositories?.map((repo: GiteaRepository) => {
|
{repositories?.map((repo: GiteaRepository) => {
|
||||||
return (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={repo.name}
|
value={`${repo.owner.username}/${repo.name}`}
|
||||||
key={repo.url}
|
key={repo.url}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
form.setValue("repository", {
|
form.setValue("repository", {
|
||||||
@@ -313,8 +317,10 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
|||||||
form.setValue("branch", "");
|
form.setValue("branch", "");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex min-w-0 items-center gap-2">
|
||||||
<span>{repo.name}</span>
|
<span className="truncate">
|
||||||
|
{repo.name}
|
||||||
|
</span>
|
||||||
<span className="text-muted-foreground text-xs">
|
<span className="text-muted-foreground text-xs">
|
||||||
{repo.owner.username}
|
{repo.owner.username}
|
||||||
</span>
|
</span>
|
||||||
@@ -322,7 +328,9 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
|||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
repo.name === field.value.repo
|
repo.name === field.value.repo &&
|
||||||
|
repo.owner.username ===
|
||||||
|
field.value.owner
|
||||||
? "opacity-100"
|
? "opacity-100"
|
||||||
: "opacity-0",
|
: "opacity-0",
|
||||||
)}
|
)}
|
||||||
@@ -371,7 +379,10 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0" align="start">
|
<PopoverContent
|
||||||
|
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search branch..."
|
placeholder="Search branch..."
|
||||||
@@ -402,7 +413,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
|||||||
form.setValue("branch", branch.name);
|
form.setValue("branch", branch.name);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{branch.name}
|
<span className="truncate">{branch.name}</span>
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
|
|||||||
@@ -252,14 +252,19 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
: isLoadingRepositories
|
: isLoadingRepositories
|
||||||
? "Loading...."
|
? "Loading...."
|
||||||
: (repositories?.find(
|
: (repositories?.find(
|
||||||
(repo) => repo.name === field.value.repo,
|
(repo) =>
|
||||||
|
repo.name === field.value.repo &&
|
||||||
|
repo.owner.login === field.value.owner,
|
||||||
)?.name ?? field.value.repo)}
|
)?.name ?? field.value.repo)}
|
||||||
|
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0" align="start">
|
<PopoverContent
|
||||||
|
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search repository..."
|
placeholder="Search repository..."
|
||||||
@@ -279,7 +284,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{repositories?.map((repo) => (
|
{repositories?.map((repo) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={repo.name}
|
value={`${repo.owner.login}/${repo.name}`}
|
||||||
key={repo.url}
|
key={repo.url}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
form.setValue("repository", {
|
form.setValue("repository", {
|
||||||
@@ -289,8 +294,8 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
form.setValue("branch", "");
|
form.setValue("branch", "");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex min-w-0 items-center gap-2">
|
||||||
<span>{repo.name}</span>
|
<span className="truncate">{repo.name}</span>
|
||||||
<span className="text-muted-foreground text-xs">
|
<span className="text-muted-foreground text-xs">
|
||||||
{repo.owner.login}
|
{repo.owner.login}
|
||||||
</span>
|
</span>
|
||||||
@@ -298,7 +303,8 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
repo.name === field.value.repo
|
repo.name === field.value.repo &&
|
||||||
|
repo.owner.login === field.value.owner
|
||||||
? "opacity-100"
|
? "opacity-100"
|
||||||
: "opacity-0",
|
: "opacity-0",
|
||||||
)}
|
)}
|
||||||
@@ -345,7 +351,10 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0" align="start">
|
<PopoverContent
|
||||||
|
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search branch..."
|
placeholder="Search branch..."
|
||||||
@@ -373,7 +382,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
form.setValue("branch", branch.name);
|
form.setValue("branch", branch.name);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{branch.name}
|
<span className="truncate">{branch.name}</span>
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
|
|||||||
@@ -272,7 +272,10 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0" align="start">
|
<PopoverContent
|
||||||
|
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search repository..."
|
placeholder="Search repository..."
|
||||||
@@ -310,8 +313,10 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
form.setValue("branch", "");
|
form.setValue("branch", "");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex min-w-0 items-center gap-2">
|
||||||
<span>{repo.name}</span>
|
<span className="truncate">
|
||||||
|
{repo.name}
|
||||||
|
</span>
|
||||||
<span className="text-muted-foreground text-xs">
|
<span className="text-muted-foreground text-xs">
|
||||||
{repo.owner.username}
|
{repo.owner.username}
|
||||||
</span>
|
</span>
|
||||||
@@ -368,7 +373,10 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0" align="start">
|
<PopoverContent
|
||||||
|
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search branch..."
|
placeholder="Search branch..."
|
||||||
@@ -396,7 +404,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
form.setValue("branch", branch.name);
|
form.setValue("branch", branch.name);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{branch.name}
|
<span className="truncate">{branch.name}</span>
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
|
|||||||
@@ -258,14 +258,19 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
: isLoadingRepositories
|
: isLoadingRepositories
|
||||||
? "Loading...."
|
? "Loading...."
|
||||||
: (repositories?.find(
|
: (repositories?.find(
|
||||||
(repo) => repo.name === field.value.repo,
|
(repo) =>
|
||||||
|
repo.name === field.value.repo &&
|
||||||
|
repo.owner.username === field.value.owner,
|
||||||
)?.name ?? "Select repository")}
|
)?.name ?? "Select repository")}
|
||||||
|
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0" align="start">
|
<PopoverContent
|
||||||
|
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search repository..."
|
placeholder="Search repository..."
|
||||||
@@ -285,7 +290,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{repositories?.map((repo) => (
|
{repositories?.map((repo) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={repo.name}
|
value={`${repo.owner.username}/${repo.name}`}
|
||||||
key={repo.url}
|
key={repo.url}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
form.setValue("repository", {
|
form.setValue("repository", {
|
||||||
@@ -296,8 +301,8 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
form.setValue("branch", "");
|
form.setValue("branch", "");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex min-w-0 items-center gap-2">
|
||||||
<span>{repo.name}</span>
|
<span className="truncate">{repo.name}</span>
|
||||||
<span className="text-muted-foreground text-xs">
|
<span className="text-muted-foreground text-xs">
|
||||||
{repo.owner.username}
|
{repo.owner.username}
|
||||||
</span>
|
</span>
|
||||||
@@ -305,7 +310,8 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
repo.name === field.value.repo
|
repo.name === field.value.repo &&
|
||||||
|
repo.owner.username === field.value.owner
|
||||||
? "opacity-100"
|
? "opacity-100"
|
||||||
: "opacity-0",
|
: "opacity-0",
|
||||||
)}
|
)}
|
||||||
@@ -352,7 +358,10 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0" align="start">
|
<PopoverContent
|
||||||
|
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search branch..."
|
placeholder="Search branch..."
|
||||||
@@ -380,7 +389,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
form.setValue("branch", branch.name);
|
form.setValue("branch", branch.name);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{branch.name}
|
<span className="truncate">{branch.name}</span>
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
|
|||||||
@@ -255,13 +255,18 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
|||||||
: isLoadingRepositories
|
: isLoadingRepositories
|
||||||
? "Loading...."
|
? "Loading...."
|
||||||
: (repositories?.find(
|
: (repositories?.find(
|
||||||
(repo) => repo.name === field.value.repo,
|
(repo) =>
|
||||||
|
repo.name === field.value.repo &&
|
||||||
|
repo.owner.username === field.value.owner,
|
||||||
)?.name ?? "Select repository")}
|
)?.name ?? "Select repository")}
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0" align="start">
|
<PopoverContent
|
||||||
|
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search repository..."
|
placeholder="Search repository..."
|
||||||
@@ -282,7 +287,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
|||||||
{repositories?.map((repo) => (
|
{repositories?.map((repo) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={repo.url}
|
key={repo.url}
|
||||||
value={repo.name}
|
value={`${repo.owner.username}/${repo.name}`}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
form.setValue("repository", {
|
form.setValue("repository", {
|
||||||
owner: repo.owner.username,
|
owner: repo.owner.username,
|
||||||
@@ -291,8 +296,8 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
|||||||
form.setValue("branch", "");
|
form.setValue("branch", "");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex min-w-0 items-center gap-2">
|
||||||
<span>{repo.name}</span>
|
<span className="truncate">{repo.name}</span>
|
||||||
<span className="text-muted-foreground text-xs">
|
<span className="text-muted-foreground text-xs">
|
||||||
{repo.owner.username}
|
{repo.owner.username}
|
||||||
</span>
|
</span>
|
||||||
@@ -300,7 +305,8 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
|||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
repo.name === field.value.repo
|
repo.name === field.value.repo &&
|
||||||
|
repo.owner.username === field.value.owner
|
||||||
? "opacity-100"
|
? "opacity-100"
|
||||||
: "opacity-0",
|
: "opacity-0",
|
||||||
)}
|
)}
|
||||||
@@ -348,7 +354,10 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0" align="start">
|
<PopoverContent
|
||||||
|
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search branches..."
|
placeholder="Search branches..."
|
||||||
@@ -365,8 +374,10 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
|||||||
form.setValue("branch", branch.name)
|
form.setValue("branch", branch.name)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex min-w-0 items-center gap-2">
|
||||||
{branch.name}
|
<span className="truncate">
|
||||||
|
{branch.name}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -245,14 +245,19 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
: isLoadingRepositories
|
: isLoadingRepositories
|
||||||
? "Loading...."
|
? "Loading...."
|
||||||
: (repositories?.find(
|
: (repositories?.find(
|
||||||
(repo) => repo.name === field.value.repo,
|
(repo) =>
|
||||||
|
repo.name === field.value.repo &&
|
||||||
|
repo.owner.login === field.value.owner,
|
||||||
)?.name ?? field.value.repo)}
|
)?.name ?? field.value.repo)}
|
||||||
|
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0" align="start">
|
<PopoverContent
|
||||||
|
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search repository..."
|
placeholder="Search repository..."
|
||||||
@@ -272,7 +277,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{repositories?.map((repo) => (
|
{repositories?.map((repo) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={repo.name}
|
value={`${repo.owner.login}/${repo.name}`}
|
||||||
key={repo.url}
|
key={repo.url}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
form.setValue("repository", {
|
form.setValue("repository", {
|
||||||
@@ -282,8 +287,8 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
form.setValue("branch", "");
|
form.setValue("branch", "");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex min-w-0 items-center gap-2">
|
||||||
<span>{repo.name}</span>
|
<span className="truncate">{repo.name}</span>
|
||||||
<span className="text-muted-foreground text-xs">
|
<span className="text-muted-foreground text-xs">
|
||||||
{repo.owner.login}
|
{repo.owner.login}
|
||||||
</span>
|
</span>
|
||||||
@@ -291,7 +296,8 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
repo.name === field.value.repo
|
repo.name === field.value.repo &&
|
||||||
|
repo.owner.login === field.value.owner
|
||||||
? "opacity-100"
|
? "opacity-100"
|
||||||
: "opacity-0",
|
: "opacity-0",
|
||||||
)}
|
)}
|
||||||
@@ -338,7 +344,10 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0" align="start">
|
<PopoverContent
|
||||||
|
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search branch..."
|
placeholder="Search branch..."
|
||||||
@@ -366,7 +375,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
form.setValue("branch", branch.name);
|
form.setValue("branch", branch.name);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{branch.name}
|
<span className="truncate">{branch.name}</span>
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
|
|||||||
@@ -274,7 +274,10 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0" align="start">
|
<PopoverContent
|
||||||
|
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search repository..."
|
placeholder="Search repository..."
|
||||||
@@ -312,8 +315,10 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
form.setValue("branch", "");
|
form.setValue("branch", "");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex min-w-0 items-center gap-2">
|
||||||
<span>{repo.name}</span>
|
<span className="truncate">
|
||||||
|
{repo.name}
|
||||||
|
</span>
|
||||||
<span className="text-muted-foreground text-xs">
|
<span className="text-muted-foreground text-xs">
|
||||||
{repo.owner.username}
|
{repo.owner.username}
|
||||||
</span>
|
</span>
|
||||||
@@ -370,7 +375,10 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0" align="start">
|
<PopoverContent
|
||||||
|
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search branch..."
|
placeholder="Search branch..."
|
||||||
@@ -398,7 +406,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
form.setValue("branch", branch.name);
|
form.setValue("branch", branch.name);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{branch.name}
|
<span className="truncate">{branch.name}</span>
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
|
|||||||
@@ -32,10 +32,11 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { API_KEY_NAME_MAX_LENGTH, apiKeyNameSchema } from "@/lib/api-keys";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: z.string().min(1, "Name is required"),
|
name: apiKeyNameSchema,
|
||||||
prefix: z.string().optional(),
|
prefix: z.string().optional(),
|
||||||
expiresIn: z.number().nullable(),
|
expiresIn: z.number().nullable(),
|
||||||
organizationId: z.string().min(1, "Organization is required"),
|
organizationId: z.string().min(1, "Organization is required"),
|
||||||
@@ -159,8 +160,15 @@ export const AddApiKey = () => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="My API Key" {...field} />
|
<Input
|
||||||
|
placeholder="My API Key"
|
||||||
|
maxLength={API_KEY_NAME_MAX_LENGTH}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Maximum {API_KEY_NAME_MAX_LENGTH} characters
|
||||||
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -227,9 +227,9 @@ export const HandleRegistry = ({ registryId }: Props) => {
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-2xl">
|
<DialogContent className="sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Add a external registry</DialogTitle>
|
<DialogTitle>Add an external registry</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Fill the next fields to add a external registry.
|
Fill in the following fields to add an external registry.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{(isError || testRegistryIsError || testRegistryByIdIsError) && (
|
{(isError || testRegistryIsError || testRegistryByIdIsError) && (
|
||||||
|
|||||||
@@ -1828,7 +1828,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
<div className="">
|
<div className="">
|
||||||
<FormLabel>App Deploy</FormLabel>
|
<FormLabel>App Deploy</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Trigger the action when a app is deployed.
|
Trigger the action when an app is deployed.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -1890,7 +1890,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel>Dokploy Backup</FormLabel>
|
<FormLabel>Dokploy Backup</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Trigger the action when a dokploy backup is created.
|
Trigger the action when a Dokploy backup is created.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -1932,7 +1932,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel>Docker Cleanup</FormLabel>
|
<FormLabel>Docker Cleanup</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Trigger the action when the docker cleanup is
|
Trigger the action when Docker cleanup is
|
||||||
performed.
|
performed.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
@@ -1955,7 +1955,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel>Dokploy Restart</FormLabel>
|
<FormLabel>Dokploy Restart</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Trigger the action when dokploy is restarted.
|
Trigger the action when Dokploy is restarted.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ const addServerDomain = z
|
|||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.refine((val) => VALID_HOSTNAME_REGEX.test(val), {
|
// empty clears the server domain and reverts to IP-only access
|
||||||
|
.refine((val) => val === "" || VALID_HOSTNAME_REGEX.test(val), {
|
||||||
message: INVALID_HOSTNAME_MESSAGE,
|
message: INVALID_HOSTNAME_MESSAGE,
|
||||||
}),
|
}),
|
||||||
letsEncryptEmail: z.string(),
|
letsEncryptEmail: z.string(),
|
||||||
@@ -51,7 +52,7 @@ const addServerDomain = z
|
|||||||
certificateType: z.enum(["letsencrypt", "none", "custom"]),
|
certificateType: z.enum(["letsencrypt", "none", "custom"]),
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
if (data.https && !data.certificateType) {
|
if (data.domain && data.https && !data.certificateType) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
path: ["certificateType"],
|
path: ["certificateType"],
|
||||||
@@ -59,6 +60,7 @@ const addServerDomain = z
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
data.domain &&
|
||||||
data.https &&
|
data.https &&
|
||||||
data.certificateType === "letsencrypt" &&
|
data.certificateType === "letsencrypt" &&
|
||||||
!data.letsEncryptEmail
|
!data.letsEncryptEmail
|
||||||
|
|||||||
@@ -151,13 +151,15 @@ function CommandItem({
|
|||||||
<CommandPrimitive.Item
|
<CommandPrimitive.Item
|
||||||
data-slot="command-item"
|
data-slot="command-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/command-item relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 in-data-[slot=dialog-content]:py-3 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-selected:bg-muted data-selected:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 in-data-[slot=dialog-content]:[&_svg:not([class*='size-'])]:size-5 data-selected:*:[svg]:text-foreground",
|
"group/command-item relative flex cursor-default items-center gap-2 rounded-md px-2 py-1.5 in-data-[slot=dialog-content]:py-3 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-selected:bg-muted data-selected:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 in-data-[slot=dialog-content]:[&_svg:not([class*='size-'])]:size-5 data-selected:*:[svg]:text-foreground",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<CheckIcon className="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" />
|
{"data-checked" in props && (
|
||||||
|
<CheckIcon className="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" />
|
||||||
|
)}
|
||||||
</CommandPrimitive.Item>
|
</CommandPrimitive.Item>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ function SelectContent({
|
|||||||
<SelectPrimitive.Viewport
|
<SelectPrimitive.Viewport
|
||||||
data-position={position}
|
data-position={position}
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)",
|
"p-1 data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)",
|
||||||
position === "popper" && "",
|
position === "popper" && "",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -114,7 +114,7 @@ function SelectItem({
|
|||||||
<SelectPrimitive.Item
|
<SelectPrimitive.Item
|
||||||
data-slot="select-item"
|
data-slot="select-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
23
apps/dokploy/lib/api-keys.ts
Normal file
23
apps/dokploy/lib/api-keys.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum length allowed for an API key name.
|
||||||
|
*
|
||||||
|
* This mirrors the default `maximumNameLength` enforced by the
|
||||||
|
* `@better-auth/api-key` plugin. Names longer than this are rejected by
|
||||||
|
* better-auth with a 400, so we validate against it up front to surface a
|
||||||
|
* clear field-level error instead of an opaque 500.
|
||||||
|
*/
|
||||||
|
export const API_KEY_NAME_MAX_LENGTH = 32;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared validation for an API key name, used by both the tRPC input schema
|
||||||
|
* and the client form so the two can't drift.
|
||||||
|
*/
|
||||||
|
export const apiKeyNameSchema = z
|
||||||
|
.string()
|
||||||
|
.min(1, "Name is required")
|
||||||
|
.max(
|
||||||
|
API_KEY_NAME_MAX_LENGTH,
|
||||||
|
`Name must be at most ${API_KEY_NAME_MAX_LENGTH} characters`,
|
||||||
|
);
|
||||||
@@ -35,6 +35,7 @@ import { TRPCError } from "@trpc/server";
|
|||||||
import * as bcrypt from "bcrypt";
|
import * as bcrypt from "bcrypt";
|
||||||
import { and, asc, eq, gt, ne } from "drizzle-orm";
|
import { and, asc, eq, gt, ne } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { apiKeyNameSchema } from "@/lib/api-keys";
|
||||||
import { audit } from "@/server/api/utils/audit";
|
import { audit } from "@/server/api/utils/audit";
|
||||||
import {
|
import {
|
||||||
adminProcedure,
|
adminProcedure,
|
||||||
@@ -45,7 +46,7 @@ import {
|
|||||||
} from "../trpc";
|
} from "../trpc";
|
||||||
|
|
||||||
const apiCreateApiKey = z.object({
|
const apiCreateApiKey = z.object({
|
||||||
name: z.string().min(1),
|
name: apiKeyNameSchema,
|
||||||
prefix: z.string().optional(),
|
prefix: z.string().optional(),
|
||||||
expiresIn: z.number().optional(),
|
expiresIn: z.number().optional(),
|
||||||
metadata: z.object({
|
metadata: z.object({
|
||||||
|
|||||||
@@ -26,11 +26,14 @@ export const addSuffixToVolumesInServices = (
|
|||||||
if (_.has(newServiceConfig, "volumes")) {
|
if (_.has(newServiceConfig, "volumes")) {
|
||||||
newServiceConfig.volumes = _.map(newServiceConfig.volumes, (volume) => {
|
newServiceConfig.volumes = _.map(newServiceConfig.volumes, (volume) => {
|
||||||
if (_.isString(volume)) {
|
if (_.isString(volume)) {
|
||||||
const [volumeName, path] = volume.split(":");
|
// remainder is the container path plus optional access mode (:ro, :z, :Z)
|
||||||
|
const [volumeName, ...pathAndMode] = volume.split(":");
|
||||||
|
const remainder = pathAndMode.join(":");
|
||||||
|
|
||||||
// skip bind mounts and variables (e.g. $PWD)
|
// skip bind mounts and variables (e.g. $PWD)
|
||||||
if (
|
if (
|
||||||
!volumeName ||
|
!volumeName ||
|
||||||
|
!remainder ||
|
||||||
volumeName.startsWith(".") ||
|
volumeName.startsWith(".") ||
|
||||||
volumeName.startsWith("/") ||
|
volumeName.startsWith("/") ||
|
||||||
volumeName.startsWith("$")
|
volumeName.startsWith("$")
|
||||||
@@ -43,10 +46,10 @@ export const addSuffixToVolumesInServices = (
|
|||||||
if (parts.length > 1) {
|
if (parts.length > 1) {
|
||||||
const baseName = parts[0];
|
const baseName = parts[0];
|
||||||
const rest = parts.slice(1).join("/");
|
const rest = parts.slice(1).join("/");
|
||||||
return `${baseName}-${suffix}/${rest}:${path}`;
|
return `${baseName}-${suffix}/${rest}:${remainder}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${volumeName}-${suffix}:${path}`;
|
return `${volumeName}-${suffix}:${remainder}`;
|
||||||
}
|
}
|
||||||
if (_.isObject(volume) && volume.type === "volume" && volume.source) {
|
if (_.isObject(volume) && volume.type === "volume" && volume.source) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user