Merge branch 'next' into fix-environement-route

This commit is contained in:
🏔️ Peak authored and GitHub committed 2024-11-22 15:32:28 +01:00
commit e29c202e61
10 files changed
+281 -251

No files matched your search

+15 -234
View File
@@ -3,21 +3,26 @@
namespace App\Http\Controllers\Webhook;
use App\Http\Controllers\Controller;
use App\Jobs\ServerLimitCheckJob;
use App\Jobs\SubscriptionInvoiceFailedJob;
use App\Models\Subscription;
use App\Models\Team;
use App\Jobs\StripeProcessJob;
use App\Models\Webhook;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class Stripe extends Controller
{
protected $webhook;
public function events(Request $request)
{
try {
$webhookSecret = config('subscription.stripe_webhook_secret');
$signature = $request->header('Stripe-Signature');
$event = \Stripe\Webhook::constructEvent(
$request->getContent(),
$signature,
$webhookSecret
);
if (app()->isDownForMaintenance()) {
$epoch = now()->valueOf();
$data = [
@@ -33,241 +38,17 @@ class Stripe extends Controller
$json = json_encode($data);
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Stripe::events_stripe", $json);
return;
return response('Webhook received. Cool cool cool cool cool.', 200);
}
$webhookSecret = config('subscription.stripe_webhook_secret');
$signature = $request->header('Stripe-Signature');
$excludedPlans = config('subscription.stripe_excluded_plans');
$event = \Stripe\Webhook::constructEvent(
$request->getContent(),
$signature,
$webhookSecret
);
$webhook = Webhook::create([
$this->webhook = Webhook::create([
'type' => 'stripe',
'payload' => $request->getContent(),
]);
$type = data_get($event, 'type');
$data = data_get($event, 'data.object');
switch ($type) {
case 'radar.early_fraud_warning.created':
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
$id = data_get($data, 'id');
$charge = data_get($data, 'charge');
if ($charge) {
$stripe->refunds->create(['charge' => $charge]);
}
$pi = data_get($data, 'payment_intent');
$piData = $stripe->paymentIntents->retrieve($pi, []);
$customerId = data_get($piData, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if ($subscription) {
$subscriptionId = data_get($subscription, 'stripe_subscription_id');
$stripe->subscriptions->cancel($subscriptionId, []);
$subscription->update([
'stripe_invoice_paid' => false,
]);
send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}");
} else {
send_internal_notification("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
StripeProcessJob::dispatch($event);
return response("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}", 400);
}
break;
case 'checkout.session.completed':
$clientReferenceId = data_get($data, 'client_reference_id');
if (is_null($clientReferenceId)) {
send_internal_notification('Checkout session completed without client reference id.');
break;
}
$userId = Str::before($clientReferenceId, ':');
$teamId = Str::after($clientReferenceId, ':');
$subscriptionId = data_get($data, 'subscription');
$customerId = data_get($data, 'customer');
$team = Team::find($teamId);
$found = $team->members->where('id', $userId)->first();
if (! $found->isAdmin()) {
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
return response("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.", 400);
}
$subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) {
// send_internal_notification('Old subscription activated for team: '.$teamId);
$subscription->update([
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true,
]);
} else {
// send_internal_notification('New subscription for team: '.$teamId);
Subscription::create([
'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true,
]);
}
break;
case 'invoice.paid':
$customerId = data_get($data, 'customer');
$planId = data_get($data, 'lines.data.0.plan.id');
if (Str::contains($excludedPlans, $planId)) {
// send_internal_notification('Subscription excluded.');
break;
}
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if ($subscription) {
$subscription->update([
'stripe_invoice_paid' => true,
]);
} else {
return response("No subscription found for customer: {$customerId}", 400);
}
break;
case 'invoice.payment_failed':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
// send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId);
return response('No subscription found in Coolify.');
}
$team = data_get($subscription, 'team');
if (! $team) {
// send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId);
return response('No team found in Coolify.');
}
if (! $subscription->stripe_invoice_paid) {
SubscriptionInvoiceFailedJob::dispatch($team);
// send_internal_notification('Invoice payment failed: '.$customerId);
} else {
// send_internal_notification('Invoice payment failed but already paid: '.$customerId);
}
break;
case 'payment_intent.payment_failed':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
// send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId);
return response('No subscription found in Coolify.');
}
if ($subscription->stripe_invoice_paid) {
// send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId);
return;
}
send_internal_notification('Subscription payment failed for customer: '.$customerId);
break;
case 'customer.subscription.created':
$customerId = data_get($data, 'customer');
$subscriptionId = data_get($data, 'id');
$teamId = data_get($data, 'metadata.team_id');
$userId = data_get($data, 'metadata.user_id');
if (! $teamId || ! $userId) {
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if ($subscription) {
return response("Subscription already exists for customer: {$customerId}", 200);
}
return response('No team id or user id found', 400);
}
$team = Team::find($teamId);
$found = $team->members->where('id', $userId)->first();
if (! $found->isAdmin()) {
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
return response("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.", 400);
}
$subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) {
return response("Subscription already exists for team: {$teamId}", 200);
} else {
Subscription::create([
'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => false,
]);
return response('Subscription created');
}
case 'customer.subscription.updated':
$teamId = data_get($data, 'metadata.team_id');
$userId = data_get($data, 'metadata.user_id');
$customerId = data_get($data, 'customer');
$status = data_get($data, 'status');
$subscriptionId = data_get($data, 'items.data.0.subscription');
$planId = data_get($data, 'items.data.0.plan.id');
if (Str::contains($excludedPlans, $planId)) {
// send_internal_notification('Subscription excluded.');
break;
}
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
if ($status === 'incomplete_expired') {
return response('Subscription incomplete expired', 200);
}
if ($teamId) {
$subscription = Subscription::create([
'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => false,
]);
} else {
return response('No subscription and team id found', 400);
}
}
$cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
$feedback = data_get($data, 'cancellation_details.feedback');
$comment = data_get($data, 'cancellation_details.comment');
$lookup_key = data_get($data, 'items.data.0.price.lookup_key');
if (str($lookup_key)->contains('dynamic')) {
$quantity = data_get($data, 'items.data.0.quantity', 2);
$team = data_get($subscription, 'team');
if ($team) {
$team->update([
'custom_server_limit' => $quantity,
]);
}
ServerLimitCheckJob::dispatch($team);
}
$subscription->update([
'stripe_feedback' => $feedback,
'stripe_comment' => $comment,
'stripe_plan_id' => $planId,
'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
]);
if ($status === 'paused' || $status === 'incomplete_expired') {
$subscription->update([
'stripe_invoice_paid' => false,
]);
}
if ($feedback) {
$reason = "Cancellation feedback for {$customerId}: '".$feedback."'";
if ($comment) {
$reason .= ' with comment: \''.$comment."'";
}
}
break;
case 'customer.subscription.deleted':
// End subscription
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$team = data_get($subscription, 'team');
$team?->subscriptionEnded();
break;
default:
// Unhandled event type
}
return response('Webhook received. Cool cool cool cool cool.', 200);
} catch (Exception $e) {
if ($type !== 'payment_intent.payment_failed') {
send_internal_notification("Subscription webhook ($type) failed: ".$e->getMessage());
}
$webhook->update([
$this->webhook->update([
'status' => 'failed',
'failure_reason' => $e->getMessage(),
]);
+1 -3
View File
@@ -199,7 +199,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$databasesToBackup = data_get($this->backup, 'databases_to_backup');
}
if (is_null($databasesToBackup)) {
if (filled($databasesToBackup)) {
if (str($databaseType)->contains('postgres')) {
$databasesToBackup = [$this->database->postgres_db];
} elseif (str($databaseType)->contains('mongodb')) {
@@ -320,12 +320,10 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
'filename' => null,
]);
}
send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
$this->team?->notify(new BackupFailed($this->backup, $this->database, $this->backup_output, $database));
}
}
} catch (\Throwable $e) {
send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
throw $e;
} finally {
if ($this->team) {
-1
View File
@@ -89,7 +89,6 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
$this->resource?->delete_connected_networks($this->resource->uuid);
}
} catch (\Throwable $e) {
send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
throw $e;
} finally {
$this->resource->forceDelete();
+242
View File
@@ -0,0 +1,242 @@
<?php
namespace App\Jobs;
use App\Models\Subscription;
use App\Models\Team;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Str;
class StripeProcessJob implements ShouldQueue
{
use Queueable;
public $type;
public $webhook;
public $tries = 3;
public function __construct(public $event)
{
$this->onQueue('high');
}
public function handle(): void
{
$excludedPlans = config('subscription.stripe_excluded_plans');
$type = data_get($this->event, 'type');
$this->type = $type;
$data = data_get($this->event, 'data.object');
switch ($type) {
case 'radar.early_fraud_warning.created':
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
$id = data_get($data, 'id');
$charge = data_get($data, 'charge');
if ($charge) {
$stripe->refunds->create(['charge' => $charge]);
}
$pi = data_get($data, 'payment_intent');
$piData = $stripe->paymentIntents->retrieve($pi, []);
$customerId = data_get($piData, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if ($subscription) {
$subscriptionId = data_get($subscription, 'stripe_subscription_id');
$stripe->subscriptions->cancel($subscriptionId, []);
$subscription->update([
'stripe_invoice_paid' => false,
]);
send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}");
} else {
send_internal_notification("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
throw new \Exception("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
}
break;
case 'checkout.session.completed':
$clientReferenceId = data_get($data, 'client_reference_id');
if (is_null($clientReferenceId)) {
send_internal_notification('Checkout session completed without client reference id.');
break;
}
$userId = Str::before($clientReferenceId, ':');
$teamId = Str::after($clientReferenceId, ':');
$subscriptionId = data_get($data, 'subscription');
$customerId = data_get($data, 'customer');
$team = Team::find($teamId);
$found = $team->members->where('id', $userId)->first();
if (! $found->isAdmin()) {
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
throw new \Exception("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
}
$subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) {
send_internal_notification('Old subscription activated for team: '.$teamId);
$subscription->update([
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true,
]);
} else {
send_internal_notification('New subscription for team: '.$teamId);
Subscription::create([
'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true,
]);
}
break;
case 'invoice.paid':
$customerId = data_get($data, 'customer');
$planId = data_get($data, 'lines.data.0.plan.id');
if (Str::contains($excludedPlans, $planId)) {
send_internal_notification('Subscription excluded.');
break;
}
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if ($subscription) {
$subscription->update([
'stripe_invoice_paid' => true,
]);
} else {
throw new \Exception("No subscription found for customer: {$customerId}");
}
break;
case 'invoice.payment_failed':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId);
throw new \Exception("No subscription found for customer: {$customerId}");
}
$team = data_get($subscription, 'team');
if (! $team) {
send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId);
throw new \Exception("No team found in Coolify for customer: {$customerId}");
}
if (! $subscription->stripe_invoice_paid) {
SubscriptionInvoiceFailedJob::dispatch($team);
send_internal_notification('Invoice payment failed: '.$customerId);
} else {
send_internal_notification('Invoice payment failed but already paid: '.$customerId);
}
break;
case 'payment_intent.payment_failed':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId);
throw new \Exception("No subscription found in Coolify for customer: {$customerId}");
}
if ($subscription->stripe_invoice_paid) {
send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId);
return;
}
send_internal_notification('Subscription payment failed for customer: '.$customerId);
break;
case 'customer.subscription.created':
$customerId = data_get($data, 'customer');
$subscriptionId = data_get($data, 'id');
$teamId = data_get($data, 'metadata.team_id');
$userId = data_get($data, 'metadata.user_id');
if (! $teamId || ! $userId) {
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if ($subscription) {
throw new \Exception("Subscription already exists for customer: {$customerId}");
}
throw new \Exception('No team id or user id found');
}
$team = Team::find($teamId);
$found = $team->members->where('id', $userId)->first();
if (! $found->isAdmin()) {
send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
throw new \Exception("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
}
$subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) {
send_internal_notification("Subscription already exists for team: {$teamId}");
throw new \Exception("Subscription already exists for team: {$teamId}");
} else {
Subscription::create([
'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => false,
]);
}
case 'customer.subscription.updated':
$teamId = data_get($data, 'metadata.team_id');
$userId = data_get($data, 'metadata.user_id');
$customerId = data_get($data, 'customer');
$status = data_get($data, 'status');
$subscriptionId = data_get($data, 'items.data.0.subscription');
$planId = data_get($data, 'items.data.0.plan.id');
if (Str::contains($excludedPlans, $planId)) {
send_internal_notification('Subscription excluded.');
break;
}
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
if ($status === 'incomplete_expired') {
send_internal_notification('Subscription incomplete expired');
throw new \Exception('Subscription incomplete expired');
}
if ($teamId) {
$subscription = Subscription::create([
'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => false,
]);
} else {
send_internal_notification('No subscription and team id found');
throw new \Exception('No subscription and team id found');
}
}
$cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
$feedback = data_get($data, 'cancellation_details.feedback');
$comment = data_get($data, 'cancellation_details.comment');
$lookup_key = data_get($data, 'items.data.0.price.lookup_key');
if (str($lookup_key)->contains('dynamic')) {
$quantity = data_get($data, 'items.data.0.quantity', 2);
$team = data_get($subscription, 'team');
if ($team) {
$team->update([
'custom_server_limit' => $quantity,
]);
}
ServerLimitCheckJob::dispatch($team);
}
$subscription->update([
'stripe_feedback' => $feedback,
'stripe_comment' => $comment,
'stripe_plan_id' => $planId,
'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
]);
if ($status === 'paused' || $status === 'incomplete_expired') {
$subscription->update([
'stripe_invoice_paid' => false,
]);
}
if ($feedback) {
$reason = "Cancellation feedback for {$customerId}: '".$feedback."'";
if ($comment) {
$reason .= ' with comment: \''.$comment."'";
}
}
break;
case 'customer.subscription.deleted':
// End subscription
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$team = data_get($subscription, 'team');
$team?->subscriptionEnded();
break;
default:
// Unhandled event type
}
}
}
+12 -2
View File
@@ -73,6 +73,9 @@ class Email extends Component
#[Validate(['nullable', 'string'])]
public ?string $resendApiKey = null;
#[Validate(['required', 'email'])]
public string $testEmailAddress = '';
public function mount()
{
try {
@@ -132,14 +135,21 @@ class Email extends Component
}
}
public function sendTestNotification()
public function sendTestEmail()
{
try {
$this->validate([
'testEmailAddress' => 'required|email',
], [
'testEmailAddress.required' => 'Test email address is required.',
'testEmailAddress.email' => 'Please enter a valid email address.',
]);
$executed = RateLimiter::attempt(
'test-email:'.$this->team->id,
$perMinute = 0,
function () {
$this->team?->notify(new Test($this->emails));
$this->team?->notify(new Test($this->testEmailAddress));
$this->dispatch('success', 'Test Email sent.');
},
$decaySeconds = 10,
+1 -1
View File
@@ -2,7 +2,7 @@
return [
'coolify' => [
'version' => '4.0.0-beta.371',
'version' => '4.0.0-beta.372',
'self_hosted' => env('SELF_HOSTED', true),
'autoupdate' => env('AUTOUPDATE'),
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
+1 -1
View File
@@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.371';
return '4.0.0-beta.372';
@@ -16,9 +16,9 @@
@endif
@if (isEmailEnabled($team) && auth()->user()->isAdminFromSession() && isTestEmailEnabled($team))
<x-modal-input buttonTitle="Send Test Email" title="Send Test Email">
<form wire:submit='submit' class="flex flex-col w-full gap-2">
<x-forms.input placeholder="test@example.com" id="emails" label="Recipients" required />
<x-forms.button wire:click="sendTestNotification" @click="modalOpen=false">
<form wire:submit.prevent="sendTestEmail" class="flex flex-col w-full gap-2">
<x-forms.input wire:model="testEmailAddress" placeholder="test@example.com" id="testEmailAddress" label="Recipients" required />
<x-forms.button type="submit" @click="modalOpen=false">
Send Email
</x-forms.button>
</form>
+4 -4
View File
@@ -79,7 +79,7 @@
"appwrite": {
"documentation": "https://appwrite.io?utm_source=coolify.io",
"slogan": "A backend-as-a-service platform that simplifies the web & mobile app development.",
"compose": "c2VydmljZXM6CiAgYXBwd3JpdGU6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNi4wJwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlCiAgICB2b2x1bWVzOgogICAgICAtICdhcHB3cml0ZS11cGxvYWRzOi9zdG9yYWdlL3VwbG9hZHM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNhY2hlOi9zdG9yYWdlL2NhY2hlOnJ3JwogICAgICAtICdhcHB3cml0ZS1jb25maWc6L3N0b3JhZ2UvY29uZmlnOnJ3JwogICAgICAtICdhcHB3cml0ZS1jZXJ0aWZpY2F0ZXM6L3N0b3JhZ2UvY2VydGlmaWNhdGVzOnJ3JwogICAgICAtICdhcHB3cml0ZS1mdW5jdGlvbnM6L3N0b3JhZ2UvZnVuY3Rpb25zOnJ3JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9BUFBXUklURT0vCiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSAnX0FQUF9MT0NBTEU9JHtfQVBQX0xPQ0FMRTotZW59JwogICAgICAtICdfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX1JPT1Q9JHtfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX1JPT1Q6LWVuYWJsZWR9JwogICAgICAtICdfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX0VNQUlMUz0ke19BUFBfQ09OU09MRV9XSElURUxJU1RfRU1BSUxTfScKICAgICAgLSAnX0FQUF9DT05TT0xFX1dISVRFTElTVF9JUFM9JHtfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX0lQU30nCiAgICAgIC0gJ19BUFBfQ09OU09MRV9IT1NUTkFNRVM9JHtfQVBQX0NPTlNPTEVfSE9TVE5BTUVTOi1sb2NhbGhvc3QsYXBwd3JpdGUuaW8sKi5hcHB3cml0ZS5pb30nCiAgICAgIC0gJ19BUFBfU1lTVEVNX0VNQUlMX05BTUU9JHtfQVBQX1NZU1RFTV9FTUFJTF9OQU1FOi1BcHB3cml0ZX0nCiAgICAgIC0gJ19BUFBfU1lTVEVNX0VNQUlMX0FERFJFU1M9JHtfQVBQX1NZU1RFTV9FTUFJTF9BRERSRVNTOi10ZWFtQGFwcHdyaXRlLmlvfScKICAgICAgLSAnX0FQUF9TWVNURU1fVEVBTV9FTUFJTD0ke19BUFBfU1lTVEVNX1RFQU1fRU1BSUw6LXRlYW1AYXBwd3JpdGUuaW99JwogICAgICAtICdfQVBQX0VNQUlMX1NFQ1VSSVRZPSR7X0FQUF9FTUFJTF9TRUNVUklUWTotY2VydHNAYXBwd3JpdGUuaW99JwogICAgICAtICdfQVBQX1NZU1RFTV9SRVNQT05TRV9GT1JNQVQ9JHtfQVBQX1NZU1RFTV9SRVNQT05TRV9GT1JNQVR9JwogICAgICAtICdfQVBQX09QVElPTlNfQUJVU0U9JHtfQVBQX09QVElPTlNfQUJVU0U6LWVuYWJsZWR9JwogICAgICAtICdfQVBQX09QVElPTlNfUk9VVEVSX1BST1RFQ1RJT049JHtfQVBQX09QVElPTlNfUk9VVEVSX1BST1RFQ1RJT046LWRpc2FibGVkfScKICAgICAgLSAnX0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTPSR7X0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTOi1kaXNhYmxlZH0nCiAgICAgIC0gJ19BUFBfT1BUSU9OU19GVU5DVElPTlNfRk9SQ0VfSFRUUFM9JHtfQVBQX09QVElPTlNfRlVOQ1RJT05TX0ZPUkNFX0hUVFBTOi1kaXNhYmxlZH0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtIF9BUFBfRE9NQUlOPSRTRVJWSUNFX1VSTF9BUFBXUklURQogICAgICAtIF9BUFBfRE9NQUlOX1RBUkdFVD0kU0VSVklDRV9VUkxfQVBQV1JJVEUKICAgICAgLSBfQVBQX0RPTUFJTl9GVU5DVElPTlM9JFNFUlZJQ0VfVVJMX0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9ST09UX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQlJPT1QKICAgICAgLSAnX0FQUF9TTVRQX0hPU1Q9JHtfQVBQX1NNVFBfSE9TVH0nCiAgICAgIC0gJ19BUFBfU01UUF9QT1JUPSR7X0FQUF9TTVRQX1BPUlR9JwogICAgICAtICdfQVBQX1NNVFBfU0VDVVJFPSR7X0FQUF9TTVRQX1NFQ1VSRX0nCiAgICAgIC0gJ19BUFBfU01UUF9VU0VSTkFNRT0ke19BUFBfU01UUF9VU0VSTkFNRX0nCiAgICAgIC0gJ19BUFBfU01UUF9QQVNTV09SRD0ke19BUFBfU01UUF9QQVNTV09SRH0nCiAgICAgIC0gJ19BUFBfVVNBR0VfU1RBVFM9JHtfQVBQX1VTQUdFX1NUQVRTOi1lbmFibGVkfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTUlUPSR7X0FQUF9TVE9SQUdFX0xJTUlUOi0zMDAwMDAwMH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9QUkVWSUVXX0xJTUlUPSR7X0FQUF9TVE9SQUdFX1BSRVZJRVdfTElNSVQ6LTIwMDAwMDAwfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0FOVElWSVJVUz0ke19BUFBfU1RPUkFHRV9BTlRJVklSVVM6LWRpc2FibGVkfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0FOVElWSVJVU19IT1NUPSR7X0FQUF9TVE9SQUdFX0FOVElWSVJVU19IT1NUOi1hcHB3cml0ZS1jbGFtYXZ9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQU5USVZJUlVTX1BPUlQ9JHtfQVBQX1NUT1JBR0VfQU5USVZJUlVTX1BPUlQ6LTMzMTB9JwogICAgICAtICdfQVBQX1NUT1JBR0VfREVWSUNFPSR7X0FQUF9TVE9SQUdFX0RFVklDRTotbG9jYWx9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9TM19TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX1MzX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9TM19CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT049JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVRLine truncated
"compose": "c2VydmljZXM6CiAgYXBwd3JpdGU6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNi4wJwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlCiAgICB2b2x1bWVzOgogICAgICAtICdhcHB3cml0ZS11cGxvYWRzOi9zdG9yYWdlL3VwbG9hZHM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNhY2hlOi9zdG9yYWdlL2NhY2hlOnJ3JwogICAgICAtICdhcHB3cml0ZS1jb25maWc6L3N0b3JhZ2UvY29uZmlnOnJ3JwogICAgICAtICdhcHB3cml0ZS1jZXJ0aWZpY2F0ZXM6L3N0b3JhZ2UvY2VydGlmaWNhdGVzOnJ3JwogICAgICAtICdhcHB3cml0ZS1mdW5jdGlvbnM6L3N0b3JhZ2UvZnVuY3Rpb25zOnJ3JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9BUFBXUklURT0vCiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSAnX0FQUF9MT0NBTEU9JHtfQVBQX0xPQ0FMRTotZW59JwogICAgICAtICdfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX1JPT1Q9JHtfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX1JPT1Q6LWVuYWJsZWR9JwogICAgICAtICdfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX0VNQUlMUz0ke19BUFBfQ09OU09MRV9XSElURUxJU1RfRU1BSUxTfScKICAgICAgLSAnX0FQUF9DT05TT0xFX1dISVRFTElTVF9JUFM9JHtfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX0lQU30nCiAgICAgIC0gJ19BUFBfQ09OU09MRV9IT1NUTkFNRVM9JHtfQVBQX0NPTlNPTEVfSE9TVE5BTUVTOi1sb2NhbGhvc3QsYXBwd3JpdGUuaW8sKi5hcHB3cml0ZS5pb30nCiAgICAgIC0gJ19BUFBfU1lTVEVNX0VNQUlMX05BTUU9JHtfQVBQX1NZU1RFTV9FTUFJTF9OQU1FOi1BcHB3cml0ZX0nCiAgICAgIC0gJ19BUFBfU1lTVEVNX0VNQUlMX0FERFJFU1M9JHtfQVBQX1NZU1RFTV9FTUFJTF9BRERSRVNTOi10ZWFtQGFwcHdyaXRlLmlvfScKICAgICAgLSAnX0FQUF9TWVNURU1fVEVBTV9FTUFJTD0ke19BUFBfU1lTVEVNX1RFQU1fRU1BSUw6LXRlYW1AYXBwd3JpdGUuaW99JwogICAgICAtICdfQVBQX0VNQUlMX1NFQ1VSSVRZPSR7X0FQUF9FTUFJTF9TRUNVUklUWTotY2VydHNAYXBwd3JpdGUuaW99JwogICAgICAtICdfQVBQX1NZU1RFTV9SRVNQT05TRV9GT1JNQVQ9JHtfQVBQX1NZU1RFTV9SRVNQT05TRV9GT1JNQVR9JwogICAgICAtICdfQVBQX09QVElPTlNfQUJVU0U9JHtfQVBQX09QVElPTlNfQUJVU0U6LWVuYWJsZWR9JwogICAgICAtICdfQVBQX09QVElPTlNfUk9VVEVSX1BST1RFQ1RJT049JHtfQVBQX09QVElPTlNfUk9VVEVSX1BST1RFQ1RJT046LWRpc2FibGVkfScKICAgICAgLSAnX0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTPSR7X0FQUF9PUFRJT05TX0ZPUkNFX0hUVFBTOi1kaXNhYmxlZH0nCiAgICAgIC0gJ19BUFBfT1BUSU9OU19GVU5DVElPTlNfRk9SQ0VfSFRUUFM9JHtfQVBQX09QVElPTlNfRlVOQ1RJT05TX0ZPUkNFX0hUVFBTOi1kaXNhYmxlZH0nCiAgICAgIC0gX0FQUF9PUEVOU1NMX0tFWV9WMT0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtIF9BUFBfRE9NQUlOPSRTRVJWSUNFX1VSTF9BUFBXUklURQogICAgICAtIF9BUFBfRE9NQUlOX1RBUkdFVD0kU0VSVklDRV9VUkxfQVBQV1JJVEUKICAgICAgLSBfQVBQX0RPTUFJTl9GVU5DVElPTlM9JFNFUlZJQ0VfVVJMX0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfU01UUF9IT1NUPSR7X0FQUF9TTVRQX0hPU1R9JwogICAgICAtICdfQVBQX1NNVFBfUE9SVD0ke19BUFBfU01UUF9QT1JUfScKICAgICAgLSAnX0FQUF9TTVRQX1NFQ1VSRT0ke19BUFBfU01UUF9TRUNVUkV9JwogICAgICAtICdfQVBQX1NNVFBfVVNFUk5BTUU9JHtfQVBQX1NNVFBfVVNFUk5BTUV9JwogICAgICAtICdfQVBQX1NNVFBfUEFTU1dPUkQ9JHtfQVBQX1NNVFBfUEFTU1dPUkR9JwogICAgICAtICdfQVBQX1VTQUdFX1NUQVRTPSR7X0FQUF9VU0FHRV9TVEFUUzotZW5hYmxlZH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9MSU1JVD0ke19BUFBfU1RPUkFHRV9MSU1JVDotMzAwMDAwMDB9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUFJFVklFV19MSU1JVD0ke19BUFBfU1RPUkFHRV9QUkVWSUVXX0xJTUlUOi0yMDAwMDAwMH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9BTlRJVklSVVM9JHtfQVBQX1NUT1JBR0VfQU5USVZJUlVTOi1kaXNhYmxlZH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9BTlRJVklSVVNfSE9TVD0ke19BUFBfU1RPUkFHRV9BTlRJVklSVVNfSE9TVDotYXBwd3JpdGUtY2xhbWF2fScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0FOVElWSVJVU19QT1JUPSR7X0FQUF9TVE9SQUdFX0FOVElWSVJVU19QT1JUOi0zMzEwfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RFVklDRT0ke19BUFBfU1RPUkFHRV9ERVZJQ0U6LWxvY2FsfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfUzNfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfUzNfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9TM19SRUdJT046LXVzLWVhc3QtMX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfUzNfQlVDS0VUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX0RPX1NQQUNFU19BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ET19TUEFDRVNfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT046LXVzLWVhc3QtMX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX0RPX1NQQUNFU19CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9SRUdJT049JHtfQVBQX1NUT1JBR0VLine truncated
"tags": [
"backend-as-a-service",
"platform"
@@ -642,7 +642,7 @@
},
"embystat": {
"documentation": "https://github.com/mregni/EmbyStat?utm_source=coolify.io",
"slogan": "EmnyStat is a web analytics tool, designed to provide insight into website traffic and user behavior.",
"slogan": "EmbyStat is a web analytics tool, designed to provide insight into website traffic and user behavior.",
"compose": "c2VydmljZXM6CiAgZW1ieXN0YXQ6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvZW1ieXN0YXQ6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0VNQllTVEFUXzY1NTUKICAgICAgLSBQVUlEPTEwMDAKICAgICAgLSBQR0lEPTEwMDAKICAgICAgLSBUWj1FdXJvcGUvTWFkcmlkCiAgICB2b2x1bWVzOgogICAgICAtICdlbWJ5c3RhdC1jb25maWc6L2NvbmZpZycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo2NTU1JwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==",
"tags": [
"media",
@@ -1149,7 +1149,7 @@
"hoarder": {
"documentation": "https://docs.hoarder.app/?utm_source=coolify.io",
"slogan": "an open source \"Bookmark Everything\" app that uses AI for automatically tagging the content you throw at it.",
"compose": "c2VydmljZXM6CiAgd2ViOgogICAgaW1hZ2U6ICdnaGNyLmlvL2hvYXJkZXItYXBwL2hvYXJkZXI6cmVsZWFzZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2hvYXJkZXItZGF0YTovZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9IT0FSREVSCiAgICAgIC0gJ05FWFRBVVRIX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSE9BUkRFUk5FWFRBVVRIfScKICAgICAgLSAnTUVJTElfTUFTVEVSX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfTUVJTEl9JwogICAgICAtICdORVhUQVVUSF9VUkw9JHtTRVJWSUNFX0ZRRE5fSE9BUkRFUn0nCiAgICAgIC0gJ01FSUxJX0FERFI9aHR0cDovL21laWxpc2VhcmNoOjc3MDAnCiAgICAgIC0gJ0JST1dTRVJfV0VCX1VSTD1odHRwOi8vY2hyb21lOjkyMjInCiAgICAgIC0gREFUQV9ESVI9L2RhdGEKICBjaHJvbWU6CiAgICBpbWFnZTogJ2djci5pby96ZW5pa2EtaHViL2FscGluZS1jaHJvbWU6MTI0JwogICAgY29tbWFuZDoKICAgICAgLSAnLS1uby1zYW5kYm94JwogICAgICAtICctLWRpc2FibGUtZ3B1JwogICAgICAtICctLWRpc2FibGUtZGV2LXNobS11c2FnZScKICAgICAgLSAnLS1yZW1vdGUtZGVidWdnaW5nLWFkZHJlc3M9MC4wLjAuMCcKICAgICAgLSAnLS1yZW1vdGUtZGVidWdnaW5nLXBvcnQ9OTIyMicKICAgICAgLSAnLS1oaWRlLXNjcm9sbGJhcnMnCiAgbWVpbGlzZWFyY2g6CiAgICBpbWFnZTogJ2dldG1laWxpL21laWxpc2VhcmNoOnYxLjYnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTUVJTElfTk9fQU5BTFlUSUNTPSR7TUVJTElfTk9fQU5BTFlUSUNTOi10cnVlfScKICAgICAgLSAnTkVYVEFVVEhfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9IT0FSREVSTkVYVEFVVEh9JwogICAgICAtICdNRUlMSV9NQVNURVJfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9NRUlMSX0nCiAgICAgIC0gJ05FWFRBVVRIX1VSTD0ke1NFUlZJQ0VfRlFETl9IT0FSREVSfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2hvYXJkZXItbWVpbGlzZWFyY2g6L21laWxpX2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6NzcwMC9oZWFsdGgnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK",
"compose": "c2VydmljZXM6CiAgd2ViOgogICAgaW1hZ2U6ICdnaGNyLmlvL2hvYXJkZXItYXBwL2hvYXJkZXI6cmVsZWFzZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2hvYXJkZXItZGF0YTovZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9IT0FSREVSCiAgICAgIC0gJ05FWFRBVVRIX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSE9BUkRFUk5FWFRBVVRIfScKICAgICAgLSAnTUVJTElfTUFTVEVSX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfTUVJTEl9JwogICAgICAtICdORVhUQVVUSF9VUkw9JHtTRVJWSUNFX0ZRRE5fSE9BUkRFUn0nCiAgICAgIC0gJ01FSUxJX0FERFI9aHR0cDovL21laWxpc2VhcmNoOjc3MDAnCiAgICAgIC0gJ0JST1dTRVJfV0VCX1VSTD1odHRwOi8vY2hyb21lOjkyMjInCiAgICAgIC0gJ09QRU5BSV9BUElfS0VZPSR7U0VSVklDRV9PUEVOQUlfQVBJX0tFWX0nCiAgICAgIC0gJ0RJU0FCTEVfU0lHTlVQUz0ke1NFUlZJQ0VfRElTQUJMRV9TSUdOVVBTfScKICAgICAgLSBEQVRBX0RJUj0vZGF0YQogIGNocm9tZToKICAgIGltYWdlOiAnZ2NyLmlvL3plbmlrYS1odWIvYWxwaW5lLWNocm9tZToxMjQnCiAgICBjb21tYW5kOgogICAgICAtICctLW5vLXNhbmRib3gnCiAgICAgIC0gJy0tZGlzYWJsZS1ncHUnCiAgICAgIC0gJy0tZGlzYWJsZS1kZXYtc2htLXVzYWdlJwogICAgICAtICctLXJlbW90ZS1kZWJ1Z2dpbmctYWRkcmVzcz0wLjAuMC4wJwogICAgICAtICctLXJlbW90ZS1kZWJ1Z2dpbmctcG9ydD05MjIyJwogICAgICAtICctLWhpZGUtc2Nyb2xsYmFycycKICBtZWlsaXNlYXJjaDoKICAgIGltYWdlOiAnZ2V0bWVpbGkvbWVpbGlzZWFyY2g6djEuNicKICAgIGVudmlyb25tZW50OgogICAgICAtICdNRUlMSV9OT19BTkFMWVRJQ1M9JHtNRUlMSV9OT19BTkFMWVRJQ1M6LXRydWV9JwogICAgICAtICdORVhUQVVUSF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0hPQVJERVJORVhUQVVUSH0nCiAgICAgIC0gJ01FSUxJX01BU1RFUl9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX01FSUxJfScKICAgICAgLSAnTkVYVEFVVEhfVVJMPSR7U0VSVklDRV9GUUROX0hPQVJERVJ9JwogICAgdm9sdW1lczoKICAgICAgLSAnaG9hcmRlci1tZWlsaXNlYXJjaDovbWVpbGlfZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo3NzAwL2hlYWx0aCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQo=",
"tags": [
"media",
"read-it-later",
@@ -1568,7 +1568,7 @@
"mailpit": {
"documentation": "https://mailpit.axllent.org/docs/?utm_source=coolify.io",
"slogan": "Email & SMTP testing tool with API for developers",
"compose": "c2VydmljZXM6CiAgbWFpbHBpdDoKICAgIGltYWdlOiBheGxsZW50L21haWxwaXQKICAgIHZvbHVtZXM6CiAgICAgIC0gJ21haWxwaXQtZGF0YTovZGF0YScKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vaHRwYXNzd2QKICAgICAgICB0YXJnZXQ6IC9kYXRhL2h0cGFzc3dkCiAgICAgICAgaXNEaXJlY3Rvcnk6IGZhbHNlCiAgICAgICAgY29udGVudDogJycKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9NQUlMUElUXzgwMjUKICAgICAgLSBNUF9NQVhfTUVTU0FHRVM9NTAwMAogICAgICAtIE1QX0RBVEFCQVNFPS9kYXRhL21haWxwaXQuZGIKICAgICAgLSBNUF9TTVRQX0FVVEhfQUNDRVBUX0FOWT0xCiAgICAgIC0gTVBfU01UUF9BVVRIX0FMTE9XX0lOU0VDVVJFPTEKICAgICAgLSBNUF9VSV9BVVRIX0ZJTEU9L2RhdGEvaHRwYXNzd2QKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSAvbWFpbHBpdAogICAgICAgIC0gcmVhZHl6CiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK",
"compose": "c2VydmljZXM6CiAgbWFpbHBpdDoKICAgIGltYWdlOiBheGxsZW50L21haWxwaXQKICAgIHBvcnRzOgogICAgICAtICcxMDI1OjEwMjUnCiAgICB2b2x1bWVzOgogICAgICAtICdtYWlscGl0LWRhdGE6L2RhdGEnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2h0cGFzc3dkCiAgICAgICAgdGFyZ2V0OiAvZGF0YS9odHBhc3N3ZAogICAgICAgIGlzRGlyZWN0b3J5OiBmYWxzZQogICAgICAgIGNvbnRlbnQ6ICcnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTUFJTFBJVF84MDI1CiAgICAgIC0gTVBfTUFYX01FU1NBR0VTPTUwMDAKICAgICAgLSBNUF9EQVRBQkFTRT0vZGF0YS9tYWlscGl0LmRiCiAgICAgIC0gTVBfU01UUF9BVVRIX0FDQ0VQVF9BTlk9MQogICAgICAtIE1QX1NNVFBfQVVUSF9BTExPV19JTlNFQ1VSRT0xCiAgICAgIC0gTVBfVUlfQVVUSF9GSUxFPS9kYXRhL2h0cGFzc3dkCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gL21haWxwaXQKICAgICAgICAtIHJlYWR5egogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==",
"tags": [
"mailpit",
"email",
+2 -2
View File
@@ -1,10 +1,10 @@
{
"coolify": {
"v4": {
"version": "4.0.0-beta.371"
"version": "4.0.0-beta.372"
},
"nightly": {
"version": "4.0.0-beta.372"
"version": "4.0.0-beta.373"
},
"helper": {
"version": "1.0.4"