mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat(gitea): Added Gitea Repo Integration
This commit is contained in:
parent
2f074ac734
commit
027406547e
@ -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,10 +1,12 @@
|
|||||||
import { SaveDockerProvider } from "@/components/dashboard/application/general/generic/save-docker-provider";
|
import { SaveDockerProvider } from "@/components/dashboard/application/general/generic/save-docker-provider";
|
||||||
import { SaveGitProvider } from "@/components/dashboard/application/general/generic/save-git-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 { SaveGithubProvider } from "@/components/dashboard/application/general/generic/save-github-provider";
|
||||||
import {
|
import {
|
||||||
BitbucketIcon,
|
BitbucketIcon,
|
||||||
DockerIcon,
|
DockerIcon,
|
||||||
GitIcon,
|
GitIcon,
|
||||||
|
GiteaIcon,
|
||||||
GithubIcon,
|
GithubIcon,
|
||||||
GitlabIcon,
|
GitlabIcon,
|
||||||
} from "@/components/icons/data-tools-icons";
|
} from "@/components/icons/data-tools-icons";
|
||||||
@ -18,7 +20,7 @@ import { SaveBitbucketProvider } from "./save-bitbucket-provider";
|
|||||||
import { SaveDragNDrop } from "./save-drag-n-drop";
|
import { SaveDragNDrop } from "./save-drag-n-drop";
|
||||||
import { SaveGitlabProvider } from "./save-gitlab-provider";
|
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 {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
@ -29,6 +31,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
||||||
const { data: bitbucketProviders } =
|
const { data: bitbucketProviders } =
|
||||||
api.bitbucket.bitbucketProviders.useQuery();
|
api.bitbucket.bitbucketProviders.useQuery();
|
||||||
|
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
|
||||||
|
|
||||||
const { data: application } = api.application.one.useQuery({ applicationId });
|
const { data: application } = api.application.one.useQuery({ applicationId });
|
||||||
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
|
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
|
||||||
@ -78,6 +81,13 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
<BitbucketIcon className="size-4 text-current fill-current" />
|
<BitbucketIcon className="size-4 text-current fill-current" />
|
||||||
Bitbucket
|
Bitbucket
|
||||||
</TabsTrigger>
|
</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
|
<TabsTrigger
|
||||||
value="docker"
|
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"
|
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||||
@ -162,6 +172,26 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</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">
|
<TabsContent value="docker" className="w-full p-2">
|
||||||
<SaveDockerProvider applicationId={applicationId} />
|
<SaveDockerProvider applicationId={applicationId} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
@ -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,53 +2,59 @@ import {
|
|||||||
BitbucketIcon,
|
BitbucketIcon,
|
||||||
GithubIcon,
|
GithubIcon,
|
||||||
GitlabIcon,
|
GitlabIcon,
|
||||||
} from "@/components/icons/data-tools-icons";
|
GiteaIcon,
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
} from "@/components/icons/data-tools-icons";
|
||||||
import { Button, buttonVariants } from "@/components/ui/button";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import {
|
import { Button, buttonVariants } from "@/components/ui/button";
|
||||||
Card,
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
CardContent,
|
import { api } from "@/utils/api";
|
||||||
CardDescription,
|
import { useUrl } from "@/utils/hooks/use-url";
|
||||||
CardHeader,
|
import { formatDate } from "date-fns";
|
||||||
CardTitle,
|
import {
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { useUrl } from "@/utils/hooks/use-url";
|
|
||||||
import { formatDate } from "date-fns";
|
|
||||||
import {
|
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
ImportIcon,
|
ImportIcon,
|
||||||
Loader2,
|
Loader2,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { AddBitbucketProvider } from "./bitbucket/add-bitbucket-provider";
|
import { AddBitbucketProvider } from "./bitbucket/add-bitbucket-provider";
|
||||||
import { EditBitbucketProvider } from "./bitbucket/edit-bitbucket-provider";
|
import { EditBitbucketProvider } from "./bitbucket/edit-bitbucket-provider";
|
||||||
import { AddGithubProvider } from "./github/add-github-provider";
|
import { AddGithubProvider } from "./github/add-github-provider";
|
||||||
import { EditGithubProvider } from "./github/edit-github-provider";
|
import { EditGithubProvider } from "./github/edit-github-provider";
|
||||||
import { AddGitlabProvider } from "./gitlab/add-gitlab-provider";
|
import { AddGitlabProvider } from "./gitlab/add-gitlab-provider";
|
||||||
import { EditGitlabProvider } from "./gitlab/edit-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 = () => {
|
export const ShowGitProviders = () => {
|
||||||
const { data, isLoading, refetch } = api.gitProvider.getAll.useQuery();
|
const { data, isLoading, refetch } = api.gitProvider.getAll.useQuery();
|
||||||
const { mutateAsync, isLoading: isRemoving } =
|
const { mutateAsync, isLoading: isRemoving } = api.gitProvider.remove.useMutation();
|
||||||
api.gitProvider.remove.useMutation();
|
|
||||||
const url = useUrl();
|
const url = useUrl();
|
||||||
|
|
||||||
const getGitlabUrl = (
|
const getGitlabUrl = (
|
||||||
clientId: string,
|
clientId: string,
|
||||||
gitlabId: string,
|
gitlabId: string,
|
||||||
gitlabUrl: string,
|
gitlabUrl: string,
|
||||||
) => {
|
) => {
|
||||||
const redirectUri = `${url}/api/providers/gitlab/callback?gitlabId=${gitlabId}`;
|
const redirectUri = `${url}/api/providers/gitlab/callback?gitlabId=${gitlabId}`;
|
||||||
|
|
||||||
const scope = "api read_user read_repository";
|
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)}`;
|
const authUrl = `${gitlabUrl}/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scope)}`;
|
||||||
|
|
||||||
return authUrl;
|
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 (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||||
@ -82,6 +88,7 @@ export const ShowGitProviders = () => {
|
|||||||
<AddGithubProvider />
|
<AddGithubProvider />
|
||||||
<AddGitlabProvider />
|
<AddGitlabProvider />
|
||||||
<AddBitbucketProvider />
|
<AddBitbucketProvider />
|
||||||
|
<AddGiteaProvider />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -97,6 +104,7 @@ export const ShowGitProviders = () => {
|
|||||||
<AddGithubProvider />
|
<AddGithubProvider />
|
||||||
<AddGitlabProvider />
|
<AddGitlabProvider />
|
||||||
<AddBitbucketProvider />
|
<AddBitbucketProvider />
|
||||||
|
<AddGiteaProvider />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -105,18 +113,22 @@ export const ShowGitProviders = () => {
|
|||||||
{data?.map((gitProvider, _index) => {
|
{data?.map((gitProvider, _index) => {
|
||||||
const isGithub = gitProvider.providerType === "github";
|
const isGithub = gitProvider.providerType === "github";
|
||||||
const isGitlab = gitProvider.providerType === "gitlab";
|
const isGitlab = gitProvider.providerType === "gitlab";
|
||||||
const isBitbucket =
|
const isBitbucket = gitProvider.providerType === "bitbucket";
|
||||||
gitProvider.providerType === "bitbucket";
|
const isGitea = gitProvider.providerType === "gitea";
|
||||||
const haveGithubRequirements =
|
|
||||||
gitProvider.providerType === "github" &&
|
const haveGithubRequirements = isGithub &&
|
||||||
gitProvider.github?.githubPrivateKey &&
|
gitProvider.github?.githubPrivateKey &&
|
||||||
gitProvider.github?.githubAppId &&
|
gitProvider.github?.githubAppId &&
|
||||||
gitProvider.github?.githubInstallationId;
|
gitProvider.github?.githubInstallationId;
|
||||||
|
|
||||||
const haveGitlabRequirements =
|
const haveGitlabRequirements = isGitlab &&
|
||||||
gitProvider.gitlab?.accessToken &&
|
gitProvider.gitlab?.accessToken &&
|
||||||
gitProvider.gitlab?.refreshToken;
|
gitProvider.gitlab?.refreshToken;
|
||||||
|
|
||||||
|
const haveGiteaRequirements = isGitea &&
|
||||||
|
gitProvider.gitea?.accessToken &&
|
||||||
|
gitProvider.gitea?.refreshToken;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={gitProvider.gitProviderId}
|
key={gitProvider.gitProviderId}
|
||||||
@ -125,15 +137,18 @@ export const ShowGitProviders = () => {
|
|||||||
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
<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 flex-col items-center justify-between">
|
||||||
<div className="flex gap-2 flex-row items-center">
|
<div className="flex gap-2 flex-row items-center">
|
||||||
{gitProvider.providerType === "github" && (
|
{isGithub && (
|
||||||
<GithubIcon className="size-5" />
|
<GithubIcon className="size-5" />
|
||||||
)}
|
)}
|
||||||
{gitProvider.providerType === "gitlab" && (
|
{isGitlab && (
|
||||||
<GitlabIcon className="size-5" />
|
<GitlabIcon className="size-5" />
|
||||||
)}
|
)}
|
||||||
{gitProvider.providerType === "bitbucket" && (
|
{isBitbucket && (
|
||||||
<BitbucketIcon className="size-5" />
|
<BitbucketIcon className="size-5" />
|
||||||
)}
|
)}
|
||||||
|
{isGitea && (
|
||||||
|
<GiteaIcon className="size-5" />
|
||||||
|
)}
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{gitProvider.name}
|
{gitProvider.name}
|
||||||
@ -141,7 +156,7 @@ export const ShowGitProviders = () => {
|
|||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{formatDate(
|
{formatDate(
|
||||||
gitProvider.createdAt,
|
gitProvider.createdAt,
|
||||||
"yyyy-MM-dd hh:mm:ss a",
|
"yyyy-MM-dd hh:mm:ss a"
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -195,23 +210,19 @@ export const ShowGitProviders = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isGithub && haveGithubRequirements && (
|
{isGithub && haveGithubRequirements && (
|
||||||
<EditGithubProvider
|
<EditGithubProvider githubId={gitProvider.github?.githubId} />
|
||||||
githubId={gitProvider.github.githubId}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isGitlab && (
|
{isGitlab && (
|
||||||
<EditGitlabProvider
|
<EditGitlabProvider gitlabId={gitProvider.gitlab?.gitlabId} />
|
||||||
gitlabId={gitProvider.gitlab.gitlabId}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isBitbucket && (
|
{isBitbucket && (
|
||||||
<EditBitbucketProvider
|
<EditBitbucketProvider bitbucketId={gitProvider.bitbucket?.bitbucketId} />
|
||||||
bitbucketId={
|
)}
|
||||||
gitProvider.bitbucket.bitbucketId
|
|
||||||
}
|
{isGitea && (
|
||||||
/>
|
<EditGiteaProvider giteaId={gitProvider.gitea?.giteaId} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
@ -223,22 +234,18 @@ export const ShowGitProviders = () => {
|
|||||||
gitProviderId: gitProvider.gitProviderId,
|
gitProviderId: gitProvider.gitProviderId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(
|
toast.success("Git Provider deleted successfully");
|
||||||
"Git Provider deleted successfully",
|
|
||||||
);
|
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error(
|
toast.error("Error deleting Git Provider");
|
||||||
"Error deleting Git Provider",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="group hover:bg-red-500/10 "
|
className="group hover:bg-red-500/10"
|
||||||
isLoading={isRemoving}
|
isLoading={isRemoving}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
@ -263,4 +270,4 @@ export const ShowGitProviders = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</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) => {
|
export const DockerIcon = ({ className }: Props) => {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
16
apps/dokploy/drizzle/0071_flimsy_plazm.sql
Normal file
16
apps/dokploy/drizzle/0071_flimsy_plazm.sql
Normal 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;
|
9
apps/dokploy/drizzle/0072_low_redwing.sql
Normal file
9
apps/dokploy/drizzle/0072_low_redwing.sql
Normal 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;
|
6
apps/dokploy/drizzle/0073_dark_tigra.sql
Normal file
6
apps/dokploy/drizzle/0073_dark_tigra.sql
Normal 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";
|
4
apps/dokploy/drizzle/0074_military_miss_america.sql
Normal file
4
apps/dokploy/drizzle/0074_military_miss_america.sql
Normal 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;
|
6
apps/dokploy/drizzle/0075_wild_xorn.sql
Normal file
6
apps/dokploy/drizzle/0075_wild_xorn.sql
Normal 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;
|
5221
apps/dokploy/drizzle/meta/0071_snapshot.json
Normal file
5221
apps/dokploy/drizzle/meta/0071_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5278
apps/dokploy/drizzle/meta/0072_snapshot.json
Normal file
5278
apps/dokploy/drizzle/meta/0072_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5266
apps/dokploy/drizzle/meta/0073_snapshot.json
Normal file
5266
apps/dokploy/drizzle/meta/0073_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5290
apps/dokploy/drizzle/meta/0074_snapshot.json
Normal file
5290
apps/dokploy/drizzle/meta/0074_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5328
apps/dokploy/drizzle/meta/0075_snapshot.json
Normal file
5328
apps/dokploy/drizzle/meta/0075_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -498,6 +498,41 @@
|
|||||||
"when": 1741322697251,
|
"when": 1741322697251,
|
||||||
"tag": "0070_useful_serpent_society",
|
"tag": "0070_useful_serpent_society",
|
||||||
"breakpoints": true
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -36,7 +36,6 @@
|
|||||||
"test": "vitest --config __test__/vitest.config.ts"
|
"test": "vitest --config __test__/vitest.config.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ai": "^4.0.23",
|
|
||||||
"@ai-sdk/anthropic": "^1.0.6",
|
"@ai-sdk/anthropic": "^1.0.6",
|
||||||
"@ai-sdk/azure": "^1.0.15",
|
"@ai-sdk/azure": "^1.0.15",
|
||||||
"@ai-sdk/cohere": "^1.0.6",
|
"@ai-sdk/cohere": "^1.0.6",
|
||||||
@ -44,20 +43,6 @@
|
|||||||
"@ai-sdk/mistral": "^1.0.6",
|
"@ai-sdk/mistral": "^1.0.6",
|
||||||
"@ai-sdk/openai": "^1.0.12",
|
"@ai-sdk/openai": "^1.0.12",
|
||||||
"@ai-sdk/openai-compatible": "^0.0.13",
|
"@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-json": "^6.0.1",
|
||||||
"@codemirror/lang-yaml": "^6.1.1",
|
"@codemirror/lang-yaml": "^6.1.1",
|
||||||
"@codemirror/language": "^6.10.1",
|
"@codemirror/language": "^6.10.1",
|
||||||
@ -65,7 +50,10 @@
|
|||||||
"@codemirror/view": "6.29.0",
|
"@codemirror/view": "6.29.0",
|
||||||
"@dokploy/server": "workspace:*",
|
"@dokploy/server": "workspace:*",
|
||||||
"@dokploy/trpc-openapi": "0.0.4",
|
"@dokploy/trpc-openapi": "0.0.4",
|
||||||
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^3.9.0",
|
||||||
|
"@lucia-auth/adapter-drizzle": "1.0.7",
|
||||||
|
"@octokit/auth-app": "^6.0.4",
|
||||||
"@octokit/webhooks": "^13.2.7",
|
"@octokit/webhooks": "^13.2.7",
|
||||||
"@radix-ui/react-accordion": "1.1.2",
|
"@radix-ui/react-accordion": "1.1.2",
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||||
@ -86,8 +74,10 @@
|
|||||||
"@radix-ui/react-tabs": "^1.0.4",
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@radix-ui/react-toggle": "^1.0.3",
|
"@radix-ui/react-toggle": "^1.0.3",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
|
"@react-email/components": "^0.0.21",
|
||||||
"@stepperize/react": "4.0.1",
|
"@stepperize/react": "4.0.1",
|
||||||
"@stripe/stripe-js": "4.8.0",
|
"@stripe/stripe-js": "4.8.0",
|
||||||
|
"@tailwindcss/typography": "0.5.16",
|
||||||
"@tanstack/react-query": "^4.36.1",
|
"@tanstack/react-query": "^4.36.1",
|
||||||
"@tanstack/react-table": "^8.16.0",
|
"@tanstack/react-table": "^8.16.0",
|
||||||
"@trpc/client": "^10.43.6",
|
"@trpc/client": "^10.43.6",
|
||||||
@ -97,10 +87,14 @@
|
|||||||
"@uiw/codemirror-theme-github": "^4.22.1",
|
"@uiw/codemirror-theme-github": "^4.22.1",
|
||||||
"@uiw/react-codemirror": "^4.22.1",
|
"@uiw/react-codemirror": "^4.22.1",
|
||||||
"@xterm/addon-attach": "0.10.0",
|
"@xterm/addon-attach": "0.10.0",
|
||||||
"@xterm/xterm": "^5.4.0",
|
|
||||||
"@xterm/addon-clipboard": "0.1.0",
|
"@xterm/addon-clipboard": "0.1.0",
|
||||||
|
"@xterm/xterm": "^5.4.0",
|
||||||
"adm-zip": "^0.5.14",
|
"adm-zip": "^0.5.14",
|
||||||
|
"ai": "^4.0.23",
|
||||||
"bcrypt": "5.1.1",
|
"bcrypt": "5.1.1",
|
||||||
|
"better-auth": "1.2.0",
|
||||||
|
"bl": "6.0.11",
|
||||||
|
"boxen": "^7.1.1",
|
||||||
"bullmq": "5.4.2",
|
"bullmq": "5.4.2",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
@ -108,10 +102,12 @@
|
|||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"copy-webpack-plugin": "^12.0.2",
|
"copy-webpack-plugin": "^12.0.2",
|
||||||
"date-fns": "3.6.0",
|
"date-fns": "3.6.0",
|
||||||
|
"dockerode": "4.0.2",
|
||||||
"dotenv": "16.4.5",
|
"dotenv": "16.4.5",
|
||||||
"drizzle-orm": "^0.39.1",
|
"drizzle-orm": "^0.39.1",
|
||||||
"drizzle-zod": "0.5.1",
|
"drizzle-zod": "0.5.1",
|
||||||
"fancy-ansi": "^0.1.3",
|
"fancy-ansi": "^0.1.3",
|
||||||
|
"hi-base32": "^0.5.1",
|
||||||
"i18next": "^23.16.4",
|
"i18next": "^23.16.4",
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
@ -123,11 +119,17 @@
|
|||||||
"next": "^15.0.1",
|
"next": "^15.0.1",
|
||||||
"next-i18next": "^15.3.1",
|
"next-i18next": "^15.3.1",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
|
"node-os-utils": "1.3.7",
|
||||||
"node-pty": "1.0.0",
|
"node-pty": "1.0.0",
|
||||||
"node-schedule": "2.1.1",
|
"node-schedule": "2.1.1",
|
||||||
|
"nodemailer": "6.9.14",
|
||||||
"octokit": "3.1.2",
|
"octokit": "3.1.2",
|
||||||
|
"ollama-ai-provider": "^1.1.0",
|
||||||
|
"otpauth": "^9.2.3",
|
||||||
"postgres": "3.4.4",
|
"postgres": "3.4.4",
|
||||||
"public-ip": "6.0.2",
|
"public-ip": "6.0.2",
|
||||||
|
"qrcode": "^1.5.3",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-confetti-explosion": "2.1.2",
|
"react-confetti-explosion": "2.1.2",
|
||||||
"react-day-picker": "8.10.1",
|
"react-day-picker": "8.10.1",
|
||||||
@ -136,6 +138,7 @@
|
|||||||
"react-i18next": "^15.1.0",
|
"react-i18next": "^15.1.0",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"recharts": "^2.12.7",
|
"recharts": "^2.12.7",
|
||||||
|
"rotating-file-stream": "3.2.3",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"sonner": "^1.5.0",
|
"sonner": "^1.5.0",
|
||||||
"ssh2": "1.15.0",
|
"ssh2": "1.15.0",
|
||||||
@ -149,21 +152,19 @@
|
|||||||
"ws": "8.16.0",
|
"ws": "8.16.0",
|
||||||
"xterm-addon-fit": "^0.8.0",
|
"xterm-addon-fit": "^0.8.0",
|
||||||
"zod": "^3.23.4",
|
"zod": "^3.23.4",
|
||||||
"zod-form-data": "^2.0.2",
|
"zod-form-data": "^2.0.2"
|
||||||
"@faker-js/faker": "^8.4.1",
|
|
||||||
"@tailwindcss/typography": "0.5.16"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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/adm-zip": "^0.5.5",
|
||||||
"@types/bcrypt": "5.0.2",
|
"@types/bcrypt": "5.0.2",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/lodash": "4.17.4",
|
"@types/lodash": "4.17.4",
|
||||||
"@types/node": "^18.17.0",
|
"@types/node": "^18.17.0",
|
||||||
|
"@types/node-os-utils": "1.3.4",
|
||||||
"@types/node-schedule": "2.1.6",
|
"@types/node-schedule": "2.1.6",
|
||||||
|
"@types/nodemailer": "^6.4.15",
|
||||||
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/react": "^18.2.37",
|
"@types/react": "^18.2.37",
|
||||||
"@types/react-dom": "^18.2.15",
|
"@types/react-dom": "^18.2.15",
|
||||||
"@types/ssh2": "1.15.1",
|
"@types/ssh2": "1.15.1",
|
||||||
@ -194,6 +195,8 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"commitlint": {
|
"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" });
|
res.status(301).json({ message: "Branch Not Match" });
|
||||||
return;
|
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 {
|
try {
|
||||||
|
52
apps/dokploy/pages/api/providers/gitea/authorize.ts
Normal file
52
apps/dokploy/pages/api/providers/gitea/authorize.ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
186
apps/dokploy/pages/api/providers/gitea/callback.ts
Normal file
186
apps/dokploy/pages/api/providers/gitea/callback.ts
Normal 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')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@ import { domainRouter } from "./routers/domain";
|
|||||||
import { gitProviderRouter } from "./routers/git-provider";
|
import { gitProviderRouter } from "./routers/git-provider";
|
||||||
import { githubRouter } from "./routers/github";
|
import { githubRouter } from "./routers/github";
|
||||||
import { gitlabRouter } from "./routers/gitlab";
|
import { gitlabRouter } from "./routers/gitlab";
|
||||||
|
import { giteaRouter } from "./routers/gitea";
|
||||||
import { mariadbRouter } from "./routers/mariadb";
|
import { mariadbRouter } from "./routers/mariadb";
|
||||||
import { mongoRouter } from "./routers/mongo";
|
import { mongoRouter } from "./routers/mongo";
|
||||||
import { mountRouter } from "./routers/mount";
|
import { mountRouter } from "./routers/mount";
|
||||||
@ -70,6 +71,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
notification: notificationRouter,
|
notification: notificationRouter,
|
||||||
sshKey: sshRouter,
|
sshKey: sshRouter,
|
||||||
gitProvider: gitProviderRouter,
|
gitProvider: gitProviderRouter,
|
||||||
|
gitea: giteaRouter,
|
||||||
bitbucket: bitbucketRouter,
|
bitbucket: bitbucketRouter,
|
||||||
gitlab: gitlabRouter,
|
gitlab: gitlabRouter,
|
||||||
github: githubRouter,
|
github: githubRouter,
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
apiSaveGitProvider,
|
apiSaveGitProvider,
|
||||||
apiSaveGithubProvider,
|
apiSaveGithubProvider,
|
||||||
apiSaveGitlabProvider,
|
apiSaveGitlabProvider,
|
||||||
|
apiSaveGiteaProvider,
|
||||||
apiUpdateApplication,
|
apiUpdateApplication,
|
||||||
applications,
|
applications,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
@ -396,6 +397,32 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
bitbucketId: input.bitbucketId,
|
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;
|
return true;
|
||||||
}),
|
}),
|
||||||
saveDockerProvider: protectedProcedure
|
saveDockerProvider: protectedProcedure
|
||||||
|
@ -12,6 +12,7 @@ export const gitProviderRouter = createTRPCRouter({
|
|||||||
gitlab: true,
|
gitlab: true,
|
||||||
bitbucket: true,
|
bitbucket: true,
|
||||||
github: true,
|
github: true,
|
||||||
|
gitea: true,
|
||||||
},
|
},
|
||||||
orderBy: desc(gitProvider.createdAt),
|
orderBy: desc(gitProvider.createdAt),
|
||||||
where: eq(gitProvider.organizationId, ctx.session.activeOrganizationId),
|
where: eq(gitProvider.organizationId, ctx.session.activeOrganizationId),
|
||||||
|
238
apps/dokploy/server/api/routers/gitea.ts
Normal file
238
apps/dokploy/server/api/routers/gitea.ts
Normal 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 };
|
||||||
|
}),
|
||||||
|
});
|
@ -476,6 +476,7 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
"bitbucket",
|
"bitbucket",
|
||||||
"github",
|
"github",
|
||||||
"gitlab",
|
"gitlab",
|
||||||
|
"gitea",
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -75,6 +75,9 @@ export function generate(schema: Schema): Template {
|
|||||||
"",
|
"",
|
||||||
"CLIENT_ID_GITLAB_LOGIN=",
|
"CLIENT_ID_GITLAB_LOGIN=",
|
||||||
"CLIENT_SECRET_GITLAB_LOGIN=",
|
"CLIENT_SECRET_GITLAB_LOGIN=",
|
||||||
|
"",
|
||||||
|
"CLIENT_ID_GITEA_LOGIN=",
|
||||||
|
"CLIENT_SECRET_GITEA_LOGIN=",
|
||||||
"",
|
"",
|
||||||
"CAPTCHA_SECRET=",
|
"CAPTCHA_SECRET=",
|
||||||
"",
|
"",
|
||||||
|
@ -35,11 +35,11 @@
|
|||||||
"@ai-sdk/mistral": "^1.0.6",
|
"@ai-sdk/mistral": "^1.0.6",
|
||||||
"@ai-sdk/openai": "^1.0.12",
|
"@ai-sdk/openai": "^1.0.12",
|
||||||
"@ai-sdk/openai-compatible": "^0.0.13",
|
"@ai-sdk/openai-compatible": "^0.0.13",
|
||||||
"@better-auth/utils":"0.2.3",
|
"@better-auth/utils": "0.2.3",
|
||||||
"@oslojs/encoding":"1.1.0",
|
"@oslojs/encoding": "1.1.0",
|
||||||
"@oslojs/crypto":"1.0.1",
|
"@oslojs/crypto": "1.0.1",
|
||||||
"drizzle-dbml-generator":"0.10.0",
|
"drizzle-dbml-generator": "0.10.0",
|
||||||
"better-auth":"1.2.0",
|
"better-auth": "1.2.0",
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@lucia-auth/adapter-drizzle": "1.0.7",
|
"@lucia-auth/adapter-drizzle": "1.0.7",
|
||||||
"@octokit/auth-app": "^6.0.4",
|
"@octokit/auth-app": "^6.0.4",
|
||||||
|
@ -15,6 +15,7 @@ import { deployments } from "./deployment";
|
|||||||
import { domains } from "./domain";
|
import { domains } from "./domain";
|
||||||
import { github } from "./github";
|
import { github } from "./github";
|
||||||
import { gitlab } from "./gitlab";
|
import { gitlab } from "./gitlab";
|
||||||
|
import { gitea } from "./gitea";
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
import { ports } from "./port";
|
import { ports } from "./port";
|
||||||
import { previewDeployments } from "./preview-deployments";
|
import { previewDeployments } from "./preview-deployments";
|
||||||
@ -33,6 +34,7 @@ export const sourceType = pgEnum("sourceType", [
|
|||||||
"github",
|
"github",
|
||||||
"gitlab",
|
"gitlab",
|
||||||
"bitbucket",
|
"bitbucket",
|
||||||
|
"gitea",
|
||||||
"drop",
|
"drop",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -152,6 +154,13 @@ export const applications = pgTable("application", {
|
|||||||
gitlabBranch: text("gitlabBranch"),
|
gitlabBranch: text("gitlabBranch"),
|
||||||
gitlabBuildPath: text("gitlabBuildPath").default("/"),
|
gitlabBuildPath: text("gitlabBuildPath").default("/"),
|
||||||
gitlabPathNamespace: text("gitlabPathNamespace"),
|
gitlabPathNamespace: text("gitlabPathNamespace"),
|
||||||
|
// Gitea
|
||||||
|
giteaProjectId: integer("giteaProjectId"),
|
||||||
|
giteaRepository: text("giteaRepository"),
|
||||||
|
giteaOwner: text("giteaOwner"),
|
||||||
|
giteaBranch: text("giteaBranch"),
|
||||||
|
giteaBuildPath: text("giteaBuildPath").default("/"),
|
||||||
|
giteaPathNamespace: text("giteaPathNamespace"),
|
||||||
// Bitbucket
|
// Bitbucket
|
||||||
bitbucketRepository: text("bitbucketRepository"),
|
bitbucketRepository: text("bitbucketRepository"),
|
||||||
bitbucketOwner: text("bitbucketOwner"),
|
bitbucketOwner: text("bitbucketOwner"),
|
||||||
@ -209,6 +218,9 @@ export const applications = pgTable("application", {
|
|||||||
gitlabId: text("gitlabId").references(() => gitlab.gitlabId, {
|
gitlabId: text("gitlabId").references(() => gitlab.gitlabId, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
|
giteaId: text("giteaId").references(() => gitea.giteaId, {
|
||||||
|
onDelete: "set null",
|
||||||
|
}),
|
||||||
bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, {
|
bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
@ -246,6 +258,10 @@ export const applicationsRelations = relations(
|
|||||||
fields: [applications.gitlabId],
|
fields: [applications.gitlabId],
|
||||||
references: [gitlab.gitlabId],
|
references: [gitlab.gitlabId],
|
||||||
}),
|
}),
|
||||||
|
gitea: one(gitea, {
|
||||||
|
fields: [applications.giteaId],
|
||||||
|
references: [gitea.giteaId],
|
||||||
|
}),
|
||||||
bitbucket: one(bitbucket, {
|
bitbucket: one(bitbucket, {
|
||||||
fields: [applications.bitbucketId],
|
fields: [applications.bitbucketId],
|
||||||
references: [bitbucket.bitbucketId],
|
references: [bitbucket.bitbucketId],
|
||||||
@ -376,7 +392,7 @@ const createSchema = createInsertSchema(applications, {
|
|||||||
customGitUrl: z.string().optional(),
|
customGitUrl: z.string().optional(),
|
||||||
buildPath: z.string().optional(),
|
buildPath: z.string().optional(),
|
||||||
projectId: z.string(),
|
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"]),
|
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||||
buildType: z.enum([
|
buildType: z.enum([
|
||||||
"dockerfile",
|
"dockerfile",
|
||||||
@ -475,6 +491,19 @@ export const apiSaveBitbucketProvider = createSchema
|
|||||||
})
|
})
|
||||||
.required();
|
.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
|
export const apiSaveDockerProvider = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
dockerImage: true,
|
dockerImage: true,
|
||||||
|
@ -14,12 +14,14 @@ import { server } from "./server";
|
|||||||
import { applicationStatus } from "./shared";
|
import { applicationStatus } from "./shared";
|
||||||
import { sshKeys } from "./ssh-key";
|
import { sshKeys } from "./ssh-key";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
|
import { gitea } from "./gitea";
|
||||||
|
|
||||||
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
||||||
"git",
|
"git",
|
||||||
"github",
|
"github",
|
||||||
"gitlab",
|
"gitlab",
|
||||||
"bitbucket",
|
"bitbucket",
|
||||||
|
"gitea",
|
||||||
"raw",
|
"raw",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -55,6 +57,10 @@ export const compose = pgTable("compose", {
|
|||||||
bitbucketRepository: text("bitbucketRepository"),
|
bitbucketRepository: text("bitbucketRepository"),
|
||||||
bitbucketOwner: text("bitbucketOwner"),
|
bitbucketOwner: text("bitbucketOwner"),
|
||||||
bitbucketBranch: text("bitbucketBranch"),
|
bitbucketBranch: text("bitbucketBranch"),
|
||||||
|
// Gitea
|
||||||
|
giteaRepository: text("giteaRepository"),
|
||||||
|
giteaOwner: text("giteaOwner"),
|
||||||
|
giteaBranch: text("giteaBranch"),
|
||||||
// Git
|
// Git
|
||||||
customGitUrl: text("customGitUrl"),
|
customGitUrl: text("customGitUrl"),
|
||||||
customGitBranch: text("customGitBranch"),
|
customGitBranch: text("customGitBranch"),
|
||||||
@ -86,6 +92,9 @@ export const compose = pgTable("compose", {
|
|||||||
}),
|
}),
|
||||||
bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, {
|
bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
|
}),
|
||||||
|
giteaId: text("giteaId").references(() => gitea.giteaId, {
|
||||||
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
serverId: text("serverId").references(() => server.serverId, {
|
serverId: text("serverId").references(() => server.serverId, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
@ -115,6 +124,10 @@ export const composeRelations = relations(compose, ({ one, many }) => ({
|
|||||||
bitbucket: one(bitbucket, {
|
bitbucket: one(bitbucket, {
|
||||||
fields: [compose.bitbucketId],
|
fields: [compose.bitbucketId],
|
||||||
references: [bitbucket.bitbucketId],
|
references: [bitbucket.bitbucketId],
|
||||||
|
}),
|
||||||
|
gitea: one(gitea, {
|
||||||
|
fields: [compose.giteaId],
|
||||||
|
references: [gitea.giteaId],
|
||||||
}),
|
}),
|
||||||
server: one(server, {
|
server: one(server, {
|
||||||
fields: [compose.serverId],
|
fields: [compose.serverId],
|
||||||
|
@ -7,11 +7,13 @@ import { organization } from "./account";
|
|||||||
import { bitbucket } from "./bitbucket";
|
import { bitbucket } from "./bitbucket";
|
||||||
import { github } from "./github";
|
import { github } from "./github";
|
||||||
import { gitlab } from "./gitlab";
|
import { gitlab } from "./gitlab";
|
||||||
|
import { gitea } from "./gitea";
|
||||||
|
|
||||||
export const gitProviderType = pgEnum("gitProviderType", [
|
export const gitProviderType = pgEnum("gitProviderType", [
|
||||||
"github",
|
"github",
|
||||||
"gitlab",
|
"gitlab",
|
||||||
"bitbucket",
|
"bitbucket",
|
||||||
|
"gitea",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const gitProvider = pgTable("git_provider", {
|
export const gitProvider = pgTable("git_provider", {
|
||||||
@ -42,6 +44,10 @@ export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
|||||||
fields: [gitProvider.gitProviderId],
|
fields: [gitProvider.gitProviderId],
|
||||||
references: [bitbucket.gitProviderId],
|
references: [bitbucket.gitProviderId],
|
||||||
}),
|
}),
|
||||||
|
gitea: one(gitea, {
|
||||||
|
fields: [gitProvider.gitProviderId],
|
||||||
|
references: [gitea.gitProviderId],
|
||||||
|
}),
|
||||||
organization: one(organization, {
|
organization: one(organization, {
|
||||||
fields: [gitProvider.organizationId],
|
fields: [gitProvider.organizationId],
|
||||||
references: [organization.id],
|
references: [organization.id],
|
||||||
|
96
packages/server/src/db/schema/gitea.ts
Normal file
96
packages/server/src/db/schema/gitea.ts
Normal file
@ -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 "./bitbucket";
|
||||||
export * from "./github";
|
export * from "./github";
|
||||||
export * from "./gitlab";
|
export * from "./gitlab";
|
||||||
|
export * from "./gitea";
|
||||||
export * from "./server";
|
export * from "./server";
|
||||||
export * from "./utils";
|
export * from "./utils";
|
||||||
export * from "./preview-deployments";
|
export * from "./preview-deployments";
|
||||||
|
@ -46,6 +46,7 @@ enum gitProviderType {
|
|||||||
github
|
github
|
||||||
gitlab
|
gitlab
|
||||||
bitbucket
|
bitbucket
|
||||||
|
gitea
|
||||||
}
|
}
|
||||||
|
|
||||||
enum mountType {
|
enum mountType {
|
||||||
@ -98,6 +99,7 @@ enum sourceType {
|
|||||||
github
|
github
|
||||||
gitlab
|
gitlab
|
||||||
bitbucket
|
bitbucket
|
||||||
|
gitea
|
||||||
drop
|
drop
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,6 +108,7 @@ enum sourceTypeCompose {
|
|||||||
github
|
github
|
||||||
gitlab
|
gitlab
|
||||||
bitbucket
|
bitbucket
|
||||||
|
gitea
|
||||||
raw
|
raw
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,6 +208,7 @@ table application {
|
|||||||
githubId text
|
githubId text
|
||||||
gitlabId text
|
gitlabId text
|
||||||
bitbucketId text
|
bitbucketId text
|
||||||
|
giteaId text
|
||||||
serverId text
|
serverId text
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,6 +283,9 @@ table compose {
|
|||||||
bitbucketRepository text
|
bitbucketRepository text
|
||||||
bitbucketOwner text
|
bitbucketOwner text
|
||||||
bitbucketBranch text
|
bitbucketBranch text
|
||||||
|
giteaRepository text
|
||||||
|
giteaOwner text
|
||||||
|
giteaBranch text
|
||||||
customGitUrl text
|
customGitUrl text
|
||||||
customGitBranch text
|
customGitBranch text
|
||||||
customGitSSHKeyId text
|
customGitSSHKeyId text
|
||||||
@ -293,6 +300,7 @@ table compose {
|
|||||||
githubId text
|
githubId text
|
||||||
gitlabId text
|
gitlabId text
|
||||||
bitbucketId text
|
bitbucketId text
|
||||||
|
giteaId text
|
||||||
serverId text
|
serverId text
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -388,6 +396,20 @@ table gitlab {
|
|||||||
gitProviderId text [not null]
|
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 {
|
table gotify {
|
||||||
gotifyId text [pk, not null]
|
gotifyId text [pk, not null]
|
||||||
serverUrl text [not null]
|
serverUrl text [not null]
|
||||||
@ -819,6 +841,8 @@ ref: github.gitProviderId - git_provider.gitProviderId
|
|||||||
|
|
||||||
ref: gitlab.gitProviderId - git_provider.gitProviderId
|
ref: gitlab.gitProviderId - git_provider.gitProviderId
|
||||||
|
|
||||||
|
ref: gitea.gitProviderId - git_provider.gitProviderId
|
||||||
|
|
||||||
ref: git_provider.userId - user.id
|
ref: git_provider.userId - user.id
|
||||||
|
|
||||||
ref: mariadb.projectId > project.projectId
|
ref: mariadb.projectId > project.projectId
|
||||||
|
@ -28,6 +28,7 @@ export * from "./services/git-provider";
|
|||||||
export * from "./services/bitbucket";
|
export * from "./services/bitbucket";
|
||||||
export * from "./services/github";
|
export * from "./services/github";
|
||||||
export * from "./services/gitlab";
|
export * from "./services/gitlab";
|
||||||
|
export * from "./services/gitea";
|
||||||
export * from "./services/server";
|
export * from "./services/server";
|
||||||
export * from "./services/application";
|
export * from "./services/application";
|
||||||
|
|
||||||
@ -89,6 +90,7 @@ export * from "./utils/providers/docker";
|
|||||||
export * from "./utils/providers/git";
|
export * from "./utils/providers/git";
|
||||||
export * from "./utils/providers/github";
|
export * from "./utils/providers/github";
|
||||||
export * from "./utils/providers/gitlab";
|
export * from "./utils/providers/gitlab";
|
||||||
|
export * from "./utils/providers/gitea";
|
||||||
export * from "./utils/providers/raw";
|
export * from "./utils/providers/raw";
|
||||||
|
|
||||||
export * from "./utils/servers/remote-docker";
|
export * from "./utils/servers/remote-docker";
|
||||||
|
@ -34,6 +34,10 @@ import {
|
|||||||
cloneGitlabRepository,
|
cloneGitlabRepository,
|
||||||
getGitlabCloneCommand,
|
getGitlabCloneCommand,
|
||||||
} from "@dokploy/server/utils/providers/gitlab";
|
} from "@dokploy/server/utils/providers/gitlab";
|
||||||
|
import {
|
||||||
|
cloneGiteaRepository,
|
||||||
|
getGiteaCloneCommand,
|
||||||
|
} from "@dokploy/server/utils/providers/gitea";
|
||||||
import { createTraefikConfig } from "@dokploy/server/utils/traefik/application";
|
import { createTraefikConfig } from "@dokploy/server/utils/traefik/application";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
@ -111,6 +115,7 @@ export const findApplicationById = async (applicationId: string) => {
|
|||||||
gitlab: true,
|
gitlab: true,
|
||||||
github: true,
|
github: true,
|
||||||
bitbucket: true,
|
bitbucket: true,
|
||||||
|
gitea: true,
|
||||||
server: true,
|
server: true,
|
||||||
previewDeployments: true,
|
previewDeployments: true,
|
||||||
},
|
},
|
||||||
@ -197,6 +202,9 @@ export const deployApplication = async ({
|
|||||||
} else if (application.sourceType === "gitlab") {
|
} else if (application.sourceType === "gitlab") {
|
||||||
await cloneGitlabRepository(application, deployment.logPath);
|
await cloneGitlabRepository(application, deployment.logPath);
|
||||||
await buildApplication(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") {
|
} else if (application.sourceType === "bitbucket") {
|
||||||
await cloneBitbucketRepository(application, deployment.logPath);
|
await cloneBitbucketRepository(application, deployment.logPath);
|
||||||
await buildApplication(application, deployment.logPath);
|
await buildApplication(application, deployment.logPath);
|
||||||
@ -325,6 +333,11 @@ export const deployRemoteApplication = async ({
|
|||||||
application,
|
application,
|
||||||
deployment.logPath,
|
deployment.logPath,
|
||||||
);
|
);
|
||||||
|
} else if (application.sourceType === "gitea") {
|
||||||
|
command += await getGiteaCloneCommand(
|
||||||
|
application,
|
||||||
|
deployment.logPath,
|
||||||
|
);
|
||||||
} else if (application.sourceType === "git") {
|
} else if (application.sourceType === "git") {
|
||||||
command += await getCustomGitCloneCommand(
|
command += await getCustomGitCloneCommand(
|
||||||
application,
|
application,
|
||||||
|
@ -37,6 +37,10 @@ import {
|
|||||||
cloneGitlabRepository,
|
cloneGitlabRepository,
|
||||||
getGitlabCloneCommand,
|
getGitlabCloneCommand,
|
||||||
} from "@dokploy/server/utils/providers/gitlab";
|
} from "@dokploy/server/utils/providers/gitlab";
|
||||||
|
import {
|
||||||
|
cloneGiteaRepository,
|
||||||
|
getGiteaCloneCommand,
|
||||||
|
} from "@dokploy/server/utils/providers/gitea";
|
||||||
import {
|
import {
|
||||||
createComposeFile,
|
createComposeFile,
|
||||||
getCreateComposeFileCommand,
|
getCreateComposeFileCommand,
|
||||||
@ -125,6 +129,7 @@ export const findComposeById = async (composeId: string) => {
|
|||||||
github: true,
|
github: true,
|
||||||
gitlab: true,
|
gitlab: true,
|
||||||
bitbucket: true,
|
bitbucket: true,
|
||||||
|
gitea: true,
|
||||||
server: true,
|
server: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -232,6 +237,8 @@ export const deployCompose = async ({
|
|||||||
await cloneBitbucketRepository(compose, deployment.logPath, true);
|
await cloneBitbucketRepository(compose, deployment.logPath, true);
|
||||||
} else if (compose.sourceType === "git") {
|
} else if (compose.sourceType === "git") {
|
||||||
await cloneGitRepository(compose, deployment.logPath, true);
|
await cloneGitRepository(compose, deployment.logPath, true);
|
||||||
|
} else if (compose.sourceType === "gitea") {
|
||||||
|
await cloneGiteaRepository(compose, deployment.logPath, true);
|
||||||
} else if (compose.sourceType === "raw") {
|
} else if (compose.sourceType === "raw") {
|
||||||
await createComposeFile(compose, deployment.logPath);
|
await createComposeFile(compose, deployment.logPath);
|
||||||
}
|
}
|
||||||
@ -364,6 +371,12 @@ export const deployRemoteCompose = async ({
|
|||||||
);
|
);
|
||||||
} else if (compose.sourceType === "raw") {
|
} else if (compose.sourceType === "raw") {
|
||||||
command += getCreateComposeFileCommand(compose, deployment.logPath);
|
command += getCreateComposeFileCommand(compose, deployment.logPath);
|
||||||
|
} else if (compose.sourceType === "gitea") {
|
||||||
|
command += await getGiteaCloneCommand(
|
||||||
|
compose,
|
||||||
|
deployment.logPath,
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await execAsyncRemote(compose.serverId, command);
|
await execAsyncRemote(compose.serverId, command);
|
||||||
|
104
packages/server/src/services/gitea.ts
Normal file
104
packages/server/src/services/gitea.ts
Normal file
@ -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,
|
gitlab: true,
|
||||||
github: true,
|
github: true,
|
||||||
bitbucket: true,
|
bitbucket: true,
|
||||||
|
gitea: true,
|
||||||
server: true,
|
server: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -112,7 +112,9 @@ export const getBuildCommand = (
|
|||||||
|
|
||||||
export const mechanizeDockerContainer = async (
|
export const mechanizeDockerContainer = async (
|
||||||
application: ApplicationNested,
|
application: ApplicationNested,
|
||||||
) => {
|
) => {
|
||||||
|
console.log(`Starting to mechanize Docker container for ${application.appName}`);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
appName,
|
appName,
|
||||||
env,
|
env,
|
||||||
@ -193,8 +195,11 @@ export const mechanizeDockerContainer = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log(`Attempting to find existing service: ${appName}`);
|
||||||
const service = docker.getService(appName);
|
const service = docker.getService(appName);
|
||||||
const inspect = await service.inspect();
|
const inspect = await service.inspect();
|
||||||
|
console.log(`Found existing service, updating: ${appName}`);
|
||||||
|
|
||||||
await service.update({
|
await service.update({
|
||||||
version: Number.parseInt(inspect.Version.Index),
|
version: Number.parseInt(inspect.Version.Index),
|
||||||
...settings,
|
...settings,
|
||||||
@ -203,8 +208,22 @@ export const mechanizeDockerContainer = async (
|
|||||||
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
|
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (_error) {
|
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);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -81,7 +81,18 @@ export const buildNixpacks = async (
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// Only try to remove the container if it might exist
|
||||||
|
try {
|
||||||
await spawnAsync("docker", ["rm", buildContainerId], writeToStream);
|
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;
|
throw e;
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,10 @@ import {
|
|||||||
cloneRawGitlabRepository,
|
cloneRawGitlabRepository,
|
||||||
cloneRawGitlabRepositoryRemote,
|
cloneRawGitlabRepositoryRemote,
|
||||||
} from "../providers/gitlab";
|
} from "../providers/gitlab";
|
||||||
|
import {
|
||||||
|
cloneRawGiteaRepository,
|
||||||
|
cloneRawGiteaRepositoryRemote,
|
||||||
|
} from "../providers/gitea";
|
||||||
import {
|
import {
|
||||||
createComposeFileRaw,
|
createComposeFileRaw,
|
||||||
createComposeFileRawRemote,
|
createComposeFileRawRemote,
|
||||||
@ -44,6 +48,8 @@ export const cloneCompose = async (compose: Compose) => {
|
|||||||
await cloneRawBitbucketRepository(compose);
|
await cloneRawBitbucketRepository(compose);
|
||||||
} else if (compose.sourceType === "git") {
|
} else if (compose.sourceType === "git") {
|
||||||
await cloneGitRawRepository(compose);
|
await cloneGitRawRepository(compose);
|
||||||
|
} else if (compose.sourceType === "gitea") {
|
||||||
|
await cloneRawGiteaRepository(compose);
|
||||||
} else if (compose.sourceType === "raw") {
|
} else if (compose.sourceType === "raw") {
|
||||||
await createComposeFileRaw(compose);
|
await createComposeFileRaw(compose);
|
||||||
}
|
}
|
||||||
@ -58,6 +64,8 @@ export const cloneComposeRemote = async (compose: Compose) => {
|
|||||||
await cloneRawBitbucketRepositoryRemote(compose);
|
await cloneRawBitbucketRepositoryRemote(compose);
|
||||||
} else if (compose.sourceType === "git") {
|
} else if (compose.sourceType === "git") {
|
||||||
await cloneRawGitRepositoryRemote(compose);
|
await cloneRawGitRepositoryRemote(compose);
|
||||||
|
} else if (compose.sourceType === "gitea") {
|
||||||
|
await cloneRawGiteaRepository(compose);
|
||||||
} else if (compose.sourceType === "raw") {
|
} else if (compose.sourceType === "raw") {
|
||||||
await createComposeFileRawRemote(compose);
|
await createComposeFileRawRemote(compose);
|
||||||
}
|
}
|
||||||
|
@ -113,6 +113,8 @@ export const getBuildAppDirectory = (application: Application) => {
|
|||||||
buildPath = application?.gitlabBuildPath || "";
|
buildPath = application?.gitlabBuildPath || "";
|
||||||
} else if (sourceType === "bitbucket") {
|
} else if (sourceType === "bitbucket") {
|
||||||
buildPath = application?.bitbucketBuildPath || "";
|
buildPath = application?.bitbucketBuildPath || "";
|
||||||
|
} else if (sourceType === "gitea") {
|
||||||
|
buildPath = application?.giteaBuildPath || "";
|
||||||
} else if (sourceType === "drop") {
|
} else if (sourceType === "drop") {
|
||||||
buildPath = application?.dropBuildPath || "";
|
buildPath = application?.dropBuildPath || "";
|
||||||
} else if (sourceType === "git") {
|
} else if (sourceType === "git") {
|
||||||
|
582
packages/server/src/utils/providers/gitea.ts
Normal file
582
packages/server/src/utils/providers/gitea.ts
Normal file
@ -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
|
||||||
|
};
|
@ -346,6 +346,9 @@ importers:
|
|||||||
next-themes:
|
next-themes:
|
||||||
specifier: ^0.2.1
|
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)
|
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:
|
node-os-utils:
|
||||||
specifier: 1.3.7
|
specifier: 1.3.7
|
||||||
version: 1.3.7
|
version: 1.3.7
|
||||||
@ -4527,6 +4530,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==}
|
resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==}
|
||||||
engines: {node: '>=12'}
|
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:
|
date-fns@3.6.0:
|
||||||
resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
|
resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
|
||||||
|
|
||||||
@ -5003,6 +5010,10 @@ packages:
|
|||||||
fault@1.0.4:
|
fault@1.0.4:
|
||||||
resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==}
|
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:
|
file-uri-to-path@1.0.0:
|
||||||
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
||||||
|
|
||||||
@ -5043,6 +5054,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
||||||
engines: {node: '>=0.4.x'}
|
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:
|
fraction.js@4.3.7:
|
||||||
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
||||||
|
|
||||||
@ -6161,6 +6176,10 @@ packages:
|
|||||||
encoding:
|
encoding:
|
||||||
optional: true
|
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:
|
node-gyp-build-optional-packages@5.2.2:
|
||||||
resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==}
|
resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -6282,6 +6301,7 @@ packages:
|
|||||||
|
|
||||||
oslo@1.2.0:
|
oslo@1.2.0:
|
||||||
resolution: {integrity: sha512-OoFX6rDsNcOQVAD2gQD/z03u4vEjWZLzJtwkmgfRF+KpQUXwdgEXErD7zNhyowmHwHefP+PM9Pw13pgpHMRlzw==}
|
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:
|
otpauth@9.3.4:
|
||||||
resolution: {integrity: sha512-qXv+lpsCUO9ewitLYfeDKbLYt7UUCivnU/fwGK2OqhgrCBsRkTUNKWsgKAhkXG3aistOY+jEeuL90JEBu6W3mQ==}
|
resolution: {integrity: sha512-qXv+lpsCUO9ewitLYfeDKbLYt7UUCivnU/fwGK2OqhgrCBsRkTUNKWsgKAhkXG3aistOY+jEeuL90JEBu6W3mQ==}
|
||||||
@ -11662,6 +11682,8 @@ snapshots:
|
|||||||
|
|
||||||
dargs@8.1.0: {}
|
dargs@8.1.0: {}
|
||||||
|
|
||||||
|
data-uri-to-buffer@4.0.1: {}
|
||||||
|
|
||||||
date-fns@3.6.0: {}
|
date-fns@3.6.0: {}
|
||||||
|
|
||||||
dateformat@4.6.3: {}
|
dateformat@4.6.3: {}
|
||||||
@ -12090,6 +12112,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
format: 0.2.2
|
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:
|
file-uri-to-path@1.0.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -12125,6 +12152,10 @@ snapshots:
|
|||||||
|
|
||||||
format@0.2.2: {}
|
format@0.2.2: {}
|
||||||
|
|
||||||
|
formdata-polyfill@4.0.10:
|
||||||
|
dependencies:
|
||||||
|
fetch-blob: 3.2.0
|
||||||
|
|
||||||
fraction.js@4.3.7: {}
|
fraction.js@4.3.7: {}
|
||||||
|
|
||||||
fresh@0.5.2: {}
|
fresh@0.5.2: {}
|
||||||
@ -13445,6 +13476,12 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
encoding: 0.1.13
|
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:
|
node-gyp-build-optional-packages@5.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
detect-libc: 2.0.3
|
detect-libc: 2.0.3
|
||||||
|
Loading…
Reference in New Issue
Block a user