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 { 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
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;
|
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user