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