mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat(gitea): add Gitea repository support
This commit is contained in:
@@ -112,13 +112,6 @@ export default async function handler(
|
||||
res.status(301).json({ message: "Branch Not Match" });
|
||||
return;
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
} 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;
|
||||
=======
|
||||
|
||||
const commitedPaths = await extractCommitedPaths(
|
||||
req.body,
|
||||
@@ -134,7 +127,12 @@ export default async function handler(
|
||||
if (!shouldDeployPaths) {
|
||||
res.status(301).json({ message: "Watch Paths Not Match" });
|
||||
return;
|
||||
>>>>>>> fork/canary
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import { eq } from "drizzle-orm";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import {
|
||||
extractBranchName,
|
||||
extractCommitedPaths,
|
||||
extractCommitMessage,
|
||||
extractCommitedPaths,
|
||||
extractHash,
|
||||
} from "../[refreshToken]";
|
||||
|
||||
|
||||
@@ -1,52 +1,41 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { findGiteaById } from '@dokploy/server';
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { findGitea, redirectWithError } from "./helper";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
try {
|
||||
if (req.method !== 'GET') {
|
||||
return res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
try {
|
||||
if (req.method !== "GET") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
const { giteaId } = req.query;
|
||||
const { giteaId } = req.query;
|
||||
|
||||
if (!giteaId || Array.isArray(giteaId)) {
|
||||
return res.status(400).json({ error: 'Invalid Gitea provider ID' });
|
||||
}
|
||||
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' });
|
||||
}
|
||||
const gitea = await findGitea(giteaId as string);
|
||||
if (!gitea || !gitea.clientId || !gitea.redirectUri) {
|
||||
return redirectWithError(res, "Incomplete OAuth configuration");
|
||||
}
|
||||
|
||||
if (!gitea.clientId || !gitea.redirectUri) {
|
||||
return res.status(400).json({
|
||||
error: 'Incomplete OAuth configuration',
|
||||
missingClientId: !gitea.clientId,
|
||||
missingRedirectUri: !gitea.redirectUri
|
||||
});
|
||||
}
|
||||
// Generate the Gitea authorization URL
|
||||
const authorizationUrl = new URL(`${gitea.giteaUrl}/login/oauth/authorize`);
|
||||
authorizationUrl.searchParams.append("client_id", gitea.clientId as string);
|
||||
authorizationUrl.searchParams.append("response_type", "code");
|
||||
authorizationUrl.searchParams.append(
|
||||
"redirect_uri",
|
||||
gitea.redirectUri as string,
|
||||
);
|
||||
authorizationUrl.searchParams.append("scope", "read:user repo");
|
||||
authorizationUrl.searchParams.append("state", giteaId as string);
|
||||
|
||||
// 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' });
|
||||
}
|
||||
}
|
||||
// Redirect user to Gitea authorization URL
|
||||
return res.redirect(307, authorizationUrl.toString());
|
||||
} catch (error) {
|
||||
console.error("Error initiating Gitea OAuth flow:", error);
|
||||
return res.status(500).json({ error: "Internal server error" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,186 +1,94 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { findGiteaById, updateGitea } from '@dokploy/server';
|
||||
import { updateGitea } from "@dokploy/server";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { type Gitea, findGitea, redirectWithError } from "./helper";
|
||||
|
||||
// Helper to parse the state parameter
|
||||
const parseState = (state: string): string | null => {
|
||||
try {
|
||||
const stateObj =
|
||||
state.startsWith("{") && state.endsWith("}") ? JSON.parse(state) : {};
|
||||
return stateObj.giteaId || state || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to fetch access token from Gitea
|
||||
const fetchAccessToken = async (gitea: Gitea, code: string) => {
|
||||
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,
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: gitea.redirectUri || "",
|
||||
}),
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
return response.ok
|
||||
? JSON.parse(responseText)
|
||||
: { error: "Token exchange failed", responseText };
|
||||
};
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
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;
|
||||
|
||||
const { code, state } = req.query;
|
||||
if (!code || Array.isArray(code) || !state || Array.isArray(state)) {
|
||||
return redirectWithError(
|
||||
res,
|
||||
"Invalid authorization code or state parameter",
|
||||
);
|
||||
}
|
||||
|
||||
// Verify received parameters
|
||||
console.log('Received Parameters:', {
|
||||
code: code ? 'Present' : 'Missing',
|
||||
state: state ? 'Present' : 'Missing'
|
||||
});
|
||||
const giteaId = parseState(state as string);
|
||||
if (!giteaId) return redirectWithError(res, "Invalid state format");
|
||||
|
||||
if (!code || Array.isArray(code)) {
|
||||
console.error('Invalid code:', code);
|
||||
return res.redirect(
|
||||
307,
|
||||
`/dashboard/settings/git-providers?error=${encodeURIComponent('Invalid authorization code')}`
|
||||
);
|
||||
}
|
||||
const gitea = await findGitea(giteaId);
|
||||
if (!gitea) return redirectWithError(res, "Failed to find Gitea provider");
|
||||
|
||||
// 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')}`
|
||||
);
|
||||
}
|
||||
// Fetch the access token from Gitea
|
||||
const result = await fetchAccessToken(gitea, code as string);
|
||||
|
||||
// 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 (result.error) {
|
||||
console.error("Token exchange failed:", result);
|
||||
return redirectWithError(res, result.error);
|
||||
}
|
||||
|
||||
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')}`
|
||||
);
|
||||
}
|
||||
if (!result.access_token) {
|
||||
console.error("Missing access token:", result);
|
||||
return redirectWithError(res, "No access token received");
|
||||
}
|
||||
|
||||
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')}`
|
||||
);
|
||||
}
|
||||
const expiresAt = result.expires_in
|
||||
? Math.floor(Date.now() / 1000) + result.expires_in
|
||||
: null;
|
||||
|
||||
// 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
|
||||
});
|
||||
try {
|
||||
const updatedGitea = await updateGitea(gitea.giteaId, {
|
||||
accessToken: result.access_token,
|
||||
refreshToken: result.refresh_token,
|
||||
expiresAt,
|
||||
...(result.organizationName
|
||||
? { organizationName: result.organizationName }
|
||||
: {}),
|
||||
});
|
||||
|
||||
// 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')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
console.log("Gitea provider updated successfully:", updatedGitea);
|
||||
return res.redirect(
|
||||
307,
|
||||
"/dashboard/settings/git-providers?connected=true",
|
||||
);
|
||||
} catch (updateError) {
|
||||
console.error("Failed to update Gitea provider:", updateError);
|
||||
return redirectWithError(res, "Failed to store access token");
|
||||
}
|
||||
}
|
||||
|
||||
42
apps/dokploy/pages/api/providers/gitea/helper.ts
Normal file
42
apps/dokploy/pages/api/providers/gitea/helper.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { findGiteaById } from "@dokploy/server";
|
||||
import type { NextApiResponse } from "next";
|
||||
|
||||
// Shared Gitea interface
|
||||
export interface Gitea {
|
||||
giteaId: string;
|
||||
gitProviderId: string;
|
||||
redirectUri: string | null;
|
||||
accessToken: string | null;
|
||||
refreshToken: string | null;
|
||||
expiresAt: number | null;
|
||||
giteaUrl: string;
|
||||
clientId: string | null;
|
||||
clientSecret: string | null;
|
||||
organizationName?: string;
|
||||
gitProvider: {
|
||||
name: string;
|
||||
gitProviderId: string;
|
||||
providerType: "github" | "gitlab" | "bitbucket" | "gitea";
|
||||
createdAt: string;
|
||||
organizationId: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Shared function to find Gitea by ID
|
||||
export const findGitea = async (giteaId: string): Promise<Gitea | null> => {
|
||||
try {
|
||||
const gitea = await findGiteaById(giteaId);
|
||||
return gitea;
|
||||
} catch (findError) {
|
||||
console.error("Error finding Gitea provider:", findError);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper for redirecting with error message
|
||||
export const redirectWithError = (res: NextApiResponse, error: string) => {
|
||||
return res.redirect(
|
||||
307,
|
||||
`/dashboard/settings/git-providers?error=${encodeURIComponent(error)}`,
|
||||
);
|
||||
};
|
||||
@@ -34,6 +34,15 @@ import {
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -47,6 +56,13 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
@@ -64,8 +80,8 @@ import {
|
||||
Loader2,
|
||||
PlusIcon,
|
||||
Search,
|
||||
X,
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
@@ -73,25 +89,9 @@ import type {
|
||||
} from "next";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { type ReactElement, useMemo, useState, useEffect } from "react";
|
||||
import { type ReactElement, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import superjson from "superjson";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
export type Services = {
|
||||
appName: string;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ShowImport } from "@/components/dashboard/application/advanced/import/show-import";
|
||||
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
|
||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
||||
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
|
||||
@@ -47,7 +48,6 @@ import { useRouter } from "next/router";
|
||||
import { type ReactElement, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import superjson from "superjson";
|
||||
import { ShowImport } from "@/components/dashboard/application/advanced/import/show-import";
|
||||
|
||||
type TabState =
|
||||
| "projects"
|
||||
|
||||
@@ -8,11 +8,11 @@ import { ShowExternalPostgresCredentials } from "@/components/dashboard/postgres
|
||||
import { ShowGeneralPostgres } from "@/components/dashboard/postgres/general/show-general-postgres";
|
||||
import { ShowInternalPostgresCredentials } from "@/components/dashboard/postgres/general/show-internal-postgres-credentials";
|
||||
import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres";
|
||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||
import { PostgresqlIcon } from "@/components/icons/data-tools-icons";
|
||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Card,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import type { ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
|
||||
Reference in New Issue
Block a user