From cee9fdc290396ff3cb3b83d89153e7eeacdd7a6c Mon Sep 17 00:00:00 2001 From: m105 Date: Mon, 27 Jul 2026 21:50:17 +0300 Subject: [PATCH] Add Webhook Endpoints for external automation - Create WebhookController with 4 endpoints: - POST /api/webhooks/posts - Create a new post - GET /api/webhooks/posts - List posts with filters - POST /api/webhooks/posts/{post}/publish - Trigger publishing - GET /api/webhooks/posts/{post}/status - Get post status - Add X-Muninn-Token header authentication - Add MUNINN_WEBHOOK_TOKEN config in services.php - Add MUNINN_WEBHOOK_TOKEN to .env.example - Add webhook routes with /webhooks prefix All endpoints return JSON responses with success/error fields. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- .env.example | 3 + .../Controllers/Api/WebhookController.php | 256 ++++++++++++++++++ config/services.php | 4 + routes/api.php | 9 + 4 files changed, 272 insertions(+) create mode 100644 app/Http/Controllers/Api/WebhookController.php diff --git a/.env.example b/.env.example index 34ce377..0a06733 100644 --- a/.env.example +++ b/.env.example @@ -62,4 +62,7 @@ AWS_DEFAULT_REGION=us-east-1 AWS_BUCKET= AWS_USE_PATH_STYLE_ENDPOINT=false +# Muninn webhook authentication token (required for webhook endpoints) +MUNINN_WEBHOOK_TOKEN= + VITE_APP_NAME="${APP_NAME}" diff --git a/app/Http/Controllers/Api/WebhookController.php b/app/Http/Controllers/Api/WebhookController.php new file mode 100644 index 0000000..0fde318 --- /dev/null +++ b/app/Http/Controllers/Api/WebhookController.php @@ -0,0 +1,256 @@ +header('X-Muninn-Token'); + + if ($validToken && $providedToken !== $validToken) { + return response()->json([ + 'success' => false, + 'error' => 'Invalid or missing webhook token', + ], 401); + } + + $validator = Validator::make($request->all(), [ + 'type' => ['required', 'string', 'in:' . implode(',', Post::TYPES)], + 'body' => ['required', 'string'], + 'title' => ['nullable', 'string', 'max:255'], + 'target' => ['nullable', 'string', 'max:255'], + 'image_url' => ['nullable', 'url', 'max:2048'], + 'status' => ['nullable', 'string', 'in:' . implode(',', Post::STATUSES)], + 'scheduled_at' => ['nullable', 'date'], + 'site_name' => ['nullable', 'string'], + 'social_accounts' => ['nullable', 'array'], + 'social_accounts.*' => ['string'], + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'error' => 'Validation failed', + 'errors' => $validator->errors(), + ], 422); + } + + $validated = $validator->validated(); + + // Find site by name if provided + $siteId = null; + if ($validated['site_name'] ?? null) { + $site = Site::where('name', $validated['site_name'])->first(); + if ($site) { + $siteId = $site->id; + } + } + + // Create the post + $post = Post::create([ + 'site_id' => $siteId, + 'type' => $validated['type'], + 'title' => $validated['title'] ?? null, + 'body' => $validated['body'], + 'target' => $validated['target'] ?? null, + 'image_url' => $validated['image_url'] ?? null, + 'status' => $validated['status'] ?? 'draft', + 'scheduled_at' => $validated['scheduled_at'] ?? null, + ]); + + // Link social accounts by username if provided + if ($validated['social_accounts'] ?? null) { + foreach ($validated['social_accounts'] as $username) { + $account = SocialAccount::where('username', $username)->first(); + if ($account) { + $post->socialAccounts()->attach($account->id); + } + } + } + + return response()->json([ + 'success' => true, + 'post' => [ + 'id' => $post->id, + 'type' => $post->type, + 'status' => $post->status, + 'created_at' => $post->created_at->toIso8601String(), + ], + ], 201); + } + + /** + * Trigger publishing for a post via webhook. + * + * POST /api/webhooks/posts/{post}/publish + * + * Required headers: + * - X-Muninn-Token: Your webhook token + */ + public function publishPost(Request $request, Post $post) + { + // Verify webhook token + $validToken = config('services.muninn.webhook_token'); + $providedToken = $request->header('X-Muninn-Token'); + + if ($validToken && $providedToken !== $validToken) { + return response()->json([ + 'success' => false, + 'error' => 'Invalid or missing webhook token', + ], 401); + } + + // Check if post can be published + $publisherManager = app(\App\Support\Publishing\PublisherManager::class); + + if (! $publisherManager->resolve($post)) { + return response()->json([ + 'success' => false, + 'error' => "No publisher available for post type '{$post->type}'", + ], 400); + } + + // Set status to publishing and dispatch job + $post->update(['status' => 'publishing', 'publish_error' => null]); + PublishPostJob::dispatch($post->id); + + return response()->json([ + 'success' => true, + 'message' => 'Publish job dispatched', + 'post_id' => $post->id, + ]); + } + + /** + * Get post status via webhook. + * + * GET /api/webhooks/posts/{post}/status + * + * Required headers: + * - X-Muninn-Token: Your webhook token + */ + public function getPostStatus(Request $request, Post $post) + { + // Verify webhook token + $validToken = config('services.muninn.webhook_token'); + $providedToken = $request->header('X-Muninn-Token'); + + if ($validToken && $providedToken !== $validToken) { + return response()->json([ + 'success' => false, + 'error' => 'Invalid or missing webhook token', + ], 401); + } + + return response()->json([ + 'success' => true, + 'post' => [ + 'id' => $post->id, + 'type' => $post->type, + 'status' => $post->status, + 'title' => $post->title, + 'published_url' => $post->published_url, + 'publish_error' => $post->publish_error, + 'created_at' => $post->created_at->toIso8601String(), + 'updated_at' => $post->updated_at->toIso8601String(), + 'scheduled_at' => $post->scheduled_at?->toIso8601String(), + ], + ]); + } + + /** + * List posts with optional filters via webhook. + * + * GET /api/webhooks/posts + * + * Query params: + * - type: Filter by post type + * - status: Filter by status + * - site_id: Filter by site + * - limit: Limit number of results (default 20) + * + * Required headers: + * - X-Muninn-Token: Your webhook token + */ + public function listPosts(Request $request) + { + // Verify webhook token + $validToken = config('services.muninn.webhook_token'); + $providedToken = $request->header('X-Muninn-Token'); + + if ($validToken && $providedToken !== $validToken) { + return response()->json([ + 'success' => false, + 'error' => 'Invalid or missing webhook token', + ], 401); + } + + $query = Post::with(['site', 'socialAccounts'])->orderByDesc('created_at'); + + if ($request->has('type')) { + $query->where('type', $request->input('type')); + } + + if ($request->has('status')) { + $query->where('status', $request->input('status')); + } + + if ($request->has('site_id')) { + $query->where('site_id', $request->input('site_id')); + } + + $limit = $request->input('limit', 20); + $posts = $query->take($limit)->get(); + + return response()->json([ + 'success' => true, + 'count' => $posts->count(), + 'posts' => $posts->map(fn ($post) => [ + 'id' => $post->id, + 'type' => $post->type, + 'status' => $post->status, + 'title' => $post->title, + 'body' => $post->body, + 'published_url' => $post->published_url, + 'publish_error' => $post->publish_error, + 'site' => $post->site ? ['id' => $post->site->id, 'name' => $post->site->name] : null, + 'social_accounts' => $post->socialAccounts->map(fn ($sa) => ['id' => $sa->id, 'username' => $sa->username, 'network' => $sa->network]), + 'created_at' => $post->created_at->toIso8601String(), + 'updated_at' => $post->updated_at->toIso8601String(), + 'scheduled_at' => $post->scheduled_at?->toIso8601String(), + ]), + ]); + } +} diff --git a/config/services.php b/config/services.php index 6a90eb8..b4b2bbf 100644 --- a/config/services.php +++ b/config/services.php @@ -35,4 +35,8 @@ return [ ], ], + 'muninn' => [ + 'webhook_token' => env('MUNINN_WEBHOOK_TOKEN', null), + ], + ]; diff --git a/routes/api.php b/routes/api.php index 949850b..6125525 100644 --- a/routes/api.php +++ b/routes/api.php @@ -3,6 +3,7 @@ use App\Http\Controllers\Api\PostController; use App\Http\Controllers\Api\SiteController; use App\Http\Controllers\Api\SocialAccountController; +use App\Http\Controllers\Api\WebhookController; use Illuminate\Support\Facades\Route; Route::get('/sites', [SiteController::class, 'index']); @@ -12,3 +13,11 @@ Route::get('/accounts', [SocialAccountController::class, 'index']); Route::get('/accounts/{socialAccount}', [SocialAccountController::class, 'show']); Route::apiResource('posts', PostController::class)->names('api.posts'); + +// Webhook endpoints (require X-Muninn-Token header) +Route::prefix('webhooks')->group(function () { + Route::post('/posts', [WebhookController::class, 'createPost']); + Route::get('/posts', [WebhookController::class, 'listPosts']); + Route::post('/posts/{post}/publish', [WebhookController::class, 'publishPost']); + Route::get('/posts/{post}/status', [WebhookController::class, 'getPostStatus']); +});