Install.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. <?php
  2. /**
  3. * The MIT License (MIT)
  4. * Copyright (c) 2023 Bubka
  5. * Copyright (c) 2018 Phan An (https://github.com/koel/koel/blob/master/app/Console/Commands/InitCommand.php)
  6. *
  7. * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
  8. * associated documentation files (the "Software"), to deal in the Software without restriction,
  9. * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
  10. * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
  11. * subject to the following conditions:
  12. *
  13. * The above copyright notice and this permission notice shall be included in all copies or substantial
  14. * portions of the Software.
  15. *
  16. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
  17. * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
  18. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  19. * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
  20. * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  21. */
  22. namespace App\Console\Commands;
  23. use Exception;
  24. use Illuminate\Console\Command;
  25. use Illuminate\Console\ConfirmableTrait;
  26. use Illuminate\Encryption\Encrypter;
  27. use Illuminate\Support\Facades\DB;
  28. use Illuminate\Support\Facades\Log;
  29. use Jackiedo\DotenvEditor\DotenvEditor;
  30. use Throwable;
  31. class Install extends Command
  32. {
  33. use ConfirmableTrait;
  34. /**
  35. * Identify if we start with an existing .env file
  36. *
  37. * @var bool
  38. */
  39. protected $envFileExists = false;
  40. /**
  41. * The name and signature of the console command.
  42. *
  43. * @var string
  44. */
  45. protected $signature = '2fauth:install {--force : Force the operation to run when in production}';
  46. /**
  47. * The console command description.
  48. *
  49. * @var string
  50. */
  51. protected $description = 'Run 2FAuth installation/update wizard';
  52. /**
  53. * Create a new command instance.
  54. *
  55. * @return void
  56. */
  57. public function __construct(
  58. protected DotenvEditor $dotenvEditor,
  59. ) {
  60. parent::__construct();
  61. }
  62. /**
  63. * Execute the console command.
  64. *
  65. * @return mixed
  66. */
  67. public function handle()
  68. {
  69. $this->newLine(2);
  70. $this->alert('2FAuth installation');
  71. if ($this->option('no-interaction')) {
  72. $this->newLine();
  73. $this->info('(Running in no-interaction mode)');
  74. $this->newLine();
  75. }
  76. $this->info('Start processing');
  77. try {
  78. $this->clearCaches();
  79. $this->loadEnvFile();
  80. $this->maybeGenerateAppKey();
  81. if ((! $this->envFileExists || $this->confirm('Existing .env file found. Do you wish to review its vars?', true)) && ! $this->option('no-interaction')) {
  82. $this->setMainEnvVars();
  83. $this->setDbEnvVars();
  84. }
  85. $this->migrateDatabase();
  86. $this->installPassport();
  87. $this->createStorageLink();
  88. $this->cacheConfig();
  89. $this->dotenvEditor->save();
  90. } catch (Throwable $e) {
  91. Log::error($e);
  92. $this->newLine();
  93. $this->line('Sorry, something went wrong :(');
  94. $this->newLine();
  95. $this->components->error($e->getMessage());
  96. $this->components->info('See the error log at storage/logs/laravel.log for the full stack trace.');
  97. $this->newLine();
  98. $this->line('Fix the error and rerun the \'2fauth:install\' command to complete installation.');
  99. $this->newLine();
  100. $this->line('As a reminder, you can always install/upgrade manually following the guide at:');
  101. $this->info(config('2fauth.installDocUrl'));
  102. $this->newLine();
  103. $this->line('You can also ask for some help at:');
  104. $this->info(config('2fauth.repository') . '/issues');
  105. return self::FAILURE;
  106. }
  107. $this->newLine();
  108. $this->output->success('Installation complete successfully');
  109. $this->line('Visit <info>' . config('app.url') . '</info> to start using 2FAuth');
  110. $this->newLine();
  111. $this->line('-----------------------------------');
  112. $this->line('.▀█▀.█▄█.█▀█.█▄.█.█▄▀ █▄█.█▀█.█─█');
  113. $this->line('─.█.─█▀█.█▀█.█.▀█.█▀▄ ─█.─█▄█.█▄█ for using 2FAuth');
  114. $this->newLine();
  115. $this->line('Want to support its development?');
  116. $this->line('You can Buy me a coffee => <info>https://ko-fi.com/bubka</info>');
  117. $this->line('You can sponsor me on GitHub => <info>https://github.com/sponsors/Bubka</info>');
  118. return self::SUCCESS;
  119. }
  120. /**
  121. * Runs the passport:install command silently
  122. */
  123. protected function installPassport() : void
  124. {
  125. $this->components->task('Setting up Passport', function () : void {
  126. $this->callSilently('passport:install', ['--no-interaction' => true]);
  127. });
  128. }
  129. /**
  130. * Runs the config:cache command silently
  131. */
  132. protected function cacheConfig() : void
  133. {
  134. $this->components->task('Caching config', function () : void {
  135. $this->callSilently('config:cache');
  136. });
  137. }
  138. /**
  139. * Runs the storage:link command silently
  140. */
  141. protected function createStorageLink() : void
  142. {
  143. if (! file_exists(public_path('storage'))) {
  144. $this->components->task('Creating storage link', function () : void {
  145. $this->callSilently('storage:link');
  146. });
  147. }
  148. }
  149. /**
  150. * Lets the user set the main environment variables
  151. */
  152. protected function setMainEnvVars() : void
  153. {
  154. while (true) {
  155. $appUrl = trim($this->ask('URL of this 2FAuth instance', config('app.url')), '/');
  156. if (filter_var($appUrl, FILTER_VALIDATE_URL)) {
  157. break;
  158. } else {
  159. $this->components->error('This is not a valid URL, please retry');
  160. }
  161. }
  162. $urlPath = parse_url($appUrl, PHP_URL_PATH);
  163. if ($urlPath && $urlPath != '/') {
  164. $urlPath = trim($urlPath, '/');
  165. $this->components->info('2Fauth will be served under subdirectory /' . $urlPath);
  166. $this->dotenvEditor->setKey('APP_SUBDIRECTORY', $urlPath);
  167. }
  168. $this->dotenvEditor->setKey('APP_URL', $appUrl);
  169. }
  170. /**
  171. * Prompts user for valid database credentials and sets them to .env file.
  172. */
  173. protected function setDbEnvVars() : void
  174. {
  175. $config = [
  176. 'DB_HOST' => '',
  177. 'DB_PORT' => '',
  178. 'DB_USERNAME' => '',
  179. 'DB_PASSWORD' => '',
  180. ];
  181. $config['DB_CONNECTION'] = $this->choice(
  182. 'Type of database',
  183. [
  184. 'mysql' => 'MySQL/MariaDB',
  185. 'pgsql' => 'PostgreSQL',
  186. 'sqlsrv' => 'SQL Server',
  187. 'sqlite' => 'SQLite',
  188. ],
  189. config('database.default')
  190. );
  191. if ($config['DB_CONNECTION'] === 'sqlite') {
  192. $databasePath = $this->dotenvEditor->getValue('DB_CONNECTION') != 'sqlite'
  193. ? database_path('database.sqlite')
  194. : config('database.connections.sqlite.database');
  195. $config['DB_DATABASE'] = $this->ask('Absolute path to the DB file', $databasePath);
  196. } else {
  197. $defaultName = $this->dotenvEditor->getValue('DB_DATABASE') ?: '2fauth';
  198. $databaseName = $this->dotenvEditor->getValue('DB_CONNECTION') == 'sqlite'
  199. ? '2fauth'
  200. : $defaultName;
  201. $config['DB_HOST'] = $this->ask('Database host', config('database.connections.' . $config['DB_CONNECTION'] . '.host'));
  202. $config['DB_PORT'] = (string) $this->ask('Database port', config('database.connections.' . $config['DB_CONNECTION'] . '.port'));
  203. $config['DB_DATABASE'] = $this->ask('Database name', $databaseName);
  204. $config['DB_USERNAME'] = $this->ask('Database user', config('database.connections.' . $config['DB_CONNECTION'] . '.username'));
  205. $config['DB_PASSWORD'] = (string) $this->secret('Database password', config('database.connections.' . $config['DB_CONNECTION'] . '.password'));
  206. // $config['DB_PASSWORD'] = (string) $this->secret('Database password', true);
  207. }
  208. $this->dotenvEditor->setKeys($config);
  209. $this->dotenvEditor->save();
  210. // Set the config so that the next DB attempt uses refreshed credentials
  211. config([
  212. 'database.default' => $config['DB_CONNECTION'],
  213. 'database.connections.' . $config['DB_CONNECTION'] . '.database' => $config['DB_DATABASE'],
  214. 'database.connections.' . $config['DB_CONNECTION'] . '.host' => $config['DB_HOST'],
  215. 'database.connections.' . $config['DB_CONNECTION'] . '.port' => $config['DB_PORT'],
  216. 'database.connections.' . $config['DB_CONNECTION'] . '.username' => $config['DB_USERNAME'],
  217. 'database.connections.' . $config['DB_CONNECTION'] . '.password' => $config['DB_PASSWORD'],
  218. ]);
  219. $this->laravel['db']->purge();
  220. }
  221. /**
  222. * Runs db migration with --force option
  223. */
  224. protected function migrateDatabase() : mixed
  225. {
  226. if (! $this->confirmToProceed()) {
  227. return 1;
  228. }
  229. return $this->call('migrate', ['--force' => $this->option('force')]);
  230. }
  231. /**
  232. * Clears some caches
  233. */
  234. protected function clearCaches() : void
  235. {
  236. $this->components->task('Clearing caches', function () : void {
  237. $this->callSilently('config:clear');
  238. $this->callSilently('cache:clear');
  239. });
  240. }
  241. /**
  242. * Loads the existing env file or creates it
  243. */
  244. protected function loadEnvFile() : void
  245. {
  246. $this->envFileExists = file_exists(base_path('.env'));
  247. if (! $this->envFileExists && $this->option('no-interaction')) {
  248. throw new Exception('--no-interaction option cannot be used during first install');
  249. }
  250. if (! $this->envFileExists) {
  251. $this->input->setOption('force', true);
  252. }
  253. $this->components->task('Preparing .env file', static function () : void {
  254. if (! file_exists(base_path('.env'))) {
  255. copy(base_path('.env.example'), base_path('.env'));
  256. }
  257. });
  258. $this->dotenvEditor->load(base_path('.env'));
  259. }
  260. /**
  261. * Generates an app key if necessary
  262. */
  263. protected function maybeGenerateAppKey() : void
  264. {
  265. $key = config('app.key');
  266. $this->components->task($key ? 'Retrieving app key' : 'Generating app key', function () use (&$key) : void {
  267. if (! $key) {
  268. // Generate the key manually to prevent some clashes with `php artisan key:generate`
  269. $key = $this->generateRandomKey();
  270. $this->dotenvEditor->setKey('APP_KEY', $key);
  271. config('app.key', $key);
  272. }
  273. });
  274. }
  275. /**
  276. * Generates a random key for the application.
  277. */
  278. protected function generateRandomKey() : string
  279. {
  280. return 'base64:' . base64_encode(Encrypter::generateKey(config('app.cipher')));
  281. }
  282. }