#Proper way for catch all domain routing?

43 messages · Page 1 of 1 (latest)

delicate mason
#

Just to clarify - does proxy server rewrite the hostname? Also, it would be nice to have better idea of how your routes are structured - especially how routes are supposed to differ based on the domain.

urban sandal
#

You're using a proxy, so you should set up the TrustProxies middleware to correctly identify proxies. You'd probably also have to set up the TrustHosts middleware

crude osprey
#

@urban sandal TrustProxies seems to be enabled by default, anything else you would need to setup? Forwarded for is in the headers and its showing the custom domain.

@delicate mason caddy listens for a hostname and will generate an SSL certificate for it, it then proxies to team.mydomain.com to fetch the data however I can't listen on team.mydomain.com route since it actually comes in as mydomain.com (which is weird)
"HTTP_HOST" => "team.mydomain.ai"
But trying to get the route on team

Route::domain('team.mydomain.ai')->group(function () {
    Route::get('/', function ($domain) {
        return 'Hello from custom domain: ' . $domain;
    });
});

Will not fire
While debugging that is the only route in my web.php trying to hook into the proxy.

urban sandal
crude osprey
#

@urban sandal request()->getHost() is returning the custom domain that the user setup. But I need to send all custom domain traffic to a specific route group.

urban sandal
#

So how have you defined this route? It would need to have more priority than a route without a domain

crude osprey
#

@urban sandal orignally I tried this

Route::domain('{domainOrSubdomain}')->middleware('setTenant')->group(function () {
    Route::name('blog.')->group(function () {
//        Route::get('/', [\App\Http\Controllers\CategoryController::class, 'index'])->name('home');
        Route::get('/', function () {
            return 'Hello from blog.domain.com!';
        });
        Route::resource('category', \App\Http\Controllers\CategoryController::class)->parameters(['post' => 'post']);
        Route::resource('category.post', \App\Http\Controllers\PostController::class)->parameters(['post' => 'post']);
        Route::resource('tag', \App\Http\Controllers\TagController::class)->parameters(['tag' => 'slug']);
        Route::get('/t/{post}', function ($id) {
            $post = Post::findOrFail($id);
            return redirect()->to(route('category.post.show', [$post->category->slug, $post->slug]));
        })->name('post.short_link');
    });
});

setTenant will abort 404 if custom domain does not exist in our database

And put this near the top of the routes. then my

Route::domain(config('social.domain'))->group(function () {
    Route::get('/', function () {
        return Inertia::render('Welcome', [
            'canLogin' => Route::has('login'),
            'canRegister' => Route::has('register'),
            'laravelVersion' => Application::VERSION,
            'phpVersion' => PHP_VERSION,
        ]);
    });
urban sandal
#

So what's the issue exactly? There's so much going on here it's impossible to pinpoint what the issue is. Trim it down to a simple test, then go from there

crude osprey
#

this route

Route::domain('{domainOrSubdomain}')-

Never gets executed, I figured its because it needs to catch on the subdomain not the "custom domain".

urban sandal
#

How do you know it "never gets called"? You have a middleware on it, maybe that screws things over. Is it present in your route list? What does happen?

crude osprey
#

my middleware contains logging and also dd at the top when I got frustrated

    public function handle(Request $request, Closure $next): Response
    {
        dd($domain = $request->route('domain'));
        $customDomain = $request->header('user-custom-domain');

        if ($customDomain) {
            Log::debug('SetTenantMiddlewareDomain', [$customDomain]);
            $customDomainRecord = CustomDomain::query()
                ->where('domain', $customDomain)
                ->first();

            if ($customDomainRecord) {
                Log::debug('SetTenantMiddlewareRecord', [$customDomainRecord->team->id]);
                request()->session()->put('tenant', $customDomainRecord->team->id);
                return $next($request);
            }
        }

        $domainParts = explode('.', $request->getHost());

        if (count($domainParts) === 3) {
            Log::debug('SetTenantMiddlewareParts', [$domainParts]);
            $subdomain = $domainParts[0];
            $mainDomain = $domainParts[1] . '.' . $domainParts[2];

            if ($mainDomain === parse_url(config('app.url'), PHP_URL_HOST)) {
                $team = Team::find($subdomain);
                if ($team) {
                    Log::debug('SetTenantMiddlewareTeam', [$team->id]);
                    request()->session()->put('tenant', $team->id);
                    return $next($request);
                }
            }
        }

        // If we reach here, we didn't find a valid custom domain or subdomain.
        // Abort the request so Laravel continues to the next route.
        Log::debug('SetTenantMiddlewareAbort');
        abort(503, 'No tenant found.');
    }

I also tried it without the middleware to see if I could just get the "hello from..." but that does not work either

#

everything is on vapor so it takes a bit to get this testing properly with the deployments

urban sandal
#

So... testing on production.. Why not make it work locally first, and then worry about deploying? That'd be much easier to debug and help you

crude osprey
#

caddy can't proxy to localhost, I was hoping i could trigger the route on team.mydomain.com initially which is why i moved testing to production

urban sandal
#

caddy can't proxy to localhost
Since when?

crude osprey
#

@urban sandal i'll give it a shot, this would speed the testing up for sure I figured it had to be live since I was hardcoding the domain in but ill just use hosts to bypass this

crude osprey
#

@urban sandal ok got it setup locally which will save loads of time debugging this. Not to sure where to start though to get the catch all route to check if its an authorized custom domain or not though

#
Route::get('/debug-route', function() {
    dd(request(), session(), request()->getHost());
});
//
Route::domain('blog.custom.test')->group(function () {
    Route::get('/', function ($domain) {
        return 'Hello from custom domain: ' . $domain;
    });
});

@urban sandal the first route will trigger if I goto debug-route but without it goes to a 404

#

getHost shows blog.custom.test

urban sandal
#

So what does your route:list look like?

crude osprey
#
  GET|HEAD  blog.custom.test/ ...............................................................
urban sandal
#

So there's only one route..?

crude osprey
urban sandal
#

Also, you don't have to tag me 20 times each time you send a message, it's kind of annoying to get multiple pings from one person wanting a bit of attention 🥲

crude osprey
#

sorry about that

#

ahhh wait a second, FF is not using the hosts file chrome seems to be,
ok so chrome will allow this

Route::domain('blog.custom.test')->group(function () {
    Route::get('/', function () {
        return 'Hello from custom domain: ';
    });
});

That works but

Route::domain('{domain}')->group(function () {
    Route::get('/', function ($domain) {
        return 'Hello from custom domain: '.$domain;
    });
});

as a wildcard does not

#

but if I do

Route::domain('{domain}.custom.test')->group(function () {
    Route::get('/', function ($domain) {
        return 'Hello from custom domain: '.$domain;
    });
});

that will fire as well, Hello from custom domain: blog

urban sandal
#

I think that wildcard doesn't work, because Laravel by default doesn't allow dots in a variable, so you'd want something like

Route::domain('{domain}')
  ->where('domain', '.*')
  ->group(function () {
    //
  });

(adjust accordingly ofc)

crude osprey
#

that seemed to do the trick perfectly, had to move the where after the group otherwise it wants an array

Route::domain('{domain}')
    ->group(function () {
    Route::get('/', function ($domain) {
        return 'Hello from custom domain: '.$domain;
    })->where('domain', '.*');
});

Thank you sir, been stumped for a couple days now. Deploying to vapor to test as well was painful as well so 2 birds on this one.

#

hmm the domain where actually does not make sense on the get itself i'll have to play around with it on pre-group

urban sandal
crude osprey
#
Middleware and where conditions are merged while names and prefixes are appended. Namespace delimiters and slashes in URI prefixes are automatically added where appropriate.

figured you could then do

Route::domain('{domain}')
    ->where(['domain' =>  '.*'])
    ->group(function () {
    Route::get('/', function ($domain) {
        return 'Hello from custom domain: '.$domain;
    });
});
#

which works, it seems this firefox issue is weird. Chrome is fine

urban sandal
#

No clue what that has to do with a browser tbh

crude osprey
#

dev in chrome for this part

#

I am guessing its some sort of privacy dns caching of some sort

#

thanks again for the help robert

crude osprey
#

@urban sandal one last thing and I think I got this handled

Route::domain('{domain}')
    ->middleware('setTenant')
    ->where(['domain' =>  '.*'])
    ->group(function () {
        Route::name('blog.')->group(function () {
        Route::get('/', [\App\Http\Controllers\CategoryController::class, 'index'])->name('home');

        Route::resource('category', \App\Http\Controllers\CategoryController::class)->parameters(['post' => 'post']);
        Route::resource('category.post', \App\Http\Controllers\PostController::class)->parameters(['post' => 'post']);
        Route::resource('tag', \App\Http\Controllers\TagController::class)->parameters(['tag' => 'slug']);
        Route::get('/t/{post}', function ($id) {
            $post = Post::findOrFail($id);
            return redirect()->to(route('category.post.show', [$post->category->slug, $post->slug]));
        })->name('post.short_link');
    });
});

is working perfectly setting the tenant etc however when the middleware fails I want it to keep looking for routes for example this one here:

Route::get('/', function () {
    return Inertia::render('Welcome', [
        'canLogin' => Route::has('login'),
        'canRegister' => Route::has('register'),
        'laravelVersion' => Application::VERSION,
        'phpVersion' => PHP_VERSION,
    ]);
});

the middleware is currently returning abort(404) when the custom domain is not found, is that not the proper way to exit out of a middleware and continue on if that group is not authorized and move onto the next group ?

#
<?php

namespace App\Http\Middleware;

use App\Models\CustomDomain;
use App\Models\Team;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;

class SetTenantMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        $customDomain = $request->host();

        if ($customDomain) {
            Log::debug('SetTenantMiddlewareDomain', [$customDomain]);
            $customDomainRecord = CustomDomain::query()
                ->where('domain', $customDomain)
                ->first();

            if ($customDomainRecord) {
                Log::debug('SetTenantMiddlewareRecord', [$customDomainRecord->team->id]);
                request()->session()->put('tenant', $customDomainRecord->team->id);
                return $next($request);
            }
        }

        $domainParts = explode('.', $request->getHost());

        if (count($domainParts) === 3) {
            Log::debug('SetTenantMiddlewareParts', [$domainParts]);
            $subdomain = $domainParts[0];
            $mainDomain = $domainParts[1] . '.' . $domainParts[2];

            if ($mainDomain === parse_url(config('app.url'), PHP_URL_HOST)) {
                $team = Team::find($subdomain);
                if ($team) {
                    Log::debug('SetTenantMiddlewareTeam', [$team->id]);
                    request()->session()->put('tenant', $team->id);
                    return $next($request);
                }
            }
        }

        // If we reach here, we didn't find a valid custom domain or subdomain.
        // Abort the request so Laravel continues to the next route.
        Log::debug('SetTenantMiddlewareAbort');
        abort(404, 'No tenant found.');
    }
}

urban sandal
crude osprey
#

if the user has a customdomain of say blog.theirdomain.com ideally I want to show their blog,
if the user does teamid.mydomain.com then i want to show their blog as well
This is handled by the middleware

However if the host is mydomain.com then I want to show our homepage and allow the dashboard/admin interface etc.

Talking this out I guess i could do where domain === ourdomain on the routes file and put that above the domain wildcard.