tenancy / multi-tenant

Run multiple websites using the same Laravel installation while keeping tenant specific data separated for fully independent multi-domain setups, previously github.com/hyn/multi-tenant
https://tenancy.dev
MIT License
2.56k stars 394 forks source link

Helper function for creating routes for Hostname #523

Closed RushabhJoshi closed 5 years ago

RushabhJoshi commented 6 years ago

I need a correct way to create routes for hostname

e.g. I need to create a route for the password reset for hostname XYZ

hostname_route($hostname, 'password.reset', $token);

which will generate appropriate route for password reset for that particular hostname

http(s)://xyz.localhost/password/reset/<token>

A solution for above requirement is also helpful, Please share if you have any

Note: tried changing the environment for that hostname but it won't work

mintunitish commented 6 years ago

If you are using Laravel's default Authentication, all you need to put a middleware that will tell the Auth to use the tenant database. A brief explanation is given here : Make Laravel out-of-the-box authentication scaffolding work with tenancy

fletch3555 commented 6 years ago

So... something like this? Where $hostname is the hostname you want to use.

Route::domain($hostname)->group(function () {
    Auth::routes(); // if you want all routes from Laravel's Auth scaffolding
    // -- OR --
    Route::get('password/reset/{token}', 'AuthController@resetPassword')->name('password.reset');
});

If you want it to be tenant-specific, then don't bother adding the route group at all, and it'll be available to your tenant. If that isn't adequate for your needs, then you'll need to provide more specific requirements.

RushabhJoshi commented 6 years ago

Maybe I am not clear in my previous question description. But what I need is to create the route for one tenant from another tenant

like I have one tenant which only have super admin authority admin.localhost

here, I can create a new user for another tenant which send email for password reset but I am facing one problem here the reset link has domain name admin

http://admin.localhost/password/reset/<token>

but I need a tenant-specific link

http://xyz.localhost/password/reset/<token>
luceos commented 6 years ago

What you could do is update app.url whenever a tenant was identified.

In addition make sure to add a connection key in the auth file: https://github.com/laravel/laravel/blob/d53539b47b6d2a20d0c3d3257ad801bc72547c9d/config/auth.php#L98

See https://github.com/hyn/multi-tenant/issues/443

If this doesn't solve it, please re-open.

luceos commented 6 years ago

Seems like I've already added updating the app url automatically:

tenancy.hostname.update-app-url configuration key 👍

RushabhJoshi commented 6 years ago

Let me describe to you my problem again I am in admin.localhost website Just created a new admin user for xyz.localhost website here I am sending a mail for password reset now here password reset show the reset link for admin.localhost not for xyz.localhost

any hack for this?

FYI I have update package to 5.2

luceos commented 6 years ago

@RushabhJoshi send the password reset inside a TenantAwareJob and use the tenant setter to force it to the new tenant; see https://laravel-tenancy.com/docs/hyn/5.2/queues

RushabhJoshi commented 6 years ago

Everything worked as expected but still password reset link has a domain name admin.localhost

PS I am testing this thing in QUEUE_DRIVER=sync

luceos commented 6 years ago

Did you set tenancy.hostname.update-app-url to true?

RushabhJoshi commented 6 years ago

Yes, it still shows http://admin.localhost maybe it does not work for sync drive of the queue

RushabhJoshi commented 6 years ago

@luceos any update on this issue?

luceos commented 6 years ago

Have you tried another queue driver?

RushabhJoshi commented 6 years ago

Yes, I tried with database driver after you asked

and the result changed but not expected. now the link address is localhost instead of admin.localhost

maybe I am missing something in a config

tenancy.php

<?php

/*
 * This file is part of the hyn/multi-tenant package.
 *
 * (c) Daniël Klabbers <daniel@klabbers.email>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @see https://laravel-tenancy.com
 * @see https://github.com/hyn/multi-tenant
 */

use Hyn\Tenancy\Database\Connection;

return [
    'models' => [
        /**
         * Specify different models to be used for the global, system database
         * connection. These are also used in their relationships. Models
         * used have to implement their respective contracts and
         * either extend the SystemModel or use the trait
         * UsesSystemConnection.
         */

        // Must implement \Hyn\Tenancy\Contracts\Hostname
        'hostname' => \Hyn\Tenancy\Models\Hostname::class,

        // Must implement \Hyn\Tenancy\Contracts\Website
        'website' => \Hyn\Tenancy\Models\Website::class
    ],
    'website' => [
        /**
         * Each website has a short random hash that identifies this entity
         * to the application. By default this id is randomized and fully
         * auto-generated. In case you want to force your own logic for
         * when you need to have a better overview of the complete
         * tenant folder structure, disable this and implement
         * your own id generation logic.
         */
        'disable-random-id' => false,

        /**
         * The random Id generator is responsible for creating the hash as mentioned
         * above. You can override what generator to use by modifying this value
         * in the configuration.
         *
         * @warn This won't work if disable-random-id is true.
         */
        'random-id-generator' => Hyn\Tenancy\Generators\Uuid\ShaGenerator::class,

        /**
         * Enable this flag in case you're using a driver that does not support
         * database username or database name with a length of more than 32 characters.
         *
         * This should be enabled for MySQL, but not for MariaDB and PostgreSQL.
         */
        'uuid-limit-length-to-32' => env('LIMIT_UUID_LENGTH_32', false),

        /**
         * Specify the disk you configured in the filesystems.php file where to store
         * the tenant specific files, including media, packages, routes and other
         * files for this particular website.
         *
         * @info If not set, will revert to the default filesystem.
         * @info If set to false will disable all tenant specific filesystem auto magic
         *       like the config, vendor overrides.
         */
        'disk' => null,

        /**
         * Automatically generate a tenant directory based on the random id of the
         * website. Uses the above disk to store files to override system-wide
         * files.
         *
         * @info set to false to disable.
         */
        'auto-create-tenant-directory' => true,

        /**
         * Automatically rename the tenant directory when the random id of the
         * website changes. This should not be too common, but in case it happens
         * we automatically want to move files accordingly.
         *
         * @info set to false to disable.
         */
        'auto-rename-tenant-directory' => true,

        /**
         * Automatically deletes the tenant specific directory and all files
         * contained within.
         *
         * @see
         * @info set to true to enable.
         */
        'auto-delete-tenant-directory' => false,

        /**
         * Time to cache websites in minutes. Set to false to disable.
         */
        'cache' => 10,
    ],
    'hostname' => [
        /**
         * If you want the multi tenant application to fall back to a default
         * hostname/website in case the requested hostname was not found
         * in the database, complete in detail the default hostname.
         *
         * @warn this must be a FQDN, these have no protocol or path!
         */
        'default' => env('TENANCY_DEFAULT_HOSTNAME'),
        /**
         * The package is able to identify the requested hostname by itself,
         * disable to get full control (and responsibility) over hostname
         * identification. The hostname identification is needed to
         * set a specific website as currently active.
         *
         * @see src/Jobs/HostnameIdentification.php
         */
        'auto-identification' => env('TENANCY_AUTO_HOSTNAME_IDENTIFICATION', true),

        /**
         * In case you want to have the tenancy environment set up early,
         * enable this flag. This will run the tenant identification
         * inside a middleware. This will eager load tenancy.
         *
         * A good use case is when you have set "tenant" as the default
         * database connection.
         */
        'early-identification' => env('TENANCY_EARLY_IDENTIFICATION', true),

        /**
         * Abort application execution in case no hostname was identified. This will throw a
         * 404 not found in case the tenant hostname was not resolved.
         */
        'abort-without-identified-hostname' => true,

        /**
         * Time to cache hostnames in minutes. Set to false to disable.
         */
        'cache' => 10,

        /**
         * Automatically update the app.url configured inside Laravel to match
         * the tenant FQDN whenever a hostname/tenant was identified.
         *
         * This will resolve issues with password reset mails etc using the
         * correct domain.
         */
        'update-app-url' => true,
    ],
    'db' => [
        /**
         * The default connection to use; this overrules the Laravel database.default
         * configuration setting. In Laravel this is normally configured to 'mysql'.
         * You can set a environment variable to override the default database
         * connection to - for instance - the tenant connection 'tenant'.
         */
        'default' => env('TENANCY_DEFAULT_CONNECTION'),
        /**
         * Used to give names to the system and tenant database connections. By
         * default we configure 'system' and 'tenant'. The tenant connection
         * is set up automatically by this package.
         *
         * @see src/Database/Connection.php
         * @var system-connection-name The database connection name to use for the global/system database.
         * @var tenant-connection-name The database connection name to use for the tenant database.
         */
        'system-connection-name' => env('TENANCY_SYSTEM_CONNECTION_NAME', Connection::DEFAULT_SYSTEM_NAME),
        'tenant-connection-name' => env('TENANCY_TENANT_CONNECTION_NAME', Connection::DEFAULT_TENANT_NAME),

        /**
         * The tenant division mode specifies to what database websites will be
         * connecting. The default setup is to use a new database per tenant.
         * In case you prefer to use the same database with a table prefix,
         * set the mode to 'prefix'.
         *
         * @see src/Database/Connection.php
         */
        'tenant-division-mode' => env('TENANCY_DATABASE_DIVISION_MODE', 'database'),

        /**
         * The database password generator takes care of creating a valid hashed
         * string used for tenants to connect to the specific database. Do
         * note that this will only work in 'division modes' that set up
         * a connection to a separate database.
         */
        'password-generator' => Hyn\Tenancy\Generators\Database\DefaultPasswordGenerator::class,

        /**
         * The tenant migrations to be run during creation of a tenant. Specify a directory
         * to run the migrations from. If specified these migrations will be executed
         * whenever a new tenant is created.
         *
         * @info set to false to disable auto migrating.
         *
         * @warn this has to be an absolute path, feel free to use helper methods like
         * base_path() or database_path() to set this up.
         */
        'tenant-migrations-path' => database_path('migrations/tenant'),

        /**
         * The default Seeder class used on newly created databases and while
         * running artisan commands that fire seeding.
         *
         * @info requires tenant-migrations-path in order to seed newly created websites.
         *
         * @warn specify a valid fully qualified class name.
         */
        'tenant-seed-class' => App\Seeders\TenantSeeder::class,
//      eg an admin seeder under `app/Seeders/AdminSeeder.php`:
//        'tenant-seed-class' => App\Seeders\AdminSeeder::class,

        /**
         * Automatically generate a tenant database based on the random id of the
         * website.
         *
         * @info set to false to disable.
         */
        'auto-create-tenant-database' => true,

        /**
         * Automatically generate the user needed to access the database.
         *
         * @info Useful in case you use root or another predefined user to access the
         *       tenant database.
         * @info Only creates in case tenant databases are set to be created.
         *
         * @info set to false to disable.
         */
        'auto-create-tenant-database-user' => true,

        /**
         * Automatically rename the tenant database when the random id of the
         * website changes. This should not be too common, but in case it happens
         * we automatically want to move databases accordingly.
         *
         * @info set to false to disable.
         */
        'auto-rename-tenant-database' => true,

        /**
         * Automatically deletes the tenant specific database and all data
         * contained within.
         *
         * @info set to true to enable.
         */
        'auto-delete-tenant-database' => env('TENANCY_DATABASE_AUTO_DELETE', false),

        /**
         * Automatically delete the user needed to access the tenant database.
         *
         * @info Set to false to disable.
         * @info Only deletes in case tenant database is set to be deleted.
         */
        'auto-delete-tenant-database-user' => env('TENANCY_DATABASE_AUTO_DELETE_USER', false),

        /**
         * Define a list of classes that you wish to force onto the tenant or system connection.
         * The connection will be forced when the Model has booted.
         *
         * @info Useful for overriding the connection of third party packages.
         */
        'force-tenant-connection-of-models' => [
//            App\User::class
        ],
        'force-system-connection-of-models' => [
//            App\User::class
        ],
    ],

    /**
     * Global tenant specific routes.
     * Making it easier to distinguish between landing and tenant routing.
     *
     * @info only works with `tenancy.hostname.auto-identification` or identification happening
     *       before the application is booted (eg inside middleware or the register method of
     *       service providers).
     */
    'routes' => [
        /**
         * Routes file to load whenever a tenant was identified.
         *
         * @info Set to false or null to disable.
         */
        'path' => base_path('routes/tenants.php'),

        /**
         * Set to true to flush all global routes before setting the routes from the
         * tenants.php routes file.
         */
        'replace-global' => false,
    ],

    /**
     * Folders configuration specific per tenant.
     * The following section relates to configuration to files inside the tenancy/<uuid>
     * tenant directory.
     */
    'folders' => [
        'config' => [
            /**
             * Merge configuration files from the config directory
             * inside the tenant directory with the global configuration files.
             */
            'enabled' => true,

            /**
             * List of configuration files to ignore, preventing override of crucial
             * application configurations.
             */
            'blacklist' => ['database', 'tenancy', 'webserver'],
        ],
        'routes' => [
            /**
             * Allows adding and overriding URL routes inside the tenant directory.
             */
            'enabled' => true,

            /**
             * Prefix all tenant routes.
             */
            'prefix' => null,
        ],
        'trans' => [
            /**
             * Allows reading translation files from a trans directory inside
             * the tenant directory.
             */
            'enabled' => true,

            /**
             * Will override the global translations with the tenant translations.
             * This is done by overriding the laravel default translator with the new path.
             */
            'override-global' => true,

            /**
             * In case you disabled global override, specify a namespace here to load the
             * tenant translation files with.
             */
            'namespace' => 'tenant',
        ],
        'vendor' => [
            /**
             * Allows using a custom vendor (composer driven) folder inside
             * the tenant directory.
             */
            'enabled' => true,
        ],
        'media' => [
            /**
             * Mounts the assets directory with (static) files for public use.
             */
            'enabled' => true,
        ],
        'views' => [
            /**
             * Adds the vendor directory of the tenant inside the application.
             */
            'enabled' => true,

            /**
             * Specify a namespace to use with which to load the views.
             *
             * @eg setting `tenant` will allow you to use `tenant::some.blade.php`
             * @info set to null to add to the global namespace.
             */
            'namespace' => null,

            /**
             * If `namespace` is set to null (thus using the global namespace)
             * make it override the global views. Disable to
             */
            'override-global' => true,
        ]
    ]
];

webserver.php

<?php

/*
 * This file is part of the hyn/multi-tenant package.
 *
 * (c) Daniël Klabbers <daniel@klabbers.email>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @see https://laravel-tenancy.com
 * @see https://github.com/hyn/multi-tenant
 */

return [

    /**
     * Apache2 is one of the most widely adopted webserver packages available.
     *
     * @see http://httpd.apache.org/docs/
     * @see https://www.digitalocean.com/community/tutorials/how-to-install-linux-apache-mysql-php-lamp-stack-on-ubuntu
     */
    'apache2' => [
        /**
         * Whether the integration with Apache2 is currently active.
         */
        'enabled' => false,

        /**
         * Define the ports of your Apache service.
         */
        'ports' => [
            /**
             * HTTP, non-SSL port.
             *
             * @default 80
             */
            'http' => 80,
            /**
             * HTTPS, SSL port.
             *
             * @default 443
             */
            'https' => 443
        ],

        /**
         * The generator taking care of hooking into the Apache services and files.
         */
        'generator' => \Hyn\Tenancy\Generators\Webserver\Vhost\ApacheGenerator::class,

        /**
         * The view that holds the vhost configuration template.
         */
        'view' => 'tenancy.generators::webserver.apache.vhost',

        /**
         * Specify the disk you configured in the filesystems.php file where to store
         * the tenant vhost configuration files.
         *
         * @info If not set, will revert to the default filesystem.
         */
        'disk' => null,

        'paths' => [

            /**
             * Location where vhost configuration files can be found.
             */
            'vhost-files' => [
                '/etc/apache2/sites-enabled/'
            ],

            /**
             * Actions to run to work with the Apache2 service.
             */
            'actions' => [
                /**
                 * Action that asserts Apache2 is installed.
                 */
                'exists' => '/etc/init.d/apache2',
                /**
                 * Action to run to test the apache configuration.
                 *
                 * @set to a boolean to force the response of the test command.
                 * @info true succeeds, false fails
                 */
                'test-config' => 'apache2ctl -t',
                /**
                 * Action to run to reload the apache service.
                 *
                 * @info set to null to disable reloading.
                 */
                'reload' => 'apache2ctl graceful'
            ]
        ]
    ],

    /**
     * Nginx webserver support.
     *
     * @see http://nginx.org
     */
    'nginx' => [
        /**
         * Whether the integration with nginx is currently active.
         */
        'enabled' => false,

        /**
         * The php sock to be used.
         */
        'php-sock' => 'unix:/var/run/php/php7.1-fpm.sock',

        /**
         * Define the ports of your nginx service.
         */
        'ports' => [
            /**
             * HTTP, non-SSL port.
             *
             * @default 80
             */
            'http' => 80,
            /**
             * HTTPS, SSL port.
             *
             * @default 443
             */
            'https' => 443
        ],

        /**
         * The generator taking care of hooking into the nginx services and files.
         */
        'generator' => \Hyn\Tenancy\Generators\Webserver\Vhost\NginxGenerator::class,

        /**
         * The view that holds the vhost configuration template.
         */
        'view' => 'tenancy.generators::webserver.nginx.vhost',

        /**
         * Specify the disk you configured in the filesystems.php file where to store
         * the tenant vhost configuration files.
         *
         * @info If not set, will revert to the default filesystem.
         */
        'disk' => null,

        'paths' => [

            /**
             * Location where vhost configuration files can be found.
             */
            'vhost-files' => [
                '/etc/nginx/sites-enabled/'
            ],

            /**
             * Actions to run to work with the Nginx service.
             */
            'actions' => [
                /**
                 * Action that asserts nginx is installed.
                 */
                'exists' => '/etc/init.d/nginx',
                /**
                 * Action to run to test the nginx configuration.
                 *
                 * @info set to a boolean to force the response of the test command.
                 *  true succeeds, false fails
                 */
                'test-config' => '/etc/init.d/nginx configtest',
                /**
                 * Action to run to reload the nginx service.
                 *
                 * @info set to null to disable reloading.
                 */
                'reload' => '/etc/init.d/nginx reload'
            ]
        ]
    ]
];

queue.php

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Default Queue Driver
    |--------------------------------------------------------------------------
    |
    | Laravel's queue API supports an assortment of back-ends via a single
    | API, giving you convenient access to each back-end using the same
    | syntax for each one. Here you may set the default queue driver.
    |
    | Supported: "sync", "database", "beanstalkd", "sqs", "redis", "null"
    |
    */

    'default' => env('QUEUE_DRIVER', 'sync'),

    /*
    |--------------------------------------------------------------------------
    | Queue Connections
    |--------------------------------------------------------------------------
    |
    | Here you may configure the connection information for each server that
    | is used by your application. A default configuration has been added
    | for each back-end shipped with Laravel. You are free to add more.
    |
    */

    'connections' => [

        'sync' => [
            'driver' => 'sync',
        ],

        'database' => [
            'driver' => 'database',
            'table' => 'jobs',
            'queue' => 'default',
            'retry_after' => 90,
            'connection' => 'system',
        ],

        'beanstalkd' => [
            'driver' => 'beanstalkd',
            'host' => 'localhost',
            'queue' => 'default',
            'retry_after' => 90,
        ],

        'sqs' => [
            'driver' => 'sqs',
            'key' => 'your-public-key',
            'secret' => 'your-secret-key',
            'prefix' => 'https://sqs.us-east-1.amazonaws.com/your-account-id',
            'queue' => 'your-queue-name',
            'region' => 'us-east-1',
        ],

        'redis' => [
            'driver' => 'redis',
            'connection' => 'default',
            'queue' => 'default',
            'retry_after' => 90,
        ],

    ],

    /*
    |--------------------------------------------------------------------------
    | Failed Queue Jobs
    |--------------------------------------------------------------------------
    |
    | These options configure the behavior of failed queue job logging so you
    | can control which database and table are used to store the jobs that
    | have failed. You may change them to any database / table you wish.
    |
    */

    'failed' => [
        'database' => env('DB_CONNECTION', 'mysql'),
        'table' => 'failed_jobs',
    ],

];
luceos commented 6 years ago

Can you dd config('app.url') inside your job when fired not as sync?

RushabhJoshi commented 6 years ago

yes, I did and its http://admin.localhost

luceos commented 6 years ago

My apologies for the delay.

Without more code it's hard to know what is happening here. Feel free to share the repo with me privately or add snippets of how you send the reset password mail.

RushabhJoshi commented 6 years ago

I am sharing you the tenant aware job which called after creating tenant

in command, I am calling job like this

            $website = new Website;
            app(WebsiteRepository::class)->create($website);

            //Create and connect hostname
            $hostname = new Hostname;
            $hostname->fqdn = $this->argument('fqdn');
            app(HostnameRepository::class)->attach($hostname, $website);

                $tenantData = [
                    'admin' => [
                        'name' => 'John Doe',
                        'email' => 'john@example.com',
                        'password' => 'password',
                    ],
                ];

                $initJob = new InitTenant($tenantData, true);
                dispatch($initJob->onTenant($website));

Tenant aware job

<?php

namespace App\Jobs;

use App\AdditionalService;
use App\Notifications\NewUserResetPassword;
use App\Permission;
use App\Role;
use App\User;
use Hyn\Tenancy\Environment;
use Hyn\Tenancy\Queue\TenantAwareJob;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Password;

class InitTenant implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, TenantAwareJob;

    public $data;

    public $adminTenant;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct($data, $adminTenant = false)
    {
        $this->data = $data;
        $this->adminTenant = $adminTenant;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        $this->seed();

        // Create Tenant admin
        $password = str_random(20);
        $adminUser = User::create([
            'name' => $this->data['admin']['name'],
            'email' => $this->data['admin']['email'],
            'password' => bcrypt($password),
        ]);

        $passwordBroker = Password::broker();
        $token = $passwordBroker->createToken($adminUser);
        $adminUser->notify(new NewUserResetPassword($token));

        // Assing tenant-admin role to user
        if ($this->adminTenant) {
            $adminUser->assignRole('superuser');
        }
        $adminUser->assignRole('admin');
    }

    private function seed()
    {
        $this->seedRoles();
        $this->seedPermissions();

        /////////////////////
        // Set permissions //
        /////////////////////
        $this->setSuperUserPermissions();
        $this->setTenantAdminPermissions();
    }

    private function seedRoles()
    {
        $roles = Role::DEFAULT_ROLES;

        foreach ($roles as $role) {
            Role::create([
                'name' => $role,
            ]);
        }
    }

    private function seedPermissions()
    {
        $permissions = Permission::DEFAULT_PERMISSIONS;

        foreach ($permissions as $permission) {
            Permission::create([
                'name' => $permission,
            ]);
        }
    }

    private function setSuperUserPermissions()
    {
        $role = Role::findByName('superuser');
        // Add all permissions to tenant-admin
        foreach (Permission::DEFAULT_PERMISSIONS as $permission) {
            $role->givePermissionTo($permission);
        }
    }

    private function setTenantAdminPermissions()
    {
        $role = Role::findByName('admin');
        $except = ['manage_tenants'];
        $permissions = array_diff(Permission::DEFAULT_PERMISSIONS, $except);
        // Add all permissions to tenant-admin
        foreach ($permissions as $permission) {
            $role->givePermissionTo($permission);
        }
    }
}

Tell me if you need anything more. I will consult with an authorized person to share repo till than I have shared a code snippet

luceos commented 6 years ago

Please share the code for NewUserResetPassword

RushabhJoshi commented 6 years ago
<?php

namespace App\Notifications;

use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class NewUserResetPassword extends Notification
{
    public $token;

    /**
     * Create a new notification instance.
     *
     * @return void
     */
    public function __construct($token)
    {
        $this->token = $token;
    }

    /**
     * Get the notification's delivery channels.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function via($notifiable)
    {
        return ['mail'];
    }

    /**
     * Get the mail representation of the notification.
     *
     * @param  mixed  $notifiable
     * @return \Illuminate\Notifications\Messages\MailMessage
     */
    public function toMail($notifiable)
    {
        $url = route('password.set', $this->token);

        return (new MailMessage)
                    ->line('You are just got registered')
                    ->action('Click here to set password', $url)
                    ->line('Thank you');
    }
}
luceos commented 6 years ago

Apply TenantAwareJob to the NewUserResetPassword class too.

RushabhJoshi commented 6 years ago

I did and still the same

I have created foo.localhost so password set link should be something like http://foo.localhost/password/set/<token> but it is still wrong http://admin.localhost/password/set/0b3bc4c1f87e1a602148ea626d45831d1fbb380090ded1133113342b88fc424f

luceos commented 6 years ago

I'm not sure. I see three stages:

  1. You create the tenant and dispatch the InitTenant job
  2. The InitTenant job that is tenant aware
  3. The NewUserResetPassword job (Notifications are also queueables) which now is tenant aware.

Please dump the tenant in each step to see which tenant is identified, by adding the following to the __construct or handle of each of the above steps:

logger(__CLASS__, app(\Hyn\Tenancy\Environment::class)->tenant());
RushabhJoshi commented 6 years ago

This time I have updated the Laravel version 5.7 and package version to 5.3

Note: I have little change in your code snippet which is

logger(__CLASS__, ['tenant' => app(\Hyn\Tenancy\Environment::class)->tenant()]);

the same story happened

[2018-10-09 10:58:31] local.DEBUG: App\Http\Controllers\Admin\TenantController {"tenant":"[object] (Hyn\\Tenancy\\Models\\Website: {\"id\":1,\"uuid\":\"7cc0e34739874ddd860e470a02132d5a\",\"created_at\":\"2018-10-09 10:44:45\",\"updated_at\":\"2018-10-09 10:44:45\",\"deleted_at\":null,\"managed_by_database_connection\":null})"} 
[2018-10-09 10:58:58] local.DEBUG: App\Jobs\InitTenant {"tenant":"[object] (Hyn\\Tenancy\\Models\\Website: {\"id\":2,\"uuid\":\"37141bbfe3aa41d18d17ea13b57377a6\",\"created_at\":\"2018-10-09 10:58:31\",\"updated_at\":\"2018-10-09 10:58:31\",\"deleted_at\":null,\"managed_by_database_connection\":null})"} 
[2018-10-09 10:59:02] local.DEBUG: App\Notifications\NewUserResetPassword {"tenant":"[object] (Hyn\\Tenancy\\Models\\Website: {\"id\":2,\"uuid\":\"37141bbfe3aa41d18d17ea13b57377a6\",\"created_at\":\"2018-10-09 10:58:31\",\"updated_at\":\"2018-10-09 10:58:31\",\"deleted_at\":null,\"managed_by_database_connection\":null})"} 

here first log is in controller and other as you said 7cc0e34739874ddd860e470a02132d5a is for admin.localhost 37141bbfe3aa41d18d17ea13b57377a6 is for foo.localhost

yes this is what expected but the route is still false http://admin.localhost/password/set/fe9959db0e4f92fdd75a442887556e02e0a28b3d904677a399c6ffaaace1c14a

luceos commented 6 years ago

Possibly what happens is that updating the app url is misbehaving. We need to dive into this. Thanks for your patience.

luceos commented 5 years ago

Have you tried this?

URL::forceRootUrl('<system url>');

before creating the route in the mail?

nevertry commented 5 years ago

Is admin.localhost is a system or tenant?

If it's a system, you may have identical route alias called password.set but Laravel only read admin.localhost's.

If it's an another tenant in a class which use TenantAwareJob, make sure current config set for the right website. As documented in https://laravel-tenancy.com/docs/hyn/5.3/identification#manual-identification you can set tenant connection (also see warning):

$website = app('Hyn\Tenancy\Environment')->tenant();
// dd($website); // otherwise set to $targetWebsite:
$website = app('Hyn\Tenancy\Environment')->tenant($targetWebsite);

Then call Laravel route:

return route('password.reset', [
    'token' => 'a-reset-token'
]);

or with subdomain routing described here: https://laravel-tenancy.com/docs/hyn/5.3/fallback#routing-with-domain

return route('password.reset', [
    'token' => 'a-reset-token',
    'customerDomain' => 'xyz', // 'xyz' is subdomain
]);
RushabhJoshi commented 5 years ago

URL::forceRootUrl('');

What could be the system url? admin.localhost or newly created tenant url?

RushabhJoshi commented 5 years ago

Is admin.localhost is a system or tenant?

If it's a system, you may have identical route alias called password.set but Laravel only read admin.localhost's.

If it's an another tenant in a class which use TenantAwareJob, make sure current config set for the right website. As documented in https://laravel-tenancy.com/docs/hyn/5.3/identification#manual-identification you can set tenant connection (also see warning):

$website = app('Hyn\Tenancy\Environment')->tenant();
// dd($website); // otherwise set to $targetWebsite:
$website = app('Hyn\Tenancy\Environment')->tenant($targetWebsite);

Then call Laravel route:

return route('password.reset', [
    'token' => 'a-reset-token'
]);

or with subdomain routing described here: https://laravel-tenancy.com/docs/hyn/5.3/fallback#routing-with-domain

return route('password.reset', [
    'token' => 'a-reset-token',
    'customerDomain' => 'xyz', // 'xyz' is subdomain
]);

Yes @nevertry admin.localhost is tenant, only this tenant can create other tenants. when I create new tenant I call tenent aware job InitTenant. Where first

Everything works fine, seeding, creating user and mail as well

one little problem is in mail password set link is http://admin.localhost/password/set/5f6f02caefc3cf192674af2ba088acbeb03ec9349331b0c32b5667eed8377e68

instead it should be tenant password set link (in my case tenant is xyz) http://xyz.localhost/password/set/5f6f02caefc3cf192674af2ba088acbeb03ec9349331b0c32b5667eed8377e68

PS: I don't know why you need me to set environment When I am using tenant aware job

ametad commented 5 years ago

I have hacked this problem with a Listener in my application that set the app.url upon website identification and -switch.

Register listener

<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        // ...
        'Hyn\Tenancy\Events\Websites\Identified' => [
            'App\Listeners\SetAppName',
            'App\Listeners\SetAppUrl',
        ],
        'Hyn\Tenancy\Events\Websites\Switched' => [
            'App\Listeners\SetAppName',
            'App\Listeners\SetAppUrl',
        ],
    ];

    /**
     * Register any events for your application.
     *
     * @return void
     */
    public function boot()
    {
        parent::boot();

        //
    }
}

Listener

<?php

namespace App\Listeners;

use Hyn\Tenancy\Contracts\Hostname;
use Illuminate\Support\Facades\URL;

class SetAppUrl
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  object $event
     * @return void
     */
    public function handle($event)
    {
        /** @var Hostname $hostname */
        if ($hostname = $this->findHostname($event)) {

            $protocol = $hostname->force_https ? 'https://' : 'http://';
            $url = "{$protocol}{$hostname->fqdn}";

            config(['app.url' => $url]);
            URL::forceRootUrl($url);
        }
    }

    /**
     * @param $event
     * @return mixed
     */
    protected function findHostname($event)
    {
        // todo make more controllable, not only first...
        return $event->website->hostnames()->first();
    }
}

Now I can generate correct urls in queued jobs (like mails), that are defined in routes/web.php.

Job example

$website = app(\Hyn\Tenancy\Contracts\Repositories\WebsiteRepository::class)->findById(2); // per example.
Mail::to($email)->send((new Payslip($this))->onTenant($website));

Urls in Payslip are generated correctly, like $url = route('login') :

fletch3555 commented 5 years ago

@ametad, unless I'm misunderstanding, that's already a feature we have. Just set set-app-url (I think that's the name at least) to true in the tenancy config file

luceos commented 5 years ago

I'm in the process of pushing a change that ensures the app url is also updated outside of HTTP requests.

ametad commented 5 years ago

@fletch3555 Yes I know the feature, thanks! (config key is 'update-app-url')

But because this is in middleware (\Hyn\Tenancy\Middleware\HostnameActions), this functionality does not work in queued job. Also the cli context does not know of any Hostname, only Website is known. Trough Website the Hostname(s) is/are known of course.

ArlonAntonius commented 5 years ago

This should be fixed by now as the package now changes url whenever a tenant is identified.