Simplify Gitea authorization flow with shared utilities

This commit is contained in:
Jason Parks
2025-03-20 16:48:59 -06:00
parent a4e4d1c467
commit 530ad31aaa
9 changed files with 558 additions and 388 deletions

View File

@@ -1,4 +1,5 @@
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Command, Command,
@@ -35,7 +36,6 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@@ -433,7 +433,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
</TooltipProvider> </TooltipProvider>
</div> </div>
<div className="flex flex-wrap gap-2 mb-2"> <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 <Badge
key={index} key={index}
variant="secondary" variant="secondary"

View File

@@ -39,6 +39,7 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import type { Branch, Repository } from "@/utils/gitea-utils";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react"; import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
@@ -47,31 +48,6 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; 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({ const GiteaProviderSchema = z.object({
composePath: z.string().min(1), composePath: z.string().min(1),
repository: z repository: z

View File

@@ -1,16 +1,16 @@
import { import {
BitbucketIcon, BitbucketIcon,
GitIcon,
GiteaIcon, GiteaIcon,
GithubIcon, GithubIcon,
GitlabIcon, GitlabIcon,
GitIcon, } from "@/components/icons/data-tools-icons";
} from "@/components/icons/data-tools-icons";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { CodeIcon, GitBranch } from "lucide-react"; import { CodeIcon, GitBranch } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useState, useEffect } from "react"; import { useState } from "react";
import { ComposeFileEditor } from "../compose-file-editor"; import { ComposeFileEditor } from "../compose-file-editor";
import { ShowConvertedCompose } from "../show-converted-compose"; import { ShowConvertedCompose } from "../show-converted-compose";
import { SaveBitbucketProviderCompose } from "./save-bitbucket-provider-compose"; import { SaveBitbucketProviderCompose } from "./save-bitbucket-provider-compose";
@@ -19,15 +19,16 @@ import { SaveGiteaProviderCompose } from "./save-gitea-provider-compose";
import { SaveGithubProviderCompose } from "./save-github-provider-compose"; import { SaveGithubProviderCompose } from "./save-github-provider-compose";
import { SaveGitlabProviderCompose } from "./save-gitlab-provider-compose"; import { SaveGitlabProviderCompose } from "./save-gitlab-provider-compose";
type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket" | "gitea"; // Adding gitea to the TabState type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket" | "gitea"; // Adding gitea to the TabState
interface Props { interface Props {
composeId: string; composeId: string;
} }
export const ShowProviderFormCompose = ({ composeId }: Props) => { export const ShowProviderFormCompose = ({ composeId }: Props) => {
const { data: githubProviders } = api.github.githubProviders.useQuery(); const { data: githubProviders } = api.github.githubProviders.useQuery();
const { data: gitlabProviders } = api.gitlab.gitlabProviders.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: giteaProviders } = api.gitea.giteaProviders.useQuery(); // Fetching Gitea providers
const { data: compose } = api.compose.one.useQuery({ composeId }); const { data: compose } = api.compose.one.useQuery({ composeId });
@@ -39,120 +40,152 @@ import { SaveGitlabProviderCompose } from "./save-gitlab-provider-compose";
const safeBitbucketProviders = bitbucketProviders || []; const safeBitbucketProviders = bitbucketProviders || [];
const safeGiteaProviders = giteaProviders || []; const safeGiteaProviders = giteaProviders || [];
const renderProviderContent = (providers: any[], providerType: string, ProviderComponent: React.ComponentType<any>) => { const renderProviderContent = (
if (providers.length > 0) { providers: any[],
return <ProviderComponent composeId={composeId} />; providerType: string,
} else { ProviderComponent: React.ComponentType<any>,
) => {
if (providers.length > 0) {
return <ProviderComponent composeId={composeId} />;
}
return ( return (
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center"> <div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
{providerType === "github" && <GithubIcon className="size-8 text-muted-foreground" />} {providerType === "github" && (
{providerType === "gitlab" && <GitlabIcon className="size-8 text-muted-foreground" />} <GithubIcon className="size-8 text-muted-foreground" />
{providerType === "bitbucket" && <BitbucketIcon className="size-8 text-muted-foreground" />} )}
{providerType === "gitea" && <GiteaIcon className="size-8 text-muted-foreground" />} {providerType === "gitlab" && (
<span className="text-base text-muted-foreground"> <GitlabIcon className="size-8 text-muted-foreground" />
To deploy using {providerType.charAt(0).toUpperCase() + providerType.slice(1)}, you need to configure your account first. )}
Please, go to{" "} {providerType === "bitbucket" && (
<Link href="/dashboard/settings/git-providers" className="text-foreground"> <BitbucketIcon className="size-8 text-muted-foreground" />
Settings )}
</Link>{" "} {providerType === "gitea" && (
to do so. <GiteaIcon className="size-8 text-muted-foreground" />
</span> )}
</div> <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 ( return (
<Card className="group relative w-full bg-transparent"> <Card className="group relative w-full bg-transparent">
<CardHeader> <CardHeader>
<CardTitle className="flex items-start justify-between"> <CardTitle className="flex items-start justify-between">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<span className="flex flex-col space-y-0.5">Provider</span> <span className="flex flex-col space-y-0.5">Provider</span>
<p className="flex items-center text-sm font-normal text-muted-foreground"> <p className="flex items-center text-sm font-normal text-muted-foreground">
Select the source of your code Select the source of your code
</p> </p>
</div> </div>
<div className="hidden space-y-1 text-sm font-normal md:flex flex-row items-center gap-2"> <div className="hidden space-y-1 text-sm font-normal md:flex flex-row items-center gap-2">
<ShowConvertedCompose composeId={composeId} /> <ShowConvertedCompose composeId={composeId} />
<GitBranch className="size-6 text-muted-foreground" /> <GitBranch className="size-6 text-muted-foreground" />
</div> </div>
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Tabs <Tabs
value={tab} value={tab}
className="w-full" className="w-full"
onValueChange={(e) => { onValueChange={(e) => {
setSab(e as TabState); 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"
> >
<GithubIcon className="size-4 text-current fill-current" /> <div className="flex flex-row items-center justify-between w-full gap-4">
GitHub <TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
</TabsTrigger> <TabsTrigger
<TabsTrigger value="github"
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"
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" />
<GitlabIcon className="size-4 text-current fill-current" /> GitHub
GitLab </TabsTrigger>
</TabsTrigger> <TabsTrigger
<TabsTrigger value="gitlab"
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"
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" />
<BitbucketIcon className="size-4 text-current fill-current" /> GitLab
Bitbucket </TabsTrigger>
</TabsTrigger> <TabsTrigger
<TabsTrigger value="bitbucket"
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"
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" />
<GiteaIcon className="size-4 text-current fill-current" /> Gitea Bitbucket
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="git" 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" className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
> >
<GitIcon /> <GiteaIcon className="size-4 text-current fill-current" /> Gitea
Git </TabsTrigger>
</TabsTrigger> <TabsTrigger
<TabsTrigger value="git"
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"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border" >
> <GitIcon />
<CodeIcon className="size-4" /> Git
Raw </TabsTrigger>
</TabsTrigger> <TabsTrigger
</TabsList> value="raw"
</div> 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"> <TabsContent value="github" className="w-full p-2">
{renderProviderContent(safeGithubProviders, "github", SaveGithubProviderCompose)} {renderProviderContent(
</TabsContent> safeGithubProviders,
<TabsContent value="gitea" className="w-full p-2"> "github",
{renderProviderContent(safeGiteaProviders, "gitea", SaveGiteaProviderCompose)} SaveGithubProviderCompose,
</TabsContent> )}
<TabsContent value="gitlab" className="w-full p-2"> </TabsContent>
{renderProviderContent(safeGitlabProviders, "gitlab", SaveGitlabProviderCompose)} <TabsContent value="gitea" className="w-full p-2">
</TabsContent> {renderProviderContent(
<TabsContent value="bitbucket" className="w-full p-2"> safeGiteaProviders,
{renderProviderContent(safeBitbucketProviders, "bitbucket", SaveBitbucketProviderCompose)} "gitea",
</TabsContent> SaveGiteaProviderCompose,
<TabsContent value="git" className="w-full p-2"> )}
<SaveGitProviderCompose composeId={composeId} /> </TabsContent>
</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"> <TabsContent value="raw" className="w-full p-2 flex flex-col gap-4">
<ComposeFileEditor composeId={composeId} /> <ComposeFileEditor composeId={composeId} />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</CardContent> </CardContent>
</Card> </Card>
); );
}; };

View File

@@ -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 { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { CardContent } from "@/components/ui/card"; import { CardContent } from "@/components/ui/card";
@@ -19,9 +19,12 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import {
type GiteaProviderResponse,
getGiteaOAuthUrl,
} from "@/utils/gitea-utils";
import { useUrl } from "@/utils/hooks/use-url"; import { useUrl } from "@/utils/hooks/use-url";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { ExternalLink } from "lucide-react"; import { ExternalLink } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@@ -51,11 +54,14 @@ const Schema = z.object({
type Schema = z.infer<typeof Schema>; type Schema = z.infer<typeof Schema>;
export const AddGiteaProvider = () => { export const AddGiteaProvider = () => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false); 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 { 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>({ const form = useForm<Schema>({
defaultValues: { defaultValues: {
@@ -78,24 +84,52 @@ export const AddGiteaProvider = () => {
name: "", name: "",
giteaUrl: "https://gitea.com", giteaUrl: "https://gitea.com",
}); });
}, [form, isOpen]); }, [form, webhookUrl, isOpen]);
const onSubmit = async (data: Schema) => { const onSubmit = async (data: Schema) => {
await mutateAsync({ try {
clientId: data.clientId || "", // Send the form data to create the Gitea provider
clientSecret: data.clientSecret || "", const result = (await mutateAsync({
name: data.name || "", clientId: data.clientId,
redirectUri: data.redirectUri || "", clientSecret: data.clientSecret,
giteaUrl: data.giteaUrl || "https://gitea.com", name: data.name,
}) redirectUri: data.redirectUri,
.then(async () => { giteaUrl: data.giteaUrl,
await utils.gitProvider.getAll.invalidate(); organizationName: data.organizationName,
toast.success("Gitea provider created successfully"); })) as unknown as GiteaProviderResponse;
setIsOpen(false);
}) // Check if we have a giteaId from the response
.catch(() => { if (!result || !result.giteaId) {
toast.error("Error configuring Gitea"); 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 ( return (
@@ -109,7 +143,7 @@ export const AddGiteaProvider = () => {
<span>Gitea</span> <span>Gitea</span>
</Button> </Button>
</DialogTrigger> </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> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
Gitea Provider <GiteaIcon className="size-5" /> Gitea Provider <GiteaIcon className="size-5" />

View File

@@ -17,6 +17,7 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { getGiteaOAuthUrl } from "@/utils/gitea-utils";
import { useUrl } from "@/utils/hooks/use-url"; import { useUrl } from "@/utils/hooks/use-url";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon } from "lucide-react"; import { PenBoxIcon } from "lucide-react";
@@ -55,7 +56,6 @@ export const EditGiteaProvider = ({ giteaId }: Props) => {
useEffect(() => { useEffect(() => {
const { connected, error } = router.query; const { connected, error } = router.query;
// Only process if router is ready and query parameters exist
if (!router.isReady) return; if (!router.isReady) return;
if (connected) { if (connected) {
@@ -64,7 +64,6 @@ export const EditGiteaProvider = ({ giteaId }: Props) => {
id: "gitea-connection-success", id: "gitea-connection-success",
}); });
refetch(); refetch();
// Clear the query parameters to prevent re-triggering
router.replace( router.replace(
{ {
pathname: router.pathname, pathname: router.pathname,
@@ -80,7 +79,6 @@ export const EditGiteaProvider = ({ giteaId }: Props) => {
description: decodeURIComponent(error as string), description: decodeURIComponent(error as string),
id: "gitea-connection-error", id: "gitea-connection-error",
}); });
// Clear the query parameters to prevent re-triggering
router.replace( router.replace(
{ {
pathname: router.pathname, pathname: router.pathname,
@@ -102,7 +100,6 @@ export const EditGiteaProvider = ({ giteaId }: Props) => {
}, },
}); });
// Update form values when data is loaded
useEffect(() => { useEffect(() => {
if (gitea) { if (gitea) {
form.reset({ form.reset({
@@ -141,7 +138,15 @@ export const EditGiteaProvider = ({ giteaId }: Props) => {
description: result, description: result,
}); });
} catch (error: any) { } 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", { toast.error("Gitea Not Connected", {
description: 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) { if (isLoading) {
return ( return (
<Button variant="ghost" size="icon" disabled> <Button variant="ghost" size="icon" disabled>
@@ -282,7 +261,13 @@ export const EditGiteaProvider = ({ giteaId }: Props) => {
type="button" type="button"
variant="outline" variant="outline"
onClick={() => { 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 !== "#") { if (authUrl !== "#") {
window.open(authUrl, "_blank"); window.open(authUrl, "_blank");
} }

View File

@@ -24,199 +24,236 @@ import { TRPCError } from "@trpc/server";
export const giteaRouter = createTRPCRouter({ export const giteaRouter = createTRPCRouter({
// Create a new Gitea provider // Create a new Gitea provider
create: protectedProcedure create: protectedProcedure
.input(apiCreateGitea) .input(apiCreateGitea)
.mutation(async ({ input, ctx }: { input: typeof apiCreateGitea._input; ctx: any }) => { .mutation(
try { async ({
return await createGitea(input, ctx.session.activeOrganizationId); input,
} catch (error) { ctx,
throw new TRPCError({ }: { input: typeof apiCreateGitea._input; ctx: any }) => {
code: "BAD_REQUEST", try {
message: "Error creating this Gitea provider", return await createGitea(input, ctx.session.activeOrganizationId);
cause: error, } catch (error) {
}); throw new TRPCError({
} code: "BAD_REQUEST",
}), message: "Error creating this Gitea provider",
cause: error,
});
}
},
),
// Fetch a specific Gitea provider by ID // Fetch a specific Gitea provider by ID
one: protectedProcedure one: protectedProcedure
.input(apiFindOneGitea) .input(apiFindOneGitea)
.query(async ({ input, ctx }: { input: typeof apiFindOneGitea._input; ctx: any }) => { .query(
const giteaProvider = await findGiteaById(input.giteaId); async ({
if ( input,
giteaProvider.gitProvider.organizationId !== ctx.session.activeOrganizationId ctx,
) { }: { input: typeof apiFindOneGitea._input; ctx: any }) => {
throw new TRPCError({ const giteaProvider = await findGiteaById(input.giteaId);
code: "UNAUTHORIZED", if (
message: "You are not allowed to access this Gitea provider", giteaProvider.gitProvider.organizationId !==
}); ctx.session.activeOrganizationId
} ) {
return giteaProvider; 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 // Fetch all Gitea providers for the active organization
giteaProviders: protectedProcedure.query(async ({ ctx }: { ctx: any }) => { giteaProviders: protectedProcedure.query(async ({ ctx }: { ctx: any }) => {
let result = await db.query.gitea.findMany({ let result = await db.query.gitea.findMany({
with: { with: {
gitProvider: true, 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; // 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 // Fetch repositories from Gitea provider
getGiteaRepositories: protectedProcedure getGiteaRepositories: protectedProcedure
.input(apiFindOneGitea) .input(apiFindOneGitea)
.query(async ({ input, ctx }: { input: typeof apiFindOneGitea._input; ctx: any }) => { .query(
const { giteaId } = input; async ({
input,
ctx,
}: { input: typeof apiFindOneGitea._input; ctx: any }) => {
const { giteaId } = input;
if (!giteaId) { if (!giteaId) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Gitea provider ID is required.", message: "Gitea provider ID is required.",
}); });
} }
const giteaProvider = await findGiteaById(giteaId); const giteaProvider = await findGiteaById(giteaId);
if ( if (
giteaProvider.gitProvider.organizationId !== ctx.session.activeOrganizationId giteaProvider.gitProvider.organizationId !==
) { ctx.session.activeOrganizationId
throw new TRPCError({ ) {
code: "UNAUTHORIZED", throw new TRPCError({
message: "You are not allowed to access this Gitea provider", code: "UNAUTHORIZED",
}); message: "You are not allowed to access this Gitea provider",
} });
}
try { try {
return await getGiteaRepositories(giteaId); return await getGiteaRepositories(giteaId);
} catch (error) { } catch (error) {
console.error("Error fetching Gitea repositories:", error); console.error("Error fetching Gitea repositories:", error);
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: error instanceof Error ? error.message : String(error), message: error instanceof Error ? error.message : String(error),
}); });
} }
}), },
),
// Fetch branches of a specific Gitea repository // Fetch branches of a specific Gitea repository
getGiteaBranches: protectedProcedure getGiteaBranches: protectedProcedure
.input(apiFindGiteaBranches) .input(apiFindGiteaBranches)
.query(async ({ input, ctx }: { input: typeof apiFindGiteaBranches._input; ctx: any }) => { .query(
const { giteaId, owner, repositoryName } = input; async ({
input,
ctx,
}: { input: typeof apiFindGiteaBranches._input; ctx: any }) => {
const { giteaId, owner, repositoryName } = input;
if (!giteaId || !owner || !repositoryName) { if (!giteaId || !owner || !repositoryName) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Gitea provider ID, owner, and repository name are required.", message:
}); "Gitea provider ID, owner, and repository name are required.",
} });
}
const giteaProvider = await findGiteaById(giteaId); const giteaProvider = await findGiteaById(giteaId);
if ( if (
giteaProvider.gitProvider.organizationId !== ctx.session.activeOrganizationId giteaProvider.gitProvider.organizationId !==
) { ctx.session.activeOrganizationId
throw new TRPCError({ ) {
code: "UNAUTHORIZED", throw new TRPCError({
message: "You are not allowed to access this Gitea provider", code: "UNAUTHORIZED",
}); message: "You are not allowed to access this Gitea provider",
} });
}
try { try {
return await getGiteaBranches({ return await getGiteaBranches({
giteaId, giteaId,
owner, owner,
repo: repositoryName, repo: repositoryName,
id: 0, // Provide a default value for the optional id id: 0, // Provide a default value for the optional id
}); });
} catch (error) { } catch (error) {
console.error("Error fetching Gitea branches:", error); console.error("Error fetching Gitea branches:", error);
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: error instanceof Error ? error.message : String(error), message: error instanceof Error ? error.message : String(error),
}); });
} }
}), },
),
// Test connection to Gitea provider // Test connection to Gitea provider
testConnection: protectedProcedure testConnection: protectedProcedure
.input(apiGiteaTestConnection) .input(apiGiteaTestConnection)
.mutation(async ({ input, ctx }: { input: typeof apiGiteaTestConnection._input; ctx: any }) => { .mutation(
const giteaId = input.giteaId ?? ""; async ({
input,
ctx,
}: { input: typeof apiGiteaTestConnection._input; ctx: any }) => {
const giteaId = input.giteaId ?? "";
try { try {
const giteaProvider = await findGiteaById(giteaId); const giteaProvider = await findGiteaById(giteaId);
if ( if (
giteaProvider.gitProvider.organizationId !== ctx.session.activeOrganizationId giteaProvider.gitProvider.organizationId !==
) { ctx.session.activeOrganizationId
throw new TRPCError({ ) {
code: "UNAUTHORIZED", throw new TRPCError({
message: "You are not allowed to access this Gitea provider", code: "UNAUTHORIZED",
}); message: "You are not allowed to access this Gitea provider",
} });
}
const result = await testGiteaConnection({ const result = await testGiteaConnection({
giteaId, giteaId,
}); });
return `Found ${result} repositories`; return `Found ${result} repositories`;
} catch (error) { } catch (error) {
console.error("Gitea connection test error:", error); console.error("Gitea connection test error:", error);
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: error instanceof Error ? error.message : String(error), message: error instanceof Error ? error.message : String(error),
}); });
} }
}), },
),
// Update an existing Gitea provider // Update an existing Gitea provider
update: protectedProcedure update: protectedProcedure
.input(apiUpdateGitea) .input(apiUpdateGitea)
.mutation(async ({ input, ctx }: { input: typeof apiUpdateGitea._input; ctx: any }) => { .mutation(
const giteaProvider = await findGiteaById(input.giteaId); async ({
if ( input,
giteaProvider.gitProvider.organizationId !== ctx.session.activeOrganizationId ctx,
) { }: { input: typeof apiUpdateGitea._input; ctx: any }) => {
throw new TRPCError({ const giteaProvider = await findGiteaById(input.giteaId);
code: "UNAUTHORIZED", if (
message: "You are not allowed to access this Gitea provider", 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); console.log("Updating Gitea provider:", input);
if (input.name) { if (input.name) {
await updateGitProvider(input.gitProviderId, { await updateGitProvider(input.gitProviderId, {
name: input.name, name: input.name,
organizationId: ctx.session.activeOrganizationId, organizationId: ctx.session.activeOrganizationId,
}); });
await updateGitea(input.giteaId, { await updateGitea(input.giteaId, {
...input, ...input,
}); });
} else { } else {
await updateGitea(input.giteaId, { await updateGitea(input.giteaId, {
...input, ...input,
}); });
} }
return { success: true }; return { success: true };
}), },
}); ),
});

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

View File

@@ -1,4 +1,6 @@
// @ts-ignore: Cannot find module errors
import { db } from "@dokploy/server/db"; import { db } from "@dokploy/server/db";
// @ts-ignore: Cannot find module errors
import { import {
type apiCreateGitea, type apiCreateGitea,
gitProvider, gitProvider,
@@ -32,7 +34,7 @@ export const createGitea = async (
}); });
} }
await tx const giteaProvider = await tx
.insert(gitea) .insert(gitea)
.values({ .values({
...input, ...input,
@@ -40,6 +42,20 @@ export const createGitea = async (
}) })
.returning() .returning()
.then((response: (typeof gitea.$inferSelect)[]) => response[0]); .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,
};
}); });
}; };

View File

@@ -1,7 +1,9 @@
import { createWriteStream } from "node:fs"; import { createWriteStream } from "node:fs";
import * as fs from "node:fs/promises"; import * as fs from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
// @ts-ignore: Cannot find module errors
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
// @ts-ignore: Cannot find module errors
import { findGiteaById, updateGitea } from "@dokploy/server/services/gitea"; import { findGiteaById, updateGitea } from "@dokploy/server/services/gitea";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { recreateDirectory } from "../filesystem/directory"; import { recreateDirectory } from "../filesystem/directory";