Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redirect on Login and Tenant Creation #194

Closed
ServerJunge opened this issue Oct 21, 2019 · 20 comments
Closed

Redirect on Login and Tenant Creation #194

ServerJunge opened this issue Oct 21, 2019 · 20 comments
Assignees
Labels

Comments

@ServerJunge
Copy link

Hi,

first I have to say that your stancl tenancy is awesome!

But I have two question:

Is it the best way to create a tenant register controller and then runs all migration (like user table etc.) after registration? I know that I can activate it in the config file and I can also redirect to their domain after registration. But how can I redirect a user after login to their domain?

Thanks!

@stancl
Copy link
Member

stancl commented Oct 21, 2019

But how can I redirect a user after login to their domain?

So you want the users to be able to login on your central domain (i.e. yourapp.com) and get redirected to their domain (i.e. tenant123.yourapp.com)?

@ServerJunge
Copy link
Author

Exactly.

@drbyte
Copy link
Contributor

drbyte commented Oct 21, 2019

In RegisterController (which uses RegistersUsers trait) you could customize create() to insert the tenant User and have it set the $this->redirectTo property to the full tenant URL, and then the default code of RegistersUsers will redirect them.

@stancl
Copy link
Member

stancl commented Oct 21, 2019

And regarding the redirect on login, I'd have to see how you identify the tenant based on the user account.

If the account exists both in your central app and inside the tenant app as some sort of superadmin, I think you should be able to use Laravel's auth inside the central app to see what tenant that user "is", and then you could probably use $tenant->run() to log the admin in, inside his application.

If the account exists only as part of the tenant table, this would be easier.

Please share more information about your set up and I'll try to come up with some code you could use.

@ServerJunge
Copy link
Author

@drbyte thanks for your answer. I know how to redirect after the registration but maybe I don't understand the package correctly.

I don't have an app yet with this package. I want to create a saas app with this package, but I don't know how to use it the right way.

Let me explain: At the moment I have another small saas app with a single-db multitenancy (I use this package https://github.com/romegadigital/Multitenancy)

But I find the idea of your stancl/tenancy so much better because each tenant has it own db etc.

How would you guys create a basic saas app? I know how to create a new tenant and run the migrations after that (set to true in config). But the registered user is then outside off the tenant or am I wrong?

@ServerJunge
Copy link
Author

And regarding the redirect on login, I'd have to see how you identify the tenant based on the user account.

If the account exists only as part of the tenant table, this would be easier.

Please share more information about your set up and I'll try to come up with some code you could use.

My Idea is to create a landing page where you create the tenant in the first step, than it runs the migration with the user table inside the tenant migration and in the second step the actual user registration happens.

@stancl
Copy link
Member

stancl commented Oct 22, 2019

I see. I've added a feature that lets you do this to a dev branch yesterday, so when 2.2.0 is out, you will be able to do that even if the migration is queued.

@ServerJunge
Copy link
Author

I see. I've added a feature that lets you do this to a dev branch yesterday, so when 2.2.0 is out, you will be able to do that even if the migration is queued.

You are awesome! Thanks so much!

@drbyte
Copy link
Contributor

drbyte commented Oct 22, 2019

Something like this works:

app/Http/Controllers/Auth/RegisterController.php:

    use RegistersUsers;

    protected $redirectTo = '/login'; // default was /home

    public function __construct()
    {
        $this->middleware('guest');
    }

    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255'],
            'password' => ['required', 'string', 'min:7', 'confirmed'],
            'domain' => ['required','unique:domains,domain'],
        ]);
    }

    public function register(Request $request)
    {
        $validated = $this->validator($request->all())->validate();

        $domain = $request->input('domain');
        // TODO: consider appending a tld suffix so the redirect makes sense

        $user = (new \App\Services\TenantSetup)->createAndReturnUser($domain, $validated);

        event(new Registered($user));

        return $this->registered($request, $user)
            ?: redirect('https://' . $domain . $this->redirectTo);
    }

/app/Services/TenantSetup.php:

namespace App\Services;

use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;

class TenantSetup
{
    public static function listTenants()
    {
        return tenancy()->all();
    }

    public function createAndReturnUser($domain, $requestFields)
    {
        config()->set('tenancy.migrate_after_creation', true);
        config()->set('tenancy.seed_after_migration', true);
        config()->set('tenancy.seeder_parameters', ['--class' => 'DatabaseSeeder']);

        $tenant = \Stancl\Tenancy\Tenant::create($domain, ['id' => $requestFields['domain']]);

        return $tenant->run(
            function ($tenant) use ($requestFields) {
                [$first] = explode(' ', $requestFields['name']);
                $last = substr($requestFields['name'], strlen($first));
                $user = \App\User::create([
                    'first' => $first,
                    'last' => $last,
                    'username' => $requestFields['email'],
                    'email' => $requestFields['email'],
                    'password' => Hash::make($requestFields['password']),
                    // OPTIONAL: 'primary_domain' => \strtoupper($requestFields['domain']),
                ]);

                // @TODO: if assigning roles/permissions, do that here too

                return $user;
            }
        );
    }
}

@ServerJunge
Copy link
Author

A saw the new release of 2.2.0.

Is there any documentation or "best practice" how to use the package in a saas style app?

For example:

On the landing site (myapp.com) the user can register and login.

When a new user signs up a new tenant gets created and the tenants migration runs with the user table and also the redirect to tenants dashboard happens.

I know how to do that. But how do you guys manage the user? Dou you have two users tables? Inside the main app and inside the tenant?

How is it possible to have a central login (myapp.com/login) with the correct redirect to the tenant domain when I have different user in the tenants user table? Do I have to loop trough all tenants do find the user email adress and redirect to this tenant domain?

I hope you understand what I mean.

@ServerJunge ServerJunge reopened this Nov 11, 2019
@stancl
Copy link
Member

stancl commented Nov 11, 2019

When I was working on 2.2.0 I implemented this functionality into a test app like this:

Central app's action to register the tenant:

Tenant::new()->withDomains($domain)->withData([
    'admin_fullname' => $request->post('name'),
    'app_name' => $request->post('website_name'),
    'email' => $request->post('email'),
    'password' => Hash::make($request->post('password')),
    'primary_domain' => $domain,
    'post_registration_login_token' => $token,
    'initial_migration_complete' => false,
])->save();

return redirect()->route('_post_tenant_registration', ['token' => $token])->tenant($domain);

Tenant app:

Route::get('_post_tenant_registration/{token}', 'DashboardController@postTenantRegistration')->name('_post_tenant_registration');

// Controller
public function postTenantRegistration(string $token)
{
    if (! tenant('initial_migration_complete')) {
        return view('errors.building'); // we're building your site-type view
    }

    if (tenant('post_registration_login_token') === $token) {
        $user = User::where('email', tenant('email'))->firstOrFail();
        Auth::login($user, true);
        tenant()->deleteKey('post_registration_login_token');
    }

    return redirect()->route('admin.dashboard');
}

AppServiceProvider:

Tenancy::hook('database.creating', function (TenantManager $m, string $db, Tenant $tenant) {
    return [
        new AfterCreatingTenant($tenant->id)
    ];
});

AfterCreatingTenant job:

$tenant = tenancy()->find($this->tenantId);

$tenant->run(function ($tenant) {
    User::create(['name' => $tenant->admin_fullname, 'email' => $tenant->email, 'password' => $tenant->password, 'admin' => true]);
});

$tenant->deleteKey('admin_fullname');
$tenant->set('initial_migration_complete', true);

Does this help? :)

@ServerJunge
Copy link
Author

Thanks for your answer. This helps and thank you for for great package and support!

But I still have one question. How to you handle the problem when a user forgot his subdomain?

For example on Freshdesk (https://freshdesk.com/login) every user can enter his own email adress to get the subdomain.

Thanks!

@stancl
Copy link
Member

stancl commented Nov 11, 2019

You could do something like this:

public function submitLogin(Request $request)
{
    $this->validate($request, [
        'email' => 'required|email',
    ]);

    try {
        $domain = tenancy()->findByEmail($email = $request->post('email'))->primary_domain;
    } catch (TenantCouldNotBeIdentifiedException $e) {
        return redirect()->back()->withInput()->withErrors(["No website is associated with email $email."]);
    }

    return redirect()->route('admin.dashboard')->tenant($domain)->with('email', tenant('email'));
}

@ServerJunge
Copy link
Author

You could do something like this:

public function submitLogin(Request $request)
{
    $this->validate($request, [
        'email' => 'required|email',
    ]);

    try {
        $domain = tenancy()->findByEmail($email = $request->post('email'))->primary_domain;
    } catch (TenantCouldNotBeIdentifiedException $e) {
        return redirect()->back()->withInput()->withErrors(["No website is associated with email $email."]);
    }

    return redirect()->route('admin.dashboard')->tenant($domain)->with('email', tenant('email'));
}

But that would only work with the user how created the tenant, right? Because that email is stored within "withData" or does this search trough the tenants user tables?

@stancl
Copy link
Member

stancl commented Nov 11, 2019

This only searches in the tenant data. Searching in tenant databases comes with more complexity because the same email address can exist in multiple databases.

@dominikgeimer
Copy link

This only searches in the tenant data. Searching in tenant databases comes with more complexity because the same email address can exist in multiple databases.

I created a function wich search inside the tenants databases for the users email. I am just a developer in my spare time, but at least this code works for me.

/**
     * Forgot Domain
     */
    public function sendForgottenDomain(Request $request)
    {
        $request->validate([
            'email' => 'required|email',
        ]);

        $email = $request->input('email');

        $tenants = tenancy()->all();

        foreach($tenants as $tenant) {
            $tenant->run(function ($tenant) use ($email) {
                try {
                    $this->user = User::where('email', $email)->firstOrFail();
                } catch (ModelNotFoundException $e) {}
            });
            if(empty($this->user)) {
                return redirect()->back()->withError("The email does not exists");
            } else
            {
                return $this->user;
            }
        }
    }

@stancl
Copy link
Member

stancl commented Nov 18, 2019

Sure, if it makes sense for your app to lookup users in tenant DBs, this is the how you'd solve it.

@Jorgedev
Copy link

Tenant::new()->withDomains($domain)->withData([
    'admin_fullname' => $request->post('name'),
    'app_name' => $request->post('website_name'),
    'email' => $request->post('email'),
    'password' => Hash::make($request->post('password')),
    'primary_domain' => $domain,
    'post_registration_login_token' => $token,
    'initial_migration_complete' => false,
])->save();

Good night, I'm loving your package.

Have a more detailed example? It is very well explained, but I don't know which instances, builders, I should start at the beginning of each file. Whenever I try to give a dump-autoload composer, the prompt raises an error ....

Could you send an example already with the instances and the builders?

@stancl
Copy link
Member

stancl commented Nov 28, 2019

What do you mean? The full class names?

@khalidmaquilang

This comment was marked as off-topic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants