feat(gitea): Added Gitea Repo Integration

This commit is contained in:
Jason Parks
2025-03-16 02:11:48 -06:00
parent 2f074ac734
commit 027406547e
45 changed files with 29468 additions and 512 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -476,6 +476,7 @@ export const settingsRouter = createTRPCRouter({
"bitbucket",
"github",
"gitlab",
"gitea",
],
});

View File

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