security hardening, team invites, granular locking, view counts, board subscriptions, scheduled changelog, mentions, recovery codes, accessibility and hover states
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import crypto from 'node:crypto';
|
||||
import { Readable } from 'node:stream';
|
||||
|
||||
interface GiteaConfig {
|
||||
url: string;
|
||||
@@ -33,7 +34,8 @@ async function fetchRepos(config: GiteaConfig) {
|
||||
|
||||
while (true) {
|
||||
const res = await fetch(
|
||||
`${config.url}/api/v1/repos/search?token=${config.apiToken}&limit=50&page=${page}`
|
||||
`${config.url}/api/v1/repos/search?limit=50&page=${page}`,
|
||||
{ headers: { Authorization: `token ${config.apiToken}` } }
|
||||
);
|
||||
if (!res.ok) break;
|
||||
|
||||
@@ -48,8 +50,9 @@ async function fetchRepos(config: GiteaConfig) {
|
||||
return repos;
|
||||
}
|
||||
|
||||
function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean {
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -60,9 +63,19 @@ export const giteaPlugin = {
|
||||
onRegister(app: FastifyInstance) {
|
||||
const config = getConfig();
|
||||
|
||||
app.post('/api/v1/plugins/gitea/webhook', async (req, reply) => {
|
||||
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 = JSON.stringify(req.body);
|
||||
const rawBody: Buffer = (req as any).rawBody;
|
||||
|
||||
if (!sig || !verifyWebhookSignature(rawBody, sig, config.webhookSecret)) {
|
||||
return reply.status(401).send({ error: 'invalid signature' });
|
||||
@@ -102,11 +115,11 @@ export const giteaPlugin = {
|
||||
return reply.status(200).send({ ok: true });
|
||||
});
|
||||
|
||||
app.get('/api/v1/plugins/gitea/sync-status', async (_req, reply) => {
|
||||
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', async (_req, reply) => {
|
||||
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 } } };
|
||||
|
||||
@@ -137,7 +150,7 @@ export const giteaPlugin = {
|
||||
|
||||
getAdminRoutes() {
|
||||
return [
|
||||
{ path: '/admin/gitea', label: 'Gitea Sync', icon: 'git-branch' },
|
||||
{ path: '/admin/gitea', label: 'Gitea Sync', component: 'gitea-sync' },
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user