mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Simplify Gitea authorization flow with shared utilities
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
@@ -35,7 +36,6 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -433,7 +433,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{field.value && field.value.map((path: string, index: number) => (
|
||||
{field.value?.map((path: string, index: number) => (
|
||||
<Badge
|
||||
key={index}
|
||||
variant="secondary"
|
||||
@@ -504,4 +504,4 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import type { Branch, Repository } from "@/utils/gitea-utils";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -47,31 +48,6 @@ import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
interface Repository {
|
||||
name: string;
|
||||
url: string;
|
||||
id: number;
|
||||
owner: {
|
||||
username: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface Branch {
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface GiteaProviderType {
|
||||
giteaId: string;
|
||||
gitProvider: {
|
||||
name: string;
|
||||
gitProviderId: string;
|
||||
providerType: "github" | "gitlab" | "bitbucket" | "gitea";
|
||||
createdAt: string;
|
||||
organizationId: string;
|
||||
};
|
||||
name: string;
|
||||
}
|
||||
|
||||
const GiteaProviderSchema = z.object({
|
||||
composePath: z.string().min(1),
|
||||
repository: z
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import {
|
||||
BitbucketIcon,
|
||||
GitIcon,
|
||||
GiteaIcon,
|
||||
GithubIcon,
|
||||
GitlabIcon,
|
||||
GitIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { api } from "@/utils/api";
|
||||
import { CodeIcon, GitBranch } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import { ComposeFileEditor } from "../compose-file-editor";
|
||||
import { ShowConvertedCompose } from "../show-converted-compose";
|
||||
import { SaveBitbucketProviderCompose } from "./save-bitbucket-provider-compose";
|
||||
@@ -18,141 +18,174 @@ import { SaveGitProviderCompose } from "./save-git-provider-compose";
|
||||
import { SaveGiteaProviderCompose } from "./save-gitea-provider-compose";
|
||||
import { SaveGithubProviderCompose } from "./save-github-provider-compose";
|
||||
import { SaveGitlabProviderCompose } from "./save-gitlab-provider-compose";
|
||||
|
||||
type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket" | "gitea"; // Adding gitea to the TabState
|
||||
interface Props {
|
||||
|
||||
type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket" | "gitea"; // Adding gitea to the TabState
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
||||
}
|
||||
|
||||
export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
||||
const { data: githubProviders } = api.github.githubProviders.useQuery();
|
||||
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
||||
const { data: bitbucketProviders } = api.bitbucket.bitbucketProviders.useQuery();
|
||||
const { data: bitbucketProviders } =
|
||||
api.bitbucket.bitbucketProviders.useQuery();
|
||||
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery(); // Fetching Gitea providers
|
||||
|
||||
|
||||
const { data: compose } = api.compose.one.useQuery({ composeId });
|
||||
const [tab, setSab] = useState<TabState>(compose?.sourceType || "github");
|
||||
|
||||
|
||||
// Ensure we fall back to empty arrays if the data is undefined
|
||||
const safeGithubProviders = githubProviders || [];
|
||||
const safeGitlabProviders = gitlabProviders || [];
|
||||
const safeBitbucketProviders = bitbucketProviders || [];
|
||||
const safeGiteaProviders = giteaProviders || [];
|
||||
|
||||
const renderProviderContent = (providers: any[], providerType: string, ProviderComponent: React.ComponentType<any>) => {
|
||||
if (providers.length > 0) {
|
||||
return <ProviderComponent composeId={composeId} />;
|
||||
} else {
|
||||
|
||||
const renderProviderContent = (
|
||||
providers: any[],
|
||||
providerType: string,
|
||||
ProviderComponent: React.ComponentType<any>,
|
||||
) => {
|
||||
if (providers.length > 0) {
|
||||
return <ProviderComponent composeId={composeId} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||
{providerType === "github" && <GithubIcon className="size-8 text-muted-foreground" />}
|
||||
{providerType === "gitlab" && <GitlabIcon className="size-8 text-muted-foreground" />}
|
||||
{providerType === "bitbucket" && <BitbucketIcon className="size-8 text-muted-foreground" />}
|
||||
{providerType === "gitea" && <GiteaIcon className="size-8 text-muted-foreground" />}
|
||||
<span className="text-base text-muted-foreground">
|
||||
To deploy using {providerType.charAt(0).toUpperCase() + providerType.slice(1)}, 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>
|
||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||
{providerType === "github" && (
|
||||
<GithubIcon className="size-8 text-muted-foreground" />
|
||||
)}
|
||||
{providerType === "gitlab" && (
|
||||
<GitlabIcon className="size-8 text-muted-foreground" />
|
||||
)}
|
||||
{providerType === "bitbucket" && (
|
||||
<BitbucketIcon className="size-8 text-muted-foreground" />
|
||||
)}
|
||||
{providerType === "gitea" && (
|
||||
<GiteaIcon className="size-8 text-muted-foreground" />
|
||||
)}
|
||||
<span className="text-base text-muted-foreground">
|
||||
To deploy using{" "}
|
||||
{providerType.charAt(0).toUpperCase() + providerType.slice(1)}, 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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Card className="group relative w-full bg-transparent">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-start justify-between">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="flex flex-col space-y-0.5">Provider</span>
|
||||
<p className="flex items-center text-sm font-normal text-muted-foreground">
|
||||
Select the source of your code
|
||||
</p>
|
||||
</div>
|
||||
<div className="hidden space-y-1 text-sm font-normal md:flex flex-row items-center gap-2">
|
||||
<ShowConvertedCompose composeId={composeId} />
|
||||
<GitBranch className="size-6 text-muted-foreground" />
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs
|
||||
value={tab}
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
|
||||
<TabsTrigger
|
||||
value="github"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
<Card className="group relative w-full bg-transparent">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-start justify-between">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="flex flex-col space-y-0.5">Provider</span>
|
||||
<p className="flex items-center text-sm font-normal text-muted-foreground">
|
||||
Select the source of your code
|
||||
</p>
|
||||
</div>
|
||||
<div className="hidden space-y-1 text-sm font-normal md:flex flex-row items-center gap-2">
|
||||
<ShowConvertedCompose composeId={composeId} />
|
||||
<GitBranch className="size-6 text-muted-foreground" />
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs
|
||||
value={tab}
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
}}
|
||||
>
|
||||
<GithubIcon className="size-4 text-current fill-current" />
|
||||
GitHub
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="gitlab"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<GitlabIcon className="size-4 text-current fill-current" />
|
||||
GitLab
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="bitbucket"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<BitbucketIcon className="size-4 text-current fill-current" />
|
||||
Bitbucket
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="gitea" // Added Gitea tab
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<GiteaIcon className="size-4 text-current fill-current" /> Gitea
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="git"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<GitIcon />
|
||||
Git
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="raw"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<CodeIcon className="size-4" />
|
||||
Raw
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="github" className="w-full p-2">
|
||||
{renderProviderContent(safeGithubProviders, "github", SaveGithubProviderCompose)}
|
||||
</TabsContent>
|
||||
<TabsContent value="gitea" className="w-full p-2">
|
||||
{renderProviderContent(safeGiteaProviders, "gitea", SaveGiteaProviderCompose)}
|
||||
</TabsContent>
|
||||
<TabsContent value="gitlab" className="w-full p-2">
|
||||
{renderProviderContent(safeGitlabProviders, "gitlab", SaveGitlabProviderCompose)}
|
||||
</TabsContent>
|
||||
<TabsContent value="bitbucket" className="w-full p-2">
|
||||
{renderProviderContent(safeBitbucketProviders, "bitbucket", SaveBitbucketProviderCompose)}
|
||||
</TabsContent>
|
||||
<TabsContent value="git" className="w-full p-2">
|
||||
<SaveGitProviderCompose composeId={composeId} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="raw" className="w-full p-2 flex flex-col gap-4">
|
||||
<ComposeFileEditor composeId={composeId} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
|
||||
<TabsTrigger
|
||||
value="github"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<GithubIcon className="size-4 text-current fill-current" />
|
||||
GitHub
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="gitlab"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<GitlabIcon className="size-4 text-current fill-current" />
|
||||
GitLab
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="bitbucket"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<BitbucketIcon className="size-4 text-current fill-current" />
|
||||
Bitbucket
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="gitea" // Added Gitea tab
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<GiteaIcon className="size-4 text-current fill-current" /> Gitea
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="git"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<GitIcon />
|
||||
Git
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="raw"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<CodeIcon className="size-4" />
|
||||
Raw
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="github" className="w-full p-2">
|
||||
{renderProviderContent(
|
||||
safeGithubProviders,
|
||||
"github",
|
||||
SaveGithubProviderCompose,
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="gitea" className="w-full p-2">
|
||||
{renderProviderContent(
|
||||
safeGiteaProviders,
|
||||
"gitea",
|
||||
SaveGiteaProviderCompose,
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="gitlab" className="w-full p-2">
|
||||
{renderProviderContent(
|
||||
safeGitlabProviders,
|
||||
"gitlab",
|
||||
SaveGitlabProviderCompose,
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="bitbucket" className="w-full p-2">
|
||||
{renderProviderContent(
|
||||
safeBitbucketProviders,
|
||||
"bitbucket",
|
||||
SaveBitbucketProviderCompose,
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="git" className="w-full p-2">
|
||||
<SaveGitProviderCompose composeId={composeId} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="raw" className="w-full p-2 flex flex-col gap-4">
|
||||
<ComposeFileEditor composeId={composeId} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GiteaIcon } from "@/components/icons/data-tools-icons"; // Use GiteaIcon for Gitea
|
||||
import { GiteaIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardContent } from "@/components/ui/card";
|
||||
@@ -19,9 +19,12 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
type GiteaProviderResponse,
|
||||
getGiteaOAuthUrl,
|
||||
} from "@/utils/gitea-utils";
|
||||
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";
|
||||
@@ -51,11 +54,14 @@ const Schema = z.object({
|
||||
type Schema = z.infer<typeof Schema>;
|
||||
|
||||
export const AddGiteaProvider = () => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const url = useUrl();
|
||||
|
||||
const urlObj = useUrl();
|
||||
const baseUrl =
|
||||
typeof urlObj === "string" ? urlObj : (urlObj as any)?.url || "";
|
||||
|
||||
const { mutateAsync, error, isError } = api.gitea.create.useMutation();
|
||||
const webhookUrl = `${url}/api/providers/gitea/callback`;
|
||||
const webhookUrl = `${baseUrl}/api/providers/gitea/callback`;
|
||||
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
@@ -78,24 +84,52 @@ export const AddGiteaProvider = () => {
|
||||
name: "",
|
||||
giteaUrl: "https://gitea.com",
|
||||
});
|
||||
}, [form, isOpen]);
|
||||
}, [form, webhookUrl, 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",
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.gitProvider.getAll.invalidate();
|
||||
toast.success("Gitea provider created successfully");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error configuring Gitea");
|
||||
});
|
||||
try {
|
||||
// Send the form data to create the Gitea provider
|
||||
const result = (await mutateAsync({
|
||||
clientId: data.clientId,
|
||||
clientSecret: data.clientSecret,
|
||||
name: data.name,
|
||||
redirectUri: data.redirectUri,
|
||||
giteaUrl: data.giteaUrl,
|
||||
organizationName: data.organizationName,
|
||||
})) as unknown as GiteaProviderResponse;
|
||||
|
||||
// Check if we have a giteaId from the response
|
||||
if (!result || !result.giteaId) {
|
||||
toast.error("Failed to get Gitea ID from response");
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate OAuth URL using the shared utility
|
||||
const authUrl = getGiteaOAuthUrl(
|
||||
result.giteaId,
|
||||
data.clientId,
|
||||
data.giteaUrl,
|
||||
baseUrl,
|
||||
);
|
||||
|
||||
// Open the Gitea OAuth URL
|
||||
if (authUrl !== "#") {
|
||||
window.open(authUrl, "_blank");
|
||||
} else {
|
||||
toast.error("Configuration Incomplete", {
|
||||
description: "Please fill in Client ID and Gitea URL first.",
|
||||
});
|
||||
}
|
||||
|
||||
toast.success("Gitea provider created successfully");
|
||||
setIsOpen(false);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(`Error configuring Gitea: ${error.message}`);
|
||||
} else {
|
||||
toast.error("An unknown error occurred.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -109,7 +143,7 @@ export const AddGiteaProvider = () => {
|
||||
<span>Gitea</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen ">
|
||||
<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" />
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { getGiteaOAuthUrl } from "@/utils/gitea-utils";
|
||||
import { useUrl } from "@/utils/hooks/use-url";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PenBoxIcon } from "lucide-react";
|
||||
@@ -55,7 +56,6 @@ export const EditGiteaProvider = ({ giteaId }: Props) => {
|
||||
useEffect(() => {
|
||||
const { connected, error } = router.query;
|
||||
|
||||
// Only process if router is ready and query parameters exist
|
||||
if (!router.isReady) return;
|
||||
|
||||
if (connected) {
|
||||
@@ -64,7 +64,6 @@ export const EditGiteaProvider = ({ giteaId }: Props) => {
|
||||
id: "gitea-connection-success",
|
||||
});
|
||||
refetch();
|
||||
// Clear the query parameters to prevent re-triggering
|
||||
router.replace(
|
||||
{
|
||||
pathname: router.pathname,
|
||||
@@ -80,7 +79,6 @@ export const EditGiteaProvider = ({ giteaId }: Props) => {
|
||||
description: decodeURIComponent(error as string),
|
||||
id: "gitea-connection-error",
|
||||
});
|
||||
// Clear the query parameters to prevent re-triggering
|
||||
router.replace(
|
||||
{
|
||||
pathname: router.pathname,
|
||||
@@ -102,7 +100,6 @@ export const EditGiteaProvider = ({ giteaId }: Props) => {
|
||||
},
|
||||
});
|
||||
|
||||
// Update form values when data is loaded
|
||||
useEffect(() => {
|
||||
if (gitea) {
|
||||
form.reset({
|
||||
@@ -141,7 +138,15 @@ export const EditGiteaProvider = ({ giteaId }: Props) => {
|
||||
description: result,
|
||||
});
|
||||
} catch (error: any) {
|
||||
const authUrl = error.authorizationUrl || getGiteaOAuthUrl();
|
||||
const formValues = form.getValues();
|
||||
const authUrl =
|
||||
error.authorizationUrl ||
|
||||
getGiteaOAuthUrl(
|
||||
giteaId,
|
||||
formValues.clientId,
|
||||
formValues.giteaUrl,
|
||||
typeof url === "string" ? url : (url as any).url || "",
|
||||
);
|
||||
|
||||
toast.error("Gitea Not Connected", {
|
||||
description:
|
||||
@@ -157,32 +162,6 @@ export const EditGiteaProvider = ({ giteaId }: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 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>
|
||||
@@ -282,7 +261,13 @@ export const EditGiteaProvider = ({ giteaId }: Props) => {
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const authUrl = getGiteaOAuthUrl();
|
||||
const formValues = form.getValues();
|
||||
const authUrl = getGiteaOAuthUrl(
|
||||
giteaId,
|
||||
formValues.clientId,
|
||||
formValues.giteaUrl,
|
||||
typeof url === "string" ? url : (url as any).url || "",
|
||||
);
|
||||
if (authUrl !== "#") {
|
||||
window.open(authUrl, "_blank");
|
||||
}
|
||||
|
||||
@@ -24,199 +24,236 @@ import { TRPCError } from "@trpc/server";
|
||||
export const giteaRouter = createTRPCRouter({
|
||||
// Create a new Gitea provider
|
||||
create: protectedProcedure
|
||||
.input(apiCreateGitea)
|
||||
.mutation(async ({ input, ctx }: { input: typeof apiCreateGitea._input; ctx: any }) => {
|
||||
try {
|
||||
return await createGitea(input, ctx.session.activeOrganizationId);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating this Gitea provider",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
.input(apiCreateGitea)
|
||||
.mutation(
|
||||
async ({
|
||||
input,
|
||||
ctx,
|
||||
}: { input: typeof apiCreateGitea._input; ctx: any }) => {
|
||||
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 }: { input: typeof apiFindOneGitea._input; ctx: any }) => {
|
||||
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;
|
||||
}),
|
||||
|
||||
.input(apiFindOneGitea)
|
||||
.query(
|
||||
async ({
|
||||
input,
|
||||
ctx,
|
||||
}: { input: typeof apiFindOneGitea._input; ctx: any }) => {
|
||||
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 }: { ctx: any }) => {
|
||||
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,
|
||||
let result = await db.query.gitea.findMany({
|
||||
with: {
|
||||
gitProvider: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return filtered;
|
||||
|
||||
// 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 }: { input: typeof apiFindOneGitea._input; ctx: any }) => {
|
||||
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 {
|
||||
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),
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
.input(apiFindOneGitea)
|
||||
.query(
|
||||
async ({
|
||||
input,
|
||||
ctx,
|
||||
}: { input: typeof apiFindOneGitea._input; ctx: any }) => {
|
||||
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 {
|
||||
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 }: { input: typeof apiFindGiteaBranches._input; ctx: any }) => {
|
||||
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 {
|
||||
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),
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
.input(apiFindGiteaBranches)
|
||||
.query(
|
||||
async ({
|
||||
input,
|
||||
ctx,
|
||||
}: { input: typeof apiFindGiteaBranches._input; ctx: any }) => {
|
||||
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 {
|
||||
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 }: { input: typeof apiGiteaTestConnection._input; ctx: any }) => {
|
||||
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),
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
.input(apiGiteaTestConnection)
|
||||
.mutation(
|
||||
async ({
|
||||
input,
|
||||
ctx,
|
||||
}: { input: typeof apiGiteaTestConnection._input; ctx: any }) => {
|
||||
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 }: { input: typeof apiUpdateGitea._input; ctx: any }) => {
|
||||
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 };
|
||||
}),
|
||||
});
|
||||
.input(apiUpdateGitea)
|
||||
.mutation(
|
||||
async ({
|
||||
input,
|
||||
ctx,
|
||||
}: { input: typeof apiUpdateGitea._input; ctx: any }) => {
|
||||
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 };
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
87
apps/dokploy/utils/gitea-utils.ts
Normal file
87
apps/dokploy/utils/gitea-utils.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
// utils/gitea-utils.ts
|
||||
// This file contains client-safe utilities for Gitea integration
|
||||
|
||||
/**
|
||||
* Generates an OAuth URL for Gitea authorization
|
||||
*
|
||||
* @param giteaId The ID of the Gitea provider to be used as state
|
||||
* @param clientId The OAuth client ID
|
||||
* @param giteaUrl The base URL of the Gitea instance
|
||||
* @param baseUrl The base URL of the application for callback
|
||||
* @returns The complete OAuth authorization URL
|
||||
*/
|
||||
export const getGiteaOAuthUrl = (
|
||||
giteaId: string,
|
||||
clientId: string,
|
||||
giteaUrl: string,
|
||||
baseUrl: string,
|
||||
): string => {
|
||||
if (!clientId || !giteaUrl || !baseUrl) {
|
||||
// Return a marker that can be checked by the caller
|
||||
return "#";
|
||||
}
|
||||
|
||||
const redirectUri = `${baseUrl}/api/providers/gitea/callback`;
|
||||
const scopes = "repo repo:status read:user read:org";
|
||||
|
||||
return `${giteaUrl}/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(
|
||||
redirectUri,
|
||||
)}&response_type=code&scope=${encodeURIComponent(scopes)}&state=${encodeURIComponent(giteaId)}`;
|
||||
};
|
||||
|
||||
// Interfaces for Gitea API responses and components
|
||||
export interface Repository {
|
||||
name: string;
|
||||
url: string;
|
||||
id: number;
|
||||
owner: {
|
||||
username: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Branch {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface GiteaProviderType {
|
||||
giteaId: string;
|
||||
gitProvider: {
|
||||
name: string;
|
||||
gitProviderId: string;
|
||||
providerType: "github" | "gitlab" | "bitbucket" | "gitea";
|
||||
createdAt: string;
|
||||
organizationId: string;
|
||||
};
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface GiteaProviderResponse {
|
||||
giteaId: string;
|
||||
clientId: string;
|
||||
giteaUrl: string;
|
||||
}
|
||||
|
||||
export interface GitProvider {
|
||||
gitProviderId: string;
|
||||
name: string;
|
||||
providerType: string;
|
||||
giteaId?: string;
|
||||
gitea?: {
|
||||
giteaId: string;
|
||||
giteaUrl: string;
|
||||
clientId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GiteaProvider {
|
||||
gitea?: {
|
||||
giteaId?: string;
|
||||
giteaUrl?: string;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
redirectUri?: string;
|
||||
organizationName?: string;
|
||||
};
|
||||
name?: string;
|
||||
gitProviderId?: string;
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
// @ts-ignore: Cannot find module errors
|
||||
import { db } from "@dokploy/server/db";
|
||||
// @ts-ignore: Cannot find module errors
|
||||
import {
|
||||
type apiCreateGitea,
|
||||
gitProvider,
|
||||
@@ -32,7 +34,7 @@ export const createGitea = async (
|
||||
});
|
||||
}
|
||||
|
||||
await tx
|
||||
const giteaProvider = await tx
|
||||
.insert(gitea)
|
||||
.values({
|
||||
...input,
|
||||
@@ -40,6 +42,20 @@ export const createGitea = async (
|
||||
})
|
||||
.returning()
|
||||
.then((response: (typeof gitea.$inferSelect)[]) => response[0]);
|
||||
|
||||
if (!giteaProvider) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the Gitea provider",
|
||||
});
|
||||
}
|
||||
|
||||
// Return just the essential data needed by the frontend
|
||||
return {
|
||||
giteaId: giteaProvider.giteaId,
|
||||
clientId: giteaProvider.clientId,
|
||||
giteaUrl: giteaProvider.giteaUrl,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import * as fs from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
// @ts-ignore: Cannot find module errors
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
// @ts-ignore: Cannot find module errors
|
||||
import { findGiteaById, updateGitea } from "@dokploy/server/services/gitea";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
|
||||
Reference in New Issue
Block a user