feat: Add Gitea integration
- New /api/gitea/* routes for Gitea API - List projects, branches, commits - File tree and content viewing - User token management
This commit is contained in:
@@ -21,6 +21,7 @@ import { agentsRouter } from "./routes/agents.ts";
|
||||
import { logsRouter } from "./routes/logs.ts";
|
||||
import { userSettingsRouter } from "./routes/usersettings.ts";
|
||||
import gitlabRouter from "./routes/gitlab.ts";
|
||||
import { giteaRouter } from "./routes/gitea.ts";
|
||||
import { dockerRouter } from "./routes/docker.ts";
|
||||
import { exportRouter } from "./routes/export.ts";
|
||||
import { appUpdateRouter } from "./routes/appUpdate.ts";
|
||||
@@ -75,6 +76,8 @@ app.use(logsRouter.allowedMethods());
|
||||
app.use(userSettingsRouter.routes());
|
||||
app.use(userSettingsRouter.allowedMethods());
|
||||
app.use(gitlabRouter.routes());
|
||||
app.use(giteaRouter.routes());
|
||||
app.use(giteaRouter.allowedMethods());
|
||||
app.use(gitlabRouter.allowedMethods());
|
||||
app.use(dockerRouter.routes());
|
||||
app.use(dockerRouter.allowedMethods());
|
||||
|
||||
320
src/routes/gitea.ts
Normal file
320
src/routes/gitea.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
import { Router } from "@oak/oak";
|
||||
import { ObjectId } from "mongodb";
|
||||
import { authMiddleware } from "../middleware/auth.ts";
|
||||
import { getDB } from "../db/mongo.ts";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
// Gitea API configuration
|
||||
const GITEA_URL = Deno.env.get("GITEA_URL") || "https://git.kronos-soulution.de";
|
||||
const GITEA_TOKEN = Deno.env.get("GITEA_TOKEN") || "";
|
||||
|
||||
// Get user's Gitea token or fall back to global
|
||||
async function getUserGiteaToken(userId: string): Promise<string> {
|
||||
const db = await getDB();
|
||||
const user = await db.collection("users").findOne({ _id: new ObjectId(userId) });
|
||||
return user?.giteaToken || GITEA_TOKEN;
|
||||
}
|
||||
|
||||
interface GiteaProject {
|
||||
id: number;
|
||||
name: string;
|
||||
full_name: string;
|
||||
description: string | null;
|
||||
default_branch: string;
|
||||
html_url: string;
|
||||
clone_url: string;
|
||||
ssh_url: string;
|
||||
updated_at: string;
|
||||
owner: {
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GiteaBranch {
|
||||
name: string;
|
||||
commit: {
|
||||
id: string;
|
||||
message: string;
|
||||
author: {
|
||||
name: string;
|
||||
email: string;
|
||||
date: string;
|
||||
};
|
||||
};
|
||||
protected: boolean;
|
||||
}
|
||||
|
||||
interface GiteaCommit {
|
||||
sha: string;
|
||||
commit: {
|
||||
message: string;
|
||||
author: {
|
||||
name: string;
|
||||
email: string;
|
||||
date: string;
|
||||
};
|
||||
};
|
||||
html_url: string;
|
||||
author?: {
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GiteaTreeItem {
|
||||
name: string;
|
||||
path: string;
|
||||
type: "file" | "dir";
|
||||
size?: number;
|
||||
sha: string;
|
||||
}
|
||||
|
||||
// Helper function to call Gitea API
|
||||
async function giteaFetch<T>(endpoint: string, options: RequestInit = {}, token?: string): Promise<T> {
|
||||
const url = `${GITEA_URL}/api/v1${endpoint}`;
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
...options.headers as Record<string, string>,
|
||||
};
|
||||
|
||||
if (token || GITEA_TOKEN) {
|
||||
headers["Authorization"] = `token ${token || GITEA_TOKEN}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, { ...options, headers });
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Gitea API error (${response.status}): ${error}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// ============ ROUTES ============
|
||||
|
||||
// Get all accessible Gitea projects
|
||||
router.get("/api/gitea/projects", authMiddleware, async (ctx) => {
|
||||
try {
|
||||
const token = await getUserGiteaToken(ctx.state.user.id);
|
||||
const projects = await giteaFetch<GiteaProject[]>("/user/repos?limit=100", {}, token);
|
||||
|
||||
// Transform to common format
|
||||
const result = projects.map(p => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
fullName: p.full_name,
|
||||
path: p.full_name,
|
||||
description: p.description,
|
||||
webUrl: p.html_url,
|
||||
cloneUrl: p.clone_url,
|
||||
defaultBranch: p.default_branch,
|
||||
updatedAt: p.updated_at,
|
||||
owner: p.owner.login
|
||||
}));
|
||||
|
||||
ctx.response.body = result;
|
||||
} catch (err) {
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: err instanceof Error ? err.message : "Failed to fetch Gitea projects" };
|
||||
}
|
||||
});
|
||||
|
||||
// Get single project details
|
||||
router.get("/api/gitea/projects/:id", authMiddleware, async (ctx) => {
|
||||
try {
|
||||
const token = await getUserGiteaToken(ctx.state.user.id);
|
||||
const project = await giteaFetch<GiteaProject>(`/repositories/${ctx.params.id}`, {}, token);
|
||||
|
||||
ctx.response.body = {
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
fullName: project.full_name,
|
||||
description: project.description,
|
||||
webUrl: project.html_url,
|
||||
defaultBranch: project.default_branch
|
||||
};
|
||||
} catch (err) {
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: err instanceof Error ? err.message : "Failed to fetch project" };
|
||||
}
|
||||
});
|
||||
|
||||
// Get branches for a project
|
||||
router.get("/api/gitea/projects/:owner/:repo/branches", authMiddleware, async (ctx) => {
|
||||
try {
|
||||
const { owner, repo } = ctx.params;
|
||||
const token = await getUserGiteaToken(ctx.state.user.id);
|
||||
const branches = await giteaFetch<GiteaBranch[]>(`/repos/${owner}/${repo}/branches`, {}, token);
|
||||
|
||||
ctx.response.body = branches.map(b => ({
|
||||
name: b.name,
|
||||
protected: b.protected,
|
||||
commit: {
|
||||
id: b.commit.id,
|
||||
message: b.commit.message,
|
||||
authorName: b.commit.author.name,
|
||||
date: b.commit.author.date
|
||||
}
|
||||
}));
|
||||
} catch (err) {
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: err instanceof Error ? err.message : "Failed to fetch branches" };
|
||||
}
|
||||
});
|
||||
|
||||
// Get commits for a project
|
||||
router.get("/api/gitea/projects/:owner/:repo/commits", authMiddleware, async (ctx) => {
|
||||
try {
|
||||
const { owner, repo } = ctx.params;
|
||||
const branch = ctx.request.url.searchParams.get("branch") || "main";
|
||||
const limit = ctx.request.url.searchParams.get("limit") || "20";
|
||||
const token = await getUserGiteaToken(ctx.state.user.id);
|
||||
|
||||
const commits = await giteaFetch<GiteaCommit[]>(
|
||||
`/repos/${owner}/${repo}/commits?sha=${branch}&limit=${limit}`,
|
||||
{},
|
||||
token
|
||||
);
|
||||
|
||||
ctx.response.body = commits.map(c => ({
|
||||
id: c.sha,
|
||||
shortId: c.sha.substring(0, 8),
|
||||
title: c.commit.message.split('\n')[0],
|
||||
message: c.commit.message,
|
||||
authorName: c.commit.author.name,
|
||||
authorEmail: c.commit.author.email,
|
||||
date: c.commit.author.date,
|
||||
webUrl: c.html_url,
|
||||
authorAvatar: c.author?.avatar_url
|
||||
}));
|
||||
} catch (err) {
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: err instanceof Error ? err.message : "Failed to fetch commits" };
|
||||
}
|
||||
});
|
||||
|
||||
// Get file tree for a project
|
||||
router.get("/api/gitea/projects/:owner/:repo/tree", authMiddleware, async (ctx) => {
|
||||
try {
|
||||
const { owner, repo } = ctx.params;
|
||||
const ref = ctx.request.url.searchParams.get("ref") || "main";
|
||||
const path = ctx.request.url.searchParams.get("path") || "";
|
||||
const token = await getUserGiteaToken(ctx.state.user.id);
|
||||
|
||||
const endpoint = path
|
||||
? `/repos/${owner}/${repo}/contents/${path}?ref=${ref}`
|
||||
: `/repos/${owner}/${repo}/contents?ref=${ref}`;
|
||||
|
||||
const items = await giteaFetch<GiteaTreeItem[]>(endpoint, {}, token);
|
||||
|
||||
ctx.response.body = items.map(item => ({
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
type: item.type === "dir" ? "tree" : "blob",
|
||||
size: item.size
|
||||
}));
|
||||
} catch (err) {
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: err instanceof Error ? err.message : "Failed to fetch tree" };
|
||||
}
|
||||
});
|
||||
|
||||
// Get file content
|
||||
router.get("/api/gitea/projects/:owner/:repo/file", authMiddleware, async (ctx) => {
|
||||
try {
|
||||
const { owner, repo } = ctx.params;
|
||||
const path = ctx.request.url.searchParams.get("path");
|
||||
const ref = ctx.request.url.searchParams.get("ref") || "main";
|
||||
|
||||
if (!path) {
|
||||
ctx.response.status = 400;
|
||||
ctx.response.body = { error: "Path is required" };
|
||||
return;
|
||||
}
|
||||
|
||||
const token = await getUserGiteaToken(ctx.state.user.id);
|
||||
|
||||
interface FileContent {
|
||||
content: string;
|
||||
encoding: string;
|
||||
name: string;
|
||||
path: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
const file = await giteaFetch<FileContent>(
|
||||
`/repos/${owner}/${repo}/contents/${path}?ref=${ref}`,
|
||||
{},
|
||||
token
|
||||
);
|
||||
|
||||
let content = file.content;
|
||||
if (file.encoding === "base64") {
|
||||
content = atob(file.content);
|
||||
}
|
||||
|
||||
ctx.response.body = {
|
||||
name: file.name,
|
||||
path: file.path,
|
||||
content,
|
||||
size: file.size
|
||||
};
|
||||
} catch (err) {
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: err instanceof Error ? err.message : "Failed to fetch file" };
|
||||
}
|
||||
});
|
||||
|
||||
// Search Gitea projects
|
||||
router.get("/api/gitea/search", authMiddleware, async (ctx) => {
|
||||
try {
|
||||
const query = ctx.request.url.searchParams.get("q") || "";
|
||||
const token = await getUserGiteaToken(ctx.state.user.id);
|
||||
|
||||
interface SearchResult {
|
||||
data: GiteaProject[];
|
||||
}
|
||||
|
||||
const result = await giteaFetch<SearchResult>(
|
||||
`/repos/search?q=${encodeURIComponent(query)}&limit=20`,
|
||||
{},
|
||||
token
|
||||
);
|
||||
|
||||
ctx.response.body = result.data.map(p => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
fullName: p.full_name,
|
||||
description: p.description,
|
||||
webUrl: p.html_url,
|
||||
owner: p.owner.login
|
||||
}));
|
||||
} catch (err) {
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: err instanceof Error ? err.message : "Search failed" };
|
||||
}
|
||||
});
|
||||
|
||||
// Save user's Gitea token
|
||||
router.post("/api/gitea/token", authMiddleware, async (ctx) => {
|
||||
try {
|
||||
const body = await ctx.request.body.json();
|
||||
const { token } = body;
|
||||
|
||||
const db = await getDB();
|
||||
await db.collection("users").updateOne(
|
||||
{ _id: new ObjectId(ctx.state.user.id) },
|
||||
{ $set: { giteaToken: token } }
|
||||
);
|
||||
|
||||
ctx.response.body = { message: "Gitea token saved" };
|
||||
} catch (err) {
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = { error: err instanceof Error ? err.message : "Failed to save token" };
|
||||
}
|
||||
});
|
||||
|
||||
export const giteaRouter = router;
|
||||
@@ -43,6 +43,13 @@ interface Task {
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
interface GiteaProjectRef {
|
||||
projectId: number;
|
||||
path: string;
|
||||
url: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface GitLabProjectRef {
|
||||
projectId: number;
|
||||
path: string;
|
||||
@@ -62,6 +69,8 @@ interface Project {
|
||||
gitlabProjectId?: number;
|
||||
gitlabUrl?: string;
|
||||
gitlabPath?: string;
|
||||
// Gitea Integration
|
||||
giteaProjects?: GiteaProjectRef[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -397,7 +406,7 @@ tasksRouter.post("/projects", authMiddleware, async (ctx) => {
|
||||
}
|
||||
|
||||
const body = await ctx.request.body.json();
|
||||
const { name, description, color, gitlabProjectId, gitlabUrl, gitlabPath } = body;
|
||||
const { name, description, color, gitlabProjectId, gitlabUrl, gitlabPath, gitlabProjects, giteaProjects, rules } = body;
|
||||
|
||||
if (!name) {
|
||||
ctx.response.status = 400;
|
||||
@@ -414,7 +423,10 @@ tasksRouter.post("/projects", authMiddleware, async (ctx) => {
|
||||
color: color || "#6366f1",
|
||||
gitlabProjectId: gitlabProjectId || undefined,
|
||||
gitlabUrl: gitlabUrl || undefined,
|
||||
gitlabPath: gitlabPath || undefined,
|
||||
gitlabPath: gitlabPath || undefined,
|
||||
gitlabProjects: gitlabProjects || [],
|
||||
giteaProjects: giteaProjects || [],
|
||||
rules: rules || "",
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
} as Project);
|
||||
@@ -436,7 +448,7 @@ tasksRouter.put("/projects/:id", authMiddleware, async (ctx) => {
|
||||
|
||||
const id = ctx.params.id;
|
||||
const body = await ctx.request.body.json();
|
||||
const { name, description, color, rules, gitlabProjectId, gitlabUrl, gitlabPath, gitlabProjects } = body;
|
||||
const { name, description, color, rules, gitlabProjectId, gitlabUrl, gitlabPath, gitlabProjects, giteaProjects } = body;
|
||||
|
||||
const db = await getDB();
|
||||
|
||||
@@ -447,6 +459,7 @@ tasksRouter.put("/projects/:id", authMiddleware, async (ctx) => {
|
||||
if (rules !== undefined) (updateFields as any).rules = rules;
|
||||
// GitLab fields - Multiple projects (new)
|
||||
if (gitlabProjects !== undefined) updateFields.gitlabProjects = gitlabProjects || [];
|
||||
if (giteaProjects !== undefined) (updateFields as any).giteaProjects = giteaProjects || [];
|
||||
// Legacy single project fields (for backwards compatibility)
|
||||
if (gitlabProjectId !== undefined) updateFields.gitlabProjectId = gitlabProjectId || undefined;
|
||||
if (gitlabUrl !== undefined) updateFields.gitlabUrl = gitlabUrl || undefined;
|
||||
|
||||
Reference in New Issue
Block a user