mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-15 13:21:11 +00:00
feat(gitea): Added Gitea Repo Integration
This commit is contained in:
+414
@@ -0,0 +1,414 @@
|
||||
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";
|
||||
|
||||
// Define types for repository and branch objects
|
||||
interface GiteaRepository {
|
||||
name: string;
|
||||
url: string;
|
||||
id: number;
|
||||
owner: {
|
||||
username: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GiteaBranch {
|
||||
name: string;
|
||||
commit: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
const GiteaProviderSchema = 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"),
|
||||
giteaPathNamespace: z.string().min(1),
|
||||
id: z.number().nullable(),
|
||||
})
|
||||
.required(),
|
||||
branch: z.string().min(1, "Branch is required"),
|
||||
giteaId: z.string().min(1, "Gitea Provider is required"),
|
||||
});
|
||||
|
||||
type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
|
||||
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
||||
|
||||
const { mutateAsync, isLoading: isSavingGiteaProvider } =
|
||||
api.application.saveGiteaProvider.useMutation();
|
||||
|
||||
const form = useForm<GiteaProvider>({
|
||||
defaultValues: {
|
||||
buildPath: "/",
|
||||
repository: {
|
||||
owner: "",
|
||||
repo: "",
|
||||
giteaPathNamespace: "",
|
||||
id: null,
|
||||
},
|
||||
giteaId: "",
|
||||
branch: "",
|
||||
},
|
||||
resolver: zodResolver(GiteaProviderSchema),
|
||||
});
|
||||
|
||||
const repository = form.watch("repository");
|
||||
const giteaId = form.watch("giteaId");
|
||||
|
||||
const {
|
||||
data: repositories,
|
||||
isLoading: isLoadingRepositories,
|
||||
error,
|
||||
} = api.gitea.getGiteaRepositories.useQuery(
|
||||
{
|
||||
giteaId,
|
||||
},
|
||||
{
|
||||
enabled: !!giteaId,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: branches,
|
||||
fetchStatus,
|
||||
status,
|
||||
} = api.gitea.getGiteaBranches.useQuery(
|
||||
{
|
||||
owner: repository?.owner,
|
||||
repositoryName: repository?.repo,
|
||||
id: repository?.id || 0,
|
||||
giteaId: giteaId,
|
||||
},
|
||||
{
|
||||
enabled: !!repository?.owner && !!repository?.repo && !!giteaId,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
branch: data.giteaBranch || "",
|
||||
repository: {
|
||||
repo: data.giteaRepository || "",
|
||||
owner: data.giteaOwner || "",
|
||||
giteaPathNamespace: data.giteaPathNamespace || "",
|
||||
id: data.giteaProjectId,
|
||||
},
|
||||
buildPath: data.giteaBuildPath || "/",
|
||||
giteaId: data.giteaId || "",
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
|
||||
const onSubmit = async (data: GiteaProvider) => {
|
||||
await mutateAsync({
|
||||
giteaBranch: data.branch,
|
||||
giteaRepository: data.repository.repo,
|
||||
giteaOwner: data.repository.owner,
|
||||
giteaBuildPath: data.buildPath,
|
||||
giteaId: data.giteaId,
|
||||
applicationId,
|
||||
giteaProjectId: data.repository.id,
|
||||
giteaPathNamespace: data.repository.giteaPathNamespace,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Service Provider Saved");
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error saving the Gitea 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="giteaId"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2 flex flex-col">
|
||||
<FormLabel>Gitea Account</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
form.setValue("repository", {
|
||||
owner: "",
|
||||
repo: "",
|
||||
id: null,
|
||||
giteaPathNamespace: "",
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a Gitea Account" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{giteaProviders?.map((giteaProvider) => (
|
||||
<SelectItem
|
||||
key={giteaProvider.giteaId}
|
||||
value={giteaProvider.giteaId}
|
||||
>
|
||||
{giteaProvider.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"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{isLoadingRepositories
|
||||
? "Loading...."
|
||||
: field.value.owner
|
||||
? repositories?.find(
|
||||
(repo: GiteaRepository) => 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 && repositories.length === 0 && (
|
||||
<CommandEmpty>
|
||||
No repositories found.
|
||||
</CommandEmpty>
|
||||
)}
|
||||
{repositories?.map((repo: GiteaRepository) => {
|
||||
return (
|
||||
<CommandItem
|
||||
value={repo.name}
|
||||
key={repo.url}
|
||||
onSelect={() => {
|
||||
form.setValue("repository", {
|
||||
owner: repo.owner.username as string,
|
||||
repo: repo.name,
|
||||
id: repo.id,
|
||||
giteaPathNamespace: repo.name,
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{repo.name}</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{repo.owner.username}
|
||||
</span>
|
||||
</span>
|
||||
<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"
|
||||
className={cn(
|
||||
" w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{status === "loading" && fetchStatus === "fetching"
|
||||
? "Loading...."
|
||||
: field.value
|
||||
? branches?.find(
|
||||
(branch: GiteaBranch) => 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: GiteaBranch) => (
|
||||
<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={isSavingGiteaProvider}
|
||||
type="submit"
|
||||
className="w-fit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,12 +1,14 @@
|
||||
import { SaveDockerProvider } from "@/components/dashboard/application/general/generic/save-docker-provider";
|
||||
import { SaveGitProvider } from "@/components/dashboard/application/general/generic/save-git-provider";
|
||||
import { SaveGiteaProvider } from "@/components/dashboard/application/general/generic/save-gitea-provider";
|
||||
import { SaveGithubProvider } from "@/components/dashboard/application/general/generic/save-github-provider";
|
||||
import {
|
||||
BitbucketIcon,
|
||||
DockerIcon,
|
||||
GitIcon,
|
||||
GithubIcon,
|
||||
GitlabIcon,
|
||||
BitbucketIcon,
|
||||
DockerIcon,
|
||||
GitIcon,
|
||||
GiteaIcon,
|
||||
GithubIcon,
|
||||
GitlabIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
@@ -18,162 +20,190 @@ import { SaveBitbucketProvider } from "./save-bitbucket-provider";
|
||||
import { SaveDragNDrop } from "./save-drag-n-drop";
|
||||
import { SaveGitlabProvider } from "./save-gitlab-provider";
|
||||
|
||||
type TabState = "github" | "docker" | "git" | "drop" | "gitlab" | "bitbucket";
|
||||
type TabState = "github" | "docker" | "git" | "drop" | "gitlab" | "bitbucket" | "gitea";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const ShowProviderForm = ({ applicationId }: Props) => {
|
||||
const { data: githubProviders } = api.github.githubProviders.useQuery();
|
||||
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
||||
const { data: bitbucketProviders } =
|
||||
api.bitbucket.bitbucketProviders.useQuery();
|
||||
const { data: githubProviders } = api.github.githubProviders.useQuery();
|
||||
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
||||
const { data: bitbucketProviders } =
|
||||
api.bitbucket.bitbucketProviders.useQuery();
|
||||
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
|
||||
|
||||
const { data: application } = api.application.one.useQuery({ applicationId });
|
||||
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
|
||||
return (
|
||||
<Card className="group relative w-full bg-transparent">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-start justify-between">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="flex flex-col space-y-0.5">Provider</span>
|
||||
<p className="flex items-center text-sm font-normal text-muted-foreground">
|
||||
Select the source of your code
|
||||
</p>
|
||||
</div>
|
||||
<div className="hidden space-y-1 text-sm font-normal md:block">
|
||||
<GitBranch className="size-6 text-muted-foreground" />
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs
|
||||
value={tab}
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
const { data: application } = api.application.one.useQuery({ applicationId });
|
||||
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
|
||||
return (
|
||||
<Card className="group relative w-full bg-transparent">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-start justify-between">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="flex flex-col space-y-0.5">Provider</span>
|
||||
<p className="flex items-center text-sm font-normal text-muted-foreground">
|
||||
Select the source of your code
|
||||
</p>
|
||||
</div>
|
||||
<div className="hidden space-y-1 text-sm font-normal md:block">
|
||||
<GitBranch className="size-6 text-muted-foreground" />
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs
|
||||
value={tab}
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
}}
|
||||
>
|
||||
<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="gitea"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<GiteaIcon className="size-4 text-current fill-current" />
|
||||
Gitea
|
||||
</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">
|
||||
{githubProviders && githubProviders?.length > 0 ? (
|
||||
<SaveGithubProvider applicationId={applicationId} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||
<GithubIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To deploy using GitHub, 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="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
|
||||
</Link>{" "}
|
||||
to do so.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="docker" className="w-full p-2">
|
||||
<SaveDockerProvider applicationId={applicationId} />
|
||||
</TabsContent>
|
||||
<TabsContent value="github" className="w-full p-2">
|
||||
{githubProviders && githubProviders?.length > 0 ? (
|
||||
<SaveGithubProvider applicationId={applicationId} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||
<GithubIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To deploy using GitHub, 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="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
|
||||
</Link>{" "}
|
||||
to do so.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="gitea" className="w-full p-2">
|
||||
{giteaProviders && giteaProviders?.length > 0 ? (
|
||||
<SaveGiteaProvider applicationId={applicationId} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||
<GiteaIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To deploy using Gitea, 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="docker" className="w-full p-2">
|
||||
<SaveDockerProvider applicationId={applicationId} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="git" className="w-full p-2">
|
||||
<SaveGitProvider applicationId={applicationId} />
|
||||
</TabsContent>
|
||||
<TabsContent value="drop" className="w-full p-2">
|
||||
<SaveDragNDrop applicationId={applicationId} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
<TabsContent value="git" className="w-full p-2">
|
||||
<SaveGitProvider applicationId={applicationId} />
|
||||
</TabsContent>
|
||||
<TabsContent value="drop" className="w-full p-2">
|
||||
<SaveDragNDrop applicationId={applicationId} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,251 @@
|
||||
import { GiteaIcon } from "@/components/icons/data-tools-icons"; // Use GiteaIcon for Gitea
|
||||
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 { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const Schema = z.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Name is required",
|
||||
}),
|
||||
giteaUrl: z.string().min(1, {
|
||||
message: "Gitea URL is required",
|
||||
}),
|
||||
clientId: z.string().min(1, {
|
||||
message: "Client ID is required",
|
||||
}),
|
||||
clientSecret: z.string().min(1, {
|
||||
message: "Client Secret is required",
|
||||
}),
|
||||
redirectUri: z.string().min(1, {
|
||||
message: "Redirect URI is required",
|
||||
}),
|
||||
organizationName: z.string().optional(), // Added organizationName to the schema
|
||||
});
|
||||
|
||||
type Schema = z.infer<typeof Schema>;
|
||||
|
||||
export const AddGiteaProvider = () => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const url = useUrl();
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { mutateAsync, error, isError } = api.gitea.create.useMutation(); // Updated API call for Gitea
|
||||
const webhookUrl = `${url}/api/providers/gitea/callback`; // Updated webhook URL for Gitea
|
||||
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
redirectUri: webhookUrl,
|
||||
name: "",
|
||||
giteaUrl: "https://gitea.com",
|
||||
},
|
||||
resolver: zodResolver(Schema),
|
||||
});
|
||||
|
||||
const giteaUrl = form.watch("giteaUrl");
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
redirectUri: webhookUrl,
|
||||
name: "",
|
||||
giteaUrl: "https://gitea.com",
|
||||
});
|
||||
}, [form, isOpen]);
|
||||
|
||||
const onSubmit = async (data: Schema) => {
|
||||
await mutateAsync({
|
||||
clientId: data.clientId || "",
|
||||
clientSecret: data.clientSecret || "",
|
||||
name: data.name || "",
|
||||
redirectUri: data.redirectUri || "",
|
||||
giteaUrl: data.giteaUrl || "https://gitea.com", // Use Gitea URL
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.gitProvider.getAll.invalidate();
|
||||
toast.success("Gitea provider created successfully");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error configuring Gitea");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className="flex items-center space-x-1 bg-green-700 text-white hover:bg-green-500"
|
||||
>
|
||||
<GiteaIcon />
|
||||
<span>Gitea</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen ">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
Gitea Provider <GiteaIcon className="size-5" />
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-add-gitea"
|
||||
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 Gitea account, you need to create a new
|
||||
application in your Gitea 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 Gitea settings{" "}
|
||||
<Link
|
||||
href={`${giteaUrl}/user/settings/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>Select Permissions - organization: read, user: read, repository: read/write</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
After creating, you'll receive an 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="giteaUrl" // Ensure consistent name for Gitea URL
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Gitea URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://gitea.com/" {...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="clientId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Client ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Client ID" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clientSecret"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Client Secret</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Client Secret"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button isLoading={form.formState.isSubmitting}>
|
||||
Configure Gitea App {/* Ensured consistency with Gitea */}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,280 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
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 { PenBoxIcon } from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
giteaUrl: z.string().min(1, "Gitea URL is required"),
|
||||
clientId: z.string().min(1, "Client ID is required"),
|
||||
clientSecret: z.string().min(1, "Client Secret is required"),
|
||||
});
|
||||
|
||||
interface Props {
|
||||
giteaId: string;
|
||||
}
|
||||
|
||||
export const EditGiteaProvider = ({ giteaId }: Props) => {
|
||||
const router = useRouter();
|
||||
const [open, setOpen] = useState(false);
|
||||
const { data: gitea, isLoading, refetch } = api.gitea.one.useQuery({ giteaId });
|
||||
const { mutateAsync, isLoading: isUpdating } = api.gitea.update.useMutation();
|
||||
const { mutateAsync: testConnection, isLoading: isTesting } = api.gitea.testConnection.useMutation();
|
||||
const url = useUrl();
|
||||
const utils = api.useUtils();
|
||||
|
||||
// Handle OAuth redirect results
|
||||
useEffect(() => {
|
||||
const { connected, error } = router.query;
|
||||
|
||||
// Only process if router is ready and query parameters exist
|
||||
if (!router.isReady) return;
|
||||
|
||||
if (connected) {
|
||||
toast.success("Successfully connected to Gitea", {
|
||||
description: "Your Gitea provider has been authorized.",
|
||||
id: 'gitea-connection-success'
|
||||
});
|
||||
refetch();
|
||||
// Clear the query parameters to prevent re-triggering
|
||||
router.replace({
|
||||
pathname: router.pathname,
|
||||
query: {}
|
||||
}, undefined, { shallow: true });
|
||||
}
|
||||
|
||||
if (error) {
|
||||
toast.error("Gitea Connection Failed", {
|
||||
description: decodeURIComponent(error as string),
|
||||
id: 'gitea-connection-error'
|
||||
});
|
||||
// Clear the query parameters to prevent re-triggering
|
||||
router.replace({
|
||||
pathname: router.pathname,
|
||||
query: {}
|
||||
}, undefined, { shallow: true });
|
||||
}
|
||||
}, [router.query, router.isReady, refetch]);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
giteaUrl: "https://gitea.com",
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
},
|
||||
});
|
||||
|
||||
// Update form values when data is loaded
|
||||
useEffect(() => {
|
||||
if (gitea) {
|
||||
form.reset({
|
||||
name: gitea.gitProvider?.name || "",
|
||||
giteaUrl: gitea.giteaUrl || "https://gitea.com",
|
||||
clientId: gitea.clientId || "",
|
||||
clientSecret: gitea.clientSecret || "",
|
||||
});
|
||||
}
|
||||
}, [gitea, form.reset]);
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
await mutateAsync({
|
||||
giteaId: giteaId,
|
||||
gitProviderId: gitea?.gitProvider?.gitProviderId || "",
|
||||
name: values.name,
|
||||
giteaUrl: values.giteaUrl,
|
||||
clientId: values.clientId,
|
||||
clientSecret: values.clientSecret,
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.gitProvider.getAll.invalidate();
|
||||
toast.success("Gitea provider updated successfully");
|
||||
await refetch();
|
||||
setOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating Gitea provider");
|
||||
});
|
||||
};
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
try {
|
||||
const result = await testConnection({ giteaId });
|
||||
toast.success("Gitea Connection Verified", {
|
||||
description: result
|
||||
});
|
||||
} catch (error: any) {
|
||||
const authUrl = error.authorizationUrl || getGiteaOAuthUrl();
|
||||
|
||||
toast.error("Gitea Not Connected", {
|
||||
description: error.message || "Please complete the OAuth authorization process.",
|
||||
action: authUrl && authUrl !== "#" ? {
|
||||
label: "Authorize Now",
|
||||
onClick: () => window.open(authUrl, "_blank")
|
||||
} : undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Generate Gitea OAuth URL with state parameter
|
||||
const getGiteaOAuthUrl = () => {
|
||||
const clientId = form.getValues().clientId;
|
||||
const giteaUrl = form.getValues().giteaUrl;
|
||||
|
||||
if (!clientId || !giteaUrl) {
|
||||
toast.error("Configuration Incomplete", {
|
||||
description: "Please fill in Client ID and Gitea URL first."
|
||||
});
|
||||
return "#";
|
||||
}
|
||||
|
||||
const redirectUri = `${url}/api/providers/gitea/callback`;
|
||||
|
||||
// Use the scopes from the gitea data (if available), else fallback to default scopes
|
||||
const scopes = gitea?.scopes?.split(',').join(' ') || 'repo repo:status read:user read:org';
|
||||
//const scopes = gitea?.scopes || 'repo,repo:status,read:user,read:org';
|
||||
|
||||
const state = giteaId;
|
||||
|
||||
return `${giteaUrl}/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scopes)}&state=${encodeURIComponent(state)}`;
|
||||
};
|
||||
|
||||
// Show loading state if data is being fetched
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Button variant="ghost" size="icon" disabled>
|
||||
<PenBoxIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="group hover:bg-blue-500/10">
|
||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Gitea Provider</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update your Gitea provider details.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="My Gitea" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="giteaUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Gitea URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://gitea.example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clientId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Client ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Client ID" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clientSecret"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Client Secret</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Client Secret"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleTestConnection}
|
||||
isLoading={isTesting}
|
||||
>
|
||||
Test Connection
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const authUrl = getGiteaOAuthUrl();
|
||||
if (authUrl !== "#") {
|
||||
window.open(authUrl, "_blank");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Connect to Gitea
|
||||
</Button>
|
||||
|
||||
<Button type="submit" isLoading={isUpdating}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -2,265 +2,272 @@ import {
|
||||
BitbucketIcon,
|
||||
GithubIcon,
|
||||
GitlabIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { useUrl } from "@/utils/hooks/use-url";
|
||||
import { formatDate } from "date-fns";
|
||||
import {
|
||||
GiteaIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { useUrl } from "@/utils/hooks/use-url";
|
||||
import { formatDate } from "date-fns";
|
||||
import {
|
||||
ExternalLinkIcon,
|
||||
GitBranch,
|
||||
ImportIcon,
|
||||
Loader2,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { toast } from "sonner";
|
||||
import { AddBitbucketProvider } from "./bitbucket/add-bitbucket-provider";
|
||||
import { EditBitbucketProvider } from "./bitbucket/edit-bitbucket-provider";
|
||||
import { AddGithubProvider } from "./github/add-github-provider";
|
||||
import { EditGithubProvider } from "./github/edit-github-provider";
|
||||
import { AddGitlabProvider } from "./gitlab/add-gitlab-provider";
|
||||
import { EditGitlabProvider } from "./gitlab/edit-gitlab-provider";
|
||||
|
||||
export const ShowGitProviders = () => {
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { toast } from "sonner";
|
||||
import { AddBitbucketProvider } from "./bitbucket/add-bitbucket-provider";
|
||||
import { EditBitbucketProvider } from "./bitbucket/edit-bitbucket-provider";
|
||||
import { AddGithubProvider } from "./github/add-github-provider";
|
||||
import { EditGithubProvider } from "./github/edit-github-provider";
|
||||
import { AddGitlabProvider } from "./gitlab/add-gitlab-provider";
|
||||
import { EditGitlabProvider } from "./gitlab/edit-gitlab-provider";
|
||||
import { AddGiteaProvider } from "./gitea/add-gitea-provider";
|
||||
import { EditGiteaProvider } from "./gitea/edit-gitea-provider";
|
||||
|
||||
export const ShowGitProviders = () => {
|
||||
const { data, isLoading, refetch } = api.gitProvider.getAll.useQuery();
|
||||
const { mutateAsync, isLoading: isRemoving } =
|
||||
api.gitProvider.remove.useMutation();
|
||||
const { mutateAsync, isLoading: isRemoving } = api.gitProvider.remove.useMutation();
|
||||
const url = useUrl();
|
||||
|
||||
const getGitlabUrl = (
|
||||
clientId: string,
|
||||
gitlabId: string,
|
||||
gitlabUrl: string,
|
||||
clientId: string,
|
||||
gitlabId: string,
|
||||
gitlabUrl: string,
|
||||
) => {
|
||||
const redirectUri = `${url}/api/providers/gitlab/callback?gitlabId=${gitlabId}`;
|
||||
|
||||
const scope = "api read_user read_repository";
|
||||
|
||||
const authUrl = `${gitlabUrl}/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scope)}`;
|
||||
|
||||
return authUrl;
|
||||
const redirectUri = `${url}/api/providers/gitlab/callback?gitlabId=${gitlabId}`;
|
||||
const scope = "api read_user read_repository";
|
||||
const authUrl = `${gitlabUrl}/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scope)}`;
|
||||
return authUrl;
|
||||
};
|
||||
|
||||
const getGiteaUrl = (
|
||||
clientId: string,
|
||||
giteaId: string,
|
||||
giteaUrl: string,
|
||||
) => {
|
||||
const redirectUri = `${url}/api/providers/gitea/callback?giteaId=${giteaId}`;
|
||||
const scope = "repo";
|
||||
const authUrl = `${giteaUrl}/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scope)}`;
|
||||
return authUrl;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||
<div className="rounded-xl bg-background shadow-md ">
|
||||
<CardHeader className="">
|
||||
<CardTitle className="text-xl flex flex-row gap-2">
|
||||
<GitBranch className="size-6 text-muted-foreground self-center" />
|
||||
Git Providers
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Connect your Git provider for authentication.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 py-8 border-t">
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[25vh]">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{data?.length === 0 ? (
|
||||
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||
<GitBranch className="size-8 self-center text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground text-center">
|
||||
Create your first Git Provider
|
||||
</span>
|
||||
<div>
|
||||
<div className="flex items-center bg-sidebar p-1 w-full rounded-lg">
|
||||
<div className="flex items-center gap-4 p-3.5 rounded-lg bg-background border w-full">
|
||||
<AddGithubProvider />
|
||||
<AddGitlabProvider />
|
||||
<AddBitbucketProvider />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||
<div className="flex flex-col gap-2 rounded-lg ">
|
||||
<span className="text-base font-medium">
|
||||
Available Providers
|
||||
</span>
|
||||
<div className="flex items-center bg-sidebar p-1 w-full rounded-lg">
|
||||
<div className="flex items-center gap-4 p-3.5 rounded-lg bg-background border w-full">
|
||||
<AddGithubProvider />
|
||||
<AddGitlabProvider />
|
||||
<AddBitbucketProvider />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 rounded-lg ">
|
||||
{data?.map((gitProvider, _index) => {
|
||||
const isGithub = gitProvider.providerType === "github";
|
||||
const isGitlab = gitProvider.providerType === "gitlab";
|
||||
const isBitbucket =
|
||||
gitProvider.providerType === "bitbucket";
|
||||
const haveGithubRequirements =
|
||||
gitProvider.providerType === "github" &&
|
||||
gitProvider.github?.githubPrivateKey &&
|
||||
gitProvider.github?.githubAppId &&
|
||||
gitProvider.github?.githubInstallationId;
|
||||
|
||||
const haveGitlabRequirements =
|
||||
gitProvider.gitlab?.accessToken &&
|
||||
gitProvider.gitlab?.refreshToken;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={gitProvider.gitProviderId}
|
||||
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
||||
>
|
||||
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
||||
<div className="flex flex-col items-center justify-between">
|
||||
<div className="flex gap-2 flex-row items-center">
|
||||
{gitProvider.providerType === "github" && (
|
||||
<GithubIcon className="size-5" />
|
||||
)}
|
||||
{gitProvider.providerType === "gitlab" && (
|
||||
<GitlabIcon className="size-5" />
|
||||
)}
|
||||
{gitProvider.providerType === "bitbucket" && (
|
||||
<BitbucketIcon className="size-5" />
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-sm font-medium">
|
||||
{gitProvider.name}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatDate(
|
||||
gitProvider.createdAt,
|
||||
"yyyy-MM-dd hh:mm:ss a",
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-1">
|
||||
{!haveGithubRequirements && isGithub && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<Link
|
||||
href={`${gitProvider?.github?.githubAppName}/installations/new?state=gh_setup:${gitProvider?.github.githubId}`}
|
||||
className={buttonVariants({
|
||||
size: "icon",
|
||||
variant: "ghost",
|
||||
})}
|
||||
>
|
||||
<ImportIcon className="size-4 text-primary" />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{haveGithubRequirements && isGithub && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<Link
|
||||
href={`${gitProvider?.github?.githubAppName}`}
|
||||
target="_blank"
|
||||
className={buttonVariants({
|
||||
size: "icon",
|
||||
variant: "ghost",
|
||||
})}
|
||||
>
|
||||
<ExternalLinkIcon className="size-4 text-primary" />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{!haveGitlabRequirements && isGitlab && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<Link
|
||||
href={getGitlabUrl(
|
||||
gitProvider.gitlab?.applicationId || "",
|
||||
gitProvider.gitlab?.gitlabId || "",
|
||||
gitProvider.gitlab?.gitlabUrl,
|
||||
)}
|
||||
target="_blank"
|
||||
className={buttonVariants({
|
||||
size: "icon",
|
||||
variant: "ghost",
|
||||
})}
|
||||
>
|
||||
<ImportIcon className="size-4 text-primary" />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{isGithub && haveGithubRequirements && (
|
||||
<EditGithubProvider
|
||||
githubId={gitProvider.github.githubId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isGitlab && (
|
||||
<EditGitlabProvider
|
||||
gitlabId={gitProvider.gitlab.gitlabId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isBitbucket && (
|
||||
<EditBitbucketProvider
|
||||
bitbucketId={
|
||||
gitProvider.bitbucket.bitbucketId
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DialogAction
|
||||
title="Delete Git Provider"
|
||||
description="Are you sure you want to delete this Git Provider?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
gitProviderId: gitProvider.gitProviderId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"Git Provider deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error deleting Git Provider",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10 "
|
||||
isLoading={isRemoving}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||
{/* <AddCertificate /> */}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
<div className="w-full">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||
<div className="rounded-xl bg-background shadow-md ">
|
||||
<CardHeader className="">
|
||||
<CardTitle className="text-xl flex flex-row gap-2">
|
||||
<GitBranch className="size-6 text-muted-foreground self-center" />
|
||||
Git Providers
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Connect your Git provider for authentication.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 py-8 border-t">
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[25vh]">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{data?.length === 0 ? (
|
||||
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||
<GitBranch className="size-8 self-center text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground text-center">
|
||||
Create your first Git Provider
|
||||
</span>
|
||||
<div>
|
||||
<div className="flex items-center bg-sidebar p-1 w-full rounded-lg">
|
||||
<div className="flex items-center gap-4 p-3.5 rounded-lg bg-background border w-full">
|
||||
<AddGithubProvider />
|
||||
<AddGitlabProvider />
|
||||
<AddBitbucketProvider />
|
||||
<AddGiteaProvider />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||
<div className="flex flex-col gap-2 rounded-lg ">
|
||||
<span className="text-base font-medium">
|
||||
Available Providers
|
||||
</span>
|
||||
<div className="flex items-center bg-sidebar p-1 w-full rounded-lg">
|
||||
<div className="flex items-center gap-4 p-3.5 rounded-lg bg-background border w-full">
|
||||
<AddGithubProvider />
|
||||
<AddGitlabProvider />
|
||||
<AddBitbucketProvider />
|
||||
<AddGiteaProvider />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 rounded-lg ">
|
||||
{data?.map((gitProvider, _index) => {
|
||||
const isGithub = gitProvider.providerType === "github";
|
||||
const isGitlab = gitProvider.providerType === "gitlab";
|
||||
const isBitbucket = gitProvider.providerType === "bitbucket";
|
||||
const isGitea = gitProvider.providerType === "gitea";
|
||||
|
||||
const haveGithubRequirements = isGithub &&
|
||||
gitProvider.github?.githubPrivateKey &&
|
||||
gitProvider.github?.githubAppId &&
|
||||
gitProvider.github?.githubInstallationId;
|
||||
|
||||
const haveGitlabRequirements = isGitlab &&
|
||||
gitProvider.gitlab?.accessToken &&
|
||||
gitProvider.gitlab?.refreshToken;
|
||||
|
||||
const haveGiteaRequirements = isGitea &&
|
||||
gitProvider.gitea?.accessToken &&
|
||||
gitProvider.gitea?.refreshToken;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={gitProvider.gitProviderId}
|
||||
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
||||
>
|
||||
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
||||
<div className="flex flex-col items-center justify-between">
|
||||
<div className="flex gap-2 flex-row items-center">
|
||||
{isGithub && (
|
||||
<GithubIcon className="size-5" />
|
||||
)}
|
||||
{isGitlab && (
|
||||
<GitlabIcon className="size-5" />
|
||||
)}
|
||||
{isBitbucket && (
|
||||
<BitbucketIcon className="size-5" />
|
||||
)}
|
||||
{isGitea && (
|
||||
<GiteaIcon className="size-5" />
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-sm font-medium">
|
||||
{gitProvider.name}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatDate(
|
||||
gitProvider.createdAt,
|
||||
"yyyy-MM-dd hh:mm:ss a"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-1">
|
||||
{!haveGithubRequirements && isGithub && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<Link
|
||||
href={`${gitProvider?.github?.githubAppName}/installations/new?state=gh_setup:${gitProvider?.github.githubId}`}
|
||||
className={buttonVariants({
|
||||
size: "icon",
|
||||
variant: "ghost",
|
||||
})}
|
||||
>
|
||||
<ImportIcon className="size-4 text-primary" />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{haveGithubRequirements && isGithub && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<Link
|
||||
href={`${gitProvider?.github?.githubAppName}`}
|
||||
target="_blank"
|
||||
className={buttonVariants({
|
||||
size: "icon",
|
||||
variant: "ghost",
|
||||
})}
|
||||
>
|
||||
<ExternalLinkIcon className="size-4 text-primary" />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{!haveGitlabRequirements && isGitlab && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<Link
|
||||
href={getGitlabUrl(
|
||||
gitProvider.gitlab?.applicationId || "",
|
||||
gitProvider.gitlab?.gitlabId || "",
|
||||
gitProvider.gitlab?.gitlabUrl,
|
||||
)}
|
||||
target="_blank"
|
||||
className={buttonVariants({
|
||||
size: "icon",
|
||||
variant: "ghost",
|
||||
})}
|
||||
>
|
||||
<ImportIcon className="size-4 text-primary" />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{isGithub && haveGithubRequirements && (
|
||||
<EditGithubProvider githubId={gitProvider.github?.githubId} />
|
||||
)}
|
||||
|
||||
{isGitlab && (
|
||||
<EditGitlabProvider gitlabId={gitProvider.gitlab?.gitlabId} />
|
||||
)}
|
||||
|
||||
{isBitbucket && (
|
||||
<EditBitbucketProvider bitbucketId={gitProvider.bitbucket?.bitbucketId} />
|
||||
)}
|
||||
|
||||
{isGitea && (
|
||||
<EditGiteaProvider giteaId={gitProvider.gitea?.giteaId} />
|
||||
)}
|
||||
|
||||
<DialogAction
|
||||
title="Delete Git Provider"
|
||||
description="Are you sure you want to delete this Git Provider?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
gitProviderId: gitProvider.gitProviderId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Git Provider deleted successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting Git Provider");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10"
|
||||
isLoading={isRemoving}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||
{/* <AddCertificate /> */}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -238,6 +238,41 @@ export const BitbucketIcon = ({ className }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const GiteaIcon = ({ className }: Props) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
version="1.1"
|
||||
id="main_outline"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="5.67 143.05 628.65 387.55"
|
||||
enableBackground="new 0 0 640 640"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
id="teabag"
|
||||
style={{ fill: '#FFFFFF' }}
|
||||
d="M395.9,484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5,21.2-17.9,33.8-11.8c17.2,8.3,27.1,13,27.1,13l-0.1-109.2l16.7-0.1l0.1,117.1c0,0,57.4,24.2,83.1,40.1c3.7,2.3,10.2,6.8,12.9,14.4c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z"
|
||||
/>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
style={{ fill: '#609926' }}
|
||||
d="M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: '#609926' }}
|
||||
d="M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8C343.2,346.5,335,363.3,326.8,380.1z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const DockerIcon = ({ className }: Props) => {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
ALTER TYPE "public"."gitProviderType" ADD VALUE 'gitea';--> statement-breakpoint
|
||||
CREATE TABLE "gitea" (
|
||||
"giteaId" text PRIMARY KEY NOT NULL,
|
||||
"giteaUrl" text DEFAULT 'https://gitea.com' NOT NULL,
|
||||
"application_id" text,
|
||||
"redirect_uri" text,
|
||||
"secret" text,
|
||||
"access_token" text,
|
||||
"refresh_token" text,
|
||||
"organization_name" text,
|
||||
"expires_at" integer,
|
||||
"gitProviderId" text NOT NULL,
|
||||
"gitea_username" text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "gitea" ADD CONSTRAINT "gitea_gitProviderId_git_provider_gitProviderId_fk" FOREIGN KEY ("gitProviderId") REFERENCES "public"."git_provider"("gitProviderId") ON DELETE cascade ON UPDATE no action;
|
||||
@@ -0,0 +1,9 @@
|
||||
ALTER TYPE "public"."sourceType" ADD VALUE 'gitea' BEFORE 'drop';--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "giteaProjectId" integer;--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "giteaRepository" text;--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "giteaOwner" text;--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "giteaBranch" text;--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "giteaBuildPath" text DEFAULT '/';--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "giteaPathNamespace" text;--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "giteaId" text;--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD CONSTRAINT "application_giteaId_gitea_giteaId_fk" FOREIGN KEY ("giteaId") REFERENCES "public"."gitea"("giteaId") ON DELETE set null ON UPDATE no action;
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE "gitea" ADD COLUMN "client_id" text;--> statement-breakpoint
|
||||
ALTER TABLE "gitea" ADD COLUMN "client_secret" text;--> statement-breakpoint
|
||||
ALTER TABLE "gitea" DROP COLUMN "application_id";--> statement-breakpoint
|
||||
ALTER TABLE "gitea" DROP COLUMN "secret";--> statement-breakpoint
|
||||
ALTER TABLE "gitea" DROP COLUMN "refresh_token";--> statement-breakpoint
|
||||
ALTER TABLE "gitea" DROP COLUMN "organization_name";
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE "gitea" ADD COLUMN "refresh_token" text;--> statement-breakpoint
|
||||
ALTER TABLE "gitea" ADD COLUMN "organization_name" text;--> statement-breakpoint
|
||||
ALTER TABLE "gitea" ADD COLUMN "scopes" text;--> statement-breakpoint
|
||||
ALTER TABLE "gitea" ADD COLUMN "last_authenticated_at" integer;
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TYPE "public"."sourceTypeCompose" ADD VALUE 'gitea' BEFORE 'raw';--> statement-breakpoint
|
||||
ALTER TABLE "compose" ADD COLUMN "giteaRepository" text;--> statement-breakpoint
|
||||
ALTER TABLE "compose" ADD COLUMN "giteaOwner" text;--> statement-breakpoint
|
||||
ALTER TABLE "compose" ADD COLUMN "giteaBranch" text;--> statement-breakpoint
|
||||
ALTER TABLE "compose" ADD COLUMN "giteaId" text;--> statement-breakpoint
|
||||
ALTER TABLE "compose" ADD CONSTRAINT "compose_giteaId_gitea_giteaId_fk" FOREIGN KEY ("giteaId") REFERENCES "public"."gitea"("giteaId") ON DELETE set null ON UPDATE no action;
|
||||
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
@@ -498,6 +498,41 @@
|
||||
"when": 1741322697251,
|
||||
"tag": "0070_useful_serpent_society",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 71,
|
||||
"version": "7",
|
||||
"when": 1741559743256,
|
||||
"tag": "0071_flimsy_plazm",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 72,
|
||||
"version": "7",
|
||||
"when": 1741593124105,
|
||||
"tag": "0072_low_redwing",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 73,
|
||||
"version": "7",
|
||||
"when": 1741645208694,
|
||||
"tag": "0073_dark_tigra",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 74,
|
||||
"version": "7",
|
||||
"when": 1741673569715,
|
||||
"tag": "0074_military_miss_america",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 75,
|
||||
"version": "7",
|
||||
"when": 1742018928109,
|
||||
"tag": "0075_wild_xorn",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
+26
-23
@@ -36,7 +36,6 @@
|
||||
"test": "vitest --config __test__/vitest.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"ai": "^4.0.23",
|
||||
"@ai-sdk/anthropic": "^1.0.6",
|
||||
"@ai-sdk/azure": "^1.0.15",
|
||||
"@ai-sdk/cohere": "^1.0.6",
|
||||
@@ -44,20 +43,6 @@
|
||||
"@ai-sdk/mistral": "^1.0.6",
|
||||
"@ai-sdk/openai": "^1.0.12",
|
||||
"@ai-sdk/openai-compatible": "^0.0.13",
|
||||
"ollama-ai-provider": "^1.1.0",
|
||||
"better-auth": "1.2.0",
|
||||
"bl": "6.0.11",
|
||||
"rotating-file-stream": "3.2.3",
|
||||
"qrcode": "^1.5.3",
|
||||
"otpauth": "^9.2.3",
|
||||
"hi-base32": "^0.5.1",
|
||||
"boxen": "^7.1.1",
|
||||
"@octokit/auth-app": "^6.0.4",
|
||||
"nodemailer": "6.9.14",
|
||||
"@react-email/components": "^0.0.21",
|
||||
"node-os-utils": "1.3.7",
|
||||
"@lucia-auth/adapter-drizzle": "1.0.7",
|
||||
"dockerode": "4.0.2",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-yaml": "^6.1.1",
|
||||
"@codemirror/language": "^6.10.1",
|
||||
@@ -65,7 +50,10 @@
|
||||
"@codemirror/view": "6.29.0",
|
||||
"@dokploy/server": "workspace:*",
|
||||
"@dokploy/trpc-openapi": "0.0.4",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@lucia-auth/adapter-drizzle": "1.0.7",
|
||||
"@octokit/auth-app": "^6.0.4",
|
||||
"@octokit/webhooks": "^13.2.7",
|
||||
"@radix-ui/react-accordion": "1.1.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
@@ -86,8 +74,10 @@
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toggle": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@react-email/components": "^0.0.21",
|
||||
"@stepperize/react": "4.0.1",
|
||||
"@stripe/stripe-js": "4.8.0",
|
||||
"@tailwindcss/typography": "0.5.16",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@tanstack/react-table": "^8.16.0",
|
||||
"@trpc/client": "^10.43.6",
|
||||
@@ -97,10 +87,14 @@
|
||||
"@uiw/codemirror-theme-github": "^4.22.1",
|
||||
"@uiw/react-codemirror": "^4.22.1",
|
||||
"@xterm/addon-attach": "0.10.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
"@xterm/addon-clipboard": "0.1.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
"adm-zip": "^0.5.14",
|
||||
"ai": "^4.0.23",
|
||||
"bcrypt": "5.1.1",
|
||||
"better-auth": "1.2.0",
|
||||
"bl": "6.0.11",
|
||||
"boxen": "^7.1.1",
|
||||
"bullmq": "5.4.2",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
@@ -108,10 +102,12 @@
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"date-fns": "3.6.0",
|
||||
"dockerode": "4.0.2",
|
||||
"dotenv": "16.4.5",
|
||||
"drizzle-orm": "^0.39.1",
|
||||
"drizzle-zod": "0.5.1",
|
||||
"fancy-ansi": "^0.1.3",
|
||||
"hi-base32": "^0.5.1",
|
||||
"i18next": "^23.16.4",
|
||||
"input-otp": "^1.2.4",
|
||||
"js-cookie": "^3.0.5",
|
||||
@@ -123,11 +119,17 @@
|
||||
"next": "^15.0.1",
|
||||
"next-i18next": "^15.3.1",
|
||||
"next-themes": "^0.2.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"node-os-utils": "1.3.7",
|
||||
"node-pty": "1.0.0",
|
||||
"node-schedule": "2.1.1",
|
||||
"nodemailer": "6.9.14",
|
||||
"octokit": "3.1.2",
|
||||
"ollama-ai-provider": "^1.1.0",
|
||||
"otpauth": "^9.2.3",
|
||||
"postgres": "3.4.4",
|
||||
"public-ip": "6.0.2",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "18.2.0",
|
||||
"react-confetti-explosion": "2.1.2",
|
||||
"react-day-picker": "8.10.1",
|
||||
@@ -136,6 +138,7 @@
|
||||
"react-i18next": "^15.1.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"recharts": "^2.12.7",
|
||||
"rotating-file-stream": "3.2.3",
|
||||
"slugify": "^1.6.6",
|
||||
"sonner": "^1.5.0",
|
||||
"ssh2": "1.15.0",
|
||||
@@ -149,21 +152,19 @@
|
||||
"ws": "8.16.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"zod": "^3.23.4",
|
||||
"zod-form-data": "^2.0.2",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@tailwindcss/typography": "0.5.16"
|
||||
"zod-form-data": "^2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/nodemailer": "^6.4.15",
|
||||
"@types/node-os-utils": "1.3.4",
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
"@types/bcrypt": "5.0.2",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/lodash": "4.17.4",
|
||||
"@types/node": "^18.17.0",
|
||||
"@types/node-os-utils": "1.3.4",
|
||||
"@types/node-schedule": "2.1.6",
|
||||
"@types/nodemailer": "^6.4.15",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@types/ssh2": "1.15.1",
|
||||
@@ -194,6 +195,8 @@
|
||||
]
|
||||
},
|
||||
"commitlint": {
|
||||
"extends": ["@commitlint/config-conventional"]
|
||||
"extends": [
|
||||
"@commitlint/config-conventional"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,12 @@ export default async function handler(
|
||||
res.status(301).json({ message: "Branch Not Match" });
|
||||
return;
|
||||
}
|
||||
} else if (sourceType === "gitea") {
|
||||
const branchName = extractBranchName(req.headers, req.body);
|
||||
if (!branchName || branchName !== application.giteaBranch) {
|
||||
res.status(301).json({ message: "Branch Not Match" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { findGiteaById } from '@dokploy/server';
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
try {
|
||||
if (req.method !== 'GET') {
|
||||
return res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
const { giteaId } = req.query;
|
||||
|
||||
if (!giteaId || Array.isArray(giteaId)) {
|
||||
return res.status(400).json({ error: 'Invalid Gitea provider ID' });
|
||||
}
|
||||
|
||||
let gitea;
|
||||
try {
|
||||
gitea = await findGiteaById(giteaId);
|
||||
} catch (findError) {
|
||||
console.error('Error finding Gitea provider:', findError);
|
||||
return res.status(404).json({ error: 'Failed to find Gitea provider' });
|
||||
}
|
||||
|
||||
if (!gitea.clientId || !gitea.redirectUri) {
|
||||
return res.status(400).json({
|
||||
error: 'Incomplete OAuth configuration',
|
||||
missingClientId: !gitea.clientId,
|
||||
missingRedirectUri: !gitea.redirectUri
|
||||
});
|
||||
}
|
||||
|
||||
// Use the state parameter to pass the giteaId
|
||||
// This is more secure than adding it to the redirect URI
|
||||
const state = giteaId;
|
||||
|
||||
const authorizationUrl = new URL(`${gitea.giteaUrl}/login/oauth/authorize`);
|
||||
authorizationUrl.searchParams.append('client_id', gitea.clientId);
|
||||
authorizationUrl.searchParams.append('response_type', 'code');
|
||||
authorizationUrl.searchParams.append('redirect_uri', gitea.redirectUri);
|
||||
authorizationUrl.searchParams.append('scope', 'read:user repo');
|
||||
authorizationUrl.searchParams.append('state', state);
|
||||
|
||||
// Redirect the user to the Gitea authorization page
|
||||
res.redirect(307, authorizationUrl.toString());
|
||||
} catch (error) {
|
||||
console.error('Error initiating Gitea OAuth flow:', error);
|
||||
return res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { findGiteaById, updateGitea } from '@dokploy/server';
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
try {
|
||||
console.log('Full Callback Request:', {
|
||||
query: req.query,
|
||||
method: req.method,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
const { code, state } = req.query;
|
||||
|
||||
// Verify received parameters
|
||||
console.log('Received Parameters:', {
|
||||
code: code ? 'Present' : 'Missing',
|
||||
state: state ? 'Present' : 'Missing'
|
||||
});
|
||||
|
||||
if (!code || Array.isArray(code)) {
|
||||
console.error('Invalid code:', code);
|
||||
return res.redirect(
|
||||
307,
|
||||
`/dashboard/settings/git-providers?error=${encodeURIComponent('Invalid authorization code')}`
|
||||
);
|
||||
}
|
||||
|
||||
// The state parameter now contains the giteaId
|
||||
if (!state || Array.isArray(state)) {
|
||||
console.error('Invalid state parameter:', state);
|
||||
return res.redirect(
|
||||
307,
|
||||
`/dashboard/settings/git-providers?error=${encodeURIComponent('Invalid state parameter')}`
|
||||
);
|
||||
}
|
||||
|
||||
// Extract the giteaId from the state parameter
|
||||
let giteaId: string;
|
||||
try {
|
||||
// The state could be a simple string or a JSON object
|
||||
if (state.startsWith('{') && state.endsWith('}')) {
|
||||
const stateObj = JSON.parse(state);
|
||||
giteaId = stateObj.giteaId;
|
||||
} else {
|
||||
giteaId = state;
|
||||
}
|
||||
|
||||
if (!giteaId) {
|
||||
throw new Error('giteaId not found in state parameter');
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.error('Error parsing state parameter:', parseError);
|
||||
return res.redirect(
|
||||
307,
|
||||
`/dashboard/settings/git-providers?error=${encodeURIComponent('Invalid state format')}`
|
||||
);
|
||||
}
|
||||
|
||||
let gitea;
|
||||
try {
|
||||
gitea = await findGiteaById(giteaId);
|
||||
} catch (findError) {
|
||||
console.error('Error finding Gitea provider:', findError);
|
||||
return res.redirect(
|
||||
307,
|
||||
`/dashboard/settings/git-providers?error=${encodeURIComponent('Failed to find Gitea provider')}`
|
||||
);
|
||||
}
|
||||
|
||||
// Extensive logging of Gitea provider details
|
||||
console.log('Gitea Provider Details:', {
|
||||
id: gitea.giteaId,
|
||||
url: gitea.giteaUrl,
|
||||
clientId: gitea.clientId ? 'Present' : 'Missing',
|
||||
clientSecret: gitea.clientSecret ? 'Present' : 'Missing',
|
||||
redirectUri: gitea.redirectUri
|
||||
});
|
||||
|
||||
// Validate required OAuth parameters
|
||||
if (!gitea.clientId || !gitea.clientSecret) {
|
||||
console.error('Missing OAuth configuration:', {
|
||||
hasClientId: !!gitea.clientId,
|
||||
hasClientSecret: !!gitea.clientSecret
|
||||
});
|
||||
return res.redirect(
|
||||
307,
|
||||
`/dashboard/settings/git-providers?error=${encodeURIComponent('Incomplete OAuth configuration')}`
|
||||
);
|
||||
}
|
||||
|
||||
const response = await fetch(`${gitea.giteaUrl}/login/oauth/access_token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
client_id: gitea.clientId as string,
|
||||
client_secret: gitea.clientSecret as string,
|
||||
code: code as string,
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: gitea.redirectUri || '',
|
||||
}),
|
||||
});
|
||||
|
||||
// Log raw response details
|
||||
const responseText = await response.text();
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = JSON.parse(responseText);
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse response:', {
|
||||
error: parseError,
|
||||
responseText
|
||||
});
|
||||
return res.redirect(
|
||||
307,
|
||||
`/dashboard/settings/git-providers?error=${encodeURIComponent('Failed to parse token response')}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Gitea token exchange failed:', {
|
||||
result,
|
||||
responseStatus: response.status
|
||||
});
|
||||
return res.redirect(
|
||||
307,
|
||||
`/dashboard/settings/git-providers?error=${encodeURIComponent(result.error || 'Token exchange failed')}`
|
||||
);
|
||||
}
|
||||
|
||||
// Validate token response
|
||||
if (!result.access_token) {
|
||||
console.error('Missing access token in response:', {
|
||||
fullResponse: result
|
||||
});
|
||||
return res.redirect(
|
||||
307,
|
||||
`/dashboard/settings/git-providers?error=${encodeURIComponent('No access token received')}`
|
||||
);
|
||||
}
|
||||
|
||||
const expiresAt = result.expires_in
|
||||
? Math.floor(Date.now() / 1000) + result.expires_in
|
||||
: null;
|
||||
|
||||
try {
|
||||
// Perform the update
|
||||
const updatedGitea = await updateGitea(gitea.giteaId, {
|
||||
accessToken: result.access_token,
|
||||
refreshToken: result.refresh_token,
|
||||
expiresAt,
|
||||
...(result.organizationName ? { organizationName: result.organizationName } : {}),
|
||||
});
|
||||
|
||||
// Log successful update
|
||||
console.log('Gitea provider updated successfully:', {
|
||||
hasAccessToken: !!updatedGitea.accessToken,
|
||||
hasRefreshToken: !!updatedGitea.refreshToken,
|
||||
expiresAt: updatedGitea.expiresAt
|
||||
});
|
||||
|
||||
return res.redirect(307, "/dashboard/settings/git-providers?connected=true");
|
||||
} catch (updateError) {
|
||||
console.error('Failed to update Gitea provider:', {
|
||||
error: updateError,
|
||||
giteaId: gitea.giteaId
|
||||
});
|
||||
return res.redirect(
|
||||
307,
|
||||
`/dashboard/settings/git-providers?error=${encodeURIComponent('Failed to store access token')}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Comprehensive Callback Error:', error);
|
||||
return res.redirect(
|
||||
307,
|
||||
`/dashboard/settings/git-providers?error=${encodeURIComponent('Internal server error')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { domainRouter } from "./routers/domain";
|
||||
import { gitProviderRouter } from "./routers/git-provider";
|
||||
import { githubRouter } from "./routers/github";
|
||||
import { gitlabRouter } from "./routers/gitlab";
|
||||
import { giteaRouter } from "./routers/gitea";
|
||||
import { mariadbRouter } from "./routers/mariadb";
|
||||
import { mongoRouter } from "./routers/mongo";
|
||||
import { mountRouter } from "./routers/mount";
|
||||
@@ -70,6 +71,7 @@ export const appRouter = createTRPCRouter({
|
||||
notification: notificationRouter,
|
||||
sshKey: sshRouter,
|
||||
gitProvider: gitProviderRouter,
|
||||
gitea: giteaRouter,
|
||||
bitbucket: bitbucketRouter,
|
||||
gitlab: gitlabRouter,
|
||||
github: githubRouter,
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
apiSaveGitProvider,
|
||||
apiSaveGithubProvider,
|
||||
apiSaveGitlabProvider,
|
||||
apiSaveGiteaProvider,
|
||||
apiUpdateApplication,
|
||||
applications,
|
||||
} from "@/server/db/schema";
|
||||
@@ -396,6 +397,32 @@ export const applicationRouter = createTRPCRouter({
|
||||
bitbucketId: input.bitbucketId,
|
||||
});
|
||||
|
||||
return true;
|
||||
}),
|
||||
saveGiteaProvider: protectedProcedure
|
||||
.input(apiSaveGiteaProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this gitea provider",
|
||||
});
|
||||
}
|
||||
await updateApplication(input.applicationId, {
|
||||
giteaRepository: input.giteaRepository,
|
||||
giteaOwner: input.giteaOwner,
|
||||
giteaBranch: input.giteaBranch,
|
||||
giteaBuildPath: input.giteaBuildPath,
|
||||
sourceType: "gitea",
|
||||
applicationStatus: "idle",
|
||||
giteaId: input.giteaId,
|
||||
giteaProjectId: input.giteaProjectId,
|
||||
giteaPathNamespace: input.giteaPathNamespace,
|
||||
});
|
||||
|
||||
return true;
|
||||
}),
|
||||
saveDockerProvider: protectedProcedure
|
||||
|
||||
@@ -12,6 +12,7 @@ export const gitProviderRouter = createTRPCRouter({
|
||||
gitlab: true,
|
||||
bitbucket: true,
|
||||
github: true,
|
||||
gitea: true,
|
||||
},
|
||||
orderBy: desc(gitProvider.createdAt),
|
||||
where: eq(gitProvider.organizationId, ctx.session.activeOrganizationId),
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
apiCreateGitea,
|
||||
apiFindOneGitea,
|
||||
apiGiteaTestConnection,
|
||||
apiUpdateGitea,
|
||||
apiFindGiteaBranches,
|
||||
} from "@/server/db/schema";
|
||||
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
createGitea,
|
||||
findGiteaById,
|
||||
haveGiteaRequirements,
|
||||
testGiteaConnection,
|
||||
updateGitProvider,
|
||||
updateGitea,
|
||||
getGiteaBranches,
|
||||
getGiteaRepositories,
|
||||
} from "@dokploy/server";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
// Gitea Router
|
||||
export const giteaRouter = createTRPCRouter({
|
||||
// Create a new Gitea provider
|
||||
create: protectedProcedure
|
||||
.input(apiCreateGitea)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createGitea(input, ctx.session.activeOrganizationId);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating this Gitea provider",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
// Fetch a specific Gitea provider by ID
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneGitea)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const giteaProvider = await findGiteaById(input.giteaId);
|
||||
if (
|
||||
giteaProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this Gitea provider",
|
||||
});
|
||||
}
|
||||
return giteaProvider;
|
||||
}),
|
||||
|
||||
// Fetch all Gitea providers for the active organization
|
||||
giteaProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
let result = await db.query.gitea.findMany({
|
||||
with: {
|
||||
gitProvider: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Filter by organization ID
|
||||
result = result.filter(
|
||||
(provider) =>
|
||||
provider.gitProvider.organizationId === ctx.session.activeOrganizationId
|
||||
);
|
||||
|
||||
// Filter providers that meet the requirements
|
||||
const filtered = result
|
||||
.filter((provider) => haveGiteaRequirements(provider))
|
||||
.map((provider) => {
|
||||
return {
|
||||
giteaId: provider.giteaId,
|
||||
gitProvider: {
|
||||
...provider.gitProvider,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}),
|
||||
|
||||
// Fetch repositories from Gitea provider
|
||||
getGiteaRepositories: protectedProcedure
|
||||
.input(apiFindOneGitea)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { giteaId } = input;
|
||||
|
||||
if (!giteaId) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Gitea provider ID is required.",
|
||||
});
|
||||
}
|
||||
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
if (
|
||||
giteaProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this Gitea provider",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Call the service layer function to get repositories
|
||||
console.log('Calling getGiteaRepositories with giteaId:', giteaId);
|
||||
return await getGiteaRepositories(giteaId);
|
||||
} catch (error) {
|
||||
console.error('Error fetching Gitea repositories:', error);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
// Fetch branches of a specific Gitea repository
|
||||
getGiteaBranches: protectedProcedure
|
||||
.input(apiFindGiteaBranches)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { giteaId, owner, repositoryName } = input;
|
||||
|
||||
if (!giteaId || !owner || !repositoryName) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Gitea provider ID, owner, and repository name are required.",
|
||||
});
|
||||
}
|
||||
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
if (
|
||||
giteaProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this Gitea provider",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Call the service layer function with the required parameters
|
||||
console.log('Calling getGiteaBranches with:', {
|
||||
giteaId,
|
||||
owner,
|
||||
repo: repositoryName
|
||||
});
|
||||
|
||||
return await getGiteaBranches({
|
||||
giteaId,
|
||||
owner,
|
||||
repo: repositoryName,
|
||||
id: 0 // Provide a default value for the optional id
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching Gitea branches:', error);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
// Test connection to Gitea provider
|
||||
testConnection: protectedProcedure
|
||||
.input(apiGiteaTestConnection)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
// Ensure giteaId is always a non-empty string
|
||||
const giteaId = input.giteaId ?? '';
|
||||
|
||||
try {
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
if (
|
||||
giteaProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this Gitea provider",
|
||||
});
|
||||
}
|
||||
|
||||
const result = await testGiteaConnection({
|
||||
giteaId,
|
||||
});
|
||||
|
||||
return `Found ${result} repositories`;
|
||||
} catch (error) {
|
||||
console.error('Gitea connection test error:', error);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
// Update an existing Gitea provider
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateGitea)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const giteaProvider = await findGiteaById(input.giteaId);
|
||||
if (
|
||||
giteaProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not allowed to access this Gitea provider",
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Updating Gitea provider:', input);
|
||||
|
||||
if (input.name) {
|
||||
await updateGitProvider(input.gitProviderId, {
|
||||
name: input.name,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
|
||||
await updateGitea(input.giteaId, {
|
||||
...input,
|
||||
});
|
||||
} else {
|
||||
await updateGitea(input.giteaId, {
|
||||
...input,
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
});
|
||||
@@ -476,6 +476,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
"bitbucket",
|
||||
"github",
|
||||
"gitlab",
|
||||
"gitea",
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -75,6 +75,9 @@ export function generate(schema: Schema): Template {
|
||||
"",
|
||||
"CLIENT_ID_GITLAB_LOGIN=",
|
||||
"CLIENT_SECRET_GITLAB_LOGIN=",
|
||||
"",
|
||||
"CLIENT_ID_GITEA_LOGIN=",
|
||||
"CLIENT_SECRET_GITEA_LOGIN=",
|
||||
"",
|
||||
"CAPTCHA_SECRET=",
|
||||
"",
|
||||
|
||||
@@ -35,11 +35,11 @@
|
||||
"@ai-sdk/mistral": "^1.0.6",
|
||||
"@ai-sdk/openai": "^1.0.12",
|
||||
"@ai-sdk/openai-compatible": "^0.0.13",
|
||||
"@better-auth/utils":"0.2.3",
|
||||
"@oslojs/encoding":"1.1.0",
|
||||
"@oslojs/crypto":"1.0.1",
|
||||
"drizzle-dbml-generator":"0.10.0",
|
||||
"better-auth":"1.2.0",
|
||||
"@better-auth/utils": "0.2.3",
|
||||
"@oslojs/encoding": "1.1.0",
|
||||
"@oslojs/crypto": "1.0.1",
|
||||
"drizzle-dbml-generator": "0.10.0",
|
||||
"better-auth": "1.2.0",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@lucia-auth/adapter-drizzle": "1.0.7",
|
||||
"@octokit/auth-app": "^6.0.4",
|
||||
@@ -53,7 +53,7 @@
|
||||
"date-fns": "3.6.0",
|
||||
"dockerode": "4.0.2",
|
||||
"dotenv": "16.4.5",
|
||||
"drizzle-orm": "^0.39.1",
|
||||
"drizzle-orm": "^0.39.1",
|
||||
"drizzle-zod": "0.5.1",
|
||||
"hi-base32": "^0.5.1",
|
||||
"js-yaml": "4.1.0",
|
||||
|
||||
@@ -15,6 +15,7 @@ import { deployments } from "./deployment";
|
||||
import { domains } from "./domain";
|
||||
import { github } from "./github";
|
||||
import { gitlab } from "./gitlab";
|
||||
import { gitea } from "./gitea";
|
||||
import { mounts } from "./mount";
|
||||
import { ports } from "./port";
|
||||
import { previewDeployments } from "./preview-deployments";
|
||||
@@ -33,6 +34,7 @@ export const sourceType = pgEnum("sourceType", [
|
||||
"github",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
"gitea",
|
||||
"drop",
|
||||
]);
|
||||
|
||||
@@ -152,6 +154,13 @@ export const applications = pgTable("application", {
|
||||
gitlabBranch: text("gitlabBranch"),
|
||||
gitlabBuildPath: text("gitlabBuildPath").default("/"),
|
||||
gitlabPathNamespace: text("gitlabPathNamespace"),
|
||||
// Gitea
|
||||
giteaProjectId: integer("giteaProjectId"),
|
||||
giteaRepository: text("giteaRepository"),
|
||||
giteaOwner: text("giteaOwner"),
|
||||
giteaBranch: text("giteaBranch"),
|
||||
giteaBuildPath: text("giteaBuildPath").default("/"),
|
||||
giteaPathNamespace: text("giteaPathNamespace"),
|
||||
// Bitbucket
|
||||
bitbucketRepository: text("bitbucketRepository"),
|
||||
bitbucketOwner: text("bitbucketOwner"),
|
||||
@@ -209,6 +218,9 @@ export const applications = pgTable("application", {
|
||||
gitlabId: text("gitlabId").references(() => gitlab.gitlabId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
giteaId: text("giteaId").references(() => gitea.giteaId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
@@ -246,6 +258,10 @@ export const applicationsRelations = relations(
|
||||
fields: [applications.gitlabId],
|
||||
references: [gitlab.gitlabId],
|
||||
}),
|
||||
gitea: one(gitea, {
|
||||
fields: [applications.giteaId],
|
||||
references: [gitea.giteaId],
|
||||
}),
|
||||
bitbucket: one(bitbucket, {
|
||||
fields: [applications.bitbucketId],
|
||||
references: [bitbucket.bitbucketId],
|
||||
@@ -376,7 +392,7 @@ const createSchema = createInsertSchema(applications, {
|
||||
customGitUrl: z.string().optional(),
|
||||
buildPath: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
sourceType: z.enum(["github", "docker", "git"]).optional(),
|
||||
sourceType: z.enum(["github", "docker", "git", "gitlab", "bitbucket", "gitea", "drop"]).optional(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
buildType: z.enum([
|
||||
"dockerfile",
|
||||
@@ -475,6 +491,19 @@ export const apiSaveBitbucketProvider = createSchema
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveGiteaProvider = createSchema
|
||||
.pick({
|
||||
applicationId: true,
|
||||
giteaBranch: true,
|
||||
giteaBuildPath: true,
|
||||
giteaOwner: true,
|
||||
giteaRepository: true,
|
||||
giteaId: true,
|
||||
giteaProjectId: true,
|
||||
giteaPathNamespace: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveDockerProvider = createSchema
|
||||
.pick({
|
||||
dockerImage: true,
|
||||
@@ -518,4 +547,4 @@ export const apiUpdateApplication = createSchema
|
||||
.extend({
|
||||
applicationId: z.string().min(1),
|
||||
})
|
||||
.omit({ serverId: true });
|
||||
.omit({ serverId: true });
|
||||
@@ -14,12 +14,14 @@ import { server } from "./server";
|
||||
import { applicationStatus } from "./shared";
|
||||
import { sshKeys } from "./ssh-key";
|
||||
import { generateAppName } from "./utils";
|
||||
import { gitea } from "./gitea";
|
||||
|
||||
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
||||
"git",
|
||||
"github",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
"gitea",
|
||||
"raw",
|
||||
]);
|
||||
|
||||
@@ -55,6 +57,10 @@ export const compose = pgTable("compose", {
|
||||
bitbucketRepository: text("bitbucketRepository"),
|
||||
bitbucketOwner: text("bitbucketOwner"),
|
||||
bitbucketBranch: text("bitbucketBranch"),
|
||||
// Gitea
|
||||
giteaRepository: text("giteaRepository"),
|
||||
giteaOwner: text("giteaOwner"),
|
||||
giteaBranch: text("giteaBranch"),
|
||||
// Git
|
||||
customGitUrl: text("customGitUrl"),
|
||||
customGitBranch: text("customGitBranch"),
|
||||
@@ -87,6 +93,9 @@ export const compose = pgTable("compose", {
|
||||
bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
giteaId: text("giteaId").references(() => gitea.giteaId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
@@ -116,6 +125,10 @@ export const composeRelations = relations(compose, ({ one, many }) => ({
|
||||
fields: [compose.bitbucketId],
|
||||
references: [bitbucket.bitbucketId],
|
||||
}),
|
||||
gitea: one(gitea, {
|
||||
fields: [compose.giteaId],
|
||||
references: [gitea.giteaId],
|
||||
}),
|
||||
server: one(server, {
|
||||
fields: [compose.serverId],
|
||||
references: [server.serverId],
|
||||
|
||||
@@ -7,11 +7,13 @@ import { organization } from "./account";
|
||||
import { bitbucket } from "./bitbucket";
|
||||
import { github } from "./github";
|
||||
import { gitlab } from "./gitlab";
|
||||
import { gitea } from "./gitea";
|
||||
|
||||
export const gitProviderType = pgEnum("gitProviderType", [
|
||||
"github",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
"gitea",
|
||||
]);
|
||||
|
||||
export const gitProvider = pgTable("git_provider", {
|
||||
@@ -42,6 +44,10 @@ export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
||||
fields: [gitProvider.gitProviderId],
|
||||
references: [bitbucket.gitProviderId],
|
||||
}),
|
||||
gitea: one(gitea, {
|
||||
fields: [gitProvider.gitProviderId],
|
||||
references: [gitea.gitProviderId],
|
||||
}),
|
||||
organization: one(organization, {
|
||||
fields: [gitProvider.organizationId],
|
||||
references: [organization.id],
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { gitProvider } from "./git-provider";
|
||||
|
||||
// Gitea table definition
|
||||
export const gitea = pgTable("gitea", {
|
||||
giteaId: text("giteaId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()), // Using nanoid for unique ID
|
||||
giteaUrl: text("giteaUrl").default("https://gitea.com").notNull(), // Default URL for Gitea
|
||||
redirectUri: text("redirect_uri"),
|
||||
clientId: text("client_id"),
|
||||
clientSecret: text("client_secret"),
|
||||
gitProviderId: text("gitProviderId")
|
||||
.notNull()
|
||||
.references(() => gitProvider.gitProviderId, { onDelete: "cascade" }),
|
||||
giteaUsername: text("gitea_username"),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
expiresAt: integer("expires_at"),
|
||||
scopes: text("scopes").default('repo,repo:status,read:user,read:org'),
|
||||
lastAuthenticatedAt: integer("last_authenticated_at"),
|
||||
});
|
||||
|
||||
// Gitea relations with gitProvider
|
||||
export const giteaProviderRelations = relations(gitea, ({ one }) => ({
|
||||
gitProvider: one(gitProvider, {
|
||||
fields: [gitea.gitProviderId],
|
||||
references: [gitProvider.gitProviderId],
|
||||
}),
|
||||
}));
|
||||
|
||||
// Create schema for Gitea
|
||||
const createSchema = createInsertSchema(gitea);
|
||||
|
||||
// API schema for creating a Gitea instance
|
||||
export const apiCreateGitea = createSchema.extend({
|
||||
clientId: z.string().optional(),
|
||||
clientSecret: z.string().optional(),
|
||||
gitProviderId: z.string().optional(),
|
||||
redirectUri: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
giteaUrl: z.string().min(1),
|
||||
giteaUsername: z.string().optional(),
|
||||
accessToken: z.string().optional(),
|
||||
refreshToken: z.string().optional(),
|
||||
expiresAt: z.number().optional(),
|
||||
organizationName: z.string().optional(),
|
||||
scopes: z.string().optional(),
|
||||
lastAuthenticatedAt: z.number().optional(),
|
||||
});
|
||||
|
||||
// API schema for finding one Gitea instance
|
||||
export const apiFindOneGitea = createSchema
|
||||
.extend({
|
||||
giteaId: z.string().min(1),
|
||||
})
|
||||
.pick({ giteaId: true });
|
||||
|
||||
// API schema for testing Gitea connection
|
||||
export const apiGiteaTestConnection = createSchema
|
||||
.extend({
|
||||
organizationName: z.string().optional(),
|
||||
})
|
||||
.pick({ giteaId: true, organizationName: true });
|
||||
|
||||
export type ApiGiteaTestConnection = z.infer<typeof apiGiteaTestConnection>;
|
||||
|
||||
// API schema for finding branches in Gitea
|
||||
export const apiFindGiteaBranches = z.object({
|
||||
id: z.number().optional(),
|
||||
owner: z.string().min(1),
|
||||
repositoryName: z.string().min(1),
|
||||
giteaId: z.string().optional(),
|
||||
});
|
||||
|
||||
// API schema for updating Gitea instance
|
||||
export const apiUpdateGitea = createSchema.extend({
|
||||
clientId: z.string().optional(),
|
||||
clientSecret: z.string().optional(),
|
||||
redirectUri: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
giteaId: z.string().min(1),
|
||||
giteaUrl: z.string().min(1),
|
||||
giteaUsername: z.string().optional(),
|
||||
accessToken: z.string().optional(),
|
||||
refreshToken: z.string().optional(),
|
||||
expiresAt: z.number().optional(),
|
||||
organizationName: z.string().optional(),
|
||||
scopes: z.string().optional(),
|
||||
lastAuthenticatedAt: z.number().optional(),
|
||||
});
|
||||
@@ -25,6 +25,7 @@ export * from "./git-provider";
|
||||
export * from "./bitbucket";
|
||||
export * from "./github";
|
||||
export * from "./gitlab";
|
||||
export * from "./gitea";
|
||||
export * from "./server";
|
||||
export * from "./utils";
|
||||
export * from "./preview-deployments";
|
||||
|
||||
@@ -46,6 +46,7 @@ enum gitProviderType {
|
||||
github
|
||||
gitlab
|
||||
bitbucket
|
||||
gitea
|
||||
}
|
||||
|
||||
enum mountType {
|
||||
@@ -98,6 +99,7 @@ enum sourceType {
|
||||
github
|
||||
gitlab
|
||||
bitbucket
|
||||
gitea
|
||||
drop
|
||||
}
|
||||
|
||||
@@ -106,6 +108,7 @@ enum sourceTypeCompose {
|
||||
github
|
||||
gitlab
|
||||
bitbucket
|
||||
gitea
|
||||
raw
|
||||
}
|
||||
|
||||
@@ -205,6 +208,7 @@ table application {
|
||||
githubId text
|
||||
gitlabId text
|
||||
bitbucketId text
|
||||
giteaId text
|
||||
serverId text
|
||||
}
|
||||
|
||||
@@ -279,6 +283,9 @@ table compose {
|
||||
bitbucketRepository text
|
||||
bitbucketOwner text
|
||||
bitbucketBranch text
|
||||
giteaRepository text
|
||||
giteaOwner text
|
||||
giteaBranch text
|
||||
customGitUrl text
|
||||
customGitBranch text
|
||||
customGitSSHKeyId text
|
||||
@@ -293,6 +300,7 @@ table compose {
|
||||
githubId text
|
||||
gitlabId text
|
||||
bitbucketId text
|
||||
giteaId text
|
||||
serverId text
|
||||
}
|
||||
|
||||
@@ -388,6 +396,20 @@ table gitlab {
|
||||
gitProviderId text [not null]
|
||||
}
|
||||
|
||||
table gitea {
|
||||
giteaId text [pk, not null]
|
||||
giteaUrl text [not null, default: 'https://gitea.com']
|
||||
redirect_uri text
|
||||
client_id text [not null]
|
||||
client_secret text [not null]
|
||||
access_token text
|
||||
refresh_token text
|
||||
expires_at integer
|
||||
gitProviderId text [not null]
|
||||
scopes text [default: 'repo,repo:status,read:user,read:org']
|
||||
last_authenticated_at integer
|
||||
}
|
||||
|
||||
table gotify {
|
||||
gotifyId text [pk, not null]
|
||||
serverUrl text [not null]
|
||||
@@ -819,6 +841,8 @@ ref: github.gitProviderId - git_provider.gitProviderId
|
||||
|
||||
ref: gitlab.gitProviderId - git_provider.gitProviderId
|
||||
|
||||
ref: gitea.gitProviderId - git_provider.gitProviderId
|
||||
|
||||
ref: git_provider.userId - user.id
|
||||
|
||||
ref: mariadb.projectId > project.projectId
|
||||
|
||||
@@ -28,6 +28,7 @@ export * from "./services/git-provider";
|
||||
export * from "./services/bitbucket";
|
||||
export * from "./services/github";
|
||||
export * from "./services/gitlab";
|
||||
export * from "./services/gitea";
|
||||
export * from "./services/server";
|
||||
export * from "./services/application";
|
||||
|
||||
@@ -89,6 +90,7 @@ export * from "./utils/providers/docker";
|
||||
export * from "./utils/providers/git";
|
||||
export * from "./utils/providers/github";
|
||||
export * from "./utils/providers/gitlab";
|
||||
export * from "./utils/providers/gitea";
|
||||
export * from "./utils/providers/raw";
|
||||
|
||||
export * from "./utils/servers/remote-docker";
|
||||
|
||||
@@ -34,6 +34,10 @@ import {
|
||||
cloneGitlabRepository,
|
||||
getGitlabCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/gitlab";
|
||||
import {
|
||||
cloneGiteaRepository,
|
||||
getGiteaCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/gitea";
|
||||
import { createTraefikConfig } from "@dokploy/server/utils/traefik/application";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
@@ -111,6 +115,7 @@ export const findApplicationById = async (applicationId: string) => {
|
||||
gitlab: true,
|
||||
github: true,
|
||||
bitbucket: true,
|
||||
gitea: true,
|
||||
server: true,
|
||||
previewDeployments: true,
|
||||
},
|
||||
@@ -197,6 +202,9 @@ export const deployApplication = async ({
|
||||
} else if (application.sourceType === "gitlab") {
|
||||
await cloneGitlabRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "gitea") {
|
||||
await cloneGiteaRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "bitbucket") {
|
||||
await cloneBitbucketRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
@@ -325,6 +333,11 @@ export const deployRemoteApplication = async ({
|
||||
application,
|
||||
deployment.logPath,
|
||||
);
|
||||
} else if (application.sourceType === "gitea") {
|
||||
command += await getGiteaCloneCommand(
|
||||
application,
|
||||
deployment.logPath,
|
||||
);
|
||||
} else if (application.sourceType === "git") {
|
||||
command += await getCustomGitCloneCommand(
|
||||
application,
|
||||
|
||||
@@ -37,6 +37,10 @@ import {
|
||||
cloneGitlabRepository,
|
||||
getGitlabCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/gitlab";
|
||||
import {
|
||||
cloneGiteaRepository,
|
||||
getGiteaCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/gitea";
|
||||
import {
|
||||
createComposeFile,
|
||||
getCreateComposeFileCommand,
|
||||
@@ -125,6 +129,7 @@ export const findComposeById = async (composeId: string) => {
|
||||
github: true,
|
||||
gitlab: true,
|
||||
bitbucket: true,
|
||||
gitea: true,
|
||||
server: true,
|
||||
},
|
||||
});
|
||||
@@ -232,9 +237,11 @@ export const deployCompose = async ({
|
||||
await cloneBitbucketRepository(compose, deployment.logPath, true);
|
||||
} else if (compose.sourceType === "git") {
|
||||
await cloneGitRepository(compose, deployment.logPath, true);
|
||||
} else if (compose.sourceType === "gitea") {
|
||||
await cloneGiteaRepository(compose, deployment.logPath, true);
|
||||
} else if (compose.sourceType === "raw") {
|
||||
await createComposeFile(compose, deployment.logPath);
|
||||
}
|
||||
}
|
||||
await buildCompose(compose, deployment.logPath);
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateCompose(composeId, {
|
||||
@@ -364,6 +371,12 @@ export const deployRemoteCompose = async ({
|
||||
);
|
||||
} else if (compose.sourceType === "raw") {
|
||||
command += getCreateComposeFileCommand(compose, deployment.logPath);
|
||||
} else if (compose.sourceType === "gitea") {
|
||||
command += await getGiteaCloneCommand(
|
||||
compose,
|
||||
deployment.logPath,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type apiCreateGitea,
|
||||
gitProvider,
|
||||
gitea,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
export type Gitea = typeof gitea.$inferSelect;
|
||||
|
||||
export const createGitea = async (
|
||||
input: typeof apiCreateGitea._type,
|
||||
organizationId: string,
|
||||
) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
// Insert new Git provider (Gitea)
|
||||
const newGitProvider = await tx
|
||||
.insert(gitProvider)
|
||||
.values({
|
||||
providerType: "gitea", // Set providerType to 'gitea'
|
||||
organizationId: organizationId,
|
||||
name: input.name,
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
|
||||
if (!newGitProvider) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the Git provider",
|
||||
});
|
||||
}
|
||||
|
||||
// Insert the Gitea data into the `gitea` table
|
||||
await tx
|
||||
.insert(gitea)
|
||||
.values({
|
||||
...input,
|
||||
gitProviderId: newGitProvider?.gitProviderId,
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
});
|
||||
};
|
||||
|
||||
export const findGiteaById = async (giteaId: string) => {
|
||||
try {
|
||||
|
||||
const giteaProviderResult = await db.query.gitea.findFirst({
|
||||
where: eq(gitea.giteaId, giteaId),
|
||||
with: {
|
||||
gitProvider: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!giteaProviderResult) {
|
||||
console.error('No Gitea Provider found:', { giteaId });
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
return giteaProviderResult;
|
||||
} catch (error) {
|
||||
console.error('Error finding Gitea Provider:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateGitea = async (
|
||||
giteaId: string,
|
||||
input: Partial<Gitea>,
|
||||
) => {
|
||||
console.log('Updating Gitea Provider:', {
|
||||
giteaId,
|
||||
updateData: {
|
||||
accessTokenPresent: !!input.accessToken,
|
||||
refreshTokenPresent: !!input.refreshToken,
|
||||
expiresAt: input.expiresAt,
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const updateResult = await db
|
||||
.update(gitea)
|
||||
.set(input)
|
||||
.where(eq(gitea.giteaId, giteaId))
|
||||
.returning();
|
||||
|
||||
// Explicitly type the result and handle potential undefined
|
||||
const result = updateResult[0] as Gitea | undefined;
|
||||
|
||||
if (!result) {
|
||||
console.error('No rows were updated', { giteaId, input });
|
||||
throw new Error(`Failed to update Gitea provider with ID ${giteaId}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error updating Gitea provider:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -62,6 +62,7 @@ export const findApplicationByPreview = async (applicationId: string) => {
|
||||
gitlab: true,
|
||||
github: true,
|
||||
bitbucket: true,
|
||||
gitea: true,
|
||||
server: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -112,99 +112,118 @@ export const getBuildCommand = (
|
||||
|
||||
export const mechanizeDockerContainer = async (
|
||||
application: ApplicationNested,
|
||||
) => {
|
||||
) => {
|
||||
console.log(`Starting to mechanize Docker container for ${application.appName}`);
|
||||
|
||||
const {
|
||||
appName,
|
||||
env,
|
||||
mounts,
|
||||
cpuLimit,
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuReservation,
|
||||
command,
|
||||
ports,
|
||||
appName,
|
||||
env,
|
||||
mounts,
|
||||
cpuLimit,
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuReservation,
|
||||
command,
|
||||
ports,
|
||||
} = application;
|
||||
|
||||
|
||||
const resources = calculateResources({
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
|
||||
|
||||
const volumesMount = generateVolumeMounts(mounts);
|
||||
|
||||
|
||||
const {
|
||||
HealthCheck,
|
||||
RestartPolicy,
|
||||
Placement,
|
||||
Labels,
|
||||
Mode,
|
||||
RollbackConfig,
|
||||
UpdateConfig,
|
||||
Networks,
|
||||
HealthCheck,
|
||||
RestartPolicy,
|
||||
Placement,
|
||||
Labels,
|
||||
Mode,
|
||||
RollbackConfig,
|
||||
UpdateConfig,
|
||||
Networks,
|
||||
} = generateConfigContainer(application);
|
||||
|
||||
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const filesMount = generateFileMounts(appName, application);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
application.project.env,
|
||||
env,
|
||||
application.project.env,
|
||||
);
|
||||
|
||||
|
||||
const image = getImageName(application);
|
||||
const authConfig = getAuthConfig(application);
|
||||
const docker = await getRemoteDocker(application.serverId);
|
||||
|
||||
|
||||
const settings: CreateServiceOptions = {
|
||||
authconfig: authConfig,
|
||||
Name: appName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
HealthCheck,
|
||||
Image: image,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
...(command
|
||||
? {
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", command],
|
||||
}
|
||||
: {}),
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
RestartPolicy,
|
||||
Placement,
|
||||
Resources: {
|
||||
...resources,
|
||||
},
|
||||
authconfig: authConfig,
|
||||
Name: appName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
HealthCheck,
|
||||
Image: image,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
...(command
|
||||
? {
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", command],
|
||||
}
|
||||
: {}),
|
||||
Labels,
|
||||
},
|
||||
Mode,
|
||||
RollbackConfig,
|
||||
EndpointSpec: {
|
||||
Ports: ports.map((port) => ({
|
||||
Protocol: port.protocol,
|
||||
TargetPort: port.targetPort,
|
||||
PublishedPort: port.publishedPort,
|
||||
})),
|
||||
Networks,
|
||||
RestartPolicy,
|
||||
Placement,
|
||||
Resources: {
|
||||
...resources,
|
||||
},
|
||||
UpdateConfig,
|
||||
},
|
||||
Mode,
|
||||
RollbackConfig,
|
||||
EndpointSpec: {
|
||||
Ports: ports.map((port) => ({
|
||||
Protocol: port.protocol,
|
||||
TargetPort: port.targetPort,
|
||||
PublishedPort: port.publishedPort,
|
||||
})),
|
||||
},
|
||||
UpdateConfig,
|
||||
};
|
||||
|
||||
|
||||
try {
|
||||
const service = docker.getService(appName);
|
||||
const inspect = await service.inspect();
|
||||
await service.update({
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
TaskTemplate: {
|
||||
...settings.TaskTemplate,
|
||||
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
|
||||
},
|
||||
});
|
||||
} catch (_error) {
|
||||
console.log(`Attempting to find existing service: ${appName}`);
|
||||
const service = docker.getService(appName);
|
||||
const inspect = await service.inspect();
|
||||
console.log(`Found existing service, updating: ${appName}`);
|
||||
|
||||
await service.update({
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
TaskTemplate: {
|
||||
...settings.TaskTemplate,
|
||||
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
|
||||
},
|
||||
});
|
||||
console.log(`Service updated successfully: ${appName}`);
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.log(`Service not found or error: ${errorMessage}`);
|
||||
console.log(`Creating new service: ${appName}`);
|
||||
|
||||
try {
|
||||
await docker.createService(settings);
|
||||
console.log(`Service created successfully: ${appName}`);
|
||||
} catch (createError: unknown) {
|
||||
const createErrorMessage = createError instanceof Error
|
||||
? createError.message
|
||||
: 'Unknown error';
|
||||
console.error(`Failed to create service: ${createErrorMessage}`);
|
||||
throw createError;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export const buildNixpacks = async (
|
||||
for (const env of envVariables) {
|
||||
args.push("--env", env);
|
||||
}
|
||||
|
||||
|
||||
if (publishDirectory) {
|
||||
/* No need for any start command, since we'll use nginx later on */
|
||||
args.push("--no-error-without-start");
|
||||
@@ -81,7 +81,18 @@ export const buildNixpacks = async (
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
await spawnAsync("docker", ["rm", buildContainerId], writeToStream);
|
||||
// Only try to remove the container if it might exist
|
||||
try {
|
||||
await spawnAsync("docker", ["rm", buildContainerId], writeToStream);
|
||||
} catch (rmError) {
|
||||
// Ignore errors from container removal
|
||||
const errorMessage = rmError instanceof Error
|
||||
? rmError.message
|
||||
: 'Unknown container cleanup error';
|
||||
|
||||
// Just log it but don't let it cause another error
|
||||
writeToStream(`Container cleanup attempt: ${errorMessage}\n`);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ import {
|
||||
cloneRawGitlabRepository,
|
||||
cloneRawGitlabRepositoryRemote,
|
||||
} from "../providers/gitlab";
|
||||
import {
|
||||
cloneRawGiteaRepository,
|
||||
cloneRawGiteaRepositoryRemote,
|
||||
} from "../providers/gitea";
|
||||
import {
|
||||
createComposeFileRaw,
|
||||
createComposeFileRawRemote,
|
||||
@@ -44,6 +48,8 @@ export const cloneCompose = async (compose: Compose) => {
|
||||
await cloneRawBitbucketRepository(compose);
|
||||
} else if (compose.sourceType === "git") {
|
||||
await cloneGitRawRepository(compose);
|
||||
} else if (compose.sourceType === "gitea") {
|
||||
await cloneRawGiteaRepository(compose);
|
||||
} else if (compose.sourceType === "raw") {
|
||||
await createComposeFileRaw(compose);
|
||||
}
|
||||
@@ -58,6 +64,8 @@ export const cloneComposeRemote = async (compose: Compose) => {
|
||||
await cloneRawBitbucketRepositoryRemote(compose);
|
||||
} else if (compose.sourceType === "git") {
|
||||
await cloneRawGitRepositoryRemote(compose);
|
||||
} else if (compose.sourceType === "gitea") {
|
||||
await cloneRawGiteaRepository(compose);
|
||||
} else if (compose.sourceType === "raw") {
|
||||
await createComposeFileRawRemote(compose);
|
||||
}
|
||||
|
||||
@@ -113,6 +113,8 @@ export const getBuildAppDirectory = (application: Application) => {
|
||||
buildPath = application?.gitlabBuildPath || "";
|
||||
} else if (sourceType === "bitbucket") {
|
||||
buildPath = application?.bitbucketBuildPath || "";
|
||||
} else if (sourceType === "gitea") {
|
||||
buildPath = application?.giteaBuildPath || "";
|
||||
} else if (sourceType === "drop") {
|
||||
buildPath = application?.dropBuildPath || "";
|
||||
} else if (sourceType === "git") {
|
||||
|
||||
@@ -0,0 +1,582 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import fs from "fs/promises";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { findGiteaById, updateGitea } from "@dokploy/server/services/gitea";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
/**
|
||||
* Wrapper function to maintain compatibility with the existing implementation
|
||||
*/
|
||||
export const fetchGiteaBranches = async (
|
||||
giteaId: string,
|
||||
repoFullName: string
|
||||
) => {
|
||||
// Ensure owner and repo are non-empty strings
|
||||
const parts = repoFullName.split('/');
|
||||
|
||||
// Validate that we have exactly two parts
|
||||
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
||||
throw new Error(`Invalid repository name format: ${repoFullName}. Expected format: owner/repo`);
|
||||
}
|
||||
|
||||
const [owner, repo] = parts;
|
||||
|
||||
// Call the existing getGiteaBranches function with the correct object structure
|
||||
return await getGiteaBranches({
|
||||
giteaId,
|
||||
owner,
|
||||
repo,
|
||||
id: 0 // Provide a default value for optional id
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to check if the required fields are filled for Gitea repository operations
|
||||
*/
|
||||
export const getErrorCloneRequirements = (entity: {
|
||||
giteaRepository?: string | null;
|
||||
giteaOwner?: string | null;
|
||||
giteaBranch?: string | null;
|
||||
giteaPathNamespace?: string | null
|
||||
}) => {
|
||||
const reasons: string[] = [];
|
||||
const { giteaBranch, giteaOwner, giteaRepository, giteaPathNamespace } = entity;
|
||||
|
||||
if (!giteaRepository) reasons.push("1. Repository not assigned.");
|
||||
if (!giteaOwner) reasons.push("2. Owner not specified.");
|
||||
if (!giteaBranch) reasons.push("3. Branch not defined.");
|
||||
if (!giteaPathNamespace) reasons.push("4. Path namespace not defined.");
|
||||
|
||||
return reasons;
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to refresh the Gitea token if expired
|
||||
*/
|
||||
export const refreshGiteaToken = async (giteaProviderId: string) => {
|
||||
try {
|
||||
console.log('Attempting to refresh Gitea token:', {
|
||||
giteaProviderId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
const giteaProvider = await findGiteaById(giteaProviderId);
|
||||
|
||||
if (!giteaProvider?.clientId || !giteaProvider?.clientSecret || !giteaProvider?.refreshToken) {
|
||||
console.warn('Missing credentials for token refresh');
|
||||
return giteaProvider?.accessToken || null;
|
||||
}
|
||||
|
||||
const tokenEndpoint = `${giteaProvider.giteaUrl}/login/oauth/access_token`;
|
||||
const params = new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: giteaProvider.refreshToken,
|
||||
client_id: giteaProvider.clientId,
|
||||
client_secret: giteaProvider.clientSecret,
|
||||
});
|
||||
|
||||
console.log('Token Endpoint:', tokenEndpoint);
|
||||
console.log('Request Parameters:', params.toString());
|
||||
|
||||
const response = await fetch(tokenEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: params.toString(),
|
||||
});
|
||||
|
||||
console.log('Token Refresh Response:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('Token Refresh Failed:', errorText);
|
||||
return giteaProvider?.accessToken || null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const { access_token, refresh_token, expires_in } = data;
|
||||
|
||||
if (!access_token) {
|
||||
console.error('Missing access token in refresh response');
|
||||
return giteaProvider?.accessToken || null;
|
||||
}
|
||||
|
||||
const expiresAt = Date.now() + ((expires_in || 3600) * 1000);
|
||||
const expiresAtSeconds = Math.floor(expiresAt / 1000);
|
||||
|
||||
await updateGitea(giteaProviderId, {
|
||||
accessToken: access_token,
|
||||
refreshToken: refresh_token || giteaProvider.refreshToken,
|
||||
expiresAt: expiresAtSeconds,
|
||||
});
|
||||
|
||||
console.log('Gitea token refreshed successfully.');
|
||||
return access_token;
|
||||
} catch (error) {
|
||||
console.error('Token Refresh Error:', error);
|
||||
// Return the existing token if refresh fails
|
||||
const giteaProvider = await findGiteaById(giteaProviderId);
|
||||
return giteaProvider?.accessToken || null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a secure Git clone command with proper validation
|
||||
*/
|
||||
export const getGiteaCloneCommand = async (entity: any, logPath: string, isCompose = false) => {
|
||||
const { appName, giteaBranch, giteaId, giteaOwner, giteaRepository, serverId } = entity;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (!giteaId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Use paths(true) for remote operations
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, '');
|
||||
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
|
||||
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`;
|
||||
|
||||
const cloneCommand = `
|
||||
# Ensure output directory exists and is empty
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
|
||||
# Clone with detailed logging
|
||||
echo "Cloning repository to ${outputPath}" >> ${logPath};
|
||||
echo "Repository: ${repoClone}" >> ${logPath};
|
||||
|
||||
if ! git clone --branch ${giteaBranch} --depth 1 --recurse-submodules ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Failed to clone the repository ${repoClone}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
# Verify clone
|
||||
CLONE_COUNT=$(find ${outputPath} -type f | wc -l)
|
||||
echo "Files cloned: $CLONE_COUNT" >> ${logPath};
|
||||
|
||||
if [ "$CLONE_COUNT" -eq 0 ]; then
|
||||
echo "⚠️ WARNING: No files cloned" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
echo "Cloned ${repoClone} to ${outputPath}: ✅" >> ${logPath};
|
||||
`;
|
||||
|
||||
return cloneCommand;
|
||||
};
|
||||
|
||||
/**
|
||||
* Main function to clone a Gitea repository with improved validation and robust directory handling
|
||||
*/
|
||||
export const cloneGiteaRepository = async (
|
||||
entity: any,
|
||||
logPath?: string,
|
||||
isCompose: boolean = false
|
||||
) => {
|
||||
// If logPath is not provided, generate a default log path
|
||||
const actualLogPath = logPath || join(
|
||||
paths()[isCompose ? 'COMPOSE_PATH' : 'APPLICATIONS_PATH'],
|
||||
entity.appName,
|
||||
'clone.log'
|
||||
);
|
||||
|
||||
const writeStream = createWriteStream(actualLogPath, { flags: "a" });
|
||||
const { appName, giteaBranch, giteaId, giteaOwner, giteaRepository } = entity;
|
||||
|
||||
try {
|
||||
if (!giteaId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh the access token
|
||||
await refreshGiteaToken(giteaId);
|
||||
|
||||
// Fetch the Gitea provider
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
if (!giteaProvider) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea provider not found in the database",
|
||||
});
|
||||
}
|
||||
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths();
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
|
||||
// Log path information
|
||||
writeStream.write(`\nPath Information:\n`);
|
||||
writeStream.write(`Base Path: ${basePath}\n`);
|
||||
writeStream.write(`Output Path: ${outputPath}\n`);
|
||||
|
||||
writeStream.write(`\nRecreating directory: ${outputPath}\n`);
|
||||
await recreateDirectory(outputPath);
|
||||
|
||||
// Additional step - verify directory exists and is empty
|
||||
try {
|
||||
const filesCheck = await fs.readdir(outputPath);
|
||||
writeStream.write(`Directory after cleanup - files: ${filesCheck.length}\n`);
|
||||
|
||||
if (filesCheck.length > 0) {
|
||||
writeStream.write(`WARNING: Directory not empty after cleanup!\n`);
|
||||
|
||||
// Force remove with shell command if recreateDirectory didn't work
|
||||
if (entity.serverId) {
|
||||
writeStream.write(`Attempting forceful cleanup via shell command\n`);
|
||||
await execAsyncRemote(entity.serverId, `rm -rf "${outputPath}" && mkdir -p "${outputPath}"`,
|
||||
data => writeStream.write(`Cleanup output: ${data}\n`));
|
||||
} else {
|
||||
// Fallback to direct fs operations if serverId not available
|
||||
writeStream.write(`Attempting direct fs removal\n`);
|
||||
await fs.rm(outputPath, { recursive: true, force: true });
|
||||
await fs.mkdir(outputPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
} catch (verifyError) {
|
||||
writeStream.write(`Error verifying directory: ${verifyError}\n`);
|
||||
// Continue anyway - the clone operation might handle this
|
||||
}
|
||||
|
||||
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, '');
|
||||
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`;
|
||||
|
||||
writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}...\n`);
|
||||
writeStream.write(`Clone URL (masked): https://oauth2:***@${baseUrl}/${repoClone}\n`);
|
||||
|
||||
// First try standard git clone
|
||||
try {
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
giteaBranch,
|
||||
"--depth",
|
||||
"1",
|
||||
"--recurse-submodules",
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress"
|
||||
],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
}
|
||||
);
|
||||
writeStream.write(`\nStandard git clone succeeded\n`);
|
||||
} catch (cloneError) {
|
||||
writeStream.write(`\nStandard git clone failed: ${cloneError}\n`);
|
||||
writeStream.write(`Falling back to git init + fetch approach...\n`);
|
||||
|
||||
// Retry cleanup one more time
|
||||
if (entity.serverId) {
|
||||
await execAsyncRemote(entity.serverId, `rm -rf "${outputPath}" && mkdir -p "${outputPath}"`,
|
||||
data => writeStream.write(`Cleanup retry: ${data}\n`));
|
||||
} else {
|
||||
await fs.rm(outputPath, { recursive: true, force: true });
|
||||
await fs.mkdir(outputPath, { recursive: true });
|
||||
}
|
||||
|
||||
// Initialize git repo
|
||||
writeStream.write(`Initializing git repository...\n`);
|
||||
await spawnAsync("git", ["init", outputPath], data => writeStream.write(data));
|
||||
|
||||
// Set remote origin
|
||||
writeStream.write(`Setting remote origin...\n`);
|
||||
await spawnAsync(
|
||||
"git",
|
||||
["-C", outputPath, "remote", "add", "origin", cloneUrl],
|
||||
data => writeStream.write(data)
|
||||
);
|
||||
|
||||
// Fetch branch
|
||||
writeStream.write(`Fetching branch: ${giteaBranch}...\n`);
|
||||
await spawnAsync(
|
||||
"git",
|
||||
["-C", outputPath, "fetch", "--depth", "1", "origin", giteaBranch],
|
||||
data => writeStream.write(data)
|
||||
);
|
||||
|
||||
// Checkout branch
|
||||
writeStream.write(`Checking out branch: ${giteaBranch}...\n`);
|
||||
await spawnAsync(
|
||||
"git",
|
||||
["-C", outputPath, "checkout", "FETCH_HEAD"],
|
||||
data => writeStream.write(data)
|
||||
);
|
||||
|
||||
writeStream.write(`Git init and fetch completed successfully\n`);
|
||||
}
|
||||
|
||||
// Verify clone
|
||||
const files = await fs.readdir(outputPath);
|
||||
writeStream.write(`\nClone Verification:\n`);
|
||||
writeStream.write(`Files found: ${files.length}\n`);
|
||||
if (files.length > 0) {
|
||||
files.slice(0, 10).forEach(file => writeStream.write(`- ${file}\n`));
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
throw new Error("Repository clone failed - directory is empty");
|
||||
}
|
||||
|
||||
writeStream.write(`\nCloned ${repoClone} successfully: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`\nClone Error: ${error}\n`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clone a Gitea repository locally for a Compose configuration
|
||||
* Leverages the existing comprehensive cloneGiteaRepository function
|
||||
*/
|
||||
export const cloneRawGiteaRepository = async (entity: any) => {
|
||||
// Merge the existing entity with compose-specific properties
|
||||
const composeEntity = {
|
||||
...entity,
|
||||
sourceType: 'compose',
|
||||
isCompose: true
|
||||
};
|
||||
|
||||
// Call cloneGiteaRepository with the modified entity
|
||||
await cloneGiteaRepository(composeEntity);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clone a Gitea repository remotely for a Compose configuration
|
||||
* Uses the existing getGiteaCloneCommand function for remote cloning
|
||||
*/
|
||||
export const cloneRawGiteaRepositoryRemote = async (compose: any) => {
|
||||
const { COMPOSE_PATH } = paths(true);
|
||||
const logPath = join(COMPOSE_PATH, compose.appName, 'clone.log');
|
||||
|
||||
// Reuse the existing getGiteaCloneCommand function
|
||||
const command = await getGiteaCloneCommand({
|
||||
...compose,
|
||||
isCompose: true
|
||||
}, logPath, true);
|
||||
|
||||
if (!compose.serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Execute the clone command on the remote server
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
};
|
||||
|
||||
// Helper function to check if a Gitea provider meets the necessary requirements
|
||||
export const haveGiteaRequirements = (giteaProvider: any) => {
|
||||
return !!(giteaProvider?.clientId && giteaProvider?.clientSecret);
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to test the connection to a Gitea provider
|
||||
*/
|
||||
export const testGiteaConnection = async (input: { giteaId: string }) => {
|
||||
try {
|
||||
const { giteaId } = input;
|
||||
|
||||
if (!giteaId) {
|
||||
throw new Error("Gitea provider not found");
|
||||
}
|
||||
|
||||
// Fetch the Gitea provider from the database
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
if (!giteaProvider) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea provider not found in the database",
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Gitea Provider Found:', {
|
||||
id: giteaProvider.giteaId,
|
||||
url: giteaProvider.giteaUrl,
|
||||
hasAccessToken: !!giteaProvider.accessToken,
|
||||
});
|
||||
|
||||
// Refresh the token if needed
|
||||
await refreshGiteaToken(giteaId);
|
||||
|
||||
// Fetch the provider again in case the token was refreshed
|
||||
const provider = await findGiteaById(giteaId);
|
||||
if (!provider || !provider.accessToken) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "No access token available. Please authorize with Gitea.",
|
||||
});
|
||||
}
|
||||
|
||||
// Make API request to test connection
|
||||
console.log('Making API request to test connection...');
|
||||
|
||||
// Construct proper URL for the API request
|
||||
const baseUrl = provider.giteaUrl.replace(/\/+$/, ''); // Remove trailing slashes
|
||||
const url = `${baseUrl}/api/v1/user/repos`;
|
||||
|
||||
console.log(`Testing connection to: ${url}`);
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `token ${provider.accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('Repository API failed:', errorText);
|
||||
throw new Error(`Failed to connect to Gitea API: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const repos = await response.json();
|
||||
console.log(`Successfully connected to Gitea API. Found ${repos.length} repositories.`);
|
||||
|
||||
// Update lastAuthenticatedAt
|
||||
await updateGitea(giteaId, {
|
||||
lastAuthenticatedAt: Math.floor(Date.now() / 1000)
|
||||
});
|
||||
|
||||
return repos.length;
|
||||
} catch (error) {
|
||||
console.error('Gitea Connection Test Error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to fetch repositories from a Gitea provider
|
||||
*/
|
||||
export const getGiteaRepositories = async (giteaId?: string) => {
|
||||
if (!giteaId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Refresh the token
|
||||
await refreshGiteaToken(giteaId);
|
||||
|
||||
// Fetch the Gitea provider
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
|
||||
// Construct the URL for fetching repositories
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, '');
|
||||
const url = `${baseUrl}/api/v1/user/repos`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `token ${giteaProvider.accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const repositories = await response.json();
|
||||
|
||||
// Map repositories to a consistent format
|
||||
const mappedRepositories = repositories.map((repo: any) => ({
|
||||
id: repo.id,
|
||||
name: repo.name,
|
||||
url: repo.full_name,
|
||||
owner: {
|
||||
username: repo.owner.login
|
||||
}
|
||||
}));
|
||||
|
||||
return mappedRepositories;
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to fetch branches for a specific Gitea repository
|
||||
*/
|
||||
export const getGiteaBranches = async (input: {
|
||||
id?: number;
|
||||
giteaId?: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
}) => {
|
||||
if (!input.giteaId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Fetch the Gitea provider
|
||||
const giteaProvider = await findGiteaById(input.giteaId);
|
||||
|
||||
// Construct the URL for fetching branches
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, '');
|
||||
const url = `${baseUrl}/api/v1/repos/${input.owner}/${input.repo}/branches`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `token ${giteaProvider.accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch branches: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const branches = await response.json();
|
||||
|
||||
// Map branches to a consistent format
|
||||
return branches.map((branch: any) => ({
|
||||
id: branch.name,
|
||||
name: branch.name,
|
||||
commit: {
|
||||
id: branch.commit.id
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
export default {
|
||||
cloneGiteaRepository,
|
||||
cloneRawGiteaRepository,
|
||||
cloneRawGiteaRepositoryRemote,
|
||||
refreshGiteaToken,
|
||||
haveGiteaRequirements,
|
||||
testGiteaConnection,
|
||||
getGiteaRepositories,
|
||||
getGiteaBranches,
|
||||
fetchGiteaBranches
|
||||
};
|
||||
Generated
+37
@@ -346,6 +346,9 @@ importers:
|
||||
next-themes:
|
||||
specifier: ^0.2.1
|
||||
version: 0.2.1(next@15.0.1(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
node-fetch:
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2
|
||||
node-os-utils:
|
||||
specifier: 1.3.7
|
||||
version: 1.3.7
|
||||
@@ -4527,6 +4530,10 @@ packages:
|
||||
resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
data-uri-to-buffer@4.0.1:
|
||||
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
date-fns@3.6.0:
|
||||
resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
|
||||
|
||||
@@ -5003,6 +5010,10 @@ packages:
|
||||
fault@1.0.4:
|
||||
resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==}
|
||||
|
||||
fetch-blob@3.2.0:
|
||||
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
|
||||
engines: {node: ^12.20 || >= 14.13}
|
||||
|
||||
file-uri-to-path@1.0.0:
|
||||
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
||||
|
||||
@@ -5043,6 +5054,10 @@ packages:
|
||||
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
||||
engines: {node: '>=0.4.x'}
|
||||
|
||||
formdata-polyfill@4.0.10:
|
||||
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
|
||||
fraction.js@4.3.7:
|
||||
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
||||
|
||||
@@ -6161,6 +6176,10 @@ packages:
|
||||
encoding:
|
||||
optional: true
|
||||
|
||||
node-fetch@3.3.2:
|
||||
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
node-gyp-build-optional-packages@5.2.2:
|
||||
resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==}
|
||||
hasBin: true
|
||||
@@ -6282,6 +6301,7 @@ packages:
|
||||
|
||||
oslo@1.2.0:
|
||||
resolution: {integrity: sha512-OoFX6rDsNcOQVAD2gQD/z03u4vEjWZLzJtwkmgfRF+KpQUXwdgEXErD7zNhyowmHwHefP+PM9Pw13pgpHMRlzw==}
|
||||
deprecated: Package is no longer supported. Please see https://oslojs.dev for the successor project.
|
||||
|
||||
otpauth@9.3.4:
|
||||
resolution: {integrity: sha512-qXv+lpsCUO9ewitLYfeDKbLYt7UUCivnU/fwGK2OqhgrCBsRkTUNKWsgKAhkXG3aistOY+jEeuL90JEBu6W3mQ==}
|
||||
@@ -11662,6 +11682,8 @@ snapshots:
|
||||
|
||||
dargs@8.1.0: {}
|
||||
|
||||
data-uri-to-buffer@4.0.1: {}
|
||||
|
||||
date-fns@3.6.0: {}
|
||||
|
||||
dateformat@4.6.3: {}
|
||||
@@ -12090,6 +12112,11 @@ snapshots:
|
||||
dependencies:
|
||||
format: 0.2.2
|
||||
|
||||
fetch-blob@3.2.0:
|
||||
dependencies:
|
||||
node-domexception: 1.0.0
|
||||
web-streams-polyfill: 3.3.3
|
||||
|
||||
file-uri-to-path@1.0.0:
|
||||
optional: true
|
||||
|
||||
@@ -12125,6 +12152,10 @@ snapshots:
|
||||
|
||||
format@0.2.2: {}
|
||||
|
||||
formdata-polyfill@4.0.10:
|
||||
dependencies:
|
||||
fetch-blob: 3.2.0
|
||||
|
||||
fraction.js@4.3.7: {}
|
||||
|
||||
fresh@0.5.2: {}
|
||||
@@ -13445,6 +13476,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
encoding: 0.1.13
|
||||
|
||||
node-fetch@3.3.2:
|
||||
dependencies:
|
||||
data-uri-to-buffer: 4.0.1
|
||||
fetch-blob: 3.2.0
|
||||
formdata-polyfill: 4.0.10
|
||||
|
||||
node-gyp-build-optional-packages@5.2.2:
|
||||
dependencies:
|
||||
detect-libc: 2.0.3
|
||||
|
||||
Reference in New Issue
Block a user