From 5de9a2df27a83873fa19a5d9ce7c9ed897a96bf3 Mon Sep 17 00:00:00 2001
From: Bubka <858858+Bubka@users.noreply.github.com>
Date: Tue, 18 Apr 2023 08:43:36 +0200
Subject: [PATCH] Add artisan ':install' command
---
.env.example | 45 ++++-
.gitignore | 1 +
app/Console/Commands/Install.php | 333 +++++++++++++++++++++++++++++++
bootstrap/app.php | 18 ++
composer.json | 5 +-
composer.lock | 115 ++++++++++-
config/2fauth.php | 1 +
config/app.php | 2 +-
config/database.php | 30 +--
config/dotenv-editor.php | 37 ++++
10 files changed, 559 insertions(+), 28 deletions(-)
create mode 100644 app/Console/Commands/Install.php
create mode 100644 config/dotenv-editor.php
diff --git a/.env.example b/.env.example
index 72c2cc41..63006bea 100644
--- a/.env.example
+++ b/.env.example
@@ -21,9 +21,12 @@ SITE_OWNER=mail@example.com
# The encryption key for your database and sessions. Keep this very secure.
# If you generate a new one all existing data must be considered LOST.
-# Change it to a string of exactly 32 chars or use command `php artisan key:generate` to generate it
+#
+# You can leave this empty if you use `php artisan 2fauth:install`.
+# Otherwise, change it to a string of exactly 32 chars or use command
+# `php artisan key:generate` to generate it.
-APP_KEY=SomeRandomStringOf32CharsExactly
+APP_KEY=
# This variable must match your installation's external address but keep in mind that
@@ -74,17 +77,41 @@ FILESYSTEM_DRIVER=local
#### Database config & credentials ####
-DB_CONNECTION=sqlite
-DB_DATABASE="path/to/your/database.sqlite"
-
-# or if you want to use SQL (uncomment lines)
+# Supported values for DB_CONNECTION: mysql|pgsql|sqlsrv|sqlite
+# mysql => MySQL
+# pgsql => PostGreSQL
+# sqlsrv => SQL server
+# sqlite => SQLite
+# Example for a MySQL database connection
+#
# DB_CONNECTION=mysql
+# DB_DATABASE=my_2fauth_DB_name
# DB_HOST=127.0.0.1
# DB_PORT=3306
-# DB_DATABASE=homestead
-# DB_USERNAME=homestead
-# DB_PASSWORD=secret
+# DB_USERNAME=my_2fauth_db_user
+# DB_PASSWORD=My_d8_S3cr3t
+
+# Example for SQLite (linux)
+#
+# DB_CONNECTION=sqlite
+# DB_DATABASE="path/to/your/database.sqlite"
+
+# Example for SQLite (windows)
+#
+# DB_CONNECTION=sqlite
+# DB_DATABASE="C:\\path\\to\\your\\database.sqlite"
+
+DB_CONNECTION=
+DB_DATABASE=
+DB_HOST=
+DB_PORT=
+DB_USERNAME=
+DB_PASSWORD=
+
+# The absolute path to the root CA bundle if you're connecting to the MySQL database via SSL.
+
+MYSQL_ATTR_SSL_CA=
#### Mail settings ####
diff --git a/.gitignore b/.gitignore
index cd20911b..abeb72fa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
/public/hot
/public/storage
/storage/*.key
+/storage/**/.env*
/tests/Coverage
/tests/EndToEnd/**/*.html
/tests/EndToEnd/**/output.xml
diff --git a/app/Console/Commands/Install.php b/app/Console/Commands/Install.php
new file mode 100644
index 00000000..c29f65fb
--- /dev/null
+++ b/app/Console/Commands/Install.php
@@ -0,0 +1,333 @@
+newLine(2);
+ $this->alert('2FAuth installation');
+
+ if ($this->option('no-interaction')) {
+ $this->newLine();
+ $this->info('(Running in no-interaction mode)');
+ $this->newLine();
+ }
+
+ $this->info('Start processing');
+ try {
+ $this->clearCaches();
+ $this->loadEnvFile();
+ $this->maybeGenerateAppKey();
+
+ if ((! $this->envFileExists || $this->confirm('Existing .env file found. Do you wish to review its vars?', true)) && ! $this->option('no-interaction')) {
+ $this->setMainEnvVars();
+ $this->setDbEnvVars();
+ }
+
+ $this->migrateDatabase();
+ $this->installPassport();
+ $this->createStorageLink();
+ $this->cacheConfig();
+
+ $this->dotenvEditor->save();
+ } catch (Throwable $e) {
+ Log::error($e);
+
+ $this->newLine();
+ $this->line('Sorry, something went wrong :(');
+ $this->newLine();
+ $this->components->error($e->getMessage());
+ $this->components->info('See the error log at storage/logs/laravel.log for the full stack trace.');
+
+ $this->newLine();
+ $this->line('Fix the error and rerun the \'2fauth:install\' command to complete installation.');
+ $this->newLine();
+ $this->line('As a reminder, you can always install/upgrade manually following the guide at:');
+ $this->info(config('2fauth.installDocUrl'));
+ $this->newLine();
+ $this->line('You can also ask for some help at:');
+ $this->info(config('2fauth.repository') . '/issues');
+
+ return self::FAILURE;
+ }
+
+ $this->newLine();
+ $this->output->success('Installation complete successfully');
+ $this->line('Visit ' . config('app.url') . ' to start using 2FAuth');
+ $this->newLine();
+ $this->line('-----------------------------------');
+ $this->line('.▀█▀.█▄█.█▀█.█▄.█.█▄▀ █▄█.█▀█.█─█');
+ $this->line('─.█.─█▀█.█▀█.█.▀█.█▀▄ ─█.─█▄█.█▄█ for using 2FAuth');
+ $this->newLine();
+ $this->line('Want to support its development?');
+ $this->line('You can Buy me a coffee => https://ko-fi.com/bubka');
+
+ return self::SUCCESS;
+ }
+
+ /**
+ *
+ */
+ protected function installPassport() : void
+ {
+ $this->components->task('Setting up Passport', function () : void {
+ $this->callSilently('passport:install');
+ });
+ }
+
+ /**
+ *
+ */
+ protected function cacheConfig() : void
+ {
+ $this->components->task('Caching config', function () : void {
+ $this->callSilently('config:cache');
+ });
+ }
+
+ /**
+ *
+ */
+ protected function createStorageLink() : void
+ {
+ if (!file_exists(public_path('storage'))) {
+ $this->components->task('Creating storage link', function () : void {
+ $this->callSilently('storage:link');
+ });
+ }
+ }
+
+ /**
+ *
+ */
+ protected function setMainEnvVars() : void
+ {
+ while (true) {
+ $appUrl = trim($this->ask('URL of this 2FAuth instance', config('app.url')), '/');
+ if (filter_var($appUrl, FILTER_VALIDATE_URL)) {
+ break;
+ }
+ else {
+ $this->components->error('This is not a valid URL, please retry');
+ }
+ }
+
+ $urlPath = parse_url($appUrl, PHP_URL_PATH);
+
+ if ($urlPath && $urlPath != '/') {
+ $urlPath = trim($urlPath, '/');
+ $this->components->info('2Fauth will be served under subdirectory /' . $urlPath);
+ $this->dotenvEditor->setKey('APP_SUBDIRECTORY', $urlPath);
+ }
+
+ $this->dotenvEditor->setKey('APP_URL', $appUrl);
+ }
+
+
+ /**
+ * Prompt user for valid database credentials and set them to .env file.
+ */
+ protected function setDbEnvVars() : void
+ {
+ $config = [
+ 'DB_HOST' => '',
+ 'DB_PORT' => '',
+ 'DB_USERNAME' => '',
+ 'DB_PASSWORD' => '',
+ ];
+
+ $config['DB_CONNECTION'] = $this->choice(
+ 'Type of database',
+ [
+ 'mysql' => 'MySQL/MariaDB',
+ 'pgsql' => 'PostgreSQL',
+ 'sqlsrv' => 'SQL Server',
+ 'sqlite' => 'SQLite',
+ ],
+ config('database.default')
+ );
+
+ if ($config['DB_CONNECTION'] === 'sqlite') {
+ $databasePath = $this->dotenvEditor->getValue('DB_CONNECTION') != 'sqlite'
+ ? database_path('database.sqlite')
+ : config('database.connections.sqlite.database');
+
+ $config['DB_DATABASE'] = $this->ask('Absolute path to the DB file', $databasePath);
+ } else {
+ $defaultName = $this->dotenvEditor->getValue('DB_DATABASE') ?: '2fauth';
+ $databaseName = $this->dotenvEditor->getValue('DB_CONNECTION') == 'sqlite'
+ ? '2fauth'
+ : $defaultName;
+
+ $config['DB_HOST'] = $this->ask('Database host', config('database.connections.' . $config['DB_CONNECTION'] . '.host'));
+ $config['DB_PORT'] = (string) $this->ask('Database port', config('database.connections.' . $config['DB_CONNECTION'] . '.port'));
+ $config['DB_DATABASE'] = $this->ask('Database name', $databaseName);
+ $config['DB_USERNAME'] = $this->ask('Database user', config('database.connections.' . $config['DB_CONNECTION'] . '.username'));
+ $config['DB_PASSWORD'] = (string) $this->secret('Database password', config('database.connections.' . $config['DB_CONNECTION'] . '.password'));
+ // $config['DB_PASSWORD'] = (string) $this->secret('Database password', true);
+ }
+
+ $this->dotenvEditor->setKeys($config);
+ $this->dotenvEditor->save();
+
+ // Set the config so that the next DB attempt uses refreshed credentials
+ config([
+ 'database.default' => $config['DB_CONNECTION'],
+ 'database.connections.' . $config['DB_CONNECTION'] . '.database' => $config['DB_DATABASE'],
+ 'database.connections.' . $config['DB_CONNECTION'] . '.host' => $config['DB_HOST'],
+ 'database.connections.' . $config['DB_CONNECTION'] . '.port' => $config['DB_PORT'],
+ 'database.connections.' . $config['DB_CONNECTION'] . '.username' => $config['DB_USERNAME'],
+ 'database.connections.' . $config['DB_CONNECTION'] . '.password' => $config['DB_PASSWORD'],
+ ]);
+ $this->laravel['db']->purge();
+ }
+
+ /**
+ *
+ */
+ protected function migrateDatabase() : mixed
+ {
+ if (! $this->confirmToProceed()) {
+ return 1;
+ }
+
+ return $this->call('migrate', ['--force' => $this->option('force')]);
+ }
+
+ /**
+ *
+ */
+ protected function clearCaches() : void
+ {
+ $this->components->task('Clearing caches', function () : void {
+ $this->callSilently('config:clear');
+ $this->callSilently('cache:clear');
+ });
+ }
+
+ /**
+ *
+ */
+ protected function loadEnvFile() : void
+ {
+ $this->envFileExists = file_exists(base_path('.env'));
+
+ if (! $this->envFileExists && $this->option('no-interaction')) {
+ throw new Exception('--no-interaction option cannot be used during first install');
+ }
+
+ if (! $this->envFileExists) {
+ $this->input->setOption('force', true);
+ }
+
+ $this->components->task('Preparing .env file', static function () : void {
+ if (!file_exists(base_path('.env'))) {
+ copy(base_path('.env.example'), base_path('.env'));
+ }
+ });
+
+ $this->dotenvEditor->load(base_path('.env'));
+ }
+
+ /**
+ *
+ */
+ protected function maybeGenerateAppKey() : void
+ {
+ $key = config('app.key');
+
+ $this->components->task($key ? 'Retrieving app key' : 'Generating app key', function () use (&$key) : void {
+ if (!$key) {
+ // Generate the key manually to prevent some clashes with `php artisan key:generate`
+ $key = $this->generateRandomKey();
+ $this->dotenvEditor->setKey('APP_KEY', $key);
+ config('app.key', $key);
+ }
+ });
+ }
+
+ /**
+ * Generate a random key for the application.
+ */
+ protected function generateRandomKey() : string
+ {
+ return 'base64:' . base64_encode(Encrypter::generateKey(config('app.cipher')));
+ }
+}
diff --git a/bootstrap/app.php b/bootstrap/app.php
index 037e17df..29eaef8c 100644
--- a/bootstrap/app.php
+++ b/bootstrap/app.php
@@ -11,6 +11,24 @@
|
*/
+if (! function_exists('envUnlessEmpty')) {
+ /**
+ * @param string $key
+ * @param null $default
+ *
+ * @return mixed|null
+ */
+ function envUnlessEmpty(string $key, $default = null)
+ {
+ $result = env($key, $default);
+ if (is_string($result) && '' === $result) {
+ $result = $default;
+ }
+
+ return $result;
+ }
+}
+
$app = new Illuminate\Foundation\Application(
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);
diff --git a/composer.json b/composer.json
index 87b5a462..02a5f19e 100644
--- a/composer.json
+++ b/composer.json
@@ -21,12 +21,13 @@
"ext-tokenizer": "*",
"ext-xml": "*",
"chillerlan/php-qrcode": "^4.3",
- "laragear/webauthn": "^1.2.0",
"doctormckay/steam-totp": "^1.0",
"doctrine/dbal": "^3.4",
"google/protobuf": "^3.21",
"guzzlehttp/guzzle": "^7.2",
+ "jackiedo/dotenv-editor": "^2.1",
"khanamiryan/qrcode-detector-decoder": "^2.0.2",
+ "laragear/webauthn": "^1.2.0",
"laravel/framework": "^9.0",
"laravel/passport": "^11.2",
"laravel/tinker": "^2.7",
@@ -94,4 +95,4 @@
"vendor/bin/phpunit --coverage-html tests/Coverage/"
]
}
-}
\ No newline at end of file
+}
diff --git a/composer.lock b/composer.lock
index f9676dd4..b294ffc1 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "60cc94b258157b8eb06ef75ef7fab121",
+ "content-hash": "209aec6ab7d505a0e25f420b43e4a337",
"packages": [
{
"name": "brick/math",
@@ -1671,6 +1671,119 @@
],
"time": "2021-10-07T12:57:01+00:00"
},
+ {
+ "name": "jackiedo/dotenv-editor",
+ "version": "2.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/JackieDo/Laravel-Dotenv-Editor.git",
+ "reference": "24667e3704b281e0d6c56de340bed1dc718072a1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/JackieDo/Laravel-Dotenv-Editor/zipball/24667e3704b281e0d6c56de340bed1dc718072a1",
+ "reference": "24667e3704b281e0d6c56de340bed1dc718072a1",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/console": "^10.0|^9.0|^8.0|^7.0|^6.0|^5.8",
+ "illuminate/contracts": "^10.0|^9.0|^8.0|^7.0|^6.0|^5.8",
+ "illuminate/support": "^10.0|^9.0|^8.0|^7.0|^6.0|^5.8",
+ "jackiedo/path-helper": "^1.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.x-dev"
+ },
+ "laravel": {
+ "providers": [
+ "Jackiedo\\DotenvEditor\\DotenvEditorServiceProvider"
+ ],
+ "aliases": {
+ "DotenvEditor": "Jackiedo\\DotenvEditor\\Facades\\DotenvEditor"
+ }
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Jackiedo\\DotenvEditor\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jackie Do",
+ "email": "anhvudo@gmail.com"
+ }
+ ],
+ "description": "The .env file editor tool for Laravel 5.8+",
+ "keywords": [
+ "dotenv",
+ "dotenv-editor",
+ "laravel"
+ ],
+ "support": {
+ "issues": "https://github.com/JackieDo/Laravel-Dotenv-Editor/issues",
+ "source": "https://github.com/JackieDo/Laravel-Dotenv-Editor/tree/2.1.0"
+ },
+ "time": "2023-02-19T00:32:40+00:00"
+ },
+ {
+ "name": "jackiedo/path-helper",
+ "version": "v1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/JackieDo/Path-Helper.git",
+ "reference": "43179cfa17d01f94f4889f286430c2cec1071fb2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/JackieDo/Path-Helper/zipball/43179cfa17d01f94f4889f286430c2cec1071fb2",
+ "reference": "43179cfa17d01f94f4889f286430c2cec1071fb2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/helpers.php"
+ ],
+ "psr-4": {
+ "Jackiedo\\PathHelper\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jackie Do",
+ "email": "anhvudo@gmail.com"
+ }
+ ],
+ "description": "Helper class for working with local paths in PHP",
+ "homepage": "https://github.com/JackieDo/path-helper",
+ "keywords": [
+ "helper",
+ "helpers",
+ "library",
+ "path",
+ "paths",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/JackieDo/Path-Helper/issues",
+ "source": "https://github.com/JackieDo/Path-Helper/tree/v1.0.0"
+ },
+ "time": "2022-03-07T20:28:08+00:00"
+ },
{
"name": "khanamiryan/qrcode-detector-decoder",
"version": "2.0.2",
diff --git a/config/2fauth.php b/config/2fauth.php
index c80e6466..b208b0c9 100644
--- a/config/2fauth.php
+++ b/config/2fauth.php
@@ -12,6 +12,7 @@ return [
'version' => '4.0.2',
'repository' => 'https://github.com/Bubka/2FAuth',
'latestReleaseUrl' => 'https://api.github.com/repos/Bubka/2FAuth/releases/latest',
+ 'installDocUrl' => 'https://docs.2fauth.app/getting-started/installation/self-hosted-server/',
/*
|--------------------------------------------------------------------------
diff --git a/config/app.php b/config/app.php
index 94719955..75749772 100644
--- a/config/app.php
+++ b/config/app.php
@@ -121,7 +121,7 @@ return [
|
*/
- 'key' => strpos(env('APP_KEY'), 'base64:') !== false ? env('APP_KEY') : substr(env('APP_KEY'), 0, 32),
+ 'key' => str_starts_with(env('APP_KEY')??'', 'base64:') ? env('APP_KEY') : substr(env('APP_KEY')??'', 0, 32),
'cipher' => 'AES-256-CBC',
diff --git a/config/database.php b/config/database.php
index 92a285db..5fe023cf 100644
--- a/config/database.php
+++ b/config/database.php
@@ -15,7 +15,7 @@ return [
|
*/
- 'default' => env('DB_CONNECTION', 'mysql'),
+ 'default' => envUnlessEmpty('DB_CONNECTION', 'mysql'),
/*
|--------------------------------------------------------------------------
@@ -38,7 +38,7 @@ return [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DATABASE_URL'),
- 'database' => env('DB_DATABASE', database_path('database.sqlite')),
+ 'database' => envUnlessEmpty('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
],
@@ -53,10 +53,10 @@ return [
'mysql' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
- 'host' => env('DB_HOST', '127.0.0.1'),
- 'port' => env('DB_PORT', '3306'),
- 'database' => env('DB_DATABASE', 'forge'),
- 'username' => env('DB_USERNAME', 'forge'),
+ 'host' => envUnlessEmpty('DB_HOST', '127.0.0.1'),
+ 'port' => envUnlessEmpty('DB_PORT', '3306'),
+ 'database' => envUnlessEmpty('DB_DATABASE', '2fauth'),
+ 'username' => envUnlessEmpty('DB_USERNAME', '2fauth'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => 'utf8mb4',
@@ -66,17 +66,17 @@ return [
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
- PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
+ PDO::MYSQL_ATTR_SSL_CA => envUnlessEmpty('MYSQL_ATTR_SSL_CA', null),
]) : [],
],
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DATABASE_URL'),
- 'host' => env('DB_HOST', '127.0.0.1'),
- 'port' => env('DB_PORT', '5432'),
- 'database' => env('DB_DATABASE', 'forge'),
- 'username' => env('DB_USERNAME', 'forge'),
+ 'host' => envUnlessEmpty('DB_HOST', '127.0.0.1'),
+ 'port' => envUnlessEmpty('DB_PORT', '5432'),
+ 'database' => envUnlessEmpty('DB_DATABASE', '2fauth'),
+ 'username' => envUnlessEmpty('DB_USERNAME', '2fauth'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
@@ -88,10 +88,10 @@ return [
'sqlsrv' => [
'driver' => 'sqlsrv',
'url' => env('DATABASE_URL'),
- 'host' => env('DB_HOST', 'localhost'),
- 'port' => env('DB_PORT', '1433'),
- 'database' => env('DB_DATABASE', 'forge'),
- 'username' => env('DB_USERNAME', 'forge'),
+ 'host' => envUnlessEmpty('DB_HOST', 'localhost'),
+ 'port' => envUnlessEmpty('DB_PORT', '1433'),
+ 'database' => envUnlessEmpty('DB_DATABASE', '2fauth'),
+ 'username' => envUnlessEmpty('DB_USERNAME', '2fauth'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
diff --git a/config/dotenv-editor.php b/config/dotenv-editor.php
new file mode 100644
index 00000000..3367076c
--- /dev/null
+++ b/config/dotenv-editor.php
@@ -0,0 +1,37 @@
+ false,
+
+ /*
+ |----------------------------------------------------------------------
+ | Backup location
+ |----------------------------------------------------------------------
+ |
+ | This value is used when you backup your file. This value is the sub
+ | path from root folder of project application.
+ */
+
+ 'backupPath' => base_path('storage/dotenv-backups/'),
+
+ /*
+ |----------------------------------------------------------------------
+ | Always create backup folder
+ |----------------------------------------------------------------------
+ |
+ | If this setting is set to true, the backup folder set up in the
+ | 'backupPath' setting will always be created regardless of whether the
+ | backup is performed or not.
+ */
+
+ 'alwaysCreateBackupFolder' => false,
+];