feat(providers): add gitlab bitbucket and github providers

This commit is contained in:
Mauricio Siu
2024-08-31 22:57:41 -06:00
parent 28d8fa9834
commit 6d945371c9
58 changed files with 46852 additions and 311 deletions

View File

@@ -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>
);
};

View File

@@ -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"

View File

@@ -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>
);
};

View File

@@ -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>

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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.");
});
}}
>

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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,

View 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 $$;

View 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 $$;

View File

@@ -0,0 +1 @@
ALTER TABLE "github_provider" ADD COLUMN "githubAppName" text;

View 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 $$;

View 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 $$;

View File

@@ -0,0 +1 @@
ALTER TABLE "gitlab_provider" RENAME COLUMN "github_provider_id" TO "gitlabProviderId";

View File

@@ -0,0 +1 @@
ALTER TABLE "gitlab_provider" RENAME COLUMN "application_secret" TO "secret";

View File

@@ -0,0 +1 @@
ALTER TABLE "gitlab_provider" ADD COLUMN "token" text;

View File

@@ -0,0 +1 @@
ALTER TABLE "gitlab_provider" ADD COLUMN "redirect_uri" text;

View 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;

View File

@@ -0,0 +1 @@
ALTER TABLE "gitlab_provider" DROP COLUMN IF EXISTS "token";

View 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 $$;

View 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 '/';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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
}
]
}

View File

@@ -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",

View 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");
}

View 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`);
}
}

View 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'
// }

View 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: {},
};
}
}

View File

@@ -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>
);

View File

@@ -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

View File

@@ -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,
},
});

View File

@@ -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;

View 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;
}
}),
});

View 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;
};

View File

@@ -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),

View File

@@ -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();

View 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(),
});

View File

@@ -23,3 +23,4 @@ export * from "./compose";
export * from "./registry";
export * from "./notification";
export * from "./ssh-key";
export * from "./git-provider";

View 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
View File

@@ -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