mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat(providers): add gitlab bitbucket and github providers
This commit is contained in:
@@ -0,0 +1,374 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const BitbucketProviderSchema = z.object({
|
||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||
repository: z
|
||||
.object({
|
||||
repo: z.string().min(1, "Repo is required"),
|
||||
owner: z.string().min(1, "Owner is required"),
|
||||
})
|
||||
.required(),
|
||||
branch: z.string().min(1, "Branch is required"),
|
||||
bitbucketProviderId: z.string().min(1, "Bitbucket Provider is required"),
|
||||
});
|
||||
|
||||
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
const { data: bitbucketProviders } =
|
||||
api.gitProvider.bitbucketProviders.useQuery();
|
||||
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
||||
|
||||
const { mutateAsync, isLoading: isSavingBitbucketProvider } =
|
||||
api.application.saveBitbucketProvider.useMutation();
|
||||
|
||||
const form = useForm<BitbucketProvider>({
|
||||
defaultValues: {
|
||||
buildPath: "/",
|
||||
repository: {
|
||||
owner: "",
|
||||
repo: "",
|
||||
},
|
||||
bitbucketProviderId: "",
|
||||
branch: "",
|
||||
},
|
||||
resolver: zodResolver(BitbucketProviderSchema),
|
||||
});
|
||||
|
||||
const repository = form.watch("repository");
|
||||
const bitbucketProviderId = form.watch("bitbucketProviderId");
|
||||
|
||||
const {
|
||||
data: repositories,
|
||||
isLoading: isLoadingRepositories,
|
||||
error,
|
||||
isError,
|
||||
} = api.gitProvider.getBitbucketRepositories.useQuery({
|
||||
bitbucketProviderId,
|
||||
});
|
||||
|
||||
const {
|
||||
data: branches,
|
||||
fetchStatus,
|
||||
status,
|
||||
} = api.gitProvider.getBitbucketBranches.useQuery(
|
||||
{
|
||||
owner: repository?.owner,
|
||||
repo: repository?.repo,
|
||||
bitbucketProviderId,
|
||||
},
|
||||
{
|
||||
enabled:
|
||||
!!repository?.owner && !!repository?.repo && !!bitbucketProviderId,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
branch: data.bitbucketBranch || "",
|
||||
repository: {
|
||||
repo: data.bitbucketRepository || "",
|
||||
owner: data.bitbucketOwner || "",
|
||||
},
|
||||
buildPath: data.bitbucketBuildPath || "/",
|
||||
bitbucketProviderId: data.bitbucketProviderId || "",
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
|
||||
const onSubmit = async (data: BitbucketProvider) => {
|
||||
await mutateAsync({
|
||||
bitbucketBranch: data.branch,
|
||||
bitbucketRepository: data.repository.repo,
|
||||
bitbucketOwner: data.repository.owner,
|
||||
bitbucketBuildPath: data.buildPath,
|
||||
bitbucketProviderId: data.bitbucketProviderId,
|
||||
applicationId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Service Provided Saved");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the Bitbucket provider");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 py-3"
|
||||
>
|
||||
{error && (
|
||||
<AlertBlock type="error">Repositories: {error.message}</AlertBlock>
|
||||
)}
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bitbucketProviderId"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2 flex flex-col">
|
||||
<FormLabel>Bitbucket Account</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
form.setValue("repository", {
|
||||
owner: "",
|
||||
repo: "",
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a Bitbucket Account" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{bitbucketProviders?.map((bitbucketProvider) => (
|
||||
<SelectItem
|
||||
key={bitbucketProvider.bitbucketProviderId}
|
||||
value={bitbucketProvider.bitbucketProviderId}
|
||||
>
|
||||
{bitbucketProvider.gitProvider.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="repository"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2 flex flex-col">
|
||||
<FormLabel>Repository</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{isLoadingRepositories
|
||||
? "Loading...."
|
||||
: field.value.owner
|
||||
? repositories?.find(
|
||||
(repo) => repo.name === field.value.repo,
|
||||
)?.name
|
||||
: "Select repository"}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search repository..."
|
||||
className="h-9"
|
||||
/>
|
||||
{isLoadingRepositories && (
|
||||
<span className="py-6 text-center text-sm">
|
||||
Loading Repositories....
|
||||
</span>
|
||||
)}
|
||||
<CommandEmpty>No repositories found.</CommandEmpty>
|
||||
<ScrollArea className="h-96">
|
||||
<CommandGroup>
|
||||
{repositories?.map((repo) => (
|
||||
<CommandItem
|
||||
value={repo.url}
|
||||
key={repo.url}
|
||||
onSelect={() => {
|
||||
form.setValue("repository", {
|
||||
owner: repo.owner.username as string,
|
||||
repo: repo.name,
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
>
|
||||
{repo.name}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
repo.name === field.value.repo
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{form.formState.errors.repository && (
|
||||
<p className={cn("text-sm font-medium text-destructive")}>
|
||||
Repository is required
|
||||
</p>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="branch"
|
||||
render={({ field }) => (
|
||||
<FormItem className="block w-full">
|
||||
<FormLabel>Branch</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
" w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{status === "loading" && fetchStatus === "fetching"
|
||||
? "Loading...."
|
||||
: field.value
|
||||
? branches?.find(
|
||||
(branch) => branch.name === field.value,
|
||||
)?.name
|
||||
: "Select branch"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search branch..."
|
||||
className="h-9"
|
||||
/>
|
||||
{status === "loading" && fetchStatus === "fetching" && (
|
||||
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||
Loading Branches....
|
||||
</span>
|
||||
)}
|
||||
{!repository?.owner && (
|
||||
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||
Select a repository
|
||||
</span>
|
||||
)}
|
||||
<ScrollArea className="h-96">
|
||||
<CommandEmpty>No branch found.</CommandEmpty>
|
||||
|
||||
<CommandGroup>
|
||||
{branches?.map((branch) => (
|
||||
<CommandItem
|
||||
value={branch.name}
|
||||
key={branch.commit.sha}
|
||||
onSelect={() => {
|
||||
form.setValue("branch", branch.name);
|
||||
}}
|
||||
>
|
||||
{branch.name}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
branch.name === field.value
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
|
||||
<FormMessage />
|
||||
</Popover>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="buildPath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Build Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="/" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full justify-end">
|
||||
<Button
|
||||
isLoading={isSavingBitbucketProvider}
|
||||
type="submit"
|
||||
className="w-fit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -21,6 +21,13 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -39,6 +46,7 @@ const GithubProviderSchema = z.object({
|
||||
})
|
||||
.required(),
|
||||
branch: z.string().min(1, "Branch is required"),
|
||||
githubProviderId: z.string().min(1, "Github Provider is required"),
|
||||
});
|
||||
|
||||
type GithubProvider = z.infer<typeof GithubProviderSchema>;
|
||||
@@ -48,6 +56,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
const { data: githubProviders } = api.gitProvider.githubProviders.useQuery();
|
||||
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
||||
|
||||
const { mutateAsync, isLoading: isSavingGithubProvider } =
|
||||
@@ -60,15 +69,19 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
owner: "",
|
||||
repo: "",
|
||||
},
|
||||
githubProviderId: "",
|
||||
branch: "",
|
||||
},
|
||||
resolver: zodResolver(GithubProviderSchema),
|
||||
});
|
||||
|
||||
const repository = form.watch("repository");
|
||||
const githubProviderId = form.watch("githubProviderId");
|
||||
|
||||
const { data: repositories, isLoading: isLoadingRepositories } =
|
||||
api.admin.getRepositories.useQuery();
|
||||
api.admin.getRepositories.useQuery({
|
||||
githubProviderId,
|
||||
});
|
||||
|
||||
const {
|
||||
data: branches,
|
||||
@@ -78,8 +91,11 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
{
|
||||
owner: repository?.owner,
|
||||
repo: repository?.repo,
|
||||
githubProviderId,
|
||||
},
|
||||
{
|
||||
enabled: !!repository?.owner && !!repository?.repo && !!githubProviderId,
|
||||
},
|
||||
{ enabled: !!repository?.owner && !!repository?.repo },
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -91,6 +107,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
owner: data.owner || "",
|
||||
},
|
||||
buildPath: data.buildPath || "/",
|
||||
githubProviderId: data.githubProviderId || "",
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
@@ -102,6 +119,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
applicationId,
|
||||
owner: data.repository.owner,
|
||||
buildPath: data.buildPath,
|
||||
githubProviderId: data.githubProviderId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Service Provided Saved");
|
||||
@@ -120,6 +138,45 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
className="grid w-full gap-4 py-3"
|
||||
>
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="githubProviderId"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2 flex flex-col">
|
||||
<FormLabel>Github Account</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
form.setValue("repository", {
|
||||
owner: "",
|
||||
repo: "",
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a Github Account" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{githubProviders?.map((githubProvider) => (
|
||||
<SelectItem
|
||||
key={githubProvider.githubProviderId}
|
||||
value={githubProvider.githubProviderId}
|
||||
>
|
||||
{githubProvider.gitProvider.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="repository"
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const GitlabProviderSchema = z.object({
|
||||
buildPath: z.string().min(1, "Path is required").default("/"),
|
||||
repository: z
|
||||
.object({
|
||||
repo: z.string().min(1, "Repo is required"),
|
||||
owner: z.string().min(1, "Owner is required"),
|
||||
})
|
||||
.required(),
|
||||
branch: z.string().min(1, "Branch is required"),
|
||||
gitlabProviderId: z.string().min(1, "Gitlab Provider is required"),
|
||||
});
|
||||
|
||||
type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
const { data: gitlabProviders } = api.gitProvider.gitlabProviders.useQuery();
|
||||
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
||||
|
||||
const { mutateAsync, isLoading: isSavingGitlabProvider } =
|
||||
api.application.saveGitlabProvider.useMutation();
|
||||
|
||||
const form = useForm<GitlabProvider>({
|
||||
defaultValues: {
|
||||
buildPath: "/",
|
||||
repository: {
|
||||
owner: "",
|
||||
repo: "",
|
||||
},
|
||||
gitlabProviderId: "",
|
||||
branch: "",
|
||||
},
|
||||
resolver: zodResolver(GitlabProviderSchema),
|
||||
});
|
||||
|
||||
const repository = form.watch("repository");
|
||||
const gitlabProviderId = form.watch("gitlabProviderId");
|
||||
|
||||
const {
|
||||
data: repositories,
|
||||
isLoading: isLoadingRepositories,
|
||||
error,
|
||||
} = api.gitProvider.getGitlabRepositories.useQuery({
|
||||
gitlabProviderId,
|
||||
});
|
||||
|
||||
const {
|
||||
data: branches,
|
||||
fetchStatus,
|
||||
status,
|
||||
} = api.gitProvider.getGitlabBranches.useQuery(
|
||||
{
|
||||
owner: repository?.owner,
|
||||
repo: repository?.repo,
|
||||
gitlabProviderId: gitlabProviderId,
|
||||
},
|
||||
{
|
||||
enabled: !!repository?.owner && !!repository?.repo && !!gitlabProviderId,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
branch: data.gitlabBranch || "",
|
||||
repository: {
|
||||
repo: data.gitlabRepository || "",
|
||||
owner: data.gitlabOwner || "",
|
||||
},
|
||||
buildPath: data.gitlabBuildPath || "/",
|
||||
gitlabProviderId: data.gitlabProviderId || "",
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
|
||||
const onSubmit = async (data: GitlabProvider) => {
|
||||
await mutateAsync({
|
||||
gitlabBranch: data.branch,
|
||||
gitlabRepository: data.repository.repo,
|
||||
gitlabOwner: data.repository.owner,
|
||||
gitlabBuildPath: data.buildPath,
|
||||
gitlabProviderId: data.gitlabProviderId,
|
||||
applicationId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Service Provided Saved");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to save the gitlab provider");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 py-3"
|
||||
>
|
||||
{error && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="gitlabProviderId"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2 flex flex-col">
|
||||
<FormLabel>Gitlab Account</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
form.setValue("repository", {
|
||||
owner: "",
|
||||
repo: "",
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a Gitlab Account" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{gitlabProviders?.map((gitlabProvider) => (
|
||||
<SelectItem
|
||||
key={gitlabProvider.gitlabProviderId}
|
||||
value={gitlabProvider.gitlabProviderId}
|
||||
>
|
||||
{gitlabProvider.gitProvider.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="repository"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2 flex flex-col">
|
||||
<FormLabel>Repository</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{isLoadingRepositories
|
||||
? "Loading...."
|
||||
: field.value.owner
|
||||
? repositories?.find(
|
||||
(repo) => repo.name === field.value.repo,
|
||||
)?.name
|
||||
: "Select repository"}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search repository..."
|
||||
className="h-9"
|
||||
/>
|
||||
{isLoadingRepositories && (
|
||||
<span className="py-6 text-center text-sm">
|
||||
Loading Repositories....
|
||||
</span>
|
||||
)}
|
||||
<CommandEmpty>No repositories found.</CommandEmpty>
|
||||
<ScrollArea className="h-96">
|
||||
<CommandGroup>
|
||||
{repositories?.map((repo) => {
|
||||
return (
|
||||
<CommandItem
|
||||
value={repo.url}
|
||||
key={repo.url}
|
||||
onSelect={() => {
|
||||
form.setValue("repository", {
|
||||
owner: repo.owner.username as string,
|
||||
repo: repo.name,
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
>
|
||||
{repo.name}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
repo.name === field.value.repo
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{form.formState.errors.repository && (
|
||||
<p className={cn("text-sm font-medium text-destructive")}>
|
||||
Repository is required
|
||||
</p>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="branch"
|
||||
render={({ field }) => (
|
||||
<FormItem className="block w-full">
|
||||
<FormLabel>Branch</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
" w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{status === "loading" && fetchStatus === "fetching"
|
||||
? "Loading...."
|
||||
: field.value
|
||||
? branches?.find(
|
||||
(branch) => branch.name === field.value,
|
||||
)?.name
|
||||
: "Select branch"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search branch..."
|
||||
className="h-9"
|
||||
/>
|
||||
{status === "loading" && fetchStatus === "fetching" && (
|
||||
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||
Loading Branches....
|
||||
</span>
|
||||
)}
|
||||
{!repository?.owner && (
|
||||
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||
Select a repository
|
||||
</span>
|
||||
)}
|
||||
<ScrollArea className="h-96">
|
||||
<CommandEmpty>No branch found.</CommandEmpty>
|
||||
|
||||
<CommandGroup>
|
||||
{branches?.map((branch) => (
|
||||
<CommandItem
|
||||
value={branch.name}
|
||||
key={branch.commit.id}
|
||||
onSelect={() => {
|
||||
form.setValue("branch", branch.name);
|
||||
}}
|
||||
>
|
||||
{branch.name}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
branch.name === field.value
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
|
||||
<FormMessage />
|
||||
</Popover>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="buildPath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Build Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="/" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full justify-end">
|
||||
<Button
|
||||
isLoading={isSavingGitlabProvider}
|
||||
type="submit"
|
||||
className="w-fit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -4,20 +4,31 @@ import { SaveGithubProvider } from "@/components/dashboard/application/general/g
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { api } from "@/utils/api";
|
||||
import { GitBranch, LockIcon } from "lucide-react";
|
||||
import { GitBranch, LockIcon, UploadCloud } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { SaveDragNDrop } from "./save-drag-n-drop";
|
||||
import {
|
||||
BitbucketIcon,
|
||||
DockerIcon,
|
||||
GithubIcon,
|
||||
GitIcon,
|
||||
GitlabIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
import { SaveGitlabProvider } from "./save-gitlab-provider";
|
||||
import { SaveBitbucketProvider } from "./save-bitbucket-provider";
|
||||
|
||||
type TabState = "github" | "docker" | "git" | "drop";
|
||||
type TabState = "github" | "docker" | "git" | "drop" | "gitlab" | "bitbucket";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const ShowProviderForm = ({ applicationId }: Props) => {
|
||||
const { data: haveGithubConfigured } =
|
||||
api.admin.haveGithubConfigured.useQuery();
|
||||
const { data: githubProviders } = api.gitProvider.githubProviders.useQuery();
|
||||
const { data: gitlabProviders } = api.gitProvider.gitlabProviders.useQuery();
|
||||
const { data: bitbucketProviders } =
|
||||
api.gitProvider.bitbucketProviders.useQuery();
|
||||
|
||||
const { data: application } = api.application.one.useQuery({ applicationId });
|
||||
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
|
||||
@@ -44,34 +55,55 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
||||
setSab(e as TabState);
|
||||
}}
|
||||
>
|
||||
<TabsList className="grid w-fit grid-cols-4 bg-transparent">
|
||||
<TabsTrigger
|
||||
value="github"
|
||||
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
Github
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="docker"
|
||||
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
Docker
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="git"
|
||||
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
Git
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="drop"
|
||||
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
Drop
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
|
||||
<TabsTrigger
|
||||
value="github"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<GithubIcon className="size-4 text-current fill-current" />
|
||||
Github
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="gitlab"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<GitlabIcon className="size-4 text-current fill-current" />
|
||||
Gitlab
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="bitbucket"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<BitbucketIcon className="size-4 text-current fill-current" />
|
||||
Bitbucket
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="docker"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<DockerIcon className="size-5 text-current" />
|
||||
Docker
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="git"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<GitIcon />
|
||||
Git
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="drop"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<UploadCloud className="size-5 text-current" />
|
||||
Drop
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="github" className="w-full p-2">
|
||||
{haveGithubConfigured ? (
|
||||
{githubProviders && githubProviders?.length > 0 ? (
|
||||
<SaveGithubProvider applicationId={applicationId} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
@@ -80,7 +112,47 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
||||
To deploy using GitHub, you need to configure your account
|
||||
first. Please, go to{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/server"
|
||||
href="/dashboard/settings/git-providers"
|
||||
className="text-foreground"
|
||||
>
|
||||
Settings
|
||||
</Link>{" "}
|
||||
to do so.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="gitlab" className="w-full p-2">
|
||||
{gitlabProviders && gitlabProviders?.length > 0 ? (
|
||||
<SaveGitlabProvider applicationId={applicationId} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||
<GitlabIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To deploy using GitLab, you need to configure your account
|
||||
first. Please, go to{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/git-providers"
|
||||
className="text-foreground"
|
||||
>
|
||||
Settings
|
||||
</Link>{" "}
|
||||
to do so.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="bitbucket" className="w-full p-2">
|
||||
{bitbucketProviders && bitbucketProviders?.length > 0 ? (
|
||||
<SaveBitbucketProvider applicationId={applicationId} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||
<BitbucketIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To deploy using Bitbucket, you need to configure your account
|
||||
first. Please, go to{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/git-providers"
|
||||
className="text-foreground"
|
||||
>
|
||||
Settings
|
||||
@@ -93,6 +165,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
||||
<TabsContent value="docker" className="w-full p-2">
|
||||
<SaveDockerProvider applicationId={applicationId} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="git" className="w-full p-2">
|
||||
<SaveGitProvider applicationId={applicationId} />
|
||||
</TabsContent>
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
import {
|
||||
BitbucketIcon,
|
||||
GithubIcon,
|
||||
GitlabIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { useUrl } from "@/utils/hooks/use-url";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const Schema = z.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Name is required",
|
||||
}),
|
||||
username: z.string().min(1, {
|
||||
message: "Username is required",
|
||||
}),
|
||||
password: z.string().min(1, {
|
||||
message: "App Password is required",
|
||||
}),
|
||||
workspaceName: z.string().optional(),
|
||||
});
|
||||
|
||||
type Schema = z.infer<typeof Schema>;
|
||||
|
||||
export const AddBitbucketProvider = () => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const url = useUrl();
|
||||
const { mutateAsync, error, isError } =
|
||||
api.gitProvider.createBitbucketProvider.useMutation();
|
||||
const { data: auth } = api.auth.get.useQuery();
|
||||
const router = useRouter();
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
username: "",
|
||||
password: "",
|
||||
workspaceName: "",
|
||||
},
|
||||
resolver: zodResolver(Schema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
username: "",
|
||||
password: "",
|
||||
workspaceName: "",
|
||||
});
|
||||
}, [form, isOpen]);
|
||||
|
||||
const onSubmit = async (data: Schema) => {
|
||||
await mutateAsync({
|
||||
bitbucketUsername: data.username,
|
||||
appPassword: data.password,
|
||||
bitbucketWorkspaceName: data.workspaceName || "",
|
||||
authId: auth?.id || "",
|
||||
name: data.name || "",
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.gitProvider.getAll.invalidate();
|
||||
toast.success("Bitbucket configured successfully");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error configuring Bitbucket");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="flex items-center space-x-1 bg-blue-700 text-white hover:bg-blue-600"
|
||||
>
|
||||
<BitbucketIcon />
|
||||
<span>Bitbucket</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
Bitbucket Provider <BitbucketIcon className="size-5" />
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-add-bitbucket"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-1"
|
||||
>
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
To integrate your Bitbucket account, you need to create a new
|
||||
App Password in your Bitbucket settings. Follow these steps:
|
||||
</p>
|
||||
<ol className="list-decimal list-inside text-sm text-muted-foreground">
|
||||
<li className="flex flex-row gap-2 items-center">
|
||||
Create new App Password{" "}
|
||||
<Link
|
||||
href="https://bitbucket.org/account/settings/app-passwords/new"
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLink className="w-fit text-primary size-4" />
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
When creating the App Password, ensure you grant the
|
||||
following permissions:
|
||||
<ul className="list-disc list-inside ml-4">
|
||||
<li>Account: Read</li>
|
||||
<li>Workspace membership: Read</li>
|
||||
<li>Projects: Read</li>
|
||||
<li>Repositories: Read</li>
|
||||
<li>Pull requests: Read</li>
|
||||
<li>Webhooks: Read and write</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
After creating, you'll receive an App Password. Copy it and
|
||||
paste it below along with your Bitbucket username.
|
||||
</li>
|
||||
</ol>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Random Name eg(my-personal-account)"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Bitbucket Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Your Bitbucket username"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>App Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="ATBBPDYUC94nR96Nj7Cqpp4pfwKk03573DD2"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="workspaceName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Workspace Name (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="For organization accounts"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button isLoading={form.formState.isSubmitting}>
|
||||
Configure Bitbucket
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { useUrl } from "@/utils/hooks/use-url";
|
||||
import { format } from "date-fns";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const AddGithubProvider = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const url = useUrl();
|
||||
const { data } = api.auth.get.useQuery();
|
||||
const [manifest, setManifest] = useState("");
|
||||
const [isOrganization, setIsOrganization] = useState(false);
|
||||
const [organizationName, setOrganization] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const url = document.location.origin;
|
||||
const manifest = JSON.stringify(
|
||||
{
|
||||
redirect_url: `${origin}/api/providers/github/redirect?authId=${data?.id}`,
|
||||
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`,
|
||||
url: origin,
|
||||
hook_attributes: {
|
||||
url: `${url}/api/deploy/github`,
|
||||
},
|
||||
callback_urls: [`${origin}/api/providers/github/redirect`],
|
||||
public: false,
|
||||
request_oauth_on_install: true,
|
||||
default_permissions: {
|
||||
contents: "read",
|
||||
metadata: "read",
|
||||
emails: "read",
|
||||
pull_requests: "write",
|
||||
},
|
||||
default_events: ["pull_request", "push"],
|
||||
},
|
||||
null,
|
||||
4,
|
||||
);
|
||||
|
||||
setManifest(manifest);
|
||||
}, [data?.id]);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="secondary" className="flex items-center space-x-1">
|
||||
<GithubIcon />
|
||||
<span>Github</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-2xl ">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
Github Provider <GithubIcon className="size-5" />
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div id="hook-form-add-project" className="grid w-full gap-1">
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col ">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
To integrate your GitHub account with our services, you'll need
|
||||
to create and install a GitHub app. This process is
|
||||
straightforward and only takes a few minutes. Click the button
|
||||
below to get started.
|
||||
</p>
|
||||
<div className="mt-4 flex flex-col gap-4">
|
||||
<div className="flex flex-row gap-4">
|
||||
<span>Organization?</span>
|
||||
<Switch
|
||||
checked={isOrganization}
|
||||
onCheckedChange={(checked) => setIsOrganization(checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isOrganization && (
|
||||
<Input
|
||||
required
|
||||
placeholder="Organization name"
|
||||
onChange={(e) => setOrganization(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<form
|
||||
action={
|
||||
isOrganization
|
||||
? `https://github.com/organizations/${organizationName}/settings/apps/new?state=gh_init:${data?.id}`
|
||||
: `https://github.com/settings/apps/new?state=gh_init:${data?.id}`
|
||||
}
|
||||
method="post"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="manifest"
|
||||
id="manifest"
|
||||
defaultValue={manifest}
|
||||
className="invisible"
|
||||
/>
|
||||
<br />
|
||||
|
||||
<div className="flex w-full justify-end">
|
||||
<Button
|
||||
disabled={isOrganization && organizationName.length < 1}
|
||||
type="submit"
|
||||
className="self-end"
|
||||
>
|
||||
Create GitHub App
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,250 @@
|
||||
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { useUrl } from "@/utils/hooks/use-url";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const Schema = z.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Name is required",
|
||||
}),
|
||||
applicationId: z.string().min(1, {
|
||||
message: "Application ID is required",
|
||||
}),
|
||||
applicationSecret: z.string().min(1, {
|
||||
message: "Application Secret is required",
|
||||
}),
|
||||
|
||||
redirectUri: z.string().min(1, {
|
||||
message: "Redirect URI is required",
|
||||
}),
|
||||
groupName: z.string().optional(),
|
||||
});
|
||||
|
||||
type Schema = z.infer<typeof Schema>;
|
||||
|
||||
export const AddGitlabProvider = () => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const url = useUrl();
|
||||
const { data: auth } = api.auth.get.useQuery();
|
||||
const { mutateAsync, error, isError } =
|
||||
api.gitProvider.createGitlabProvider.useMutation();
|
||||
const webhookUrl = `${url}/api/providers/gitlab/callback`;
|
||||
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
applicationId: "",
|
||||
applicationSecret: "",
|
||||
groupName: "",
|
||||
redirectUri: webhookUrl,
|
||||
},
|
||||
resolver: zodResolver(Schema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
applicationId: "",
|
||||
applicationSecret: "",
|
||||
groupName: "",
|
||||
redirectUri: webhookUrl,
|
||||
});
|
||||
}, [form, isOpen]);
|
||||
|
||||
const onSubmit = async (data: Schema) => {
|
||||
await mutateAsync({
|
||||
applicationId: data.applicationId || "",
|
||||
secret: data.applicationSecret || "",
|
||||
groupName: data.groupName || "",
|
||||
authId: auth?.id || "",
|
||||
name: data.name || "",
|
||||
redirectUri: data.redirectUri || "",
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.gitProvider.getAll.invalidate();
|
||||
toast.success("GitLab created successfully");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error configuring GitLab");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className="flex items-center space-x-1 bg-purple-700 text-white hover:bg-purple-600"
|
||||
>
|
||||
<GitlabIcon />
|
||||
<span>GitLab</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen ">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
GitLab Provider <GitlabIcon className="size-5" />
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-add-gitlab"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-1"
|
||||
>
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
To integrate your GitLab account, you need to create a new
|
||||
application in your GitLab settings. Follow these steps:
|
||||
</p>
|
||||
<ol className="list-decimal list-inside text-sm text-muted-foreground">
|
||||
<li className="flex flex-row gap-2 items-center">
|
||||
Go to your GitLab profile settings{" "}
|
||||
<Link
|
||||
href="https://gitlab.com/-/profile/applications"
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLink className="w-fit text-primary size-4" />
|
||||
</Link>
|
||||
</li>
|
||||
<li>Navigate to Applications</li>
|
||||
<li>
|
||||
Create a new application with the following details:
|
||||
<ul className="list-disc list-inside ml-4">
|
||||
<li>Name: Dokploy</li>
|
||||
<li>
|
||||
Redirect URI:{" "}
|
||||
<span className="text-primary">{webhookUrl}</span>{" "}
|
||||
</li>
|
||||
<li>Scopes: api, read_user, read_repository</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
After creating, you'll receive an Application ID and Secret,
|
||||
copy them and paste them below.
|
||||
</li>
|
||||
</ol>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Random Name eg(my-personal-account)"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="redirectUri"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Redirect URI</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
disabled
|
||||
placeholder="Random Name eg(my-personal-account)"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="applicationId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Application ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Application ID" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="applicationSecret"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Application Secret</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Application Secret"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="groupName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Group Name (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="For organization/group access"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button isLoading={form.formState.isSubmitting}>
|
||||
Configure GitLab App
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import { AddGitlabProvider } from "./add-gitlab-provider";
|
||||
import {
|
||||
BitbucketIcon,
|
||||
GithubIcon,
|
||||
GitlabIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
import { AddGithubProvider } from "./add-github-provider";
|
||||
import { AddBitbucketProvider } from "./add-bitbucket-provider";
|
||||
import { api } from "@/utils/api";
|
||||
import Link from "next/link";
|
||||
import { RemoveGitProvider } from "../github/remove-github-app";
|
||||
import { useUrl } from "@/utils/hooks/use-url";
|
||||
|
||||
export const ShowGitProviders = () => {
|
||||
const { data } = api.gitProvider.getAll.useQuery();
|
||||
|
||||
const url = useUrl();
|
||||
|
||||
const getGitlabUrl = (clientId: string, gitlabId: string) => {
|
||||
const redirectUri = `${url}/api/providers/gitlab/callback?gitlabId=${gitlabId}`;
|
||||
|
||||
const scope = "api read_user read_repository";
|
||||
|
||||
const authUrl = `https://gitlab.com/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scope)}`;
|
||||
|
||||
return authUrl;
|
||||
};
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-bold">Git Providers</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Connect your Git Providers to use it for login.
|
||||
</p>
|
||||
</div>
|
||||
<Card className=" bg-transparent">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex gap-4 sm:flex-row flex-col w-full">
|
||||
<AddGithubProvider />
|
||||
<AddGitlabProvider />
|
||||
<AddBitbucketProvider />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{data?.map((gitProvider, index) => {
|
||||
const isGithub = gitProvider.providerType === "github";
|
||||
const isGitlab = gitProvider.providerType === "gitlab";
|
||||
const haveGithubRequirements =
|
||||
gitProvider.providerType === "github" &&
|
||||
gitProvider.githubProvider?.githubPrivateKey &&
|
||||
gitProvider.githubProvider?.githubAppId &&
|
||||
gitProvider.githubProvider?.githubInstallationId;
|
||||
|
||||
const haveGitlabRequirements =
|
||||
gitProvider.gitlabProvider?.accessToken &&
|
||||
gitProvider.gitlabProvider?.refreshToken;
|
||||
return (
|
||||
<div
|
||||
className="space-y-4"
|
||||
key={`${gitProvider.gitProviderId}-${index}`}
|
||||
>
|
||||
<Card className="flex sm:flex-row max-sm:gap-2 flex-col justify-between items-center p-4">
|
||||
<div className="flex items-center space-x-4 w-full">
|
||||
{gitProvider.providerType === "github" && (
|
||||
<GithubIcon className="w-6 h-6" />
|
||||
)}
|
||||
{gitProvider.providerType === "gitlab" && (
|
||||
<GitlabIcon className="w-6 h-6" />
|
||||
)}
|
||||
{gitProvider.providerType === "bitbucket" && (
|
||||
<BitbucketIcon className="w-6 h-6" />
|
||||
)}
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
{gitProvider.providerType === "github"
|
||||
? "GitHub"
|
||||
: gitProvider.providerType === "gitlab"
|
||||
? "GitLab"
|
||||
: "Bitbucket"}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{gitProvider.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex sm:gap-4 sm:flex-row flex-col">
|
||||
{!haveGithubRequirements && isGithub && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<Link
|
||||
href={`${gitProvider?.githubProvider?.githubAppName}/installations/new?state=gh_setup:${gitProvider?.githubProvider.githubProviderId}`}
|
||||
className={buttonVariants({ className: "w-fit" })}
|
||||
>
|
||||
Install Github App
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{haveGithubRequirements && isGithub && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<Link
|
||||
href={`${gitProvider?.githubProvider?.githubAppName}`}
|
||||
target="_blank"
|
||||
className={buttonVariants({
|
||||
className: "w-fit",
|
||||
variant: "secondary",
|
||||
})}
|
||||
>
|
||||
<span className="text-sm">Manage Github App</span>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!haveGitlabRequirements && isGitlab && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<Link
|
||||
href={getGitlabUrl(
|
||||
gitProvider.gitlabProvider?.applicationId || "",
|
||||
gitProvider.gitlabProvider?.gitlabProviderId || "",
|
||||
)}
|
||||
target="_blank"
|
||||
className={buttonVariants({
|
||||
className: "w-fit",
|
||||
variant: "secondary",
|
||||
})}
|
||||
>
|
||||
<span className="text-sm">Install Gitlab App</span>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<RemoveGitProvider
|
||||
gitProviderId={gitProvider.gitProviderId}
|
||||
gitProviderType={gitProvider.providerType}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,166 +0,0 @@
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { format } from "date-fns";
|
||||
import { BadgeCheck } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { RemoveGithubApp } from "./remove-github-app";
|
||||
|
||||
export const GithubSetup = () => {
|
||||
const [isOrganization, setIsOrganization] = useState(false);
|
||||
const { data: haveGithubConfigured } =
|
||||
api.admin.haveGithubConfigured.useQuery();
|
||||
const [manifest, setManifest] = useState<string>("");
|
||||
const [organizationName, setOrganization] = useState<string>("");
|
||||
const { data } = api.admin.one.useQuery();
|
||||
useEffect(() => {
|
||||
const url = document.location.origin;
|
||||
const manifest = JSON.stringify(
|
||||
{
|
||||
redirect_url: `${origin}/api/redirect?authId=${data?.authId}`,
|
||||
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`,
|
||||
url: origin,
|
||||
hook_attributes: {
|
||||
url: `${url}/api/deploy/github`,
|
||||
// url: `${origin}/api/webhook`, // Aquí especificas la URL del endpoint de tu webhook
|
||||
},
|
||||
callback_urls: [`${origin}/api/redirect`], // Los URLs de callback para procesos de autenticación
|
||||
public: false,
|
||||
request_oauth_on_install: true,
|
||||
default_permissions: {
|
||||
contents: "read",
|
||||
metadata: "read",
|
||||
emails: "read",
|
||||
pull_requests: "write",
|
||||
},
|
||||
default_events: ["pull_request", "push"],
|
||||
},
|
||||
null,
|
||||
4,
|
||||
);
|
||||
|
||||
setManifest(manifest);
|
||||
}, [data?.authId]);
|
||||
return (
|
||||
<Card className="bg-transparent">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Configure Github </CardTitle>
|
||||
<CardDescription>
|
||||
Setup your github account to access to your repositories.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="h-full space-y-2">
|
||||
{haveGithubConfigured ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-muted-foreground text-sm">
|
||||
Github account configured succesfully.
|
||||
</span>
|
||||
<BadgeCheck className="size-4 text-green-700" />
|
||||
</div>
|
||||
<div className="flex items-end gap-4 flex-wrap">
|
||||
<RemoveGithubApp />
|
||||
<Link
|
||||
href={`${data?.githubAppName}`}
|
||||
target="_blank"
|
||||
className={buttonVariants({
|
||||
className: "w-fit",
|
||||
variant: "secondary",
|
||||
})}
|
||||
>
|
||||
<span className="text-sm">Manage Github App</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{data?.githubAppName ? (
|
||||
<div className="flex w-fit flex-col gap-4">
|
||||
<span className="text-muted-foreground">
|
||||
You've successfully created a github app named{" "}
|
||||
<strong>{data.githubAppName}</strong>! The next step is to
|
||||
install this app in your GitHub account.
|
||||
</span>
|
||||
|
||||
<div className="flex flex-row gap-4">
|
||||
<Link
|
||||
href={`${
|
||||
data.githubAppName
|
||||
}/installations/new?state=gh_setup:${data?.authId}`}
|
||||
className={buttonVariants({ className: "w-fit" })}
|
||||
>
|
||||
Install Github App
|
||||
</Link>
|
||||
<RemoveGithubApp />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
To integrate your GitHub account with our services, you'll
|
||||
need to create and install a GitHub app. This process is
|
||||
straightforward and only takes a few minutes. Click the
|
||||
button below to get started.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-col gap-4">
|
||||
<div className="flex flex-row gap-4">
|
||||
<span>Organization?</span>
|
||||
<Switch
|
||||
checked={isOrganization}
|
||||
onCheckedChange={(checked) => setIsOrganization(checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isOrganization && (
|
||||
<Input
|
||||
required
|
||||
placeholder="Organization name"
|
||||
onChange={(e) => setOrganization(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<form
|
||||
action={
|
||||
isOrganization
|
||||
? `https://github.com/organizations/${organizationName}/settings/apps/new?state=gh_init:${data?.authId}`
|
||||
: `https://github.com/settings/apps/new?state=gh_init:${data?.authId}`
|
||||
}
|
||||
method="post"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="manifest"
|
||||
id="manifest"
|
||||
defaultValue={manifest}
|
||||
className="invisible"
|
||||
/>
|
||||
<br />
|
||||
|
||||
<Button
|
||||
disabled={isOrganization && organizationName.length < 1}
|
||||
type="submit"
|
||||
>
|
||||
Create GitHub App
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { BadgeCheck, ExternalLink } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
export const GitlabSetup = () => {
|
||||
const [applicationId, setApplicationId] = useState("");
|
||||
const [applicationSecret, setApplicationSecret] = useState("");
|
||||
const haveGitlabConfigured = false;
|
||||
const [url, setUrl] = useState("");
|
||||
// const { data: haveGitlabConfigured } =
|
||||
// api.admin.haveGitlabConfigured.useQuery();
|
||||
const { data: adminData } = api.admin.one.useQuery();
|
||||
|
||||
useEffect(() => {
|
||||
const protocolAndHost = `${window.location.protocol}//${window.location.host}`;
|
||||
|
||||
setUrl(`${protocolAndHost}`);
|
||||
}, [adminData]);
|
||||
|
||||
// const createGitlabApp = api.admin.createGitlabApp.useMutation();
|
||||
|
||||
const handleCreateApp = async () => {
|
||||
// try {
|
||||
// // await createGitlabApp.mutateAsync({
|
||||
// // applicationId,
|
||||
// // applicationSecret,
|
||||
// // callbackUrl: `${window.location.origin}/api/gitlab/callback`,
|
||||
// // });
|
||||
// // Refetch the configuration status
|
||||
// // await haveGitlabConfigured.refetch();
|
||||
// } catch (error) {
|
||||
// console.error("Failed to create GitLab app", error);
|
||||
// }
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-transparent">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Configure GitLab</CardTitle>
|
||||
<CardDescription>
|
||||
Setup your GitLab account to access your repositories.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="h-full space-y-2">
|
||||
{haveGitlabConfigured ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-muted-foreground text-sm">
|
||||
GitLab account configured successfully.
|
||||
</span>
|
||||
<BadgeCheck className="size-4 text-green-700" />
|
||||
</div>
|
||||
<div className="flex items-end gap-4 flex-wrap">
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
/* Implement remove GitLab app logic */
|
||||
}}
|
||||
>
|
||||
Remove GitLab App
|
||||
</Button>
|
||||
<Link
|
||||
href="https://gitlab.com/-/profile/applications"
|
||||
target="_blank"
|
||||
className={buttonVariants({
|
||||
className: "w-fit",
|
||||
variant: "secondary",
|
||||
})}
|
||||
>
|
||||
<span className="text-sm">Manage GitLab App</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
To integrate your GitLab account, you need to create a new
|
||||
application in your GitLab settings. Follow these steps:
|
||||
</p>
|
||||
<ol className="list-decimal list-inside text-sm text-muted-foreground">
|
||||
<li className="flex flex-row gap-2">
|
||||
Go to your GitLab profile settings{" "}
|
||||
<Link
|
||||
href="https://gitlab.com/-/profile/applications"
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLink className="w-fit text-primary size-4" />
|
||||
</Link>
|
||||
</li>
|
||||
<li>Navigate to Applications</li>
|
||||
<li>
|
||||
Create a new application with the following details:
|
||||
<ul className="list-disc list-inside ml-4">
|
||||
<li>Name: Dokploy</li>
|
||||
<li>Redirect URI: {`${url}/api/gitlab/callback`}</li>
|
||||
<li>Scopes: api, read_user, read_repository</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
After creating, you'll receive an Application ID and Secret
|
||||
</li>
|
||||
</ol>
|
||||
<Input
|
||||
placeholder="Application ID"
|
||||
value={applicationId}
|
||||
onChange={(e) => setApplicationId(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Application Secret"
|
||||
value={applicationSecret}
|
||||
onChange={(e) => setApplicationSecret(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleCreateApp}
|
||||
disabled={!applicationId || !applicationSecret}
|
||||
>
|
||||
Configure GitLab App
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -17,31 +17,40 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { InfoIcon, TrashIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const RemoveGithubApp = () => {
|
||||
const { refetch } = api.auth.get.useQuery();
|
||||
interface Props {
|
||||
gitProviderId: string;
|
||||
gitProviderType: "github" | "gitlab" | "bitbucket";
|
||||
}
|
||||
|
||||
export const RemoveGitProvider = ({
|
||||
gitProviderId,
|
||||
gitProviderType,
|
||||
}: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync } = api.admin.cleanGithubApp.useMutation();
|
||||
const { mutateAsync } = api.gitProvider.remove.useMutation();
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive">
|
||||
Remove Current Github App
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoIcon className="size-4 fill-muted-destructive text-muted-destructive" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
We recommend deleting the GitHub app first, and then removing
|
||||
the current one from here.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Button variant="ghost">
|
||||
<TrashIcon className="size-4 text-muted-destructive" />
|
||||
{gitProviderType === "github" && (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoIcon className="size-4 fill-muted-destructive text-muted-destructive" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
We recommend deleting the GitHub app first, and then removing
|
||||
the current one from here.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
@@ -56,15 +65,15 @@ export const RemoveGithubApp = () => {
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync()
|
||||
await mutateAsync({
|
||||
gitProviderId: gitProviderId,
|
||||
})
|
||||
.then(async () => {
|
||||
await refetch();
|
||||
utils.admin.one.invalidate();
|
||||
await utils.admin.haveGithubConfigured.invalidate();
|
||||
toast.success("Github application deleted succesfully.");
|
||||
utils.gitProvider.getAll.invalidate();
|
||||
toast.success("Git Provider deleted succesfully.");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to delete your github application.");
|
||||
toast.error("Error to delete your git provider.");
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import React from "react";
|
||||
import { AppearanceForm } from "./appearance-form";
|
||||
import { ShowCertificates } from "./certificates/show-certificates";
|
||||
import { ShowDestinations } from "./destination/show-destinations";
|
||||
import { GithubSetup } from "./github/github-setup";
|
||||
import { ProfileForm } from "./profile/profile-form";
|
||||
import { ShowUsers } from "./users/show-users";
|
||||
import { WebDomain } from "./web-domain";
|
||||
import { WebServer } from "./web-server";
|
||||
|
||||
export const ShowSettings = () => {
|
||||
const { data } = api.auth.get.useQuery();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"mt-6 md:grid flex flex-col gap-4 pb-20 md:grid-cols-2",
|
||||
data?.rol === "user" && "col-span-2",
|
||||
)}
|
||||
>
|
||||
<div className={cn(data?.rol === "user" && "col-span-2")}>
|
||||
<ProfileForm />
|
||||
</div>
|
||||
|
||||
{data?.rol === "admin" && (
|
||||
<>
|
||||
<GithubSetup />
|
||||
<AppearanceForm />
|
||||
<ShowDestinations />
|
||||
<ShowCertificates />
|
||||
<WebDomain />
|
||||
<WebServer />
|
||||
<ShowUsers />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import React from "react";
|
||||
|
||||
// https://worldvectorlogo.com/downloaded/redis Ref
|
||||
@@ -155,3 +156,116 @@ export const RedisIcon = ({ className }: Props) => {
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const GitlabIcon = ({ className }: Props) => {
|
||||
return (
|
||||
<svg
|
||||
aria-label="gitlab"
|
||||
height="14"
|
||||
viewBox="0 0 24 22"
|
||||
width="14"
|
||||
className={cn("fill-white text-white", className)}
|
||||
>
|
||||
<path
|
||||
d="M1.279 8.29L.044 12.294c-.117.367 0 .78.325 1.014l11.323 8.23-.009-.012-.03-.039L1.279 8.29zM22.992 13.308a.905.905 0 00.325-1.014L22.085 8.29 11.693 21.52l11.299-8.212z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M1.279 8.29l10.374 13.197.03.039.01-.006L22.085 8.29H1.28z"
|
||||
fill="currentColor"
|
||||
opacity="0.4"
|
||||
/>
|
||||
<path
|
||||
d="M15.982 8.29l-4.299 13.236-.004.011.014-.017L22.085 8.29h-6.103zM7.376 8.29H1.279l10.374 13.197L7.376 8.29z"
|
||||
fill="currentColor"
|
||||
opacity="0.6"
|
||||
/>
|
||||
<path
|
||||
d="M18.582.308l-2.6 7.982h6.103L19.48.308c-.133-.41-.764-.41-.897 0zM1.279 8.29L3.88.308c.133-.41.764-.41.897 0l2.6 7.982H1.279z"
|
||||
fill="currentColor"
|
||||
opacity="0.4"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const GithubIcon = ({ className }: Props) => {
|
||||
return (
|
||||
<svg
|
||||
aria-label="github"
|
||||
height="18"
|
||||
viewBox="0 0 14 14"
|
||||
width="18"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
d="M7 .175c-3.872 0-7 3.128-7 7 0 3.084 2.013 5.71 4.79 6.65.35.066.482-.153.482-.328v-1.181c-1.947.415-2.363-.941-2.363-.941-.328-.81-.787-1.028-.787-1.028-.634-.438.044-.416.044-.416.7.044 1.071.722 1.071.722.635 1.072 1.641.766 2.035.59.066-.459.24-.765.437-.94-1.553-.175-3.193-.787-3.193-3.456 0-.766.262-1.378.721-1.881-.065-.175-.306-.897.066-1.86 0 0 .59-.197 1.925.722a6.754 6.754 0 0 1 1.75-.24c.59 0 1.203.087 1.75.24 1.335-.897 1.925-.722 1.925-.722.372.963.131 1.685.066 1.86.46.48.722 1.115.722 1.88 0 2.691-1.641 3.282-3.194 3.457.24.219.481.634.481 1.29v1.926c0 .197.131.415.481.328C11.988 12.884 14 10.259 14 7.175c0-3.872-3.128-7-7-7z"
|
||||
fill="#fff"
|
||||
fillRule="nonzero"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const BitbucketIcon = ({ className }: Props) => {
|
||||
return (
|
||||
<svg height="14" viewBox="-2 -2 65 59" width="14" className={className}>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="bitbucket-:R7aq37rqjt7rrrmpjtuj7l9qjtsr:"
|
||||
x1="104.953%"
|
||||
x2="46.569%"
|
||||
y1="21.921%"
|
||||
y2="75.234%"
|
||||
>
|
||||
<stop offset="7%" stopColor="currentColor" stop-opacity=".4" />
|
||||
<stop offset="100%" stopColor="currentColor" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
d="M59.696 18.86h-18.77l-3.15 18.39h-13L9.426 55.47a2.71 2.71 0 001.75.66h40.74a2 2 0 002-1.68l5.78-35.59z"
|
||||
fill="url(#bitbucket-:R7aq37rqjt7rrrmpjtuj7l9qjtsr:)"
|
||||
fillRule="nonzero"
|
||||
transform="translate(-.026 .82)"
|
||||
/>
|
||||
<path
|
||||
d="M2 .82a2 2 0 00-2 2.32l8.49 51.54a2.7 2.7 0 00.91 1.61 2.71 2.71 0 001.75.66l15.76-18.88H24.7l-3.47-18.39h38.44l2.7-16.53a2 2 0 00-2-2.32L2 .82z"
|
||||
fill="currentColor"
|
||||
fillRule="nonzero"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const DockerIcon = ({ className }: Props) => {
|
||||
return (
|
||||
<svg
|
||||
height="24"
|
||||
viewBox="-.557 117.607 598.543 423.631"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g fill="#0091e2">
|
||||
<path d="m592.162 277.804c-1.664-1.37-16.642-12.597-48.815-12.597-8.321 0-16.92.822-25.24 2.191-6.102-41.898-41.327-62.162-42.714-63.257l-8.598-4.93-5.547 7.942c-6.934 10.68-12.204 22.729-15.255 35.052-5.824 23.824-2.219 46.279 9.985 65.447-14.7 8.216-38.553 10.133-43.545 10.406h-393.853c-10.262 0-18.583 8.216-18.583 18.348-.554 33.956 5.27 67.912 17.197 99.951 13.59 35.052 33.838 61.067 59.91 76.95 29.4 17.799 77.383 27.931 131.468 27.931 24.408 0 48.815-2.19 72.946-6.572 33.56-6.025 65.734-17.526 95.412-34.23a260.485 260.485 0 0 0 64.902-52.577c31.342-34.778 49.925-73.663 63.515-108.167h5.547c34.116 0 55.195-13.418 66.844-24.92 7.766-7.12 13.59-15.882 17.751-25.74l2.497-7.12z" />
|
||||
<path d="m55.193 306.83h52.698c2.497 0 4.716-1.916 4.716-4.654v-46.553c0-2.465-1.942-4.655-4.716-4.655h-52.698c-2.496 0-4.715 1.916-4.715 4.655v46.553c.277 2.738 2.219 4.655 4.715 4.655zm72.668 0h52.699c2.496 0 4.715-1.916 4.715-4.654v-46.553c0-2.465-1.942-4.655-4.715-4.655h-52.7c-2.496 0-4.715 1.916-4.715 4.655v46.553c.278 2.738 2.22 4.655 4.715 4.655m74.055 0h52.699c2.496 0 4.715-1.917 4.715-4.655v-46.553c0-2.465-1.942-4.655-4.715-4.655h-52.699c-2.496 0-4.715 1.916-4.715 4.655v46.553c0 2.738 1.942 4.655 4.715 4.655zm72.946 0h52.699c2.496 0 4.715-1.917 4.715-4.655v-46.553c0-2.465-1.942-4.655-4.715-4.655h-52.699c-2.496 0-4.715 1.916-4.715 4.655v46.553c0 2.738 2.219 4.655 4.715 4.655zm-147-66.543h52.698c2.496 0 4.715-2.19 4.715-4.655v-46.553c0-2.465-1.942-4.656-4.715-4.656h-52.699c-2.496 0-4.715 1.917-4.715 4.656v46.553c.278 2.464 2.22 4.655 4.715 4.655m74.055 0h52.699c2.496 0 4.715-2.19 4.715-4.655v-46.553c0-2.465-1.942-4.656-4.715-4.656h-52.699c-2.496 0-4.715 1.917-4.715 4.656v46.553c0 2.464 1.942 4.655 4.715 4.655m72.946 0h52.699c2.496 0 4.715-2.19 4.715-4.655v-46.553c0-2.465-2.22-4.656-4.715-4.656h-52.699c-2.496 0-4.715 1.917-4.715 4.656v46.553c0 2.464 2.219 4.655 4.715 4.655m0-66.817h52.699c2.496 0 4.715-1.917 4.715-4.655v-46.553c0-2.465-2.22-4.656-4.715-4.656h-52.699c-2.496 0-4.715 1.917-4.715 4.656v46.553c0 2.464 2.219 4.655 4.715 4.655m73.5 133.36h52.699c2.496 0 4.715-1.917 4.715-4.655v-46.553c0-2.465-1.941-4.655-4.715-4.655h-52.698c-2.497 0-4.716 1.916-4.716 4.655v46.553c.278 2.738 2.22 4.655 4.716 4.655" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const GitIcon = ({ className }: Props) => {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 256 256"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
preserveAspectRatio="xMinYMin meet"
|
||||
>
|
||||
<path
|
||||
d="M251.172 116.594L139.4 4.828c-6.433-6.437-16.873-6.437-23.314 0l-23.21 23.21 29.443 29.443c6.842-2.312 14.688-.761 20.142 4.693 5.48 5.489 7.02 13.402 4.652 20.266l28.375 28.376c6.865-2.365 14.786-.835 20.269 4.657 7.663 7.66 7.663 20.075 0 27.74-7.665 7.666-20.08 7.666-27.749 0-5.764-5.77-7.188-14.235-4.27-21.336l-26.462-26.462-.003 69.637a19.82 19.82 0 0 1 5.188 3.71c7.663 7.66 7.663 20.076 0 27.747-7.665 7.662-20.086 7.662-27.74 0-7.663-7.671-7.663-20.086 0-27.746a19.654 19.654 0 0 1 6.421-4.281V94.196a19.378 19.378 0 0 1-6.421-4.281c-5.806-5.798-7.202-14.317-4.227-21.446L81.47 39.442l-76.64 76.635c-6.44 6.443-6.44 16.884 0 23.322l111.774 111.768c6.435 6.438 16.873 6.438 23.316 0l111.251-111.249c6.438-6.44 6.438-16.887 0-23.324"
|
||||
fill="#DE4C36"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -59,6 +59,12 @@ export const SettingsLayout = ({ children }: Props) => {
|
||||
icon: KeyRound,
|
||||
href: "/dashboard/settings/ssh-keys",
|
||||
},
|
||||
{
|
||||
title: "Git ",
|
||||
label: "",
|
||||
icon: GitBranch,
|
||||
href: "/dashboard/settings/git-providers",
|
||||
},
|
||||
{
|
||||
title: "Users",
|
||||
label: "",
|
||||
@@ -102,6 +108,7 @@ import {
|
||||
Activity,
|
||||
Bell,
|
||||
Database,
|
||||
GitBranch,
|
||||
KeyRound,
|
||||
type LucideIcon,
|
||||
Route,
|
||||
|
||||
56
apps/dokploy/drizzle/0033_polite_vulture.sql
Normal file
56
apps/dokploy/drizzle/0033_polite_vulture.sql
Normal file
@@ -0,0 +1,56 @@
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "public"."gitProviderType" AS ENUM('github', 'gitlab', 'bitbucket');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "bitbucket_provider" (
|
||||
"bitbucketProviderId" text PRIMARY KEY NOT NULL,
|
||||
"bitbucketUsername" text,
|
||||
"appPassword" text,
|
||||
"bitbucketWorkspaceName" text,
|
||||
"gitProviderId" text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "git_provider" (
|
||||
"gitProviderId" text PRIMARY KEY NOT NULL,
|
||||
"providerType" "gitProviderType" DEFAULT 'github' NOT NULL,
|
||||
"createdAt" text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "github_provider" (
|
||||
"githubProviderId" text PRIMARY KEY NOT NULL,
|
||||
"githubAppId" integer,
|
||||
"githubClientId" text,
|
||||
"githubClientSecret" text,
|
||||
"githubInstallationId" text,
|
||||
"githubPrivateKey" text,
|
||||
"githubWebhookSecret" text,
|
||||
"gitProviderId" text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "gitlab_provider" (
|
||||
"github_provider_id" text PRIMARY KEY NOT NULL,
|
||||
"application_id" text,
|
||||
"application_secret" text,
|
||||
"group_name" text,
|
||||
"gitProviderId" text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "bitbucket_provider" ADD CONSTRAINT "bitbucket_provider_gitProviderId_git_provider_gitProviderId_fk" FOREIGN KEY ("gitProviderId") REFERENCES "public"."git_provider"("gitProviderId") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "github_provider" ADD CONSTRAINT "github_provider_gitProviderId_git_provider_gitProviderId_fk" FOREIGN KEY ("gitProviderId") REFERENCES "public"."git_provider"("gitProviderId") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "gitlab_provider" ADD CONSTRAINT "gitlab_provider_gitProviderId_git_provider_gitProviderId_fk" FOREIGN KEY ("gitProviderId") REFERENCES "public"."git_provider"("gitProviderId") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
6
apps/dokploy/drizzle/0034_keen_dark_phoenix.sql
Normal file
6
apps/dokploy/drizzle/0034_keen_dark_phoenix.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE "git_provider" ADD COLUMN "authId" text NOT NULL;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "git_provider" ADD CONSTRAINT "git_provider_authId_auth_id_fk" FOREIGN KEY ("authId") REFERENCES "public"."auth"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
1
apps/dokploy/drizzle/0035_crazy_misty_knight.sql
Normal file
1
apps/dokploy/drizzle/0035_crazy_misty_knight.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "github_provider" ADD COLUMN "githubAppName" text;
|
||||
6
apps/dokploy/drizzle/0036_faulty_wolverine.sql
Normal file
6
apps/dokploy/drizzle/0036_faulty_wolverine.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE "application" ADD COLUMN "githubProviderId" text;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "application" ADD CONSTRAINT "application_githubProviderId_github_provider_githubProviderId_fk" FOREIGN KEY ("githubProviderId") REFERENCES "public"."github_provider"("githubProviderId") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
8
apps/dokploy/drizzle/0037_busy_the_fury.sql
Normal file
8
apps/dokploy/drizzle/0037_busy_the_fury.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
ALTER TABLE "application" DROP CONSTRAINT "application_githubProviderId_github_provider_githubProviderId_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "git_provider" ADD COLUMN "name" text NOT NULL;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "application" ADD CONSTRAINT "application_githubProviderId_github_provider_githubProviderId_fk" FOREIGN KEY ("githubProviderId") REFERENCES "public"."github_provider"("githubProviderId") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
1
apps/dokploy/drizzle/0038_eminent_spiral.sql
Normal file
1
apps/dokploy/drizzle/0038_eminent_spiral.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "gitlab_provider" RENAME COLUMN "github_provider_id" TO "gitlabProviderId";
|
||||
1
apps/dokploy/drizzle/0039_wakeful_starbolt.sql
Normal file
1
apps/dokploy/drizzle/0039_wakeful_starbolt.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "gitlab_provider" RENAME COLUMN "application_secret" TO "secret";
|
||||
1
apps/dokploy/drizzle/0040_yummy_gambit.sql
Normal file
1
apps/dokploy/drizzle/0040_yummy_gambit.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "gitlab_provider" ADD COLUMN "token" text;
|
||||
1
apps/dokploy/drizzle/0041_tranquil_may_parker.sql
Normal file
1
apps/dokploy/drizzle/0041_tranquil_may_parker.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "gitlab_provider" ADD COLUMN "redirect_uri" text;
|
||||
2
apps/dokploy/drizzle/0042_blue_vindicator.sql
Normal file
2
apps/dokploy/drizzle/0042_blue_vindicator.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "gitlab_provider" ADD COLUMN "access_token" text;--> statement-breakpoint
|
||||
ALTER TABLE "gitlab_provider" ADD COLUMN "refresh_token" text;
|
||||
1
apps/dokploy/drizzle/0043_daily_proemial_gods.sql
Normal file
1
apps/dokploy/drizzle/0043_daily_proemial_gods.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "gitlab_provider" DROP COLUMN IF EXISTS "token";
|
||||
19
apps/dokploy/drizzle/0044_wide_human_torch.sql
Normal file
19
apps/dokploy/drizzle/0044_wide_human_torch.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
ALTER TYPE "sourceType" ADD VALUE 'gitlab';--> statement-breakpoint
|
||||
ALTER TYPE "sourceType" ADD VALUE 'bitbucket';--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "gitlabRepository" text;--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "gitlabOwner" text;--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "gitlabBranch" text;--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "gitlabBuildPath" text DEFAULT '/';--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "gitlabProviderId" text;--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "bitbucketProviderId" text;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "application" ADD CONSTRAINT "application_gitlabProviderId_gitlab_provider_gitlabProviderId_fk" FOREIGN KEY ("gitlabProviderId") REFERENCES "public"."gitlab_provider"("gitlabProviderId") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "application" ADD CONSTRAINT "application_bitbucketProviderId_bitbucket_provider_bitbucketProviderId_fk" FOREIGN KEY ("bitbucketProviderId") REFERENCES "public"."bitbucket_provider"("bitbucketProviderId") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
4
apps/dokploy/drizzle/0045_dazzling_liz_osborn.sql
Normal file
4
apps/dokploy/drizzle/0045_dazzling_liz_osborn.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE "application" ADD COLUMN "bitbucketRepository" text;--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "bitbucketOwner" text;--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "bitbucketBranch" text;--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "bitbucketBuildPath" text DEFAULT '/';
|
||||
3290
apps/dokploy/drizzle/meta/0033_snapshot.json
Normal file
3290
apps/dokploy/drizzle/meta/0033_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3310
apps/dokploy/drizzle/meta/0034_snapshot.json
Normal file
3310
apps/dokploy/drizzle/meta/0034_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3316
apps/dokploy/drizzle/meta/0035_snapshot.json
Normal file
3316
apps/dokploy/drizzle/meta/0035_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3335
apps/dokploy/drizzle/meta/0036_snapshot.json
Normal file
3335
apps/dokploy/drizzle/meta/0036_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3341
apps/dokploy/drizzle/meta/0037_snapshot.json
Normal file
3341
apps/dokploy/drizzle/meta/0037_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3341
apps/dokploy/drizzle/meta/0038_snapshot.json
Normal file
3341
apps/dokploy/drizzle/meta/0038_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3341
apps/dokploy/drizzle/meta/0039_snapshot.json
Normal file
3341
apps/dokploy/drizzle/meta/0039_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3347
apps/dokploy/drizzle/meta/0040_snapshot.json
Normal file
3347
apps/dokploy/drizzle/meta/0040_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3353
apps/dokploy/drizzle/meta/0041_snapshot.json
Normal file
3353
apps/dokploy/drizzle/meta/0041_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3365
apps/dokploy/drizzle/meta/0042_snapshot.json
Normal file
3365
apps/dokploy/drizzle/meta/0042_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3359
apps/dokploy/drizzle/meta/0043_snapshot.json
Normal file
3359
apps/dokploy/drizzle/meta/0043_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3424
apps/dokploy/drizzle/meta/0044_snapshot.json
Normal file
3424
apps/dokploy/drizzle/meta/0044_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3449
apps/dokploy/drizzle/meta/0045_snapshot.json
Normal file
3449
apps/dokploy/drizzle/meta/0045_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -232,6 +232,97 @@
|
||||
"when": 1723705257806,
|
||||
"tag": "0032_flashy_shadow_king",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 33,
|
||||
"version": "6",
|
||||
"when": 1725143717048,
|
||||
"tag": "0033_polite_vulture",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 34,
|
||||
"version": "6",
|
||||
"when": 1725144512654,
|
||||
"tag": "0034_keen_dark_phoenix",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 35,
|
||||
"version": "6",
|
||||
"when": 1725145310771,
|
||||
"tag": "0035_crazy_misty_knight",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 36,
|
||||
"version": "6",
|
||||
"when": 1725151725193,
|
||||
"tag": "0036_faulty_wolverine",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 37,
|
||||
"version": "6",
|
||||
"when": 1725152188339,
|
||||
"tag": "0037_busy_the_fury",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 38,
|
||||
"version": "6",
|
||||
"when": 1725154857832,
|
||||
"tag": "0038_eminent_spiral",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 39,
|
||||
"version": "6",
|
||||
"when": 1725155725647,
|
||||
"tag": "0039_wakeful_starbolt",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 40,
|
||||
"version": "6",
|
||||
"when": 1725156536999,
|
||||
"tag": "0040_yummy_gambit",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 41,
|
||||
"version": "6",
|
||||
"when": 1725157793284,
|
||||
"tag": "0041_tranquil_may_parker",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 42,
|
||||
"version": "6",
|
||||
"when": 1725158531995,
|
||||
"tag": "0042_blue_vindicator",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 43,
|
||||
"version": "6",
|
||||
"when": 1725158814900,
|
||||
"tag": "0043_daily_proemial_gods",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 44,
|
||||
"version": "6",
|
||||
"when": 1725160501885,
|
||||
"tag": "0044_wide_human_torch",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 45,
|
||||
"version": "6",
|
||||
"when": 1725162377005,
|
||||
"tag": "0045_dazzling_liz_osborn",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -34,6 +34,7 @@
|
||||
"test": "vitest --config __test__/vitest.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gitbeaker/rest": "40.1.3",
|
||||
"@aws-sdk/client-s3": "3.515.0",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-yaml": "^6.1.1",
|
||||
|
||||
58
apps/dokploy/pages/api/providers/github/redirect.ts
Normal file
58
apps/dokploy/pages/api/providers/github/redirect.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { createGithubProvider } from "@/server/api/services/git-provider";
|
||||
import { db } from "@/server/db";
|
||||
import { githubProvider } from "@/server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { Octokit } from "octokit";
|
||||
|
||||
type Query = {
|
||||
code: string;
|
||||
state: string;
|
||||
installation_id: string;
|
||||
setup_action: string;
|
||||
};
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
const { code, state, installation_id, setup_action }: Query =
|
||||
req.query as Query;
|
||||
|
||||
if (!code) {
|
||||
return res.status(400).json({ error: "Missing code parameter" });
|
||||
}
|
||||
const [action, value] = state?.split(":");
|
||||
// Value could be the authId or the githubProviderId
|
||||
|
||||
if (action === "gh_init") {
|
||||
const octokit = new Octokit({});
|
||||
const { data } = await octokit.request(
|
||||
"POST /app-manifests/{code}/conversions",
|
||||
{
|
||||
code: code as string,
|
||||
},
|
||||
);
|
||||
|
||||
await createGithubProvider({
|
||||
name: data.name,
|
||||
githubAppName: data.html_url,
|
||||
githubAppId: data.id,
|
||||
githubClientId: data.client_id,
|
||||
githubClientSecret: data.client_secret,
|
||||
githubWebhookSecret: data.webhook_secret,
|
||||
githubPrivateKey: data.pem,
|
||||
authId: value as string,
|
||||
});
|
||||
} else if (action === "gh_setup") {
|
||||
await db
|
||||
.update(githubProvider)
|
||||
.set({
|
||||
githubInstallationId: installation_id,
|
||||
})
|
||||
.where(eq(githubProvider.githubProviderId, value as string))
|
||||
.returning();
|
||||
}
|
||||
|
||||
res.redirect(307, "/dashboard/settings/git-providers");
|
||||
}
|
||||
19
apps/dokploy/pages/api/providers/github/webhook.ts
Normal file
19
apps/dokploy/pages/api/providers/github/webhook.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
if (req.method === "POST") {
|
||||
const xGitHubEvent = req.headers["x-github-event"];
|
||||
|
||||
if (xGitHubEvent === "ping") {
|
||||
res.redirect(307, "/dashboard/settings/git-providers");
|
||||
} else {
|
||||
res.redirect(307, "/dashboard/settings/git-providers");
|
||||
}
|
||||
} else {
|
||||
res.setHeader("Allow", ["POST"]);
|
||||
return res.status(405).end(`Method ${req.method} not allowed`);
|
||||
}
|
||||
}
|
||||
80
apps/dokploy/pages/api/providers/gitlab/callback.ts
Normal file
80
apps/dokploy/pages/api/providers/gitlab/callback.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
getGitlabProvider,
|
||||
updateGitlabProvider,
|
||||
} from "@/server/api/services/git-provider";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
console.log(req.body);
|
||||
const { code, gitlabId } = req.query;
|
||||
|
||||
if (!code || Array.isArray(code)) {
|
||||
return res.status(400).json({ error: "Missing or invalid code" });
|
||||
}
|
||||
|
||||
const gitlab = await getGitlabProvider(gitlabId as string);
|
||||
|
||||
const response = await fetch("https://gitlab.com/oauth/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
client_id: gitlab.applicationId as string,
|
||||
client_secret: gitlab.secret as string,
|
||||
code: code as string,
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: `${gitlab.redirectUri}?gitlabId=${gitlabId}`,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.access_token || !result.refresh_token) {
|
||||
return res.status(400).json({ error: "Missing or invalid code" });
|
||||
}
|
||||
|
||||
const updatedGiltab = await updateGitlabProvider(gitlab.gitlabProviderId, {
|
||||
accessToken: result.access_token,
|
||||
refreshToken: result.refresh_token,
|
||||
});
|
||||
|
||||
return res.redirect(307, "/dashboard/settings/git-providers");
|
||||
}
|
||||
// b7262a56a0e84690d6352e07147e0cc4ff862818efe93a5fc7a12dc99a1382fd
|
||||
// {
|
||||
// accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||||
// host: 'localhost:3000',
|
||||
// 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36',
|
||||
// 'accept-encoding': 'gzip, deflate, br, zstd',
|
||||
// 'accept-language': 'es-ES,es;q=0.9',
|
||||
// 'cache-control': 'max-age=0',
|
||||
// referer: 'https://gitlab.com/',
|
||||
// 'x-request-id': '3e925ffc549f9a3d3ef5d5f376c2a6f0',
|
||||
// 'x-real-ip': '10.240.3.64',
|
||||
// 'x-forwarded-port': '443',
|
||||
// 'x-forwarded-scheme': 'https',
|
||||
// 'x-original-uri': '/api/providers/gitlab/callback?code=f26181b5c7397444ace5211f9ac4683b2d7bd64cd9431e85d3b6b4722827fabf',
|
||||
// 'x-scheme': 'https',
|
||||
// 'sec-fetch-site': 'cross-site',
|
||||
// 'sec-fetch-mode': 'navigate',
|
||||
// 'sec-fetch-user': '?1',
|
||||
// 'sec-fetch-dest': 'document',
|
||||
// 'sec-ch-ua': '"Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"',
|
||||
// 'sec-ch-ua-mobile': '?0',
|
||||
// 'sec-ch-ua-platform': '"macOS"',
|
||||
// priority: 'u=0, i',
|
||||
// 'x-original-proto': 'https',
|
||||
// cookie: 'rl_anonymous_id=RS_ENC_v3_IjEzMzVhYzg0LTIyYjctNGExNi04YzE5LTg4M2ZiOTEwMTRmYSI%3D; rl_page_init_referrer=RS_ENC_v3_IiRkaXJlY3Qi; __adroll_fpc=7113966cdd8d59aba5e5ef62ff22c535-1715634343969; rl_session=RS_ENC_v3_eyJpZCI6MTcxNTYzNDM0Mzc1NiwiZXhwaXJlc0F0IjoxNzE1NjM3MDY5NDg1LCJ0aW1lb3V0IjoxODAwMDAwLCJhdXRvVHJhY2siOnRydWV9; _ga_65LBX6LVJK=GS1.1.1715634344.1.1.1715635269.0.0.0; __ar_v4=FZBVRO7FTNEL3NZLTQLETP%3A20240512%3A9%7CJTXM2THZSJHDPEH4IPCBUU%3A20240512%3A9%7CFPP3PVDSUZBVHNEE67AUWV%3A20240512%3A9; auth_session=ih5fycwxzb5qkubabuc7u4qvz3wn2cfjzjdnigdh',
|
||||
// 'x-forwarded-proto': 'https',
|
||||
// 'x-forwarded-host': 'mcnknfld-3000.use2.devtunnels.ms',
|
||||
// 'x-forwarded-for': '10.240.3.64',
|
||||
// 'proxy-connection': 'Keep-Alive',
|
||||
// 'x-middleware-invoke': '',
|
||||
// 'x-invoke-path': '/api/providers/gitlab/callback',
|
||||
// 'x-invoke-query': '%7B%22code%22%3A%22f26181b5c7397444ace5211f9ac4683b2d7bd64cd9431e85d3b6b4722827fabf%22%2C%22__nextDefaultLocale%22%3A%22en%22%2C%22__nextLocale%22%3A%22en%22%7D',
|
||||
// 'x-invoke-output': '/api/providers/gitlab/callback'
|
||||
// }
|
||||
85
apps/dokploy/pages/dashboard/settings/git-providers.tsx
Normal file
85
apps/dokploy/pages/dashboard/settings/git-providers.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { ShowGitProviders } from "@/components/dashboard/settings/git/show-git-providers";
|
||||
import { GitlabSetup } from "@/components/dashboard/settings/github/gitlab-setup";
|
||||
import { ShowDestinations } from "@/components/dashboard/settings/ssh-keys/show-ssh-keys";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@/server/auth/auth";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<ShowGitProviders />
|
||||
{/* <ShowDestinations /> */}
|
||||
{/* <GitlabSetup /> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
Page.getLayout = (page: ReactElement) => {
|
||||
return (
|
||||
<DashboardLayout tab={"settings"}>
|
||||
<SettingsLayout>{page}</SettingsLayout>
|
||||
</DashboardLayout>
|
||||
);
|
||||
};
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
const { req, res, resolvedUrl } = ctx;
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
const auth = await helpers.auth.get.fetch();
|
||||
|
||||
if (auth.rol === "user") {
|
||||
const user = await helpers.user.byAuthId.fetch({
|
||||
authId: auth.id,
|
||||
});
|
||||
|
||||
if (!user.canAccessToSSHKeys) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { GithubSetup } from "@/components/dashboard/settings/github/github-setup";
|
||||
import { WebDomain } from "@/components/dashboard/settings/web-domain";
|
||||
import { WebServer } from "@/components/dashboard/settings/web-server";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
@@ -11,7 +10,6 @@ const Page = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<WebDomain />
|
||||
<GithubSetup />
|
||||
<WebServer />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -25,6 +25,7 @@ import { securityRouter } from "./routers/security";
|
||||
import { settingsRouter } from "./routers/settings";
|
||||
import { sshRouter } from "./routers/ssh-key";
|
||||
import { userRouter } from "./routers/user";
|
||||
import { gitProvider } from "./routers/git-provider";
|
||||
|
||||
/**
|
||||
* This is the primary router for your server.
|
||||
@@ -58,6 +59,7 @@ export const appRouter = createTRPCRouter({
|
||||
cluster: clusterRouter,
|
||||
notification: notificationRouter,
|
||||
sshKey: sshRouter,
|
||||
gitProvider: gitProvider,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
protectedProcedure,
|
||||
publicProcedure,
|
||||
} from "../trpc";
|
||||
import { z } from "zod";
|
||||
import { getGithubProvider } from "../services/git-provider";
|
||||
|
||||
export const adminRouter = createTRPCRouter({
|
||||
one: adminProcedure.query(async () => {
|
||||
@@ -101,55 +103,61 @@ export const adminRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
|
||||
getRepositories: protectedProcedure.query(async () => {
|
||||
const admin = await findAdmin();
|
||||
|
||||
const completeRequirements = haveGithubRequirements(admin);
|
||||
|
||||
if (!completeRequirements) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Admin need to setup correctly github account",
|
||||
});
|
||||
}
|
||||
|
||||
const octokit = new Octokit({
|
||||
authStrategy: createAppAuth,
|
||||
auth: {
|
||||
appId: admin.githubAppId,
|
||||
privateKey: admin.githubPrivateKey,
|
||||
installationId: admin.githubInstallationId,
|
||||
},
|
||||
});
|
||||
|
||||
const repositories = (await octokit.paginate(
|
||||
octokit.rest.apps.listReposAccessibleToInstallation,
|
||||
)) as unknown as Awaited<
|
||||
ReturnType<typeof octokit.rest.apps.listReposAccessibleToInstallation>
|
||||
>["data"]["repositories"];
|
||||
|
||||
return repositories;
|
||||
}),
|
||||
getBranches: protectedProcedure
|
||||
.input(apiGetBranches)
|
||||
getRepositories: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
githubProviderId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
const admin = await findAdmin();
|
||||
|
||||
const completeRequirements = haveGithubRequirements(admin);
|
||||
|
||||
if (!completeRequirements) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Admin need to setup correctly github account",
|
||||
});
|
||||
if (!input.githubProviderId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const githubProvider = await getGithubProvider(input.githubProviderId);
|
||||
|
||||
const octokit = new Octokit({
|
||||
authStrategy: createAppAuth,
|
||||
auth: {
|
||||
appId: admin.githubAppId,
|
||||
privateKey: admin.githubPrivateKey,
|
||||
installationId: admin.githubInstallationId,
|
||||
appId: githubProvider.githubAppId,
|
||||
privateKey: githubProvider.githubPrivateKey,
|
||||
installationId: githubProvider.githubInstallationId,
|
||||
},
|
||||
});
|
||||
|
||||
const repositories = (await octokit.paginate(
|
||||
octokit.rest.apps.listReposAccessibleToInstallation,
|
||||
)) as unknown as Awaited<
|
||||
ReturnType<typeof octokit.rest.apps.listReposAccessibleToInstallation>
|
||||
>["data"]["repositories"];
|
||||
|
||||
return repositories;
|
||||
}),
|
||||
getBranches: protectedProcedure
|
||||
.input(apiGetBranches)
|
||||
.query(async ({ input }) => {
|
||||
// const admin = await findAdmin();
|
||||
|
||||
// const completeRequirements = haveGithubRequirements(admin);
|
||||
|
||||
// if (!completeRequirements) {
|
||||
// throw new TRPCError({
|
||||
// code: "BAD_REQUEST",
|
||||
// message: "Admin need to setup correctly github account",
|
||||
// });
|
||||
// }
|
||||
|
||||
if (!input.githubProviderId) {
|
||||
return [];
|
||||
}
|
||||
const githubProvider = await getGithubProvider(input.githubProviderId);
|
||||
|
||||
const octokit = new Octokit({
|
||||
authStrategy: createAppAuth,
|
||||
auth: {
|
||||
appId: githubProvider.githubAppId,
|
||||
privateKey: githubProvider.githubPrivateKey,
|
||||
installationId: githubProvider.githubInstallationId,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -9,11 +9,13 @@ import {
|
||||
apiFindMonitoringStats,
|
||||
apiFindOneApplication,
|
||||
apiReloadApplication,
|
||||
apiSaveBitbucketProvider,
|
||||
apiSaveBuildType,
|
||||
apiSaveDockerProvider,
|
||||
apiSaveEnvironmentVariables,
|
||||
apiSaveGitProvider,
|
||||
apiSaveGithubProvider,
|
||||
apiSaveGitlabProvider,
|
||||
apiUpdateApplication,
|
||||
applications,
|
||||
} from "@/server/db/schema/application";
|
||||
@@ -207,6 +209,37 @@ export const applicationRouter = createTRPCRouter({
|
||||
owner: input.owner,
|
||||
buildPath: input.buildPath,
|
||||
applicationStatus: "idle",
|
||||
githubProviderId: input.githubProviderId,
|
||||
});
|
||||
|
||||
return true;
|
||||
}),
|
||||
saveGitlabProvider: protectedProcedure
|
||||
.input(apiSaveGitlabProvider)
|
||||
.mutation(async ({ input }) => {
|
||||
await updateApplication(input.applicationId, {
|
||||
gitlabRepository: input.gitlabRepository,
|
||||
gitlabOwner: input.gitlabOwner,
|
||||
gitlabBranch: input.gitlabBranch,
|
||||
gitlabBuildPath: input.gitlabBuildPath,
|
||||
sourceType: "gitlab",
|
||||
applicationStatus: "idle",
|
||||
gitlabProviderId: input.gitlabProviderId,
|
||||
});
|
||||
|
||||
return true;
|
||||
}),
|
||||
saveBitbucketProvider: protectedProcedure
|
||||
.input(apiSaveBitbucketProvider)
|
||||
.mutation(async ({ input }) => {
|
||||
await updateApplication(input.applicationId, {
|
||||
bitbucketRepository: input.bitbucketRepository,
|
||||
bitbucketOwner: input.bitbucketOwner,
|
||||
bitbucketBranch: input.bitbucketBranch,
|
||||
bitbucketBuildPath: input.bitbucketBuildPath,
|
||||
sourceType: "bitbucket",
|
||||
applicationStatus: "idle",
|
||||
bitbucketProviderId: input.bitbucketProviderId,
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
326
apps/dokploy/server/api/routers/git-provider.ts
Normal file
326
apps/dokploy/server/api/routers/git-provider.ts
Normal file
@@ -0,0 +1,326 @@
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
apiCreateBitbucketProvider,
|
||||
apiCreateGitlabProvider,
|
||||
} from "@/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import {
|
||||
createBitbucketProvider,
|
||||
createGitlabProvider,
|
||||
getBitbucketProvider,
|
||||
getGitlabProvider,
|
||||
haveGithubRequirements,
|
||||
haveGitlabRequirements,
|
||||
removeGithubProvider,
|
||||
} from "../services/git-provider";
|
||||
import { z } from "zod";
|
||||
|
||||
export const gitProvider = createTRPCRouter({
|
||||
getAll: protectedProcedure.query(async () => {
|
||||
return await db.query.gitProvider.findMany({
|
||||
with: {
|
||||
gitlabProvider: true,
|
||||
bitbucketProvider: true,
|
||||
githubProvider: true,
|
||||
},
|
||||
});
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.input(z.object({ gitProviderId: z.string() }))
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
return await removeGithubProvider(input.gitProviderId);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to delete this git provider",
|
||||
});
|
||||
}
|
||||
}),
|
||||
createGitlabProvider: protectedProcedure
|
||||
.input(apiCreateGitlabProvider)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
return await createGitlabProvider(input);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create this gitlab provider",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
createBitbucketProvider: protectedProcedure
|
||||
.input(apiCreateBitbucketProvider)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
return await createBitbucketProvider(input);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create this bitbucket provider",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
githubProviders: protectedProcedure.query(async () => {
|
||||
const result = await db.query.githubProvider.findMany({
|
||||
with: {
|
||||
gitProvider: true,
|
||||
},
|
||||
});
|
||||
|
||||
const filtered = result
|
||||
.filter((provider) => haveGithubRequirements(provider))
|
||||
.map((provider) => {
|
||||
return {
|
||||
githubProviderId: provider.githubProviderId,
|
||||
gitProvider: {
|
||||
...provider.gitProvider,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}),
|
||||
gitlabProviders: protectedProcedure.query(async () => {
|
||||
const result = await db.query.gitlabProvider.findMany({
|
||||
with: {
|
||||
gitProvider: true,
|
||||
},
|
||||
});
|
||||
const filtered = result
|
||||
.filter((provider) => haveGitlabRequirements(provider))
|
||||
.map((provider) => {
|
||||
return {
|
||||
gitlabProviderId: provider.gitlabProviderId,
|
||||
gitProvider: {
|
||||
...provider.gitProvider,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}),
|
||||
bitbucketProviders: protectedProcedure.query(async () => {
|
||||
const result = await db.query.bitbucketProvider.findMany({
|
||||
with: {
|
||||
gitProvider: true,
|
||||
},
|
||||
columns: {
|
||||
bitbucketProviderId: true,
|
||||
},
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
|
||||
getGitlabRepositories: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
gitlabProviderId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
if (!input.gitlabProviderId) {
|
||||
return [];
|
||||
}
|
||||
const gitlabProvider = await getGitlabProvider(input.gitlabProviderId);
|
||||
const response = await fetch(
|
||||
`https://gitlab.com/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const repositories = await response.json();
|
||||
return repositories as {
|
||||
name: string;
|
||||
url: string;
|
||||
owner: {
|
||||
username: string;
|
||||
};
|
||||
}[];
|
||||
}),
|
||||
|
||||
getGitlabBranches: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
owner: z.string(),
|
||||
repo: z.string(),
|
||||
gitlabProviderId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
if (!input.gitlabProviderId) {
|
||||
return [];
|
||||
}
|
||||
const gitlabProvider = await getGitlabProvider(input.gitlabProviderId);
|
||||
|
||||
const projectResponse = await fetch(
|
||||
`https://gitlab.com/api/v4/projects?search=${input.repo}&owned=true&page=1&per_page=100`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.refreshToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!projectResponse.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Failed to fetch repositories: ${projectResponse.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const projects = await projectResponse.json();
|
||||
const project = projects.find(
|
||||
(p) => p.namespace.path === input.owner && p.name === input.repo,
|
||||
);
|
||||
|
||||
if (!project) {
|
||||
throw new Error(`Project not found: ${input.owner}/${input.repo}`);
|
||||
}
|
||||
|
||||
const branchesResponse = await fetch(
|
||||
`https://gitlab.com/api/v4/projects/${project.id}/repository/branches`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!branchesResponse.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch branches: ${branchesResponse.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const branches = await branchesResponse.json();
|
||||
|
||||
return branches as {
|
||||
name: string;
|
||||
commit: {
|
||||
id: string;
|
||||
};
|
||||
}[];
|
||||
}),
|
||||
getBitbucketRepositories: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
bitbucketProviderId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
if (!input.bitbucketProviderId) {
|
||||
return [];
|
||||
}
|
||||
const bitbucketProvider = await getBitbucketProvider(
|
||||
input.bitbucketProviderId,
|
||||
);
|
||||
|
||||
const url = `https://api.bitbucket.org/2.0/repositories/${bitbucketProvider.bitbucketUsername}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const mappedData = data.values.map((repo) => {
|
||||
return {
|
||||
name: repo.name,
|
||||
url: repo.links.html.href,
|
||||
owner: {
|
||||
username: repo.workspace.slug,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return mappedData as {
|
||||
name: string;
|
||||
url: string;
|
||||
owner: {
|
||||
username: string;
|
||||
};
|
||||
}[];
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
getBitbucketBranches: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
owner: z.string(),
|
||||
repo: z.string(),
|
||||
bitbucketProviderId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
if (!input.bitbucketProviderId) {
|
||||
return [];
|
||||
}
|
||||
const bitbucketProvider = await getBitbucketProvider(
|
||||
input.bitbucketProviderId,
|
||||
);
|
||||
const { owner, repo } = input;
|
||||
const url = `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}/refs/branches`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `HTTP error! status: ${response.status}`,
|
||||
});
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const mappedData = data.values.map((branch) => {
|
||||
return {
|
||||
name: branch.name,
|
||||
commit: {
|
||||
sha: branch.target.hash,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return mappedData as {
|
||||
name: string;
|
||||
commit: {
|
||||
sha: string;
|
||||
};
|
||||
}[];
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
});
|
||||
192
apps/dokploy/server/api/services/git-provider.ts
Normal file
192
apps/dokploy/server/api/services/git-provider.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
type apiCreateBitbucketProvider,
|
||||
type apiCreateGithubProvider,
|
||||
type apiCreateGitlabProvider,
|
||||
bitbucketProvider,
|
||||
githubProvider,
|
||||
gitlabProvider,
|
||||
gitProvider,
|
||||
} from "@/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export type GithubProvider = typeof githubProvider.$inferSelect;
|
||||
|
||||
export type GitlabProvider = typeof gitlabProvider.$inferSelect;
|
||||
export const createGithubProvider = async (
|
||||
input: typeof apiCreateGithubProvider._type,
|
||||
) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const newGitProvider = await tx
|
||||
.insert(gitProvider)
|
||||
.values({
|
||||
providerType: "github",
|
||||
authId: input.authId,
|
||||
name: input.name,
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
|
||||
if (!newGitProvider) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the git provider",
|
||||
});
|
||||
}
|
||||
|
||||
return await tx
|
||||
.insert(githubProvider)
|
||||
.values({
|
||||
...input,
|
||||
gitProviderId: newGitProvider?.gitProviderId,
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
});
|
||||
};
|
||||
|
||||
export const createGitlabProvider = async (
|
||||
input: typeof apiCreateGitlabProvider._type,
|
||||
) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const newGitProvider = await tx
|
||||
.insert(gitProvider)
|
||||
.values({
|
||||
providerType: "gitlab",
|
||||
authId: input.authId,
|
||||
name: input.name,
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
|
||||
if (!newGitProvider) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the git provider",
|
||||
});
|
||||
}
|
||||
|
||||
await tx
|
||||
.insert(gitlabProvider)
|
||||
.values({
|
||||
...input,
|
||||
gitProviderId: newGitProvider?.gitProviderId,
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
});
|
||||
};
|
||||
|
||||
export const createBitbucketProvider = async (
|
||||
input: typeof apiCreateBitbucketProvider._type,
|
||||
) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const newGitProvider = await tx
|
||||
.insert(gitProvider)
|
||||
.values({
|
||||
providerType: "bitbucket",
|
||||
authId: input.authId,
|
||||
name: input.name,
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
|
||||
if (!newGitProvider) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the git provider",
|
||||
});
|
||||
}
|
||||
|
||||
await tx
|
||||
.insert(bitbucketProvider)
|
||||
.values({
|
||||
...input,
|
||||
gitProviderId: newGitProvider?.gitProviderId,
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
});
|
||||
};
|
||||
|
||||
export const removeGithubProvider = async (gitProviderId: string) => {
|
||||
const result = await db
|
||||
.delete(gitProvider)
|
||||
.where(eq(gitProvider.gitProviderId, gitProviderId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const getGithubProvider = async (githubProviderId: string) => {
|
||||
const githubProviderResult = await db.query.githubProvider.findFirst({
|
||||
where: eq(githubProvider.githubProviderId, githubProviderId),
|
||||
});
|
||||
|
||||
if (!githubProviderResult) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Github Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
return githubProviderResult;
|
||||
};
|
||||
|
||||
export const haveGithubRequirements = (githubProvider: GithubProvider) => {
|
||||
return !!(
|
||||
githubProvider?.githubAppId &&
|
||||
githubProvider?.githubPrivateKey &&
|
||||
githubProvider?.githubInstallationId
|
||||
);
|
||||
};
|
||||
|
||||
export const haveGitlabRequirements = (gitlabProvider: GitlabProvider) => {
|
||||
return !!(gitlabProvider?.accessToken && gitlabProvider?.refreshToken);
|
||||
};
|
||||
|
||||
export const getGitlabProvider = async (gitlabProviderId: string) => {
|
||||
const gitlabProviderResult = await db.query.gitlabProvider.findFirst({
|
||||
where: eq(gitlabProvider.gitlabProviderId, gitlabProviderId),
|
||||
});
|
||||
|
||||
if (!gitlabProviderResult) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitlab Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
return gitlabProviderResult;
|
||||
};
|
||||
|
||||
export const updateGitlabProvider = async (
|
||||
gitlabProviderId: string,
|
||||
input: Partial<GitlabProvider>,
|
||||
) => {
|
||||
const result = await db
|
||||
.update(gitlabProvider)
|
||||
.set({
|
||||
...input,
|
||||
})
|
||||
.where(eq(gitlabProvider.gitlabProviderId, gitlabProviderId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const getBitbucketProvider = async (bitbucketProviderId: string) => {
|
||||
const bitbucketProviderResult = await db.query.bitbucketProvider.findFirst({
|
||||
where: eq(bitbucketProvider.bitbucketProviderId, bitbucketProviderId),
|
||||
});
|
||||
|
||||
if (!bitbucketProviderResult) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Bitbucket Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
return bitbucketProviderResult;
|
||||
};
|
||||
@@ -85,6 +85,7 @@ export const apiTraefikConfig = z.object({
|
||||
export const apiGetBranches = z.object({
|
||||
repo: z.string().min(1),
|
||||
owner: z.string().min(1),
|
||||
githubProviderId: z.string().optional(),
|
||||
});
|
||||
export const apiModifyTraefikConfig = z.object({
|
||||
path: z.string().min(1),
|
||||
|
||||
@@ -22,11 +22,18 @@ import { security } from "./security";
|
||||
import { applicationStatus } from "./shared";
|
||||
import { sshKeys } from "./ssh-key";
|
||||
import { generateAppName } from "./utils";
|
||||
import {
|
||||
bitbucketProvider,
|
||||
githubProvider,
|
||||
gitlabProvider,
|
||||
} from "./git-provider";
|
||||
|
||||
export const sourceType = pgEnum("sourceType", [
|
||||
"docker",
|
||||
"git",
|
||||
"github",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
"drop",
|
||||
]);
|
||||
|
||||
@@ -126,6 +133,16 @@ export const applications = pgTable("application", {
|
||||
branch: text("branch"),
|
||||
buildPath: text("buildPath").default("/"),
|
||||
autoDeploy: boolean("autoDeploy").$defaultFn(() => true),
|
||||
// Gitlab
|
||||
gitlabRepository: text("gitlabRepository"),
|
||||
gitlabOwner: text("gitlabOwner"),
|
||||
gitlabBranch: text("gitlabBranch"),
|
||||
gitlabBuildPath: text("gitlabBuildPath").default("/"),
|
||||
// Bitbucket
|
||||
bitbucketRepository: text("bitbucketRepository"),
|
||||
bitbucketOwner: text("bitbucketOwner"),
|
||||
bitbucketBranch: text("bitbucketBranch"),
|
||||
bitbucketBuildPath: text("bitbucketBuildPath").default("/"),
|
||||
// Docker
|
||||
username: text("username"),
|
||||
password: text("password"),
|
||||
@@ -169,6 +186,24 @@ export const applications = pgTable("application", {
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||
githubProviderId: text("githubProviderId").references(
|
||||
() => githubProvider.githubProviderId,
|
||||
{
|
||||
onDelete: "set null",
|
||||
},
|
||||
),
|
||||
gitlabProviderId: text("gitlabProviderId").references(
|
||||
() => gitlabProvider.gitlabProviderId,
|
||||
{
|
||||
onDelete: "set null",
|
||||
},
|
||||
),
|
||||
bitbucketProviderId: text("bitbucketProviderId").references(
|
||||
() => bitbucketProvider.bitbucketProviderId,
|
||||
{
|
||||
onDelete: "set null",
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
export const applicationsRelations = relations(
|
||||
@@ -192,6 +227,18 @@ export const applicationsRelations = relations(
|
||||
fields: [applications.registryId],
|
||||
references: [registry.registryId],
|
||||
}),
|
||||
githubProvider: one(githubProvider, {
|
||||
fields: [applications.githubProviderId],
|
||||
references: [githubProvider.githubProviderId],
|
||||
}),
|
||||
gitlabProvider: one(gitlabProvider, {
|
||||
fields: [applications.gitlabProviderId],
|
||||
references: [gitlabProvider.gitlabProviderId],
|
||||
}),
|
||||
bitbucketProvider: one(bitbucketProvider, {
|
||||
fields: [applications.bitbucketProviderId],
|
||||
references: [bitbucketProvider.bitbucketProviderId],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -369,6 +416,29 @@ export const apiSaveGithubProvider = createSchema
|
||||
branch: true,
|
||||
owner: true,
|
||||
buildPath: true,
|
||||
githubProviderId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveGitlabProvider = createSchema
|
||||
.pick({
|
||||
applicationId: true,
|
||||
gitlabBranch: true,
|
||||
gitlabBuildPath: true,
|
||||
gitlabOwner: true,
|
||||
gitlabRepository: true,
|
||||
gitlabProviderId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveBitbucketProvider = createSchema
|
||||
.pick({
|
||||
bitbucketBranch: true,
|
||||
bitbucketBuildPath: true,
|
||||
bitbucketOwner: true,
|
||||
bitbucketRepository: true,
|
||||
bitbucketProviderId: true,
|
||||
applicationId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
|
||||
150
apps/dokploy/server/db/schema/git-provider.ts
Normal file
150
apps/dokploy/server/db/schema/git-provider.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgTable, text, pgEnum, integer } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { auth } from "./auth";
|
||||
|
||||
export const gitProviderType = pgEnum("gitProviderType", [
|
||||
"github",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
]);
|
||||
|
||||
export const gitProvider = pgTable("git_provider", {
|
||||
gitProviderId: text("gitProviderId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
providerType: gitProviderType("providerType").notNull().default("github"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
authId: text("authId")
|
||||
.notNull()
|
||||
.references(() => auth.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const gitProviderRelations = relations(gitProvider, ({ one, many }) => ({
|
||||
githubProvider: one(githubProvider, {
|
||||
fields: [gitProvider.gitProviderId],
|
||||
references: [githubProvider.gitProviderId],
|
||||
}),
|
||||
gitlabProvider: one(gitlabProvider, {
|
||||
fields: [gitProvider.gitProviderId],
|
||||
references: [gitlabProvider.gitProviderId],
|
||||
}),
|
||||
bitbucketProvider: one(bitbucketProvider, {
|
||||
fields: [gitProvider.gitProviderId],
|
||||
references: [bitbucketProvider.gitProviderId],
|
||||
}),
|
||||
auth: one(auth, {
|
||||
fields: [gitProvider.authId],
|
||||
references: [auth.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const githubProvider = pgTable("github_provider", {
|
||||
githubProviderId: text("githubProviderId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
githubAppName: text("githubAppName"),
|
||||
githubAppId: integer("githubAppId"),
|
||||
githubClientId: text("githubClientId"),
|
||||
githubClientSecret: text("githubClientSecret"),
|
||||
githubInstallationId: text("githubInstallationId"),
|
||||
githubPrivateKey: text("githubPrivateKey"),
|
||||
githubWebhookSecret: text("githubWebhookSecret"),
|
||||
gitProviderId: text("gitProviderId")
|
||||
.notNull()
|
||||
.references(() => gitProvider.gitProviderId, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const githubProviderRelations = relations(
|
||||
githubProvider,
|
||||
({ one, many }) => ({
|
||||
gitProvider: one(gitProvider, {
|
||||
fields: [githubProvider.gitProviderId],
|
||||
references: [gitProvider.gitProviderId],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
export const gitlabProvider = pgTable("gitlab_provider", {
|
||||
gitlabProviderId: text("gitlabProviderId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
applicationId: text("application_id"),
|
||||
redirectUri: text("redirect_uri"),
|
||||
secret: text("secret"),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
groupName: text("group_name"),
|
||||
gitProviderId: text("gitProviderId")
|
||||
.notNull()
|
||||
.references(() => gitProvider.gitProviderId, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const gitlabProviderRelations = relations(
|
||||
gitlabProvider,
|
||||
({ one, many }) => ({
|
||||
gitProvider: one(gitProvider, {
|
||||
fields: [gitlabProvider.gitProviderId],
|
||||
references: [gitProvider.gitProviderId],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
export const bitbucketProvider = pgTable("bitbucket_provider", {
|
||||
bitbucketProviderId: text("bitbucketProviderId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
bitbucketUsername: text("bitbucketUsername"),
|
||||
appPassword: text("appPassword"),
|
||||
bitbucketWorkspaceName: text("bitbucketWorkspaceName"),
|
||||
gitProviderId: text("gitProviderId")
|
||||
.notNull()
|
||||
.references(() => gitProvider.gitProviderId, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const bitbucketProviderRelations = relations(
|
||||
bitbucketProvider,
|
||||
({ one, many }) => ({
|
||||
gitProvider: one(gitProvider, {
|
||||
fields: [bitbucketProvider.gitProviderId],
|
||||
references: [gitProvider.gitProviderId],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const createSchema = createInsertSchema(gitProvider);
|
||||
|
||||
export const apiCreateGithubProvider = createSchema.extend({
|
||||
githubAppName: z.string().optional(),
|
||||
githubAppId: z.number().optional(),
|
||||
githubClientId: z.string().optional(),
|
||||
githubClientSecret: z.string().optional(),
|
||||
githubInstallationId: z.string().optional(),
|
||||
githubPrivateKey: z.string().optional(),
|
||||
githubWebhookSecret: z.string().nullable(),
|
||||
gitProviderId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateGitlabProvider = createSchema.extend({
|
||||
applicationId: z.string().optional(),
|
||||
secret: z.string().optional(),
|
||||
groupName: z.string().optional(),
|
||||
gitProviderId: z.string().optional(),
|
||||
redirectUri: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateBitbucketProvider = createSchema.extend({
|
||||
bitbucketUsername: z.string().optional(),
|
||||
appPassword: z.string().optional(),
|
||||
bitbucketWorkspaceName: z.string().optional(),
|
||||
gitProviderId: z.string().optional(),
|
||||
});
|
||||
@@ -23,3 +23,4 @@ export * from "./compose";
|
||||
export * from "./registry";
|
||||
export * from "./notification";
|
||||
export * from "./ssh-key";
|
||||
export * from "./git-provider";
|
||||
|
||||
13
apps/dokploy/utils/hooks/use-url.ts
Normal file
13
apps/dokploy/utils/hooks/use-url.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const useUrl = () => {
|
||||
const [url, setUrl] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const protocolAndHost = `${window.location.protocol}//${window.location.host}`;
|
||||
|
||||
setUrl(`${protocolAndHost}`);
|
||||
}, []);
|
||||
|
||||
return url;
|
||||
};
|
||||
55
pnpm-lock.yaml
generated
55
pnpm-lock.yaml
generated
@@ -123,6 +123,9 @@ importers:
|
||||
'@faker-js/faker':
|
||||
specifier: ^8.4.1
|
||||
version: 8.4.1
|
||||
'@gitbeaker/rest':
|
||||
specifier: 40.1.3
|
||||
version: 40.1.3
|
||||
'@hookform/resolvers':
|
||||
specifier: ^3.3.4
|
||||
version: 3.9.0(react-hook-form@7.52.1(react@18.2.0))
|
||||
@@ -1637,6 +1640,18 @@ packages:
|
||||
'@formatjs/intl-localematcher@0.5.4':
|
||||
resolution: {integrity: sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==}
|
||||
|
||||
'@gitbeaker/core@40.1.3':
|
||||
resolution: {integrity: sha512-704bRTVFI+2rropt/ZCxMp7HdlAbuOlvzlB5Hu8icBauh+NMOhAJFISDE3LG/Tbf1LXa1F5hSg8dVYzXYJNB9w==}
|
||||
engines: {node: '>=18.20.0'}
|
||||
|
||||
'@gitbeaker/requester-utils@40.1.3':
|
||||
resolution: {integrity: sha512-ruHu/lvvTdE6JPoUzEmiZY4Ef9U+5Iam5cgcB/vMYTfBx89iwlPGj/sGHIbJPWSdwoGnrn+sGp8+KygqDr3Zgw==}
|
||||
engines: {node: '>=18.20.0'}
|
||||
|
||||
'@gitbeaker/rest@40.1.3':
|
||||
resolution: {integrity: sha512-3xzImKoCTdlFyLUgnG+RjnWJmOtOhAFf7+A5+3r3nOCKOAZHGlknvPouNBQ2hCl8lRLBEHXgBA40ASv1SGvRfQ==}
|
||||
engines: {node: '>=18.20.0'}
|
||||
|
||||
'@hapi/bourne@3.0.0':
|
||||
resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==}
|
||||
|
||||
@@ -6938,6 +6953,10 @@ packages:
|
||||
picocolors@1.0.1:
|
||||
resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
|
||||
|
||||
picomatch-browser@2.2.6:
|
||||
resolution: {integrity: sha512-0ypsOQt9D4e3hziV8O4elD9uN0z/jtUEfxVRtNaAAtXIyUx9m/SzlO020i8YNL2aL/E6blOvvHQcin6HZlFy/w==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
picomatch@2.3.1:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
engines: {node: '>=8.6'}
|
||||
@@ -7172,6 +7191,9 @@ packages:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
rate-limiter-flexible@4.0.1:
|
||||
resolution: {integrity: sha512-2/dGHpDFpeA0+755oUkW+EKyklqLS9lu0go9pDsbhqQjZcxfRyJ6LA4JI0+HAdZ2bemD/oOjUeZQB2lCZqXQfQ==}
|
||||
|
||||
raw-body@2.5.2:
|
||||
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -8366,6 +8388,9 @@ packages:
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
xcase@2.0.1:
|
||||
resolution: {integrity: sha512-UmFXIPU+9Eg3E9m/728Bii0lAIuoc+6nbrNUKaRPJOFp91ih44qqGlWtxMB6kXFrRD6po+86ksHM5XHCfk6iPw==}
|
||||
|
||||
xml-but-prettier@1.0.1:
|
||||
resolution: {integrity: sha512-C2CJaadHrZTqESlH03WOyw0oZTtoy2uEg6dSDF6YRg+9GnYNub53RRemLpnvtbHDFelxMx4LajiFsYeR6XJHgQ==}
|
||||
|
||||
@@ -9644,6 +9669,24 @@ snapshots:
|
||||
dependencies:
|
||||
tslib: 2.6.3
|
||||
|
||||
'@gitbeaker/core@40.1.3':
|
||||
dependencies:
|
||||
'@gitbeaker/requester-utils': 40.1.3
|
||||
qs: 6.12.3
|
||||
xcase: 2.0.1
|
||||
|
||||
'@gitbeaker/requester-utils@40.1.3':
|
||||
dependencies:
|
||||
picomatch-browser: 2.2.6
|
||||
qs: 6.12.3
|
||||
rate-limiter-flexible: 4.0.1
|
||||
xcase: 2.0.1
|
||||
|
||||
'@gitbeaker/rest@40.1.3':
|
||||
dependencies:
|
||||
'@gitbeaker/core': 40.1.3
|
||||
'@gitbeaker/requester-utils': 40.1.3
|
||||
|
||||
'@hapi/bourne@3.0.0': {}
|
||||
|
||||
'@headlessui/react@1.7.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
@@ -14040,7 +14083,7 @@ snapshots:
|
||||
eslint: 8.45.0
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0))(eslint@8.45.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1)(eslint@8.45.0)
|
||||
eslint-plugin-jsx-a11y: 6.9.0(eslint@8.45.0)
|
||||
eslint-plugin-react: 7.35.0(eslint@8.45.0)
|
||||
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.45.0)
|
||||
@@ -14064,7 +14107,7 @@ snapshots:
|
||||
enhanced-resolve: 5.17.1
|
||||
eslint: 8.45.0
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0))(eslint@8.45.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0))(eslint@8.45.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1)(eslint@8.45.0)
|
||||
fast-glob: 3.3.2
|
||||
get-tsconfig: 4.7.5
|
||||
is-core-module: 2.15.0
|
||||
@@ -14086,7 +14129,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0))(eslint@8.45.0):
|
||||
eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1)(eslint@8.45.0):
|
||||
dependencies:
|
||||
array-includes: 3.1.8
|
||||
array.prototype.findlastindex: 1.2.5
|
||||
@@ -16366,6 +16409,8 @@ snapshots:
|
||||
|
||||
picocolors@1.0.1: {}
|
||||
|
||||
picomatch-browser@2.2.6: {}
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
|
||||
pidtree@0.6.0: {}
|
||||
@@ -16538,6 +16583,8 @@ snapshots:
|
||||
|
||||
range-parser@1.2.1: {}
|
||||
|
||||
rate-limiter-flexible@4.0.1: {}
|
||||
|
||||
raw-body@2.5.2:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
@@ -18025,6 +18072,8 @@ snapshots:
|
||||
|
||||
ws@8.16.0: {}
|
||||
|
||||
xcase@2.0.1: {}
|
||||
|
||||
xml-but-prettier@1.0.1:
|
||||
dependencies:
|
||||
repeat-string: 1.6.1
|
||||
|
||||
Reference in New Issue
Block a user