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:
FluxKit
2026-02-19 14:07:41 +00:00
parent 656a37efda
commit bebfcbb816
3 changed files with 339 additions and 3 deletions

View File

@@ -21,6 +21,7 @@ import { agentsRouter } from "./routes/agents.ts";
import { logsRouter } from "./routes/logs.ts"; import { logsRouter } from "./routes/logs.ts";
import { userSettingsRouter } from "./routes/usersettings.ts"; import { userSettingsRouter } from "./routes/usersettings.ts";
import gitlabRouter from "./routes/gitlab.ts"; import gitlabRouter from "./routes/gitlab.ts";
import { giteaRouter } from "./routes/gitea.ts";
import { dockerRouter } from "./routes/docker.ts"; import { dockerRouter } from "./routes/docker.ts";
import { exportRouter } from "./routes/export.ts"; import { exportRouter } from "./routes/export.ts";
import { appUpdateRouter } from "./routes/appUpdate.ts"; import { appUpdateRouter } from "./routes/appUpdate.ts";
@@ -75,6 +76,8 @@ app.use(logsRouter.allowedMethods());
app.use(userSettingsRouter.routes()); app.use(userSettingsRouter.routes());
app.use(userSettingsRouter.allowedMethods()); app.use(userSettingsRouter.allowedMethods());
app.use(gitlabRouter.routes()); app.use(gitlabRouter.routes());
app.use(giteaRouter.routes());
app.use(giteaRouter.allowedMethods());
app.use(gitlabRouter.allowedMethods()); app.use(gitlabRouter.allowedMethods());
app.use(dockerRouter.routes()); app.use(dockerRouter.routes());
app.use(dockerRouter.allowedMethods()); app.use(dockerRouter.allowedMethods());

320
src/routes/gitea.ts Normal file
View 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;

View File

@@ -43,6 +43,13 @@ interface Task {
updatedAt: Date; updatedAt: Date;
} }
interface GiteaProjectRef {
projectId: number;
path: string;
url: string;
name: string;
}
interface GitLabProjectRef { interface GitLabProjectRef {
projectId: number; projectId: number;
path: string; path: string;
@@ -62,6 +69,8 @@ interface Project {
gitlabProjectId?: number; gitlabProjectId?: number;
gitlabUrl?: string; gitlabUrl?: string;
gitlabPath?: string; gitlabPath?: string;
// Gitea Integration
giteaProjects?: GiteaProjectRef[];
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
} }
@@ -397,7 +406,7 @@ tasksRouter.post("/projects", authMiddleware, async (ctx) => {
} }
const body = await ctx.request.body.json(); 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) { if (!name) {
ctx.response.status = 400; ctx.response.status = 400;
@@ -415,6 +424,9 @@ tasksRouter.post("/projects", authMiddleware, async (ctx) => {
gitlabProjectId: gitlabProjectId || undefined, gitlabProjectId: gitlabProjectId || undefined,
gitlabUrl: gitlabUrl || undefined, gitlabUrl: gitlabUrl || undefined,
gitlabPath: gitlabPath || undefined, gitlabPath: gitlabPath || undefined,
gitlabProjects: gitlabProjects || [],
giteaProjects: giteaProjects || [],
rules: rules || "",
createdAt: now, createdAt: now,
updatedAt: now updatedAt: now
} as Project); } as Project);
@@ -436,7 +448,7 @@ tasksRouter.put("/projects/:id", authMiddleware, async (ctx) => {
const id = ctx.params.id; const id = ctx.params.id;
const body = await ctx.request.body.json(); 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(); const db = await getDB();
@@ -447,6 +459,7 @@ tasksRouter.put("/projects/:id", authMiddleware, async (ctx) => {
if (rules !== undefined) (updateFields as any).rules = rules; if (rules !== undefined) (updateFields as any).rules = rules;
// GitLab fields - Multiple projects (new) // GitLab fields - Multiple projects (new)
if (gitlabProjects !== undefined) updateFields.gitlabProjects = gitlabProjects || []; if (gitlabProjects !== undefined) updateFields.gitlabProjects = gitlabProjects || [];
if (giteaProjects !== undefined) (updateFields as any).giteaProjects = giteaProjects || [];
// Legacy single project fields (for backwards compatibility) // Legacy single project fields (for backwards compatibility)
if (gitlabProjectId !== undefined) updateFields.gitlabProjectId = gitlabProjectId || undefined; if (gitlabProjectId !== undefined) updateFields.gitlabProjectId = gitlabProjectId || undefined;
if (gitlabUrl !== undefined) updateFields.gitlabUrl = gitlabUrl || undefined; if (gitlabUrl !== undefined) updateFields.gitlabUrl = gitlabUrl || undefined;