diff --git a/Modules/Helpdesk/Http/Controllers/GitWebhookController.php b/Modules/Helpdesk/Http/Controllers/GitWebhookController.php index bdf6f4a4..c107928a 100644 --- a/Modules/Helpdesk/Http/Controllers/GitWebhookController.php +++ b/Modules/Helpdesk/Http/Controllers/GitWebhookController.php @@ -6,101 +6,160 @@ namespace Modules\Helpdesk\Http\Controllers; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Log; use JsonException; +use Modules\Helpdesk\Models\GitCommit; use Modules\Helpdesk\Models\GitRepository; use Modules\Helpdesk\Services\GitWebhookIngestionService; final class GitWebhookController { /** - * Ingest a signed Git provider webhook for a registered Helpdesk repository. + * Handle an incoming Gitea webhook delivery. */ - public function __invoke(Request $request, GitRepository $repository, GitWebhookIngestionService $service): JsonResponse + public function __invoke(Request $request): JsonResponse { - $provider = strtolower((string) ($repository->provider ?: 'git')); - $event = $this->eventName($request, $provider); - $deliveryId = $this->deliveryId($request, $provider); - $rawPayload = $request->getContent(); + Log::info('Gitea webhook endpoint hit.', [ + 'event' => $request->header('X-Gitea-Event'), + 'github_event' => $request->header('X-GitHub-Event'), + 'delivery' => $request->header('X-Gitea-Delivery'), + 'authorization_present' => $request->headers->has('Authorization'), + 'signature_present' => $request->headers->has('X-Gitea-Signature'), + ]); - if (! $repository->is_active) { - $result = ['status' => 'rejected', 'message' => 'Repository is inactive.', 'commit_count' => 0, 'linked_count' => 0, 'ignored_count' => 1]; - $service->logWebhook($repository, $provider, $deliveryId, $event, $result); + $authResponse = $this->validateAuthorization($request); - return response()->json($result, 403); + if ($authResponse instanceof JsonResponse) { + return $authResponse; } - if (! $this->hasValidSignature($request, $repository, $provider, $rawPayload)) { - $result = ['status' => 'rejected', 'message' => 'Git webhook signature or token validation failed.', 'commit_count' => 0, 'linked_count' => 0, 'ignored_count' => 1]; - $service->logWebhook($repository, $provider, $deliveryId, $event, $result); + $signatureResponse = $this->validateSignature($request); - return response()->json($result, 403); + if ($signatureResponse instanceof JsonResponse) { + return $signatureResponse; } - try { - /** @var array $payload */ - $payload = json_decode($rawPayload, true, 512, JSON_THROW_ON_ERROR); - } catch (JsonException) { - $result = ['status' => 'rejected', 'message' => 'Invalid JSON webhook payload.', 'commit_count' => 0, 'linked_count' => 0, 'ignored_count' => 1]; - $service->logWebhook($repository, $provider, $deliveryId, $event, $result); + $event = (string) ($request->header('X-Gitea-Event') ?: $request->header('X-GitHub-Event')); + $payload = $request->json()->all(); - return response()->json($result, 400); + if ($event !== 'push') { + Log::info('Gitea webhook ignored.', [ + 'event' => $event, + 'delivery' => $request->header('X-Gitea-Delivery'), + ]); + + return response()->json([ + 'message' => 'Gitea webhook ignored.', + 'event' => $event, + ]); } - $result = $service->ingest($repository, $payload, $provider, $deliveryId, $event); + $commits = data_get($payload, 'commits', []); - return response()->json($result, 202); + Log::info('Gitea push webhook received.', [ + 'repository' => data_get($payload, 'repository.full_name'), + 'ref' => data_get($payload, 'ref'), + 'branch' => str_replace('refs/heads/', '', (string) data_get($payload, 'ref')), + 'before' => data_get($payload, 'before'), + 'after' => data_get($payload, 'after'), + 'commit_count' => is_countable($commits) ? count($commits) : 0, + ]); + + foreach ($commits as $commit) { + GitCommit::updateOrCreate( + [ + 'sha' => data_get($commit, 'id'), + ], + [ + 'repository_full_name' => data_get($payload, 'repository.full_name'), + 'repository_name' => data_get($payload, 'repository.name'), + 'branch' => str_replace( + 'refs/heads/', + '', + (string) data_get($payload, 'ref'), + ), + 'author_name' => data_get($commit, 'author.name'), + 'author_email' => data_get($commit, 'author.email'), + 'message' => data_get($commit, 'message'), + 'commit_url' => data_get($commit, 'url'), + 'committed_at' => data_get($commit, 'timestamp') + ? Carbon::parse(data_get($commit, 'timestamp')) + : now(), + 'payload' => $commit, + ], + ); + + Log::info('Git commit stored.', [ + 'sha' => data_get($commit, 'id'), + 'message' => data_get($commit, 'message'), + ]); + } + + return response()->json([ + 'message' => 'Gitea push processed.', + 'repository' => data_get($payload, 'repository.full_name'), + 'branch' => str_replace('refs/heads/', '', (string) data_get($payload, 'ref')), + 'commit_count' => is_countable($commits) ? count($commits) : 0, + ]); } /** - * Validate provider-specific webhook signatures or tokens. + * Validate the optional Gitea Authorization header. */ - private function hasValidSignature(Request $request, GitRepository $repository, string $provider, string $rawPayload): bool + private function validateAuthorization(Request $request): ?JsonResponse { - $secret = trim((string) $repository->webhook_secret); + $expected = (string) config('services.gitea.webhook_authorization'); + + if ($expected === '') { + return null; + } + + $actual = (string) $request->header('Authorization'); + + if (hash_equals($expected, $actual)) { + return null; + } + + Log::warning('Rejected Gitea webhook because Authorization header was invalid.', [ + 'delivery' => $request->header('X-Gitea-Delivery'), + 'authorization_present' => $request->headers->has('Authorization'), + ]); + + return response()->json([ + 'message' => 'Invalid authorization header.', + ], 403); + } + + /** + * Validate the optional Gitea HMAC SHA-256 signature. + */ + private function validateSignature(Request $request): ?JsonResponse + { + $secret = (string) config('services.gitea.webhook_secret'); if ($secret === '') { - return false; + return null; } - return match ($provider) { - 'github' => $this->hasValidGitHubSignature($request, $secret, $rawPayload), - 'gitlab' => hash_equals($secret, (string) $request->header('X-Gitlab-Token', '')), - default => hash_equals($secret, (string) $request->header('X-Helpdesk-Token', '')), - }; - } + $payload = $request->getContent(); + $signature = (string) $request->header('X-Gitea-Signature'); - /** - * Validate GitHub's X-Hub-Signature-256 HMAC header. - */ - private function hasValidGitHubSignature(Request $request, string $secret, string $rawPayload): bool - { - $signature = (string) $request->header('X-Hub-Signature-256', ''); - $expected = 'sha256=' . hash_hmac('sha256', $rawPayload, $secret); + $expected = hash_hmac('sha256', $payload, $secret); - return hash_equals($expected, $signature); - } + if ($signature !== '' && hash_equals($expected, $signature)) { + return null; + } - /** - * Read the event name from provider-specific headers. - */ - private function eventName(Request $request, string $provider): ?string - { - return match ($provider) { - 'github' => $request->header('X-GitHub-Event'), - 'gitlab' => $request->header('X-Gitlab-Event'), - default => $request->header('X-Helpdesk-Event', 'push'), - }; - } + Log::warning('Rejected Gitea webhook because signature was invalid.', [ + 'delivery' => $request->header('X-Gitea-Delivery'), + 'signature_present' => $request->headers->has('X-Gitea-Signature'), + 'expected' => $expected, + 'actual' => $signature, + ]); - /** - * Read the provider delivery/request id used for idempotent audit tracing. - */ - private function deliveryId(Request $request, string $provider): ?string - { - return match ($provider) { - 'github' => $request->header('X-GitHub-Delivery'), - 'gitlab' => $request->header('X-Gitlab-Event-UUID') ?: $request->header('X-Request-Id'), - default => $request->header('X-Helpdesk-Delivery'), - }; + return response()->json([ + 'message' => 'Invalid Gitea signature.', + ], 403); } } diff --git a/Modules/Helpdesk/Routes/api.php b/Modules/Helpdesk/Routes/api.php index 38392698..a98d267d 100644 --- a/Modules/Helpdesk/Routes/api.php +++ b/Modules/Helpdesk/Routes/api.php @@ -13,3 +13,5 @@ Route::get('/status', static fn (): array => [ Route::post('/git/webhook/{repository}', GitWebhookController::class) ->whereNumber('repository') ->name('git.webhook'); +Route::post('/git/gitea/webhook', GitWebhookController::class) + ->name('git.gitea.webhook'); diff --git a/app/Http/Controllers/Git/GiteaWebhookController.php b/app/Http/Controllers/Git/GiteaWebhookController.php deleted file mode 100644 index 65c31e93..00000000 --- a/app/Http/Controllers/Git/GiteaWebhookController.php +++ /dev/null @@ -1,163 +0,0 @@ - $request->header('X-Gitea-Event'), - 'github_event' => $request->header('X-GitHub-Event'), - 'delivery' => $request->header('X-Gitea-Delivery'), - 'authorization_present' => $request->headers->has('Authorization'), - 'signature_present' => $request->headers->has('X-Gitea-Signature'), - ]); - - $authResponse = $this->validateAuthorization($request); - - if ($authResponse instanceof JsonResponse) { - return $authResponse; - } - - $signatureResponse = $this->validateSignature($request); - - if ($signatureResponse instanceof JsonResponse) { - return $signatureResponse; - } - - $event = (string) ($request->header('X-Gitea-Event') ?: $request->header('X-GitHub-Event')); - $payload = $request->json()->all(); - - if ($event !== 'push') { - Log::info('Gitea webhook ignored.', [ - 'event' => $event, - 'delivery' => $request->header('X-Gitea-Delivery'), - ]); - - return response()->json([ - 'message' => 'Gitea webhook ignored.', - 'event' => $event, - ]); - } - - $commits = data_get($payload, 'commits', []); - - Log::info('Gitea push webhook received.', [ - 'repository' => data_get($payload, 'repository.full_name'), - 'ref' => data_get($payload, 'ref'), - 'branch' => str_replace('refs/heads/', '', (string) data_get($payload, 'ref')), - 'before' => data_get($payload, 'before'), - 'after' => data_get($payload, 'after'), - 'commit_count' => is_countable($commits) ? count($commits) : 0, - ]); - - foreach ($commits as $commit) { - GitCommit::updateOrCreate( - [ - 'sha' => data_get($commit, 'id'), - ], - [ - 'repository_full_name' => data_get($payload, 'repository.full_name'), - 'repository_name' => data_get($payload, 'repository.name'), - 'branch' => str_replace( - 'refs/heads/', - '', - (string) data_get($payload, 'ref'), - ), - 'author_name' => data_get($commit, 'author.name'), - 'author_email' => data_get($commit, 'author.email'), - 'message' => data_get($commit, 'message'), - 'commit_url' => data_get($commit, 'url'), - 'committed_at' => data_get($commit, 'timestamp') - ? Carbon::parse(data_get($commit, 'timestamp')) - : now(), - 'payload' => $commit, - ], - ); - - Log::info('Git commit stored.', [ - 'sha' => data_get($commit, 'id'), - 'message' => data_get($commit, 'message'), - ]); - } - - return response()->json([ - 'message' => 'Gitea push processed.', - 'repository' => data_get($payload, 'repository.full_name'), - 'branch' => str_replace('refs/heads/', '', (string) data_get($payload, 'ref')), - 'commit_count' => is_countable($commits) ? count($commits) : 0, - ]); - } - - /** - * Validate the optional Gitea Authorization header. - */ - private function validateAuthorization(Request $request): ?JsonResponse - { - $expected = (string) config('services.gitea.webhook_authorization'); - - if ($expected === '') { - return null; - } - - $actual = (string) $request->header('Authorization'); - - if (hash_equals($expected, $actual)) { - return null; - } - - Log::warning('Rejected Gitea webhook because Authorization header was invalid.', [ - 'delivery' => $request->header('X-Gitea-Delivery'), - 'authorization_present' => $request->headers->has('Authorization'), - ]); - - return response()->json([ - 'message' => 'Invalid authorization header.', - ], 403); - } - - /** - * Validate the optional Gitea HMAC SHA-256 signature. - */ - private function validateSignature(Request $request): ?JsonResponse - { - $secret = (string) config('services.gitea.webhook_secret'); - - if ($secret === '') { - return null; - } - - $payload = $request->getContent(); - $signature = (string) $request->header('X-Gitea-Signature'); - - $expected = hash_hmac('sha256', $payload, $secret); - - if ($signature !== '' && hash_equals($expected, $signature)) { - return null; - } - - Log::warning('Rejected Gitea webhook because signature was invalid.', [ - 'delivery' => $request->header('X-Gitea-Delivery'), - 'signature_present' => $request->headers->has('X-Gitea-Signature'), - 'expected' => $expected, - 'actual' => $signature, - ]); - - return response()->json([ - 'message' => 'Invalid Gitea signature.', - ], 403); - } -} diff --git a/routes/api.php b/routes/api.php index 54dc47ed..a4abe2da 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,6 +1,2 @@ name('git.gitea.webhook');