7 |
8 |
9 |
15 |
16 | Successful
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/Services/CmdWordService.php:
--------------------------------------------------------------------------------
1 | $tenantId,
22 | ]);
23 |
24 | $tenant = Tenant::find($tenantId);
25 | $tenant?->update([
26 | 'name' => $user['name'],
27 | 'email' => $user['email'],
28 | ]);
29 |
30 | $userModel = User::where('name', $user['name'])->firstOrFail();
31 |
32 | $tenant?->run(function () use ($userModel) {
33 | User::create([
34 | 'name' => $userModel->name,
35 | 'password' => $userModel->password,
36 | 'email' => $userModel->email,
37 | ]);
38 | });
39 |
40 | return redirect('http://'.$tenantId.'.translate-merit-based.hecs.iwnweb.com');
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/Console/Commands/stubs/AuthenticateTenantSession.stub:
--------------------------------------------------------------------------------
1 | check()) {
22 | /** @var \App\Models\User */
23 | $user = auth()->user();
24 |
25 | $tenantModel = Tenant::where('data->name', auth()->user()->name)->first();
26 | $domain = $tenantModel?->domains()->value('domain');
27 |
28 | // TODO: 租户信息验证
29 |
30 |
31 | // 租户自动登录
32 | if ($tenantModel && ! tenant()) {
33 | $data = encrypt(['user' => $user->toArray()]);
34 | $tenantUrl = "http://{$domain}/login?encryptData=".$data;
35 |
36 | auth()->logout($user);
37 |
38 | return Inertia::location($tenantUrl);
39 | }
40 | }
41 |
42 | return $next($request);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/routes/web.php:
--------------------------------------------------------------------------------
1 | group(function() {
24 | Route::get('/', [WebController\LaravelSaasController::class, 'index'])->name('laravel-saas.index');
25 | Route::get('setting', [WebController\LaravelSaasController::class, 'showSettingView'])->name('laravel-saas.setting');
26 | Route::post('setting', [WebController\LaravelSaasController::class, 'saveSetting']);
27 | });
28 |
29 | // without VerifyCsrfToken
30 | // Route::prefix('laravel-saas')->withoutMiddleware([
31 | // \App\Http\Middleware\EncryptCookies::class,
32 | // \App\Http\Middleware\VerifyCsrfToken::class,
33 | // ])->group(function() {
34 | // Route::get('/', [WebController\LaravelSaasController::class, 'index']);
35 | // });
36 |
--------------------------------------------------------------------------------
/app/Providers/CmdWordServiceProvider.php:
--------------------------------------------------------------------------------
1 | AWordService::CMD_TEST, 'provider' => [AWordService::class, 'handleTest']],
29 | // ['word' => BWordService::CMD_STATIC_TEST, 'provider' => [BWordService::class, 'handleStaticTest']],
30 | // ['word' => TestModel::CMD_MODEL_TEST, 'provider' => [TestModel::class, 'handleModelTest']],
31 | // ['word' => 'addTenant', 'provider' => [CmdWordService::class, 'addTenant']],
32 | ['word' => 'addTenant', 'provider' => [CmdWordService::class, 'addTenant']],
33 | ];
34 |
35 | /**
36 | * Register the service provider.
37 | */
38 | public function register(): void
39 | {
40 | $this->registerCmdWordProvider();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/Providers/ExceptionServiceProvider.php:
--------------------------------------------------------------------------------
1 | reportable($this->reportable());
25 | }
26 |
27 | if (method_exists($handler, 'renderable')) {
28 | $handler->renderable($this->renderable());
29 | }
30 | }
31 |
32 | /**
33 | * Register a reportable callback.
34 | *
35 | * @param callable $reportUsing
36 | * @return \Illuminate\Foundation\Exceptions\ReportableHandler
37 | */
38 | public function reportable()
39 | {
40 | return function (\Throwable $e) {
41 | //
42 | };
43 | }
44 |
45 | /**
46 | * Register a renderable callback.
47 | *
48 | * @param callable $renderUsing
49 | * @return $this
50 | */
51 | public function renderable()
52 | {
53 | return function (\Throwable $e) {
54 | //
55 | };
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/app/Http/Controllers/LaravelSaasController.php:
--------------------------------------------------------------------------------
1 | has('is_enabled')) {
21 | $where['is_enabled'] = \request('is_enabled');
22 | }
23 |
24 | // $data = Tenant::query()->where($where)->get();
25 |
26 | return view('LaravelSaas::index', [
27 | 'configs' => $configs,
28 | ]);
29 | }
30 |
31 | public function showSettingView()
32 | {
33 | $configs = Config::getValueByKeys([
34 | // 'item_key1',
35 | // 'item_key2',
36 | ]);
37 |
38 | return view('LaravelSaas::setting', [
39 | 'configs' => $configs,
40 | ]);
41 | }
42 |
43 | public function saveSetting()
44 | {
45 | \request()->validate([
46 | // 'item_key1' => 'required|url',
47 | // 'item_key2' => 'nullable|url',
48 | ]);
49 |
50 | $itemKeys = [
51 | // 'item_key1',
52 | // 'item_key2',
53 | ];
54 |
55 | // Config::updateConfigs($itemKeys, 'laravel-saas');
56 |
57 | return redirect(route('laravel-saas.setting'));
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/database/migrations/init_laravel_saas_config.php:
--------------------------------------------------------------------------------
1 | SubscribeUtility::TYPE_USER_ACTIVITY,
18 | // 'fskey' => 'laravel-saas',
19 | // 'cmdWord' => 'stats',
20 | ];
21 |
22 | protected $fresnsConfigItems = [
23 | [
24 | 'item_tag' => 'laravel-saas',
25 | 'item_key' => 'tenant_user_register_service',
26 | 'item_type' => 'string',
27 | 'item_value' => 'LaravelSaas',
28 | ],
29 | ];
30 |
31 | /**
32 | * Run the migrations.
33 | */
34 | public function up(): void
35 | {
36 | // addSubscribeItem
37 | // \FresnsCmdWord::plugin()->addSubscribeItem($this->fresnsWordBody);
38 |
39 | // addKeyValues to Config table
40 | Config::addKeyValues($this->fresnsConfigItems);
41 | }
42 |
43 | /**
44 | * Reverse the migrations.
45 | */
46 | public function down(): void
47 | {
48 | // removeSubscribeItem
49 | // \FresnsCmdWord::plugin()->removeSubscribeItem($this->fresnsWordBody);
50 |
51 | // removeKeyValues from Config table
52 | Config::removeKeyValues($this->fresnsConfigItems);
53 | }
54 | };
55 |
--------------------------------------------------------------------------------
/app/Providers/EventServiceProvider.php:
--------------------------------------------------------------------------------
1 | >
22 | */
23 | protected $listen = [
24 | Registered::class => [
25 | // SendEmailVerificationNotification::class,
26 | \Plugins\LaravelSaas\Listeners\AddTenant::class,
27 | ],
28 | "plugins.clean_data" => [
29 | // When the user uninstalls, if the data needs to be deleted, the listener is configured here.
30 | ],
31 | ];
32 |
33 | /**
34 | * The subscribers to register.
35 | *
36 | * @var array
37 | */
38 | protected $subscribe = [
39 | //
40 | ];
41 |
42 | /**
43 | * Register any events for your application.
44 | */
45 | public function boot(): void
46 | {
47 | //
48 | }
49 |
50 | /**
51 | * Determine if events and listeners should be automatically discovered.
52 | */
53 | public function shouldDiscoverEvents(): bool
54 | {
55 | return false;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/app/Console/Commands/SaasTenantStorageLinkCommand.php:
--------------------------------------------------------------------------------
1 | runForEach(function ($tenant) {
35 | $target = base_path(sprintf("storage/%s%s/app/public",
36 | config('tenancy.filesystem.suffix_base'),
37 | $tenant->id));
38 | $link = str_replace('%tenant_id%', $tenant->id, config('tenancy.filesystem.url_override.public', 'public-%tenant_id%'));
39 |
40 | $message = "fix storage:link $link ========> $target";
41 | dump($message);
42 |
43 | chdir(public_path());
44 | \Illuminate\Support\Facades\File::ensureDirectoryExists(dirname($target));
45 | \Illuminate\Support\Facades\File::link($target, $link);
46 | });
47 |
48 | return 0;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/Providers/CommandServiceProvider.php:
--------------------------------------------------------------------------------
1 | load($commandsDirectory);
27 | }
28 | }
29 |
30 | /**
31 | * Register all of the commands in the given directory.
32 | *
33 | * @param array|string $paths
34 | */
35 | protected function load($paths): void
36 | {
37 | $paths = array_unique(Arr::wrap($paths));
38 |
39 | $paths = array_filter($paths, function ($path) {
40 | return is_dir($path);
41 | });
42 |
43 | if (empty($paths)) {
44 | return;
45 | }
46 |
47 | $commands = [];
48 | foreach ((new Finder)->in($paths)->files() as $command) {
49 | $commandClass = Str::before(self::class, 'Providers\\') . 'Console\\Commands\\' . str_replace('.php', '', $command->getBasename());
50 | if (class_exists($commandClass)) {
51 | $commands[] = $commandClass;
52 | }
53 | }
54 |
55 | $this->commands($commands);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/app/Console/Commands/stubs/tenant_model.stub:
--------------------------------------------------------------------------------
1 | id));
34 | $link = str_replace('%tenant_id%', $tenant->id, config('tenancy.filesystem.url_override.public', 'public-%tenant_id%'));
35 |
36 | chdir(public_path());
37 | \Illuminate\Support\Facades\File::ensureDirectoryExists($target);
38 | \Illuminate\Support\Facades\File::ensureDirectoryExists(dirname($link));
39 | \Illuminate\Support\Facades\File::link($target, $link);
40 | }
41 |
42 | public static function removeStorageLink($tenant)
43 | {
44 | $target = base_path(sprintf("storage/tenants/%s%s/app/public",
45 | config('tenancy.filesystem.suffix_base'),
46 | $tenant->id));
47 | $link = str_replace('%tenant_id%', $tenant->id, config('tenancy.filesystem.url_override.public', 'public-%tenant_id%'));
48 |
49 | chdir(public_path());
50 | \Illuminate\Support\Facades\File::delete($link);
51 | \Illuminate\Support\Facades\File::deleteDirectory(dirname(dirname($target)));
52 | }
53 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LaravelSaas
2 |
3 | [](https://packagist.org/packages/plugins-world/laravel-saas)
4 | [](https://packagist.org/packages/plugins-world/laravel-saas)
5 | [](https://packagist.org/packages/plugins-world/laravel-saas) [](https://packagist.org/packages/plugins-world/laravel-saas)
6 | [](https://packagist.org/packages/plugins-world/laravel-saas)
7 |
8 | 在最新的 laravel 框架中使用 saas 功能的项目。
9 |
10 | 依赖项目:
11 | - [插件管理器 fresns/plugin-manager](https://pm.fresns.org/zh-Hans/)
12 | - [应用市场管理器 plugins-world/market-manager](https://github.com/plugins-world/MarketManager)
13 | - [Tenancy 3.x](https://tenancyforlaravel.com/)
14 | - [Laravel](https://laravel.com/)
15 |
16 | ## 前置要求
17 |
18 | - Laravel 9+
19 | - Tenancy 3+
20 | - fresns/plugin-manager ^2
21 | - fresns/market-manager ^1
22 | - fresns/cmd-word-manager ^1
23 | - 项目已完成 plugins-world/market-manager 的安装。点击查看[Laravel 插件管理器,安装指南](https://discuss.plugins-world.cn/post/9S19kdNL)
24 |
25 |
26 | ## 安装
27 |
28 | 1. 在插件管理后台安装并启用租户插件
29 | 2. 初始化插件
30 |
31 | ```bash
32 | php artisan saas:install # 需要配置数据库的 root 账号密码
33 | ```
34 |
35 |
36 | ## 命令行
37 |
38 | ``` php
39 | php artisan saas # 查看当前可以使用的与 saas 相关的指令
40 | php artisan saas:tenant-add --tenant=foo # 添加租户,默认添加名称为 foo 的租户
41 | php artisan saas:tenant-del --tenant=foo # 删除租户,默认删除名称为 foo 的租户
42 | php artisan saas:tenant-list # 当前 saas 列表
43 | php artisan tenants:migrate --tenants=foo # 执行 foo 租户的迁移,开发阶段建议指定租户,部署阶段可不指定,以批量运行租户迁移
44 | php artisan tenants:migrate-rollback --tenants=foo # 回滚 foo 租户的迁移,开发阶段建议指定租户,部署阶段可不指定,以批量运行租户迁移的回滚操作
45 | php artisan ...
46 | ```
47 |
48 |
49 | ## 使用
50 |
51 | 参考 [tencentforlaravel](https://doc.wyz.xyz/pages/30ee05/) 翻译文档
52 |
--------------------------------------------------------------------------------
/app/Providers/LaravelSaasServiceProvider.php:
--------------------------------------------------------------------------------
1 | registerTranslations();
21 | $this->registerConfig();
22 | $this->registerViews();
23 |
24 | $this->loadMigrationsFrom(dirname(__DIR__, 2) . '/database/migrations');
25 |
26 | $this->app->register(RouteServiceProvider::class);
27 |
28 | // Event::listen(UserCreated::class, UserCreatedListener::class);
29 | }
30 |
31 | /**
32 | * Register the service provider.
33 | */
34 | public function register(): void
35 | {
36 | // if ($this->app->runningInConsole()) {
37 | $this->app->register(CommandServiceProvider::class);
38 | // }
39 | }
40 |
41 | /**
42 | * Register config.
43 | */
44 | protected function registerConfig(): void
45 | {
46 | $this->mergeConfigFrom(
47 | dirname(__DIR__, 2) . '/config/laravel-saas.php', 'laravel-saas'
48 | );
49 | }
50 |
51 | /**
52 | * Register views.
53 | */
54 | public function registerViews(): void
55 | {
56 | $this->loadViewsFrom(dirname(__DIR__, 2) . '/resources/views', 'LaravelSaas');
57 | }
58 |
59 | /**
60 | * Register translations.
61 | */
62 | public function registerTranslations(): void
63 | {
64 | $this->loadTranslationsFrom(dirname(__DIR__, 2) . '/resources/lang', 'LaravelSaas');
65 | }
66 |
67 | /**
68 | * Get the services provided by the provider.
69 | */
70 | public function provides(): array
71 | {
72 | return [];
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/resources/views/setting.blade.php:
--------------------------------------------------------------------------------
1 | @extends('LaravelSaas::layouts.master')
2 |
3 | @section('content')
4 |
39 | @endsection
40 |
--------------------------------------------------------------------------------
/app/Providers/RouteServiceProvider.php:
--------------------------------------------------------------------------------
1 | where('fskey', $fskey)->first();
43 |
44 | // Cache::put($cacheKey, $pluginModel, now()->addMinutes(30));
45 | // }
46 |
47 | // $pluginHost = $pluginModel?->plugin_host ?? '';
48 |
49 | // $host = str_replace(['http://', 'https://'], '', rtrim($pluginHost, '/'));
50 | // }
51 | // } catch (\Throwable $e) {
52 | // info("get plugin host failed: " . $e->getMessage());
53 | // }
54 |
55 | Route::group([
56 | 'domain' => $host,
57 | ], function () {
58 | $this->mapApiRoutes();
59 |
60 | $this->mapWebRoutes();
61 | });
62 | }
63 |
64 | /**
65 | * Define the "web" routes for the application.
66 | *
67 | * These routes all receive session state, CSRF protection, etc.
68 | */
69 | protected function mapWebRoutes(): void
70 | {
71 | Route::middleware('web')
72 | ->group(dirname(__DIR__, 2) . '/routes/web.php');
73 | }
74 |
75 | /**
76 | * Define the "api" routes for the application.
77 | *
78 | * These routes are typically stateless.
79 | */
80 | protected function mapApiRoutes(): void
81 | {
82 | Route::prefix('api')
83 | ->middleware('api')
84 | ->group(dirname(__DIR__, 2) . '/routes/api.php');
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/app/Console/Commands/SaasCommand.php:
--------------------------------------------------------------------------------
1 | info(static::$logo);
42 |
43 | $this->comment('');
44 | $this->comment('Available commands:');
45 |
46 | $this->comment('');
47 | $this->comment('saas');
48 | $this->listAdminCommands();
49 | }
50 |
51 | protected function listAdminCommands(): void
52 | {
53 | $commands = collect(\Illuminate\Support\Facades\Artisan::all())->mapWithKeys(function ($command, $key) {
54 | if (
55 | \Illuminate\Support\Str::startsWith($key, 'saas')
56 | || \Illuminate\Support\Str::startsWith($key, 'tenants')
57 | ) {
58 | return [$key => $command];
59 | }
60 |
61 | return [];
62 | })->toArray();
63 |
64 | \ksort($commands);
65 |
66 | $width = $this->getColumnWidth($commands);
67 |
68 | /** @var Command $command */
69 | foreach ($commands as $command) {
70 | $this->info(sprintf(" %-{$width}s %s", $command->getName(), $command->getDescription()));
71 | }
72 | }
73 |
74 | private function getColumnWidth(array $commands): int
75 | {
76 | $widths = [];
77 |
78 | foreach ($commands as $command) {
79 | $widths[] = static::strlen($command->getName());
80 | foreach ($command->getAliases() as $alias) {
81 | $widths[] = static::strlen($alias);
82 | }
83 | }
84 |
85 | return $widths ? max($widths) + 2 : 0;
86 | }
87 |
88 | /**
89 | * Returns the length of a string, using mb_strwidth if it is available.
90 | *
91 | * @param string $string The string to check its length
92 | * @return int The length of the string
93 | */
94 | public static function strlen($string): int
95 | {
96 | if (false === $encoding = mb_detect_encoding($string, null, true)) {
97 | return strlen($string);
98 | }
99 |
100 | return mb_strwidth($string, $encoding);
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/resources/views/commons/head.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
{{ $title ?? '' }}
7 |
8 | @stack('headcss')
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
100 |
101 | @stack('headjs')
102 |
--------------------------------------------------------------------------------
/app/Console/Commands/SaasInstallCommand.php:
--------------------------------------------------------------------------------
1 | call('tenancy:install');
33 |
34 | $this->initTenantMigrations();
35 | $this->initTenantPublicAssets();
36 | $this->setTenantPrefix();
37 | // $this->registerDomains($this->option('domain')); # 已经通过 setTenantPrefix 初始化了
38 | $this->addInitTenantAction();
39 | $this->resetRegisterPasswordLength();
40 | $this->createTenantMigrationsDir();
41 | $this->resetLoginLogic();
42 | $this->registerInteriaFlashMessage();
43 | $this->registerProvider();
44 | $this->registerRoutes();
45 |
46 | $this->call('migrate');
47 | $this->call('tenants:migrate');
48 | }
49 |
50 | /**
51 | * Replace a given string within a given file.
52 | *
53 | * @param string $search
54 | * @param string $replace
55 | * @param string $path
56 | * @return void
57 | */
58 | protected function replaceInFile($search, $replace, $path)
59 | {
60 | if (!is_file($path)) {
61 | return;
62 | }
63 |
64 | $content = file_get_contents($path);
65 | if (! str_contains($content, $replace)) {
66 | file_put_contents($path, str_replace($search, $replace, $content));
67 | }
68 | }
69 |
70 | /**
71 | * Install the provider in the plugin.json file.
72 | *
73 | * @param string $after
74 | * @param string $name
75 | * @param string $group
76 | */
77 | protected function installPluginProviderAfter(string $after, string $name, string $appConfigPath): void
78 | {
79 | $appConfig = file_get_contents($appConfigPath);
80 |
81 | $providers = Str::before(Str::after($appConfig, '\'providers\' => ServiceProvider::defaultProviders()->merge(['), sprintf('])->toArray(),', PHP_EOL));
82 |
83 | if (! Str::contains($providers, $name)) {
84 | $modifiedProviders = str_replace(
85 | sprintf('%s,', $after),
86 | sprintf('%s,', $after).PHP_EOL.' '.sprintf('%s', $name),
87 | $providers,
88 | );
89 |
90 | $this->replaceInFile(
91 | $providers,
92 | $modifiedProviders,
93 | $appConfigPath,
94 | );
95 | }
96 | }
97 |
98 | /**
99 | * Install the provider in the plugin.json file.
100 | *
101 | * @param string $after
102 | * @param string $name
103 | * @param string $group
104 | */
105 | protected function installBuildCommandAfter(string $after, string $name, string $packageJsonPath): void
106 | {
107 | $packageJson = file_get_contents($packageJsonPath);
108 |
109 | $scripts = Str::before(Str::after($packageJson, '"build": "'), sprintf('"', PHP_EOL));
110 |
111 | if (! Str::contains($scripts, $name)) {
112 | $modifiedScripts = preg_replace(
113 | sprintf('/"?%s/', $after),
114 | sprintf('%s', $after).' && '.sprintf('%s', $name),
115 | $scripts,
116 | 1,
117 | );
118 |
119 | $this->replaceInFile(
120 | $scripts,
121 | $modifiedScripts,
122 | $packageJsonPath,
123 | );
124 | }
125 | }
126 |
127 | public function registerProvider()
128 | {
129 | $this->installPluginProviderAfter('App\Providers\RouteServiceProvider::class', 'App\Providers\TenancyServiceProvider::class, // <-- here', config_path('app.php'));
130 | }
131 |
132 | public function registerRoutes()
133 | {
134 | if (! is_file($tenantSessionMiddlewareFile = app_path('Http/Middleware/AuthenticateTenantSession.php'))) {
135 | copy(__DIR__.'/stubs/AuthenticateTenantSession.stub', $tenantSessionMiddlewareFile);
136 | }
137 |
138 | $this->replaceInFile(<<<'TXT'
139 | Route::middleware([
140 | 'web',
141 | InitializeTenancyByDomain::class,
142 | PreventAccessFromCentralDomains::class,
143 | ])->group(function () {
144 | Route::get('/', function () {
145 | return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id');
146 | });
147 | });
148 | TXT,
149 | <<<'TXT'
150 | \Stancl\Tenancy\Middleware\InitializeTenancyByDomain::$onFail = function ($exception, $request, $next) {
151 | return redirect(config('app.url'));
152 | };
153 |
154 | Route::middleware([
155 | 'web',
156 | InitializeTenancyByDomain::class,
157 | PreventAccessFromCentralDomains::class,
158 | ])->group(function () {
159 | // Route::get('/', function () {
160 | // return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id');
161 | // });
162 |
163 | require __DIR__.'/web.php';
164 | });
165 | TXT, base_path('routes/tenant.php'));
166 |
167 | $this->replaceInFile(<<<'TXT'
168 | public function boot(): void
169 | {
170 | RateLimiter::for('api', function (Request $request) {
171 | return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
172 | });
173 |
174 | $this->routes(function () {
175 | Route::middleware('api')
176 | ->prefix('api')
177 | ->group(base_path('routes/api.php'));
178 |
179 | Route::middleware('web')
180 | ->group(base_path('routes/web.php'));
181 | });
182 | }
183 | TXT,
184 | <<<'TXT'
185 | public function boot(): void
186 | {
187 | RateLimiter::for('api', function (Request $request) {
188 | return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
189 | });
190 |
191 | $this->routes(function () {
192 | foreach ($this->centralDomains() as $domain) {
193 | Route::middleware(['api', \App\Http\Middleware\AuthenticateTenantSession::class])
194 | ->domain($domain)
195 | ->prefix('api')
196 | ->group(base_path('routes/api.php'));
197 |
198 | Route::middleware(['web', \App\Http\Middleware\AuthenticateTenantSession::class])
199 | ->domain($domain)
200 | ->group(base_path('routes/web.php'));
201 | }
202 | });
203 | }
204 |
205 | protected function centralDomains(): array
206 | {
207 | return config('tenancy.central_domains');
208 | }
209 | TXT, app_path('Providers/RouteServiceProvider.php'));
210 | }
211 |
212 | public function registerDomains($domain = null)
213 | {
214 | if (! $domain) {
215 | $urlInfo = parse_url(config('app.url'));
216 |
217 | $domain = $urlInfo['host'];
218 | }
219 |
220 | if (! $domain) {
221 | return;
222 | }
223 |
224 | $this->replaceInFile(<<<'TXT'
225 | 'central_domains' => [
226 | '127.0.0.1',
227 | 'localhost',
228 | ],
229 | TXT,
230 | <<<'TXT'
231 | 'central_domains' => [
232 | '$domain',
233 | '127.0.0.1',
234 | 'localhost',
235 | ],
236 | TXT, config_path('tenancy.php'));
237 | }
238 |
239 | public function createTenantMigrationsDir()
240 | {
241 | $path = database_path('migrations/tenant');
242 |
243 | if (!is_dir($path)) {
244 | @mkdir($path, 0755, true);
245 | @touch($path.'/.gitkeep');
246 | }
247 | }
248 |
249 | public function setTenantPrefix()
250 | {
251 | $content = file_get_contents($filePath = config_path('tenancy.php'));
252 |
253 | if (str_contains($content, "url_override")) {
254 | return;
255 | }
256 |
257 | $newContent = str_replace(
258 | [
259 | "use Stancl\Tenancy\Database\Models\Tenant;\n\nreturn [",
260 | "'tenant_model' => Tenant::class,",
261 | "'localhost',\n ],",
262 | "'prefix' => 'tenant',",
263 | "'suffix_base' => 'tenant',",
264 | "_base' => 'tenant',",
265 | "// '--force' => true,",
266 | "'public' => '%storage_path%/app/public/',\n ],",
267 | ],
268 | [
269 | "use Stancl\Tenancy\Database\Models\Tenant;\n\n\$prefix = env('DB_DATABASE') . '_';\n\nreturn [",
270 | "'tenant_model' => \App\Models\Tenant::class,",
271 | "'localhost',\n\t\tstr_replace(['http://', 'https://'], '', trim(env('APP_URL', ''), '/')),\n ],",
272 | "'prefix' => \$prefix,",
273 | "'suffix_base' => \"tenants/\".\$prefix,",
274 | "_base' => \$prefix,",
275 | "'--force' => true,",
276 | "'public' => '%storage_path%/app/public/',
277 | ],
278 |
279 | /*
280 | * Use this to support Storage url method on local driver disks.
281 | * You should create a symbolic link which points to the public directory using command: artisan tenants:link
282 | * Then you can use tenant aware Storage url: Storage::disk('public')->url('file.jpg')
283 | *
284 | * See https://github.com/archtechx/tenancy/pull/689
285 | */
286 | 'url_override' => [
287 | // The array key is local disk (must exist in root_override) and value is public directory (%tenant_id% will be replaced with actual tenant id).
288 | 'public' => 'tenants/public-%tenant_id%',
289 | ],",
290 | ],
291 | $content
292 | );
293 | file_put_contents($filePath, $newContent);
294 | }
295 |
296 | public function addInitTenantAction()
297 | {
298 | $tenantModelFile = app_path('Models/Tenant.php');
299 |
300 | if (file_exists($tenantModelFile)) {
301 | return;
302 | }
303 |
304 | copy(__DIR__.'/stubs/tenant_model.stub', app_path('Models/Tenant.php'));
305 |
306 | $content = file_get_contents($filePath = base_path('app/Providers/TenancyServiceProvider.php'));
307 |
308 | $newContent = str_replace(
309 | [
310 | "// Jobs\SeedDatabase::class,",
311 | "send(function (Events\TenantCreated \$event) {
312 | return \$event->tenant;
313 | })",
314 | "send(function (Events\TenantDeleted \$event) {
315 | return \$event->tenant;
316 | })",
317 | ],
318 | [
319 | "Jobs\SeedDatabase::class,",
320 | "send(function (Events\TenantCreated \$event) {
321 | \App\Models\Tenant::createStorageLink(\$event->tenant); // <-- here.
322 | return \$event->tenant;
323 | })",
324 | "send(function (Events\TenantDeleted \$event) {
325 | \App\Models\Tenant::removeStorageLink(\$event->tenant); // <-- here.
326 | return \$event->tenant;
327 | })",
328 | ],
329 | $content
330 | );
331 | file_put_contents($filePath, $newContent);
332 | }
333 |
334 | public function resetRegisterPasswordLength()
335 | {
336 | if (! class_exists(\App\Http\Controllers\Auth\RegisteredUserController::class)) {
337 | return;
338 | }
339 |
340 | $this->replaceInFile('Rules\Password::defaults()', "'min:6'", app_path('Http/Controllers/Auth/RegisteredUserController.php'));
341 | }
342 |
343 | public function initTenantMigrations()
344 | {
345 | $path = database_path('migrations/tenant');
346 |
347 | $files = glob(database_path('migrations/*create_users_table*'));
348 | $createUsersTableMigration = $files ? $files[0] : null;
349 | if (! $createUsersTableMigration) {
350 | return;
351 | }
352 |
353 | $migrationFile = basename($createUsersTableMigration);
354 | $createUsersTableMigrationWithTenant = database_path("migrations/tenant/{$migrationFile}");
355 |
356 | if (! is_file($createUsersTableMigrationWithTenant)) {
357 | copy($createUsersTableMigration, $createUsersTableMigrationWithTenant);
358 | }
359 |
360 | if (file_exists($path.'/.gitkeep')) {
361 | unlink($path.'/.gitkeep');
362 | }
363 | }
364 |
365 | public function initTenantPublicAssets()
366 | {
367 | if (is_dir(base_path('public/build')) && is_dir(base_path('public/tenancy/assets'))) {
368 | $this->installBuildCommandAfter('vite build', 'cp -r public/build public/tenancy/assets/build', base_path('package.json'));
369 | }
370 |
371 | tap(new Filesystem, function ($files) {
372 | $path = public_path('tenancy/assets');
373 | $files->ensureDirectoryExists($path);
374 |
375 | // build assets
376 | $build = public_path('build');
377 | $buildWithTenancy = public_path('tenancy/assets/build');
378 |
379 | $files->copyDirectory($build, $buildWithTenancy);
380 | });
381 |
382 |
383 | }
384 |
385 | public function resetLoginLogic()
386 | {
387 | $this->replaceInFile(<<<'TXT'
388 | public function create(): Response
389 | {
390 | return Inertia::render('Auth/Login', [
391 | 'canResetPassword' => Route::has('password.request'),
392 | 'status' => session('status'),
393 | ]);
394 | }
395 | TXT,
396 | <<<'TXT'
397 | public function create(): Response|RedirectResponse
398 | {
399 | if ($encryptData = \request('encryptData')) {
400 | $data = decrypt($encryptData);
401 | $userArray = $data['user'] ?? null;
402 | $user = \App\Models\User::where('name', $userArray['name'])->first();
403 | auth()->login($user);
404 |
405 | return redirect('/login');
406 | }
407 |
408 | return Inertia::render('Auth/Login', [
409 | 'canResetPassword' => Route::has('password.request'),
410 | 'status' => session('status'),
411 | ]);
412 | }
413 | TXT, app_path('Http/Controllers/Auth/AuthenticatedSessionController.php'));
414 | }
415 |
416 | public function registerInteriaFlashMessage()
417 | {
418 | $this->replaceInFile(<<<'TXT'
419 | public function share(Request $request): array
420 | {
421 | return array_merge(parent::share($request), [
422 | 'auth' => [
423 | 'user' => $request->user(),
424 | ],
425 | 'ziggy' => function () use ($request) {
426 | return array_merge((new Ziggy)->toArray(), [
427 | 'location' => $request->url(),
428 | ]);
429 | },
430 | ]);
431 | }
432 | TXT,
433 | <<<'TXT'
434 | public function share(Request $request): array
435 | {
436 | return array_merge(parent::share($request), [
437 | 'auth' => [
438 | 'user' => $request->user(),
439 | ],
440 | 'ziggy' => function () use ($request) {
441 | return array_merge((new Ziggy)->toArray(), [
442 | 'location' => $request->url(),
443 | ]);
444 | },
445 | 'flash' => session()->all(),
446 | ]);
447 | }
448 | TXT, app_path('Http/Middleware/HandleInertiaRequests.php'));
449 | }
450 | }
451 |
--------------------------------------------------------------------------------