Offer opt in to usage and error reporting during install (#13906)

and on the about page
This commit is contained in:
Tony Murray
2022-12-15 19:52:22 -06:00
committed by GitHub
parent 510f9d340d
commit 8ea3f5cd06
19 changed files with 336 additions and 192 deletions

View File

@@ -25,6 +25,7 @@
namespace LibreNMS;
use App\Models\Callback;
use App\Models\GraphType;
use Exception;
use Illuminate\Database\QueryException;
@@ -472,6 +473,9 @@ class Config
if (! self::has('snmp.unescape')) {
self::persist('snmp.unescape', version_compare(Version::get()->netSnmp(), '5.8.0', '<'));
}
if (! self::has('reporting.usage')) {
self::persist('reporting.usage', (bool) Callback::get('enabled'));
}
self::populateTime();

View File

@@ -50,6 +50,7 @@ use App\Models\Syslog;
use App\Models\Vlan;
use App\Models\Vrf;
use App\Models\WirelessSensor;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use LibreNMS\Config;
use LibreNMS\Data\Store\Rrd;
@@ -59,12 +60,12 @@ class AboutController extends Controller
{
public function index(Request $request)
{
$callback_status = Callback::get('enabled') === '1';
$version = Version::get();
return view('about.index', [
'callback_status' => $callback_status,
'callback_uuid' => $callback_status ? Callback::get('uuid') : null,
'usage_reporting_status' => Config::get('reporting.usage'),
'error_reporting_status' => Config::get('reporting.error'),
'reporting_clearable' => Callback::whereIn('name', ['uuid', 'error_reporting_uuid'])->exists(),
'db_schema' => $version->database(),
'git_log' => $version->git->log(),
@@ -105,4 +106,21 @@ class AboutController extends Controller
'stat_wireless' => WirelessSensor::count(),
]);
}
public function clearReportingData(): JsonResponse
{
$usage_uuid = Callback::get('uuid');
// try to clear usage data if we have a uuid
if ($usage_uuid) {
if (! \Http::post(Config::get('callback_clear'), ['uuid' => $usage_uuid])->successful()) {
return response()->json([], 500); // don't clear if this fails to delete upstream data
}
}
// clear all reporting ids
Callback::truncate();
return response()->json();
}
}

View File

@@ -26,9 +26,12 @@
namespace App\Http\Controllers\Install;
use Exception;
use Illuminate\Http\Request;
use LibreNMS\Config;
use LibreNMS\Exceptions\FileWriteFailedException;
use LibreNMS\Interfaces\InstallerStep;
use LibreNMS\Util\EnvHelper;
use LibreNMS\Util\Git;
class FinalizeController extends InstallationController implements InstallerStep
{
@@ -40,6 +43,29 @@ class FinalizeController extends InstallationController implements InstallerStep
return $this->redirectToIncomplete();
}
return view('install.finish', $this->formatData([
'can_update' => Git::make()->isAvailable(),
'success' => '',
'env' => '',
'config' => '',
'messages' => '',
'env_message' => '',
'config_message' => '',
]));
}
public function saveConfig(Request $request): \Illuminate\Http\JsonResponse
{
$request->validate([
'update_channel' => 'in:master,release',
'site_style' => 'in:light,dark',
]);
$this->saveSetting('update_channel', $request->get('update_channel', 'master'));
$this->saveSetting('site_style', $request->get('site_style'));
$this->saveSetting('reporting.error', $request->has('error_reporting'));
$this->saveSetting('reporting.usage', $request->has('usage_reporting'));
$env = '';
$config = '';
$config_file = base_path('config.php');
@@ -65,14 +91,14 @@ class FinalizeController extends InstallationController implements InstallerStep
$env_message = trans('install.finish.env_not_written');
}
return view('install.finish', $this->formatData([
return response()->json([
'success' => $success,
'env' => $env,
'config' => $config,
'messages' => $messages,
'env_message' => $env_message,
'config_message' => $config_message,
]));
]);
}
private function writeEnvFile()
@@ -138,6 +164,18 @@ class FinalizeController extends InstallationController implements InstallerStep
);
}
/**
* @param string $name
* @param mixed $value
* @return void
*/
private function saveSetting(string $name, $value): void
{
if (Config::get($name) !== $value) {
Config::persist($name, $value);
}
}
public function enabled(): bool
{
foreach ($this->hydrateControllers() as $step => $controller) {

View File

@@ -60,8 +60,8 @@ class AppServiceProvider extends ServiceProvider
private function bootCustomBladeDirectives()
{
Blade::if('config', function ($key) {
return \LibreNMS\Config::get($key);
Blade::if('config', function ($key, $value = true) {
return \LibreNMS\Config::get($key) == $value;
});
Blade::if('notconfig', function ($key) {
return ! \LibreNMS\Config::get($key);

View File

@@ -1,21 +0,0 @@
<?php
/*
* LibreNMS
*
* Copyright (c) 2014 Neil Lathwood <https://github.com/laf/ http://www.lathwood.co.uk/fa>
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version. Please see LICENSE.txt at the top level of
* the source code distribution for details.
*/
header('Content-type: text/plain');
if (! Auth::user()->hasGlobalAdmin()) {
exit('ERROR: You need to be admin');
}
\App\Models\Callback::set('enabled', '2');

View File

@@ -1,21 +0,0 @@
<?php
/*
* LibreNMS
*
* Copyright (c) 2014 Neil Lathwood <https://github.com/laf/ http://www.lathwood.co.uk/fa>
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version. Please see LICENSE.txt at the top level of
* the source code distribution for details.
*/
header('Content-type: text/plain');
if (! Auth::user()->hasGlobalAdmin()) {
exit('ERROR: You need to be admin');
}
\App\Models\Callback::set('enabled', (int) ($_POST['state'] == 'true'));

View File

@@ -4866,6 +4866,16 @@
"type": "boolean"
},
"reporting.dump_errors": {
"group": "system",
"section": "reporting",
"order": 1,
"default": false,
"type": "boolean"
},
"reporting.usage": {
"group": "system",
"section": "reporting",
"order": 0,
"default": false,
"type": "boolean"
},

View File

@@ -737,8 +737,8 @@ return [
'update_channel' => [
'description' => 'Definiere Updatekanal',
'options' => [
'master' => 'master',
'release' => 'release',
'master' => 'Daily',
'release' => 'Monthly',
],
],
'virsh' => [

View File

@@ -28,18 +28,19 @@ return [
'config_not_written' => 'Could not write config.php',
'config_written' => 'config.php file written',
'copied' => 'Copied to clipboard',
'dashboard' => 'Dashboard',
'env_manual' => 'Manually update :file with the following content',
'env_not_written' => 'Could not write .env file',
'env_written' => '.env file written',
'failed' => 'Failed to save .env',
'finish' => 'Finish Install',
'manual_copy' => 'Press Ctrl-C to copy',
'not_finished' => 'You have not quite finished yet!',
'retry' => 'Retry',
'statistics' => 'It would be great if you would consider contributing to our statistics, you can do this on the :about and check the box under Statistics.',
'statistics_link' => 'About LibreNMS Page',
'settings' => 'Additional Settings',
'success' => 'Install Complete',
'thanks' => 'Thank you for setting up LibreNMS.',
'title' => 'Finish Install',
'validate' => 'First, you need to :validate and fix any issues.',
'validate_link' => 'validate your install',
'validate_button' => 'Validate Install',
],
'install' => 'Install',
'migrate' => [

View File

@@ -72,6 +72,7 @@ return [
'proxy' => ['name' => 'Proxy'],
'updates' => ['name' => 'Updates'],
'server' => ['name' => 'Server'],
'reporting' => ['name' => 'Reporting'],
],
'webui' => [
'availability-map' => ['name' => 'Availability Map Settings'],
@@ -1250,6 +1251,16 @@ return [
'help' => 'Networks/IPs which will not be discovered automatically. Excludes also IPs from Autodiscovery Networks',
],
],
'reporting' => [
'error' => [
'description' => 'Send Error Reports',
'help' => 'Sends some errors to LibreNMS for analysis and fixing',
],
'usage' => [
'description' => 'Send Usage Reports',
'help' => 'Reports usage and versions to LibreNMS. To delete anonymous stats, visit the about page. You can view stats at https://stats.librenms.org',
],
],
'route_purge' => [
'description' => 'Route entries older than',
'help' => 'Cleanup done by daily.sh',
@@ -1381,7 +1392,7 @@ return [
'help' => 'Shrinks hostname to maximum length, but always complete subdomain parts',
],
'site_style' => [
'description' => 'Set the site css style',
'description' => 'Default Theme',
'options' => [
'blue' => 'Blue',
'dark' => 'Dark',
@@ -1511,10 +1522,10 @@ return [
'description' => 'Enable updates in ./daily.sh',
],
'update_channel' => [
'description' => 'Set update Channel',
'description' => 'Update Channel',
'options' => [
'master' => 'master',
'release' => 'release',
'master' => 'Daily',
'release' => 'Monthly',
],
],
'uptime_warning' => [

View File

@@ -1077,8 +1077,8 @@ return [
'update_channel' => [
'description' => 'Choisir le canal des mises à jour',
'options' => [
'master' => 'master',
'release' => 'release',
'master' => 'Daily',
'release' => 'Monthly',
],
],
'uptime_warning' => [

View File

@@ -1491,8 +1491,8 @@ return [
'update_channel' => [
'description' => 'Set update Channel',
'options' => [
'master' => 'master',
'release' => 'release',
'master' => 'Daily',
'release' => 'Monthly',
],
],
'uptime_warning' => [

View File

@@ -1482,8 +1482,8 @@ return [
'update_channel' => [
'description' => 'Визначити канал оновлень',
'options' => [
'master' => 'master',
'release' => 'release',
'master' => 'Daily',
'release' => 'Monthly',
],
],
'uptime_warning' => [

View File

@@ -752,8 +752,8 @@ return [
'update_channel' => [
'description' => '设定更新频道',
'options' => [
'master' => 'master',
'release' => 'release',
'master' => 'Daily',
'release' => 'Monthly',
],
],
'virsh' => [

View File

@@ -925,8 +925,8 @@ return [
'update_channel' => [
'description' => '設定更新頻道',
'options' => [
'master' => 'master',
'release' => 'release',
'master' => 'Daily',
'release' => 'Monthly',
],
],
'uptime_warning' => [

View File

@@ -90,29 +90,29 @@
</div>
<div class="col-md-6">
<h3>{{ __('Statistics') }}</h3>
<h3>{{ __('Reporting & Statistics') }}</h3>
<table class='table table-condensed'>
@admin
<tr>
<td colspan='4'>
<span class='bg-danger'>
<label for="callback">{{ __('Opt in to send anonymous usage statistics to LibreNMS?') }}</label><br />
</span>
<input type="checkbox" id="callback" data-size="normal" name="statistics" @if($callback_status) checked @endif>
<br />
{{ __('Online stats:') }} <a target="_blank" href='https://stats.librenms.org/'>stats.librenms.org</a>
<div>
<label for="reporting.usage" class="bg-info">{{ __('Opt in to send anonymous reports to LibreNMS?') }}</label>
</div>
<div>
{{ __('Error reporting:') }} <input type="checkbox" id="reporting.error" name="reporting" data-size="small" @if($error_reporting_status) checked @endif>
</div>
<div class="tw-mt-2">
{{ __('Usage statistics:') }} <input type="checkbox" id="reporting.usage" name="reporting" data-size="small" @if($usage_reporting_status) checked @endif> <a target="_blank" href='https://stats.librenms.org/'>stats.librenms.org</a>
</div>
@if($reporting_clearable)
<div class="tw-mt-2">
<button class='btn btn-danger btn-xs' type='submit' name='clear-reporting' id='clear-reporting'>{{ __('Clear reporting data') }}</button>
</div>
@endif
</td>
</tr>
@isset($callback_uuid)
<tr>
<td colspan='4'>
<button class='btn btn-danger btn-xs' type='submit' name='clear-stats' id='clear-stats'>{{ __('Clear remote stats') }}</button>
</td>
</tr>
@endisset
@endadmin
<tr>
@@ -202,29 +202,29 @@ along with this program. If not, see <a target="_blank" href="https://www.gnu.o
@section('scripts')
<script>
$("[name='statistics']").bootstrapSwitch('offColor','danger','size','mini');
$('input[name="statistics"]').on('switchChange.bootstrapSwitch', function(event, state) {
$("[name='reporting']").bootstrapSwitch('offColor','danger','size','mini');
$('input[name="reporting"]').on('switchChange.bootstrapSwitch', function(event, state) {
event.preventDefault();
const type = event.target.id;
$.ajax({
type: 'POST',
url: 'ajax_form.php',
data: { type: "callback-statistics", state: state},
dataType: "json",
type: 'PUT',
url: '{{ route('settings.update', '?') }}'.replace('?', type),
data: JSON.stringify({value: state}),
contentType: "application/json",
success: function(data){},
error:function(){
return $("#switch-state").bootstrapSwitch("toggle");
return $("#" + type).bootstrapSwitch("toggle");
}
});
});
$('#clear-stats').on("click", function(event) {
$('#clear-reporting').on("click", function(event) {
event.preventDefault();
$.ajax({
type: 'POST',
url: 'ajax_form.php',
data: { type: "callback-clear"},
dataType: "json",
success: function(data){
location.reload(true);
type: 'DELETE',
url: '{{ route('reporting.clear') }}',
success: function(){
$('#clear-reporting').remove();
$("#callback").bootstrapSwitch('state', false);
},
error:function(){}
});

View File

@@ -20,7 +20,7 @@
@endif
</span>
{{ __('install.database.credentials') }}
<span class="fa-pull-right"><i class="fa-solid fa-lg fa-chevron-down rotate-if-collapsed"></i></span>
<span class="float-right"><i class="fa-solid fa-lg fa-chevron-down rotate-if-collapsed"></i></span>
</div>
<div id="db-form-container" class="card-body collapse @if(!$valid_credentials) show @endif">
<form id="database-form" class="form-horizontal" role="form" method="post" action="{{ route('install.acton.test-database') }}">
@@ -90,7 +90,7 @@
@endif
</span>
{{ __('install.migrate.migrate') }}
<span class="fa-pull-right"><i class="fa-solid fa-lg fa-chevron-down rotate-if-collapsed"></i></span>
<span class="float-right"><i class="fa-solid fa-lg fa-chevron-down rotate-if-collapsed"></i></span>
</div>
<div id="migrate-container" class="card-body collapse @if(!$migrated) show @endif">
<div class="row">

View File

@@ -2,18 +2,71 @@
@section('content')
<div class="card mb-2">
<div class="card-header h6" data-toggle="collapse" data-target="#env-file-text" aria-expanded="{{ $success ? 'false' : 'true' }}">
@if($success)
<i class="fa-solid fa-lg fa-square-check text-success"></i>
@else
<i class="fa-solid fa-lg fa-rectangle-xmark text-danger"></i>
@endif
{{ $env_message }}
@if($env)<i class="fa-solid fa-lg fa-chevron-down rotate-if-collapsed pull-right"></i>@endif($env)
<div class="card-header h6">
{{ __('install.finish.settings') }}
</div>
@if($env)
<div id="env-file-text" class="card-body collapse @if(!$success) show @endif">
<button class="btn btn-primary float-right" onclick="location.reload()">{{ __('install.finish.retry') }}</button>
<div class="card-body">
<form id="settings">
@if($can_update)
<div class="mb-3">
<span>{{ __('settings.settings.update_channel.description') }}</span>
<div class="custom-control custom-radio custom-control-inline">
<input type="radio" id="update_channel_daily" name="update_channel" class="custom-control-input" value="master" @config('update_channel', 'master') checked @endconfig>
<label class="custom-control-label" for="update_channel_daily">{{ __('settings.settings.update_channel.options.master') }}</label>
</div>
<div class="custom-control custom-radio custom-control-inline">
<input type="radio" id="update_channel_monthly" name="update_channel" class="custom-control-input" value="release" @config('update_channel', 'release') checked @endconfig>
<label class="custom-control-label" for="update_channel_monthly">{{ __('settings.settings.update_channel.options.release') }}</label>
</div>
</div>
@endif
<div class="mb-3">
<span>{{ __('settings.settings.site_style.description') }}</span>
<div class="custom-control custom-radio custom-control-inline">
<input type="radio" id="site_style_light" name="site_style" class="custom-control-input" value="light" @config('site_style', 'light') checked @endconfig>
<label class="custom-control-label" for="site_style_light">{{ __('settings.settings.site_style.options.light') }}</label>
</div>
<div class="custom-control custom-radio custom-control-inline">
<input type="radio" id="site_style_dark" name="site_style" class="custom-control-input" value="dark" @config('site_style', 'dark') checked @endconfig>
<label class="custom-control-label" for="site_style_dark">{{ __('settings.settings.site_style.options.dark') }}</label>
</div>
</div>
<div class="custom-control custom-checkbox mb-3">
<input type="checkbox" class="custom-control-input" id="usage_reporting" name="usage_reporting" @config('reporting.usage') checked @endconfig>
<label class="custom-control-label" for="usage_reporting"><a target="_blank" href="https://stats.librenms.org/">{{ __('settings.settings.reporting.usage.description') }}</a></label>
</div>
<div class="custom-control custom-checkbox mb-3">
<input type="checkbox" class="custom-control-input" id="error_reporting" name="error_reporting" @config('reporting.error') checked @endconfig>
<label class="custom-control-label" for="error_reporting">{{ __('settings.settings.reporting.error.description') }}</label>
</div>
<div>
<button type="button" class="btn btn-primary finalize-buttons">{{ __('install.finish.finish') }}</button>
</div>
</form>
</div>
</div>
<div id="finished" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="card mb-2">
<div id="env-header" class="card-header h6" data-toggle="collapse" data-target="#env-file-text" aria-expanded="false">
<i id="env-icon" class="fa-solid fa-lg"></i>
<span id="env-message"></span>
<span id="env-chevron" class="float-right"><i class="fa-solid fa-lg fa-chevron-down rotate-if-collapsed"></i></span>
</div>
<div id="env-file-text" class="card-body collapse">
<button class="btn btn-primary float-right finalize-buttons">{{ __('install.finish.retry') }}</button>
<strong>
{{ __('install.finish.env_manual', ['file' => base_path('.env')]) }}
</strong>
@@ -29,17 +82,15 @@
<i class="fa-solid fa-clipboard"></i>
</button>
</div>
<pre id="env-content" class="card bg-light p-3">{{ $env }}</pre>
<pre id="env-content" class="card bg-light p-3"></pre>
</div>
@endif
</div>
<div class="card mb-2">
<div class="card-header h6" data-toggle="collapse" data-target="#config-file-text" aria-expanded="false">
<i class="fa-solid fa-lg fa-square-check text-success"></i>
{{ $config_message }}
@if($config)<i class="fa-solid fa-lg fa-chevron-down rotate-if-collapsed pull-right"></i>@endif
<i id="config-icon" class="fa-solid fa-lg"></i>
<span id="config-message"></span>
<span id="config-chevron" class="float-right"><i class="fa-solid fa-lg fa-chevron-down rotate-if-collapsed"></i></span>
</div>
@if($config)
<div id="config-file-text" class="card-body collapse">
<strong>
{{ __('install.finish.config_not_required') }}
@@ -56,38 +107,89 @@
<i class="fa-solid fa-clipboard"></i>
</button>
</div>
<pre id="config-content" class="card bg-light p-3">{{ $config }}</pre>
</div>
@endif
</div>
@if($success)
<div class="row">
<div class="col-12">
<div class="alert alert-warning">
<p>{{ __('install.finish.not_finished') }}</p>
<p>
{{ explode('|', __('install.finish.validate', ['validate' => '|']), 2)[0] }}
<a href="{{ url('validate') }}">{{ __('install.finish.validate_link') }}</a>
{{ explode('|', __('install.finish.validate', ['validate' => '|']), 2)[1] }}
</p>
<pre id="config-content" class="card bg-light p-3"></pre>
</div>
</div>
</div>
<div class="row">
<div class="row" id="success-message">
<div class="col-12">
<div class="alert alert-success">
<p>{{ __('install.finish.thanks') }}</p>
{{ explode('|', __('install.finish.statistics', ['about' => '|']), 2)[0] }}
<a href="{{ url('about') }}">{{ __('install.finish.statistics_link') }}</a>
{{ explode('|', __('install.finish.statistics', ['about' => '|']), 2)[1] }}
<i class="fa-solid fa-2x fa-heart" style="color: #ff4033;"></i>
<span class="h4 align-text-bottom">{{ __('install.finish.thanks') }}</span>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button id="modal-retry" type="button" class="btn btn-primary finalize-buttons">{{ __('install.finish.retry') }}</button>
<div id="modal-finished">
<a href="{{ route('home') }}">
<button type="button" class="btn btn-secondary">{{ __('install.finish.dashboard') }}</button>
</a>
<a href="{{ url('validate') }}">
<button type="button" class="btn btn-primary">{{ __('install.finish.validate_button') }}</button>
</a>
</div>
</div>
</div>
</div>
</div>
@endif
@endsection
@section('scripts')
<script>
$('.finalize-buttons').on('click', function (e) {
var data = $('#settings').serializeArray();
$.ajax('{{ route('install.finish.save') }}', {
method: 'post',
headers: {'X-CSRF-TOKEN': '{{ csrf_token() }}'},
data: data
}).done((result) => {
if (result.success) {
$('#env-header').attr('aria-expanded', 'false');
$('#env-file-text').addClass('show');
$('#success-message').show();
$('.modal-title').text('{{ __('install.finish.success') }}')
$('#modal-retry').hide();
$('#modal-finished').show();
} else {
$('#env-header').attr('aria-expanded', 'true');
$('#env-file-text').removeClass('show');
$('#success-message').hide();
$('.modal-title').text('{{ __('install.finish.failed') }}')
$('#modal-retry').show();
$('#modal-finished').hide();
}
$('#env-message').text(result.env_message);
$('#env-content').text(result.env);
if (result.env) {
$('#env-chevron').show();
$('#env-file-text').removeAttr('style').addClass('show');
$('#env-icon').removeClass(['fa-square-check', 'text-success']).addClass(['fa-rectangle-xmark', 'text-danger']);
} else {
$('#env-file-text').hide();
$('#env-chevron').hide();
$('#env-icon').addClass(['fa-square-check', 'text-success']).removeClass(['fa-rectangle-xmark', 'text-danger']);
}
$('#config-message').text(result.config_message);
$('#config-content').text(result.config);
if (result.config) {
$('#config-file-text').removeAttr('style');
$('#config-chevron').show();
$('#config-icon').removeClass(['fa-square-check', 'text-success']).addClass(['fa-rectangle-xmark', 'text-danger']);
} else {
$('#config-file-text').hide();
$('#config-chevron').hide();
$('#config-icon').addClass(['fa-square-check', 'text-success']).removeClass(['fa-rectangle-xmark', 'text-danger']);
}
$('#finished').modal('show')
}).fail(function (output) {
location.reload();
});
});
var clipboard = new ClipboardJS('.copy-btn');
clipboard.on('success', function (e) {
$(e.trigger).tooltip('show');

View File

@@ -51,7 +51,8 @@ Route::group(['middleware' => ['auth'], 'guard' => 'auth'], function () {
Route::get('locations', 'LocationController@index');
Route::resource('preferences', 'UserPreferencesController', ['only' => ['index', 'store']]);
Route::resource('users', 'UserController');
Route::get('about', 'AboutController@index');
Route::get('about', [\App\Http\Controllers\AboutController::class, 'index'])->name('about');
Route::delete('reporting', [\App\Http\Controllers\AboutController::class, 'clearReportingData'])->name('reporting.clear');
Route::get('authlog', 'UserController@authlog');
Route::get('overview', 'OverviewController@index')->name('overview');
Route::get('/', 'OverviewController@index')->name('home');
@@ -227,6 +228,7 @@ Route::group(['prefix' => 'install', 'namespace' => 'Install'], function () {
Route::get('/user', 'MakeUserController@index')->name('install.user');
Route::get('/finish', 'FinalizeController@index')->name('install.finish');
Route::post('/finish', 'FinalizeController@saveConfig')->name('install.finish.save');
Route::post('/user/create', 'MakeUserController@create')->name('install.action.user');
Route::post('/database/test', 'DatabaseController@test')->name('install.acton.test-database');
Route::get('/ajax/database/migrate', 'DatabaseController@migrate')->name('install.action.migrate');