remove old gitea plugin, replaced by gitea-sync
This commit is contained in:
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@echoboard/plugin-gitea",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsc",
|
|
||||||
"dev": "tsc --watch"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@echoboard/api": "*"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"typescript": "^5.7.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
import type { FastifyInstance } from 'fastify';
|
|
||||||
import crypto from 'node:crypto';
|
|
||||||
import { Readable } from 'node:stream';
|
|
||||||
|
|
||||||
interface GiteaConfig {
|
|
||||||
url: string;
|
|
||||||
apiToken: string;
|
|
||||||
webhookSecret: string;
|
|
||||||
syncOnStartup: boolean;
|
|
||||||
syncCron: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getConfig(): GiteaConfig {
|
|
||||||
const url = process.env.PLUGIN_GITEA_URL;
|
|
||||||
const apiToken = process.env.PLUGIN_GITEA_API_TOKEN;
|
|
||||||
const webhookSecret = process.env.PLUGIN_GITEA_WEBHOOK_SECRET;
|
|
||||||
|
|
||||||
if (!url || !apiToken || !webhookSecret) {
|
|
||||||
throw new Error('Missing required PLUGIN_GITEA_* env vars');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
url: url.replace(/\/$/, ''),
|
|
||||||
apiToken,
|
|
||||||
webhookSecret,
|
|
||||||
syncOnStartup: process.env.PLUGIN_GITEA_SYNC_ON_STARTUP !== 'false',
|
|
||||||
syncCron: process.env.PLUGIN_GITEA_SYNC_CRON || '0 */6 * * *',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchRepos(config: GiteaConfig) {
|
|
||||||
const repos: Array<{ id: number; name: string; full_name: string; html_url: string; description: string }> = [];
|
|
||||||
let page = 1;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const res = await fetch(
|
|
||||||
`${config.url}/api/v1/repos/search?limit=50&page=${page}`,
|
|
||||||
{ headers: { Authorization: `token ${config.apiToken}` } }
|
|
||||||
);
|
|
||||||
if (!res.ok) break;
|
|
||||||
|
|
||||||
const body = await res.json() as { data: typeof repos };
|
|
||||||
if (!body.data?.length) break;
|
|
||||||
|
|
||||||
repos.push(...body.data);
|
|
||||||
if (body.data.length < 50) break;
|
|
||||||
page++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return repos;
|
|
||||||
}
|
|
||||||
|
|
||||||
function verifyWebhookSignature(payload: Buffer | string, signature: string, secret: string): boolean {
|
|
||||||
const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex');
|
|
||||||
if (signature.length !== expected.length) return false;
|
|
||||||
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
|
|
||||||
}
|
|
||||||
|
|
||||||
export const giteaPlugin = {
|
|
||||||
name: 'gitea',
|
|
||||||
version: '0.1.0',
|
|
||||||
|
|
||||||
onRegister(app: FastifyInstance) {
|
|
||||||
const config = getConfig();
|
|
||||||
|
|
||||||
app.post('/api/v1/plugins/gitea/webhook', {
|
|
||||||
preParsing: async (req, _reply, payload) => {
|
|
||||||
const chunks: Buffer[] = [];
|
|
||||||
for await (const chunk of payload) {
|
|
||||||
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
|
|
||||||
}
|
|
||||||
const raw = Buffer.concat(chunks);
|
|
||||||
(req as any).rawBody = raw;
|
|
||||||
return Readable.from(raw);
|
|
||||||
},
|
|
||||||
}, async (req, reply) => {
|
|
||||||
const sig = req.headers['x-gitea-signature'] as string;
|
|
||||||
const rawBody: Buffer = (req as any).rawBody;
|
|
||||||
|
|
||||||
if (!sig || !verifyWebhookSignature(rawBody, sig, config.webhookSecret)) {
|
|
||||||
return reply.status(401).send({ error: 'invalid signature' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const event = req.headers['x-gitea-event'] as string;
|
|
||||||
const body = req.body as Record<string, unknown>;
|
|
||||||
|
|
||||||
if (event === 'repository' && body.action === 'created') {
|
|
||||||
const repo = body.repository as { id: number; name: string; html_url: string; description: string };
|
|
||||||
const slug = repo.name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
||||||
|
|
||||||
const { prisma } = app as unknown as { prisma: { board: { upsert: Function } } };
|
|
||||||
await prisma.board.upsert({
|
|
||||||
where: { slug },
|
|
||||||
create: {
|
|
||||||
slug,
|
|
||||||
name: repo.name,
|
|
||||||
description: repo.description || null,
|
|
||||||
externalUrl: repo.html_url,
|
|
||||||
},
|
|
||||||
update: {},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event === 'repository' && body.action === 'deleted') {
|
|
||||||
const repo = body.repository as { name: string };
|
|
||||||
const slug = repo.name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
||||||
|
|
||||||
const { prisma } = app as unknown as { prisma: { board: { updateMany: Function } } };
|
|
||||||
await prisma.board.updateMany({
|
|
||||||
where: { slug },
|
|
||||||
data: { isArchived: true },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return reply.status(200).send({ ok: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/api/v1/plugins/gitea/sync-status', { preHandler: [app.requireAdmin] }, async (_req, reply) => {
|
|
||||||
return reply.send({ status: 'ok', lastSync: new Date().toISOString() });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/api/v1/plugins/gitea/sync', { preHandler: [app.requireAdmin], config: { rateLimit: { max: 2, timeWindow: '1 minute' } } }, async (_req, reply) => {
|
|
||||||
const repos = await fetchRepos(config);
|
|
||||||
const { prisma } = app as unknown as { prisma: { board: { upsert: Function } } };
|
|
||||||
|
|
||||||
for (const repo of repos) {
|
|
||||||
const slug = repo.name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
||||||
await prisma.board.upsert({
|
|
||||||
where: { slug },
|
|
||||||
create: {
|
|
||||||
slug,
|
|
||||||
name: repo.name,
|
|
||||||
description: repo.description || null,
|
|
||||||
externalUrl: repo.html_url,
|
|
||||||
},
|
|
||||||
update: { externalUrl: repo.html_url },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return reply.send({ synced: repos.length });
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async onStartup() {
|
|
||||||
const config = getConfig();
|
|
||||||
if (!config.syncOnStartup) return;
|
|
||||||
// initial sync handled by the route handler logic
|
|
||||||
// in production this would call fetchRepos and sync
|
|
||||||
},
|
|
||||||
|
|
||||||
getAdminRoutes() {
|
|
||||||
return [
|
|
||||||
{ path: '/admin/gitea', label: 'Gitea Sync', component: 'gitea-sync' },
|
|
||||||
];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "dist",
|
|
||||||
"rootDir": "src"
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user