Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 224 additions & 0 deletions app/Http/Controllers/DynamicAssetController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
<?php

namespace App\Http\Controllers;

use App\Models\WhiteLabelConfig;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cache;

class DynamicAssetController extends Controller
{
/**
* Generate dynamic CSS based on the requesting domain.
*
* This demonstrates how the same endpoint serves different CSS
* based on the domain making the request.
*/
public function dynamicCss(Request $request): Response
{
$domain = $request->getHost();
$cacheKey = "dynamic_css:{$domain}";

// Cache the generated CSS for performance
$css = Cache::remember($cacheKey, 3600, function () use ($domain) {
return $this->generateCssForDomain($domain);
});

return response($css, 200, [
'Content-Type' => 'text/css',
'Cache-Control' => 'public, max-age=3600',
'X-Generated-For-Domain' => $domain, // Debug header
]);
}

/**
* Generate CSS content for a specific domain.
*/
private function generateCssForDomain(string $domain): string
{
// Find branding config for this domain
$branding = WhiteLabelConfig::findByDomain($domain);

if (! $branding) {
return $this->getDefaultCss();
}

// Start with base CSS
$css = $this->getBaseCss();

// Add custom CSS variables
$css .= "\n\n/* Custom theme for {$domain} */\n";
$css .= $branding->generateCssVariables();

// Add any custom CSS
if ($branding->custom_css) {
$css .= "\n\n/* Custom CSS for {$domain} */\n";
$css .= $branding->custom_css;
}
Comment on lines +50 to +58
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Custom CSS is appended twice causing duplication.

The generateCssVariables() method in WhiteLabelConfig (lines 73-90 of the model) already appends custom_css if present. Lines 55-58 here add it again, resulting in duplicate custom CSS in the output.

Remove the duplicate custom CSS block:

         // Add custom CSS variables
         $css .= "\n\n/* Custom theme for {$domain} */\n";
         $css .= $branding->generateCssVariables();

-        // Add any custom CSS
-        if ($branding->custom_css) {
-            $css .= "\n\n/* Custom CSS for {$domain} */\n";
-            $css .= $branding->custom_css;
-        }
-
         return $css;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Add custom CSS variables
$css .= "\n\n/* Custom theme for {$domain} */\n";
$css .= $branding->generateCssVariables();
// Add any custom CSS
if ($branding->custom_css) {
$css .= "\n\n/* Custom CSS for {$domain} */\n";
$css .= $branding->custom_css;
}
// Add custom CSS variables
$css .= "\n\n/* Custom theme for {$domain} */\n";
$css .= $branding->generateCssVariables();
return $css;
🤖 Prompt for AI Agents
In app/Http/Controllers/DynamicAssetController.php around lines 50 to 58, the
controller appends branding->generateCssVariables() which already includes the
custom_css, and then conditionally appends branding->custom_css again (lines
~55-58), causing duplicated CSS; remove the conditional block that appends the
custom_css (the second addition) so only generateCssVariables() supplies the CSS
variables and any custom CSS once.


return $css;
}

/**
* Get the base CSS that's common to all themes.
*/
private function getBaseCss(): string
{
$baseCssPath = resource_path('css/base-theme.css');

if (file_exists($baseCssPath)) {
return file_get_contents($baseCssPath);
}

// Fallback base CSS if file doesn't exist
return $this->getFallbackBaseCss();
}

/**
* Get default Coolify CSS.
*/
private function getDefaultCss(): string
{
$defaultCssPath = public_path('css/app.css');

if (file_exists($defaultCssPath)) {
return file_get_contents($defaultCssPath);
}

return $this->getFallbackBaseCss();
}

/**
* Fallback CSS if no files are found.
*/
private function getFallbackBaseCss(): string
{
return <<<'CSS'
/* Fallback Base CSS for Dynamic Branding Demo */
:root {
--primary-color: #3b82f6;
--secondary-color: #1f2937;
--accent-color: #10b981;
--background-color: #ffffff;
--text-color: #1f2937;
--border-color: #e5e7eb;
}

body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: var(--background-color);
color: var(--text-color);
margin: 0;
padding: 0;
}

.navbar {
background-color: var(--primary-color);
color: white;
padding: 1rem;
display: flex;
align-items: center;
gap: 1rem;
}

.navbar img {
height: 40px;
}

.platform-name {
font-size: 1.5rem;
font-weight: bold;
}

.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
color: white;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
text-decoration: none;
display: inline-block;
}

.btn-primary:hover {
opacity: 0.9;
}

.card {
background: white;
border: 1px solid var(--border-color);
border-radius: 0.5rem;
padding: 1.5rem;
margin: 1rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.text-primary {
color: var(--primary-color);
}

.text-secondary {
color: var(--secondary-color);
}

.bg-primary {
background-color: var(--primary-color);
}

.border-primary {
border-color: var(--primary-color);
}
CSS;
}

/**
* Serve dynamic favicon based on domain branding.
*/
public function dynamicFavicon(Request $request): Response
{
$domain = $request->getHost();
$branding = WhiteLabelConfig::findByDomain($domain);

if ($branding && $branding->getLogoUrl()) {
// Redirect to custom logo
return redirect($branding->getLogoUrl());
}

// Serve default favicon
$defaultFavicon = public_path('favicon.ico');
if (file_exists($defaultFavicon)) {
return response(file_get_contents($defaultFavicon), 200, [
'Content-Type' => 'image/x-icon',
'Cache-Control' => 'public, max-age=86400',
]);
}

return response('', 404);
}
Comment on lines +178 to +198
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential open redirect vulnerability via favicon endpoint.

The getLogoUrl() is user-controlled and redirecting to arbitrary URLs can be exploited for phishing. Validate the URL or serve the asset directly instead of redirecting.

Consider validating the URL scheme and domain, or proxy the asset:

         if ($branding && $branding->getLogoUrl()) {
-            // Redirect to custom logo
-            return redirect($branding->getLogoUrl());
+            $logoUrl = $branding->getLogoUrl();
+            // Only redirect to HTTPS URLs on trusted domains or relative paths
+            if (str_starts_with($logoUrl, '/') || $this->isAllowedAssetUrl($logoUrl)) {
+                return redirect($logoUrl);
+            }
+            // Fall through to default favicon if URL is not allowed
         }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/Http/Controllers/DynamicAssetController.php around lines 178 to 198, the
controller currently redirects to a user-controlled getLogoUrl(), creating an
open-redirect risk; instead validate the URL or proxy the asset: check that the
returned URL is a well-formed absolute URL with an allowed scheme (http or
https) and either matches an allowlist of trusted hostnames or is same-origin,
and only perform a redirect if it passes validation; otherwise fetch the remote
asset server-side (with a timeout and size limit), validate the Content-Type
(image/*) and return the binary data with proper headers, falling back to
serving the local favicon or returning 404 when validation/fetch fails.


/**
* Debug endpoint to show how domain detection works.
*/
public function debugBranding(Request $request): array
{
$domain = $request->getHost();
$branding = WhiteLabelConfig::findByDomain($domain);

return [
'domain' => $domain,
'has_custom_branding' => $branding !== null,
'platform_name' => $branding?->getPlatformName() ?? 'Coolify (Default)',
'custom_logo' => $branding?->getLogoUrl(),
'theme_variables' => $branding?->getThemeVariables() ?? WhiteLabelConfig::createDefault('')->getDefaultThemeVariables(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Calling createDefault('') may create invalid database records.

createDefault('') calls self::create() which persists to the database with an empty organization_id. Use getDefaultThemeVariables() on a new (unsaved) instance instead.

-            'theme_variables' => $branding?->getThemeVariables() ?? WhiteLabelConfig::createDefault('')->getDefaultThemeVariables(),
+            'theme_variables' => $branding?->getThemeVariables() ?? (new WhiteLabelConfig())->getDefaultThemeVariables(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'theme_variables' => $branding?->getThemeVariables() ?? WhiteLabelConfig::createDefault('')->getDefaultThemeVariables(),
'theme_variables' => $branding?->getThemeVariables() ?? (new WhiteLabelConfig())->getDefaultThemeVariables(),
🤖 Prompt for AI Agents
In app/Http/Controllers/DynamicAssetController.php around line 213, the code
calls WhiteLabelConfig::createDefault('') which invokes self::create() and
persists a record with an empty organization_id; replace that with a new,
unsaved WhiteLabelConfig instance and call getDefaultThemeVariables() on it
(i.e., instantiate a WhiteLabelConfig without saving and use its
getDefaultThemeVariables method) so no invalid DB record is created.

'custom_domains' => $branding?->getCustomDomains() ?? [],
'hide_coolify_branding' => $branding?->shouldHideCoolifyBranding() ?? false,
'organization_id' => $branding?->organization_id,
'request_headers' => [
'host' => $request->header('host'),
'user_agent' => $request->header('user-agent'),
'x_forwarded_host' => $request->header('x-forwarded-host'),
],
];
}
Comment on lines +203 to +223
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Debug endpoint exposes sensitive information without authorization.

This endpoint returns organization_id, request headers, and internal branding configuration without any authentication. This aids reconnaissance and should be restricted.

Restrict to non-production environments or add authorization:

     public function debugBranding(Request $request): array
     {
+        if (app()->environment('production')) {
+            abort(404);
+        }
+
         $domain = $request->getHost();
         $branding = WhiteLabelConfig::findByDomain($domain);
 
         return [
             'domain' => $domain,
             'has_custom_branding' => $branding !== null,
             'platform_name' => $branding?->getPlatformName() ?? 'Coolify (Default)',
             'custom_logo' => $branding?->getLogoUrl(),
             'theme_variables' => $branding?->getThemeVariables() ?? WhiteLabelConfig::createDefault('')->getDefaultThemeVariables(),
             'custom_domains' => $branding?->getCustomDomains() ?? [],
             'hide_coolify_branding' => $branding?->shouldHideCoolifyBranding() ?? false,
-            'organization_id' => $branding?->organization_id,
-            'request_headers' => [
-                'host' => $request->header('host'),
-                'user_agent' => $request->header('user-agent'),
-                'x_forwarded_host' => $request->header('x-forwarded-host'),
-            ],
         ];
     }
🤖 Prompt for AI Agents
In app/Http/Controllers/DynamicAssetController.php around lines 203 to 223, the
debugBranding endpoint returns sensitive data (organization_id, request headers
and internal branding) with no authorization; restrict access by gating this
endpoint to non-production environments or adding an authorization check (e.g.,
require a configured env flag like APP_DEBUG_BRANDING or an authenticated admin
role/API key) before building the response, and remove or redact sensitive
fields (organization_id, request_headers) unless the caller is authorized;
ensure any gating logic uses existing auth middleware or environment config and
return a safe minimal response (domain, has_custom_branding, non-sensitive
branding flags) for unauthorized/production requests.

}
Loading