feat(gitea): Added Gitea Repo Integration

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

View File

@ -0,0 +1,414 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
// Define types for repository and branch objects
interface GiteaRepository {
name: string;
url: string;
id: number;
owner: {
username: string;
};
}
interface GiteaBranch {
name: string;
commit: {
id: string;
};
}
const GiteaProviderSchema = z.object({
buildPath: z.string().min(1, "Path is required").default("/"),
repository: z
.object({
repo: z.string().min(1, "Repo is required"),
owner: z.string().min(1, "Owner is required"),
giteaPathNamespace: z.string().min(1),
id: z.number().nullable(),
})
.required(),
branch: z.string().min(1, "Branch is required"),
giteaId: z.string().min(1, "Gitea Provider is required"),
});
type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
interface Props {
applicationId: string;
}
export const SaveGiteaProvider = ({ applicationId }: Props) => {
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
const { mutateAsync, isLoading: isSavingGiteaProvider } =
api.application.saveGiteaProvider.useMutation();
const form = useForm<GiteaProvider>({
defaultValues: {
buildPath: "/",
repository: {
owner: "",
repo: "",
giteaPathNamespace: "",
id: null,
},
giteaId: "",
branch: "",
},
resolver: zodResolver(GiteaProviderSchema),
});
const repository = form.watch("repository");
const giteaId = form.watch("giteaId");
const {
data: repositories,
isLoading: isLoadingRepositories,
error,
} = api.gitea.getGiteaRepositories.useQuery(
{
giteaId,
},
{
enabled: !!giteaId,
},
);
const {
data: branches,
fetchStatus,
status,
} = api.gitea.getGiteaBranches.useQuery(
{
owner: repository?.owner,
repositoryName: repository?.repo,
id: repository?.id || 0,
giteaId: giteaId,
},
{
enabled: !!repository?.owner && !!repository?.repo && !!giteaId,
},
);
useEffect(() => {
if (data) {
form.reset({
branch: data.giteaBranch || "",
repository: {
repo: data.giteaRepository || "",
owner: data.giteaOwner || "",
giteaPathNamespace: data.giteaPathNamespace || "",
id: data.giteaProjectId,
},
buildPath: data.giteaBuildPath || "/",
giteaId: data.giteaId || "",
});
}
}, [form.reset, data, form]);
const onSubmit = async (data: GiteaProvider) => {
await mutateAsync({
giteaBranch: data.branch,
giteaRepository: data.repository.repo,
giteaOwner: data.repository.owner,
giteaBuildPath: data.buildPath,
giteaId: data.giteaId,
applicationId,
giteaProjectId: data.repository.id,
giteaPathNamespace: data.repository.giteaPathNamespace,
})
.then(async () => {
toast.success("Service Provider Saved");
await refetch();
})
.catch(() => {
toast.error("Error saving the Gitea provider");
});
};
return (
<div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 py-3"
>
{error && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="grid md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="giteaId"
render={({ field }) => (
<FormItem className="md:col-span-2 flex flex-col">
<FormLabel>Gitea Account</FormLabel>
<Select
onValueChange={(value) => {
field.onChange(value);
form.setValue("repository", {
owner: "",
repo: "",
id: null,
giteaPathNamespace: "",
});
form.setValue("branch", "");
}}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a Gitea Account" />
</SelectTrigger>
</FormControl>
<SelectContent>
{giteaProviders?.map((giteaProvider) => (
<SelectItem
key={giteaProvider.giteaId}
value={giteaProvider.giteaId}
>
{giteaProvider.gitProvider.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="repository"
render={({ field }) => (
<FormItem className="md:col-span-2 flex flex-col">
<FormLabel>Repository</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{isLoadingRepositories
? "Loading...."
: field.value.owner
? repositories?.find(
(repo: GiteaRepository) => repo.name === field.value.repo,
)?.name
: "Select repository"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder="Search repository..."
className="h-9"
/>
{isLoadingRepositories && (
<span className="py-6 text-center text-sm">
Loading Repositories....
</span>
)}
<CommandEmpty>No repositories found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>
{repositories && repositories.length === 0 && (
<CommandEmpty>
No repositories found.
</CommandEmpty>
)}
{repositories?.map((repo: GiteaRepository) => {
return (
<CommandItem
value={repo.name}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
owner: repo.owner.username as string,
repo: repo.name,
id: repo.id,
giteaPathNamespace: repo.name,
});
form.setValue("branch", "");
}}
>
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.username}
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
repo.name === field.value.repo
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
);
})}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
</Popover>
{form.formState.errors.repository && (
<p className={cn("text-sm font-medium text-destructive")}>
Repository is required
</p>
)}
</FormItem>
)}
/>
<FormField
control={form.control}
name="branch"
render={({ field }) => (
<FormItem className="block w-full">
<FormLabel>Branch</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
" w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{status === "loading" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
(branch: GiteaBranch) => branch.name === field.value,
)?.name
: "Select branch"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder="Search branch..."
className="h-9"
/>
{status === "loading" && fetchStatus === "fetching" && (
<span className="py-6 text-center text-sm text-muted-foreground">
Loading Branches....
</span>
)}
{!repository?.owner && (
<span className="py-6 text-center text-sm text-muted-foreground">
Select a repository
</span>
)}
<ScrollArea className="h-96">
<CommandEmpty>No branch found.</CommandEmpty>
<CommandGroup>
{branches?.map((branch: GiteaBranch) => (
<CommandItem
value={branch.name}
key={branch.commit.id}
onSelect={() => {
form.setValue("branch", branch.name);
}}
>
{branch.name}
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
branch.name === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
<FormMessage />
</Popover>
</FormItem>
)}
/>
<FormField
control={form.control}
name="buildPath"
render={({ field }) => (
<FormItem>
<FormLabel>Build Path</FormLabel>
<FormControl>
<Input placeholder="/" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button
isLoading={isSavingGiteaProvider}
type="submit"
className="w-fit"
>
Save
</Button>
</div>
</form>
</Form>
</div>
);
};

View File

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

View File

@ -0,0 +1,251 @@
import { GiteaIcon } from "@/components/icons/data-tools-icons"; // Use GiteaIcon for Gitea
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import { CardContent } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url";
import { zodResolver } from "@hookform/resolvers/zod";
import { ExternalLink } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const Schema = z.object({
name: z.string().min(1, {
message: "Name is required",
}),
giteaUrl: z.string().min(1, {
message: "Gitea URL is required",
}),
clientId: z.string().min(1, {
message: "Client ID is required",
}),
clientSecret: z.string().min(1, {
message: "Client Secret is required",
}),
redirectUri: z.string().min(1, {
message: "Redirect URI is required",
}),
organizationName: z.string().optional(), // Added organizationName to the schema
});
type Schema = z.infer<typeof Schema>;
export const AddGiteaProvider = () => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const url = useUrl();
const { data: auth } = api.user.get.useQuery();
const { mutateAsync, error, isError } = api.gitea.create.useMutation(); // Updated API call for Gitea
const webhookUrl = `${url}/api/providers/gitea/callback`; // Updated webhook URL for Gitea
const form = useForm<Schema>({
defaultValues: {
clientId: "",
clientSecret: "",
redirectUri: webhookUrl,
name: "",
giteaUrl: "https://gitea.com",
},
resolver: zodResolver(Schema),
});
const giteaUrl = form.watch("giteaUrl");
useEffect(() => {
form.reset({
clientId: "",
clientSecret: "",
redirectUri: webhookUrl,
name: "",
giteaUrl: "https://gitea.com",
});
}, [form, isOpen]);
const onSubmit = async (data: Schema) => {
await mutateAsync({
clientId: data.clientId || "",
clientSecret: data.clientSecret || "",
name: data.name || "",
redirectUri: data.redirectUri || "",
giteaUrl: data.giteaUrl || "https://gitea.com", // Use Gitea URL
})
.then(async () => {
await utils.gitProvider.getAll.invalidate();
toast.success("Gitea provider created successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error configuring Gitea");
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button
variant="default"
className="flex items-center space-x-1 bg-green-700 text-white hover:bg-green-500"
>
<GiteaIcon />
<span>Gitea</span>
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen ">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
Gitea Provider <GiteaIcon className="size-5" />
</DialogTitle>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form-add-gitea"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-1"
>
<CardContent className="p-0">
<div className="flex flex-col gap-4">
<p className="text-muted-foreground text-sm">
To integrate your Gitea account, you need to create a new
application in your Gitea settings. Follow these steps:
</p>
<ol className="list-decimal list-inside text-sm text-muted-foreground">
<li className="flex flex-row gap-2 items-center">
Go to your Gitea settings{" "}
<Link
href={`${giteaUrl}/user/settings/applications`}
target="_blank"
>
<ExternalLink className="w-fit text-primary size-4" />
</Link>
</li>
<li>Navigate to Applications</li>
<li>
Create a new application with the following details:
<ul className="list-disc list-inside ml-4">
<li>Name: Dokploy</li>
<li>
Redirect URI:{" "}
<span className="text-primary">{webhookUrl}</span>{" "}
</li>
<li>Select Permissions - organization: read, user: read, repository: read/write</li>
</ul>
</li>
<li>
After creating, you'll receive an ID and Secret,
copy them and paste them below.
</li>
</ol>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Random Name eg(my-personal-account)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="giteaUrl" // Ensure consistent name for Gitea URL
render={({ field }) => (
<FormItem>
<FormLabel>Gitea URL</FormLabel>
<FormControl>
<Input placeholder="https://gitea.com/" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="redirectUri"
render={({ field }) => (
<FormItem>
<FormLabel>Redirect URI</FormLabel>
<FormControl>
<Input
disabled
placeholder="Random Name eg(my-personal-account)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientId"
render={({ field }) => (
<FormItem>
<FormLabel>Client ID</FormLabel>
<FormControl>
<Input placeholder="Client ID" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientSecret"
render={({ field }) => (
<FormItem>
<FormLabel>Client Secret</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Client Secret"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button isLoading={form.formState.isSubmitting}>
Configure Gitea App {/* Ensured consistency with Gitea */}
</Button>
</div>
</CardContent>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@ -0,0 +1,280 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon } from "lucide-react";
import { useState, useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { useRouter } from 'next/router';
const formSchema = z.object({
name: z.string().min(1, "Name is required"),
giteaUrl: z.string().min(1, "Gitea URL is required"),
clientId: z.string().min(1, "Client ID is required"),
clientSecret: z.string().min(1, "Client Secret is required"),
});
interface Props {
giteaId: string;
}
export const EditGiteaProvider = ({ giteaId }: Props) => {
const router = useRouter();
const [open, setOpen] = useState(false);
const { data: gitea, isLoading, refetch } = api.gitea.one.useQuery({ giteaId });
const { mutateAsync, isLoading: isUpdating } = api.gitea.update.useMutation();
const { mutateAsync: testConnection, isLoading: isTesting } = api.gitea.testConnection.useMutation();
const url = useUrl();
const utils = api.useUtils();
// Handle OAuth redirect results
useEffect(() => {
const { connected, error } = router.query;
// Only process if router is ready and query parameters exist
if (!router.isReady) return;
if (connected) {
toast.success("Successfully connected to Gitea", {
description: "Your Gitea provider has been authorized.",
id: 'gitea-connection-success'
});
refetch();
// Clear the query parameters to prevent re-triggering
router.replace({
pathname: router.pathname,
query: {}
}, undefined, { shallow: true });
}
if (error) {
toast.error("Gitea Connection Failed", {
description: decodeURIComponent(error as string),
id: 'gitea-connection-error'
});
// Clear the query parameters to prevent re-triggering
router.replace({
pathname: router.pathname,
query: {}
}, undefined, { shallow: true });
}
}, [router.query, router.isReady, refetch]);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
giteaUrl: "https://gitea.com",
clientId: "",
clientSecret: "",
},
});
// Update form values when data is loaded
useEffect(() => {
if (gitea) {
form.reset({
name: gitea.gitProvider?.name || "",
giteaUrl: gitea.giteaUrl || "https://gitea.com",
clientId: gitea.clientId || "",
clientSecret: gitea.clientSecret || "",
});
}
}, [gitea, form.reset]);
const onSubmit = async (values: z.infer<typeof formSchema>) => {
await mutateAsync({
giteaId: giteaId,
gitProviderId: gitea?.gitProvider?.gitProviderId || "",
name: values.name,
giteaUrl: values.giteaUrl,
clientId: values.clientId,
clientSecret: values.clientSecret,
})
.then(async () => {
await utils.gitProvider.getAll.invalidate();
toast.success("Gitea provider updated successfully");
await refetch();
setOpen(false);
})
.catch(() => {
toast.error("Error updating Gitea provider");
});
};
const handleTestConnection = async () => {
try {
const result = await testConnection({ giteaId });
toast.success("Gitea Connection Verified", {
description: result
});
} catch (error: any) {
const authUrl = error.authorizationUrl || getGiteaOAuthUrl();
toast.error("Gitea Not Connected", {
description: error.message || "Please complete the OAuth authorization process.",
action: authUrl && authUrl !== "#" ? {
label: "Authorize Now",
onClick: () => window.open(authUrl, "_blank")
} : undefined,
});
}
};
// Generate Gitea OAuth URL with state parameter
const getGiteaOAuthUrl = () => {
const clientId = form.getValues().clientId;
const giteaUrl = form.getValues().giteaUrl;
if (!clientId || !giteaUrl) {
toast.error("Configuration Incomplete", {
description: "Please fill in Client ID and Gitea URL first."
});
return "#";
}
const redirectUri = `${url}/api/providers/gitea/callback`;
// Use the scopes from the gitea data (if available), else fallback to default scopes
const scopes = gitea?.scopes?.split(',').join(' ') || 'repo repo:status read:user read:org';
//const scopes = gitea?.scopes || 'repo,repo:status,read:user,read:org';
const state = giteaId;
return `${giteaUrl}/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scopes)}&state=${encodeURIComponent(state)}`;
};
// Show loading state if data is being fetched
if (isLoading) {
return (
<Button variant="ghost" size="icon" disabled>
<PenBoxIcon className="h-4 w-4 text-muted-foreground" />
</Button>
);
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="ghost" size="icon" className="group hover:bg-blue-500/10">
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Gitea Provider</DialogTitle>
<DialogDescription>
Update your Gitea provider details.
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="My Gitea" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="giteaUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Gitea URL</FormLabel>
<FormControl>
<Input placeholder="https://gitea.example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientId"
render={({ field }) => (
<FormItem>
<FormLabel>Client ID</FormLabel>
<FormControl>
<Input placeholder="Client ID" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientSecret"
render={({ field }) => (
<FormItem>
<FormLabel>Client Secret</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Client Secret"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
onClick={handleTestConnection}
isLoading={isTesting}
>
Test Connection
</Button>
<Button
type="button"
variant="outline"
onClick={() => {
const authUrl = getGiteaOAuthUrl();
if (authUrl !== "#") {
window.open(authUrl, "_blank");
}
}}
>
Connect to Gitea
</Button>
<Button type="submit" isLoading={isUpdating}>
Save
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

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

View File

@ -238,6 +238,41 @@ export const BitbucketIcon = ({ className }: Props) => {
); );
}; };
export const GiteaIcon = ({ className }: Props) => {
return (
<svg
className={className}
version="1.1"
id="main_outline"
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="5.67 143.05 628.65 387.55"
enableBackground="new 0 0 640 640"
>
<g>
<path
id="teabag"
style={{ fill: '#FFFFFF' }}
d="M395.9,484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5,21.2-17.9,33.8-11.8c17.2,8.3,27.1,13,27.1,13l-0.1-109.2l16.7-0.1l0.1,117.1c0,0,57.4,24.2,83.1,40.1c3.7,2.3,10.2,6.8,12.9,14.4c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z"
/>
<g>
<g>
<path
style={{ fill: '#609926' }}
d="M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z"
/>
<path
style={{ fill: '#609926' }}
d="M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8C343.2,346.5,335,363.3,326.8,380.1z"
/>
</g>
</g>
</g>
</svg>
);
};
export const DockerIcon = ({ className }: Props) => { export const DockerIcon = ({ className }: Props) => {
return ( return (
<svg <svg

View File

@ -0,0 +1,16 @@
ALTER TYPE "public"."gitProviderType" ADD VALUE 'gitea';--> statement-breakpoint
CREATE TABLE "gitea" (
"giteaId" text PRIMARY KEY NOT NULL,
"giteaUrl" text DEFAULT 'https://gitea.com' NOT NULL,
"application_id" text,
"redirect_uri" text,
"secret" text,
"access_token" text,
"refresh_token" text,
"organization_name" text,
"expires_at" integer,
"gitProviderId" text NOT NULL,
"gitea_username" text
);
--> statement-breakpoint
ALTER TABLE "gitea" ADD CONSTRAINT "gitea_gitProviderId_git_provider_gitProviderId_fk" FOREIGN KEY ("gitProviderId") REFERENCES "public"."git_provider"("gitProviderId") ON DELETE cascade ON UPDATE no action;

View File

@ -0,0 +1,9 @@
ALTER TYPE "public"."sourceType" ADD VALUE 'gitea' BEFORE 'drop';--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "giteaProjectId" integer;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "giteaRepository" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "giteaOwner" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "giteaBranch" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "giteaBuildPath" text DEFAULT '/';--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "giteaPathNamespace" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "giteaId" text;--> statement-breakpoint
ALTER TABLE "application" ADD CONSTRAINT "application_giteaId_gitea_giteaId_fk" FOREIGN KEY ("giteaId") REFERENCES "public"."gitea"("giteaId") ON DELETE set null ON UPDATE no action;

View File

@ -0,0 +1,6 @@
ALTER TABLE "gitea" ADD COLUMN "client_id" text;--> statement-breakpoint
ALTER TABLE "gitea" ADD COLUMN "client_secret" text;--> statement-breakpoint
ALTER TABLE "gitea" DROP COLUMN "application_id";--> statement-breakpoint
ALTER TABLE "gitea" DROP COLUMN "secret";--> statement-breakpoint
ALTER TABLE "gitea" DROP COLUMN "refresh_token";--> statement-breakpoint
ALTER TABLE "gitea" DROP COLUMN "organization_name";

View File

@ -0,0 +1,4 @@
ALTER TABLE "gitea" ADD COLUMN "refresh_token" text;--> statement-breakpoint
ALTER TABLE "gitea" ADD COLUMN "organization_name" text;--> statement-breakpoint
ALTER TABLE "gitea" ADD COLUMN "scopes" text;--> statement-breakpoint
ALTER TABLE "gitea" ADD COLUMN "last_authenticated_at" integer;

View File

@ -0,0 +1,6 @@
ALTER TYPE "public"."sourceTypeCompose" ADD VALUE 'gitea' BEFORE 'raw';--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "giteaRepository" text;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "giteaOwner" text;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "giteaBranch" text;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "giteaId" text;--> statement-breakpoint
ALTER TABLE "compose" ADD CONSTRAINT "compose_giteaId_gitea_giteaId_fk" FOREIGN KEY ("giteaId") REFERENCES "public"."gitea"("giteaId") ON DELETE set null ON UPDATE no action;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -498,6 +498,41 @@
"when": 1741322697251, "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
} }
] ]
} }

View File

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

View File

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

View File

@ -0,0 +1,52 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { findGiteaById } from '@dokploy/server';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
const { giteaId } = req.query;
if (!giteaId || Array.isArray(giteaId)) {
return res.status(400).json({ error: 'Invalid Gitea provider ID' });
}
let gitea;
try {
gitea = await findGiteaById(giteaId);
} catch (findError) {
console.error('Error finding Gitea provider:', findError);
return res.status(404).json({ error: 'Failed to find Gitea provider' });
}
if (!gitea.clientId || !gitea.redirectUri) {
return res.status(400).json({
error: 'Incomplete OAuth configuration',
missingClientId: !gitea.clientId,
missingRedirectUri: !gitea.redirectUri
});
}
// Use the state parameter to pass the giteaId
// This is more secure than adding it to the redirect URI
const state = giteaId;
const authorizationUrl = new URL(`${gitea.giteaUrl}/login/oauth/authorize`);
authorizationUrl.searchParams.append('client_id', gitea.clientId);
authorizationUrl.searchParams.append('response_type', 'code');
authorizationUrl.searchParams.append('redirect_uri', gitea.redirectUri);
authorizationUrl.searchParams.append('scope', 'read:user repo');
authorizationUrl.searchParams.append('state', state);
// Redirect the user to the Gitea authorization page
res.redirect(307, authorizationUrl.toString());
} catch (error) {
console.error('Error initiating Gitea OAuth flow:', error);
return res.status(500).json({ error: 'Internal server error' });
}
}

View File

@ -0,0 +1,186 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { findGiteaById, updateGitea } from '@dokploy/server';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
console.log('Full Callback Request:', {
query: req.query,
method: req.method,
timestamp: new Date().toISOString()
});
const { code, state } = req.query;
// Verify received parameters
console.log('Received Parameters:', {
code: code ? 'Present' : 'Missing',
state: state ? 'Present' : 'Missing'
});
if (!code || Array.isArray(code)) {
console.error('Invalid code:', code);
return res.redirect(
307,
`/dashboard/settings/git-providers?error=${encodeURIComponent('Invalid authorization code')}`
);
}
// The state parameter now contains the giteaId
if (!state || Array.isArray(state)) {
console.error('Invalid state parameter:', state);
return res.redirect(
307,
`/dashboard/settings/git-providers?error=${encodeURIComponent('Invalid state parameter')}`
);
}
// Extract the giteaId from the state parameter
let giteaId: string;
try {
// The state could be a simple string or a JSON object
if (state.startsWith('{') && state.endsWith('}')) {
const stateObj = JSON.parse(state);
giteaId = stateObj.giteaId;
} else {
giteaId = state;
}
if (!giteaId) {
throw new Error('giteaId not found in state parameter');
}
} catch (parseError) {
console.error('Error parsing state parameter:', parseError);
return res.redirect(
307,
`/dashboard/settings/git-providers?error=${encodeURIComponent('Invalid state format')}`
);
}
let gitea;
try {
gitea = await findGiteaById(giteaId);
} catch (findError) {
console.error('Error finding Gitea provider:', findError);
return res.redirect(
307,
`/dashboard/settings/git-providers?error=${encodeURIComponent('Failed to find Gitea provider')}`
);
}
// Extensive logging of Gitea provider details
console.log('Gitea Provider Details:', {
id: gitea.giteaId,
url: gitea.giteaUrl,
clientId: gitea.clientId ? 'Present' : 'Missing',
clientSecret: gitea.clientSecret ? 'Present' : 'Missing',
redirectUri: gitea.redirectUri
});
// Validate required OAuth parameters
if (!gitea.clientId || !gitea.clientSecret) {
console.error('Missing OAuth configuration:', {
hasClientId: !!gitea.clientId,
hasClientSecret: !!gitea.clientSecret
});
return res.redirect(
307,
`/dashboard/settings/git-providers?error=${encodeURIComponent('Incomplete OAuth configuration')}`
);
}
const response = await fetch(`${gitea.giteaUrl}/login/oauth/access_token`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
},
body: new URLSearchParams({
client_id: gitea.clientId as string,
client_secret: gitea.clientSecret as string,
code: code as string,
grant_type: "authorization_code",
redirect_uri: gitea.redirectUri || '',
}),
});
// Log raw response details
const responseText = await response.text();
let result;
try {
result = JSON.parse(responseText);
} catch (parseError) {
console.error('Failed to parse response:', {
error: parseError,
responseText
});
return res.redirect(
307,
`/dashboard/settings/git-providers?error=${encodeURIComponent('Failed to parse token response')}`
);
}
if (!response.ok) {
console.error('Gitea token exchange failed:', {
result,
responseStatus: response.status
});
return res.redirect(
307,
`/dashboard/settings/git-providers?error=${encodeURIComponent(result.error || 'Token exchange failed')}`
);
}
// Validate token response
if (!result.access_token) {
console.error('Missing access token in response:', {
fullResponse: result
});
return res.redirect(
307,
`/dashboard/settings/git-providers?error=${encodeURIComponent('No access token received')}`
);
}
const expiresAt = result.expires_in
? Math.floor(Date.now() / 1000) + result.expires_in
: null;
try {
// Perform the update
const updatedGitea = await updateGitea(gitea.giteaId, {
accessToken: result.access_token,
refreshToken: result.refresh_token,
expiresAt,
...(result.organizationName ? { organizationName: result.organizationName } : {}),
});
// Log successful update
console.log('Gitea provider updated successfully:', {
hasAccessToken: !!updatedGitea.accessToken,
hasRefreshToken: !!updatedGitea.refreshToken,
expiresAt: updatedGitea.expiresAt
});
return res.redirect(307, "/dashboard/settings/git-providers?connected=true");
} catch (updateError) {
console.error('Failed to update Gitea provider:', {
error: updateError,
giteaId: gitea.giteaId
});
return res.redirect(
307,
`/dashboard/settings/git-providers?error=${encodeURIComponent('Failed to store access token')}`
);
}
} catch (error) {
console.error('Comprehensive Callback Error:', error);
return res.redirect(
307,
`/dashboard/settings/git-providers?error=${encodeURIComponent('Internal server error')}`
);
}
}

View File

@ -15,6 +15,7 @@ import { domainRouter } from "./routers/domain";
import { gitProviderRouter } from "./routers/git-provider"; import { 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,

View File

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

View File

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

View File

@ -0,0 +1,238 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiCreateGitea,
apiFindOneGitea,
apiGiteaTestConnection,
apiUpdateGitea,
apiFindGiteaBranches,
} from "@/server/db/schema";
import { db } from "@/server/db";
import {
createGitea,
findGiteaById,
haveGiteaRequirements,
testGiteaConnection,
updateGitProvider,
updateGitea,
getGiteaBranches,
getGiteaRepositories,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
// Gitea Router
export const giteaRouter = createTRPCRouter({
// Create a new Gitea provider
create: protectedProcedure
.input(apiCreateGitea)
.mutation(async ({ input, ctx }) => {
try {
return await createGitea(input, ctx.session.activeOrganizationId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating this Gitea provider",
cause: error,
});
}
}),
// Fetch a specific Gitea provider by ID
one: protectedProcedure
.input(apiFindOneGitea)
.query(async ({ input, ctx }) => {
const giteaProvider = await findGiteaById(input.giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
return giteaProvider;
}),
// Fetch all Gitea providers for the active organization
giteaProviders: protectedProcedure.query(async ({ ctx }) => {
let result = await db.query.gitea.findMany({
with: {
gitProvider: true,
},
});
// Filter by organization ID
result = result.filter(
(provider) =>
provider.gitProvider.organizationId === ctx.session.activeOrganizationId
);
// Filter providers that meet the requirements
const filtered = result
.filter((provider) => haveGiteaRequirements(provider))
.map((provider) => {
return {
giteaId: provider.giteaId,
gitProvider: {
...provider.gitProvider,
},
};
});
return filtered;
}),
// Fetch repositories from Gitea provider
getGiteaRepositories: protectedProcedure
.input(apiFindOneGitea)
.query(async ({ input, ctx }) => {
const { giteaId } = input;
if (!giteaId) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Gitea provider ID is required.",
});
}
const giteaProvider = await findGiteaById(giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
try {
// Call the service layer function to get repositories
console.log('Calling getGiteaRepositories with giteaId:', giteaId);
return await getGiteaRepositories(giteaId);
} catch (error) {
console.error('Error fetching Gitea repositories:', error);
throw new TRPCError({
code: "BAD_REQUEST",
message: error instanceof Error ? error.message : String(error),
});
}
}),
// Fetch branches of a specific Gitea repository
getGiteaBranches: protectedProcedure
.input(apiFindGiteaBranches)
.query(async ({ input, ctx }) => {
const { giteaId, owner, repositoryName } = input;
if (!giteaId || !owner || !repositoryName) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Gitea provider ID, owner, and repository name are required.",
});
}
const giteaProvider = await findGiteaById(giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
try {
// Call the service layer function with the required parameters
console.log('Calling getGiteaBranches with:', {
giteaId,
owner,
repo: repositoryName
});
return await getGiteaBranches({
giteaId,
owner,
repo: repositoryName,
id: 0 // Provide a default value for the optional id
});
} catch (error) {
console.error('Error fetching Gitea branches:', error);
throw new TRPCError({
code: "BAD_REQUEST",
message: error instanceof Error ? error.message : String(error),
});
}
}),
// Test connection to Gitea provider
testConnection: protectedProcedure
.input(apiGiteaTestConnection)
.mutation(async ({ input, ctx }) => {
// Ensure giteaId is always a non-empty string
const giteaId = input.giteaId ?? '';
try {
const giteaProvider = await findGiteaById(giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
const result = await testGiteaConnection({
giteaId,
});
return `Found ${result} repositories`;
} catch (error) {
console.error('Gitea connection test error:', error);
throw new TRPCError({
code: "BAD_REQUEST",
message: error instanceof Error ? error.message : String(error),
});
}
}),
// Update an existing Gitea provider
update: protectedProcedure
.input(apiUpdateGitea)
.mutation(async ({ input, ctx }) => {
const giteaProvider = await findGiteaById(input.giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
console.log('Updating Gitea provider:', input);
if (input.name) {
await updateGitProvider(input.gitProviderId, {
name: input.name,
organizationId: ctx.session.activeOrganizationId,
});
await updateGitea(input.giteaId, {
...input,
});
} else {
await updateGitea(input.giteaId, {
...input,
});
}
return { success: true };
}),
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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