3
0
mirror of https://github.com/snipe/snipe-it.git synced 2026-02-04 17:15:38 +00:00

Merge pull request #18302 from grokability/preflight-quickstart-cleanup

Preflight quickstart cleanup
This commit is contained in:
snipe
2025-12-05 17:20:29 +00:00
committed by GitHub
13 changed files with 663 additions and 420 deletions

View File

@ -6,38 +6,31 @@ use App\Helpers\Helper;
use App\Helpers\StorageHelper;
use App\Http\Requests\ImageUploadRequest;
use App\Http\Requests\SettingsSamlRequest;
use App\Http\Requests\SetupUserRequest;
use App\Http\Requests\StoreLabelSettings;
use App\Http\Requests\StoreLdapSettings;
use App\Http\Requests\StoreLocalizationSettings;
use App\Http\Requests\StoreNotificationSettings;
use App\Http\Requests\StoreLabelSettings;
use App\Http\Requests\StoreSecuritySettings;
use App\Models\Asset;
use App\Models\CustomField;
use App\Models\Group;
use App\Models\Labels\Label as LabelModel;
use App\Models\Setting;
use App\Models\Asset;
use App\Models\User;
use App\Notifications\FirstAdminNotification;
use App\Notifications\MailTest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\JsonResponse;
use \Illuminate\Contracts\View\View;
use Illuminate\Support\Str;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use \Illuminate\Contracts\View\View;
/**
* This controller handles all actions related to Settings for
@ -47,224 +40,6 @@ use Symfony\Component\HttpFoundation\BinaryFileResponse;
*/
class SettingsController extends Controller
{
/**
* Checks to see whether or not the database has a migrations table
* and a user, otherwise display the setup view.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v3.0]
*
* @return \Illuminate\Contracts\View\View | \Illuminate\Http\Response
*/
public function getSetupIndex() : View
{
$start_settings['php_version_min'] = false;
if (version_compare(PHP_VERSION, config('app.min_php'), '<')) {
return response('<center><h1>This software requires PHP version '.config('app.min_php').' or greater. This server is running '.PHP_VERSION.'. </h1><h2>Please upgrade PHP on this server and try again. </h2></center>', 500);
}
try {
$conn = DB::select('select 2 + 2');
$start_settings['db_conn'] = true;
$start_settings['db_name'] = DB::connection()->getDatabaseName();
$start_settings['db_error'] = null;
} catch (\PDOException $e) {
$start_settings['db_conn'] = false;
$start_settings['db_name'] = config('database.connections.mysql.database');
$start_settings['db_error'] = $e->getMessage();
}
$start_settings['url_config'] = trim(config('app.url'), '/'). '/setup';
$start_settings['real_url'] = request()->url();
$start_settings['url_valid'] = $start_settings['url_config'] === $start_settings['real_url'];
$start_settings['php_version_min'] = true;
// Curl the .env file to make sure it's not accessible via a browser
$start_settings['env_exposed'] = $this->dotEnvFileIsExposed();
if (App::Environment('production') && (true == config('app.debug'))) {
$start_settings['debug_exposed'] = true;
} else {
$start_settings['debug_exposed'] = false;
}
$environment = app()->environment();
if ('production' != $environment) {
$start_settings['env'] = $environment;
$start_settings['prod'] = false;
} else {
$start_settings['env'] = $environment;
$start_settings['prod'] = true;
}
$start_settings['owner'] = '';
if (function_exists('posix_getpwuid')) { // Probably Linux
$owner = posix_getpwuid(fileowner($_SERVER['SCRIPT_FILENAME']));
// This *should* be an array, but we've seen this return a bool in some chrooted environments
if (is_array($owner)) {
$start_settings['owner'] = $owner['name'];
}
}
if (($start_settings['owner'] === 'root') || ($start_settings['owner'] === '0')) {
$start_settings['owner_is_admin'] = true;
} else {
$start_settings['owner_is_admin'] = false;
}
$start_settings['writable'] = $this->storagePathIsWritable();
$start_settings['gd'] = extension_loaded('gd');
return view('setup/index')
->with('step', 1)
->with('start_settings', $start_settings)
->with('section', 'Pre-Flight Check');
}
/**
* Determine if the .env file accessible via a browser.
*
* @return bool This method will return true when exceptions (such as curl exception) is thrown.
* Check the log files to see more details about the exception.
*/
protected function dotEnvFileIsExposed() : bool
{
try {
return Http::withoutVerifying()->timeout(10)
->accept('*/*')
->get(URL::to('.env'))
->successful();
} catch (\Exception $e) {
Log::debug($e->getMessage());
return true;
}
}
/**
* Determine if the app storage path is writable.
*/
protected function storagePathIsWritable(): bool
{
return File::isWritable(storage_path()) &&
File::isWritable(storage_path('framework')) &&
File::isWritable(storage_path('framework/cache')) &&
File::isWritable(storage_path('framework/sessions')) &&
File::isWritable(storage_path('framework/views')) &&
File::isWritable(storage_path('logs'));
}
/**
* Save the first admin user from Setup.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
*
*/
public function postSaveFirstAdmin(SetupUserRequest $request) : RedirectResponse
{
$user = new User();
$user->first_name = $data['first_name'] = $request->input('first_name');
$user->last_name = $request->input('last_name');
$user->email = $data['email'] = $request->input('email');
$user->activated = 1;
$permissions = ['superuser' => 1];
$user->permissions = json_encode($permissions);
$user->username = $data['username'] = $request->input('username');
$user->password = bcrypt($request->input('password'));
$data['password'] = $request->input('password');
$settings = new Setting();
$settings->full_multiple_companies_support = $request->input('full_multiple_companies_support', 0);
$settings->site_name = $request->input('site_name');
$settings->alert_email = $request->input('email');
$settings->alerts_enabled = 1;
$settings->pwd_secure_min = 10;
$settings->brand = 1;
$settings->locale = $request->input('locale', 'en-US');
$settings->default_currency = $request->input('default_currency', 'USD');
$settings->created_by = 1;
$settings->email_domain = $request->input('email_domain');
$settings->email_format = $request->input('email_format');
$settings->next_auto_tag_base = 1;
$settings->auto_increment_assets = $request->input('auto_increment_assets', 0);
$settings->auto_increment_prefix = $request->input('auto_increment_prefix');
$settings->zerofill_count = $request->input('zerofill_count') ?: 0;
if ((! $user->isValid()) || (! $settings->isValid())) {
return redirect()->back()->withInput()->withErrors($user->getErrors())->withErrors($settings->getErrors());
} else {
$user->save();
Auth::login($user, true);
$settings->save();
if ($request->input('email_creds') == '1') {
$data = [];
$data['email'] = $user->email;
$data['username'] = $user->username;
$data['first_name'] = $user->first_name;
$data['last_name'] = $user->last_name;
$data['password'] = $request->input('password');
$user->notify(new FirstAdminNotification($data));
}
return redirect()->route('setup.done');
}
}
/**
* Return the admin user creation form in Setup.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v3.0]
*/
public function getSetupUser() : View
{
return view('setup/user')
->with('step', 3)
->with('section', 'Create a User');
}
/**
* Return the view that tells the user that the Setup is done.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v3.0]
*/
public function getSetupDone() : View
{
return view('setup/done')
->with('step', 4)
->with('section', 'Done!');
}
/**
* Migrate the database tables, and return the output
* to a view for Setup.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v3.0]
*/
public function getSetupMigrate() : View
{
Artisan::call('migrate', ['--force' => true]);
if ((! file_exists(storage_path().'/oauth-private.key')) || (! file_exists(storage_path().'/oauth-public.key'))) {
Artisan::call('migrate', ['--path' => 'vendor/laravel/passport/database/migrations', '--force' => true]);
Artisan::call('passport:install', ['--no-interaction' => true]);
}
return view('setup/migrate')
->with('output', 'Databases installed!')
->with('step', 2)
->with('section', 'Create Database Tables');
}
/**
* Return a view that shows some of the key settings.

View File

@ -0,0 +1,270 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\SetupUserRequest;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\FirstAdminNotification;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\URL;
use \Illuminate\Contracts\View\View;
/**
* This controller handles all actions related to Settings for
* the Snipe-IT Asset Management application.
*
* @version v1.0
*/
class SetupController extends Controller
{
/**
* Checks to see whether or not the database has a migrations table
* and a user, otherwise display the setup view.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v3.0]
*
* @return \Illuminate\Contracts\View\View | \Illuminate\Http\Response
*/
public function getSetupIndex() : View
{
$start_settings['php_version_min'] = false;
if (version_compare(PHP_VERSION, config('app.min_php'), '<')) {
return response('<center><h1>This software requires PHP version '.config('app.min_php').' or greater. This server is running '.PHP_VERSION.'. </h1><h2>Please upgrade PHP on this server and try again. </h2></center>', 500);
}
try {
$conn = DB::select('select 2 + 2');
$start_settings['db_conn'] = true;
$start_settings['db_name'] = DB::connection()->getDatabaseName();
$start_settings['db_error'] = null;
} catch (\PDOException $e) {
$start_settings['db_conn'] = false;
$start_settings['db_name'] = config('database.connections.mysql.database');
$start_settings['db_error'] = $e->getMessage();
}
$start_settings['url_config'] = trim(config('app.url'), '/'). '/setup';
$start_settings['real_url'] = request()->url();
$start_settings['url_valid'] = $start_settings['url_config'] === $start_settings['real_url'];
$start_settings['php_version_min'] = true;
// Curl the .env file to make sure it's not accessible via a browser
$start_settings['env_exposed'] = $this->dotEnvFileIsExposed();
if (App::Environment('production') && (true == config('app.debug'))) {
$start_settings['debug_exposed'] = true;
} else {
$start_settings['debug_exposed'] = false;
}
$environment = app()->environment();
if ('production' != $environment) {
$start_settings['env'] = $environment;
$start_settings['prod'] = false;
} else {
$start_settings['env'] = $environment;
$start_settings['prod'] = true;
}
$start_settings['owner'] = '';
if (function_exists('posix_getpwuid')) { // Probably Linux
$owner = posix_getpwuid(fileowner($_SERVER['SCRIPT_FILENAME']));
// This *should* be an array, but we've seen this return a bool in some chrooted environments
if (is_array($owner)) {
$start_settings['owner'] = $owner['name'];
}
}
if (($start_settings['owner'] === 'root') || ($start_settings['owner'] === '0')) {
$start_settings['owner_is_admin'] = true;
} else {
$start_settings['owner_is_admin'] = false;
}
$start_settings['writable'] = $this->storagePathIsWritable();
$start_settings['gd'] = extension_loaded('gd');
return view('setup/index')
->with('step', 1)
->with('start_settings', $start_settings)
->with('section', trans('general.setup_config_check'))
->with('icon', 'fa-regular fa-rectangle-list');
}
/**
* Determine if the .env file accessible via a browser.
*
* @return bool This method will return true when exceptions (such as curl exception) is thrown.
* Check the log files to see more details about the exception.
*/
protected function dotEnvFileIsExposed() : bool
{
try {
return Http::withoutVerifying()->timeout(10)
->accept('*/*')
->get(URL::to('.env'))
->successful();
} catch (\Exception $e) {
Log::debug($e->getMessage());
return true;
}
}
/**
* Determine if the app storage path is writable.
*/
protected function storagePathIsWritable(): bool
{
return File::isWritable(storage_path()) &&
File::isWritable(storage_path('framework')) &&
File::isWritable(storage_path('framework/cache')) &&
File::isWritable(storage_path('framework/sessions')) &&
File::isWritable(storage_path('framework/views')) &&
File::isWritable(storage_path('logs'));
}
/**
* Save the first admin user from Setup.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
*
*/
public function postSaveFirstAdmin(SetupUserRequest $request) : RedirectResponse
{
$user = new User();
$user->first_name = $data['first_name'] = $request->input('first_name');
$user->last_name = $request->input('last_name');
$user->email = $data['email'] = $request->input('email');
$user->activated = 1;
$permissions = ['superuser' => 1];
$user->permissions = json_encode($permissions);
$user->username = $data['username'] = $request->input('username');
$user->password = bcrypt($request->input('password'));
$data['password'] = $request->input('password');
$settings = new Setting();
$settings->full_multiple_companies_support = $request->input('full_multiple_companies_support', 0);
$settings->site_name = $request->input('site_name');
$settings->alert_email = $request->input('email');
$settings->alerts_enabled = 1;
$settings->pwd_secure_min = 10;
$settings->brand = 1;
$settings->link_light_color = $request->input('link_light_color', '#296282');
$settings->link_dark_color = $request->input('link_dark_color', '#296282');
$settings->nav_link_color = $request->input('nav_link_color', '#FFFFFF');
$settings->locale = $request->input('locale', 'en-US');
$settings->default_currency = $request->input('default_currency', 'USD');
$settings->created_by = 1;
$settings->email_domain = $request->input('email_domain');
$settings->email_format = $request->input('email_format');
$settings->next_auto_tag_base = 1;
$settings->auto_increment_assets = $request->input('auto_increment_assets', 0);
$settings->auto_increment_prefix = $request->input('auto_increment_prefix');
$settings->zerofill_count = $request->input('zerofill_count') ?: 0;
if ((! $user->isValid()) || (! $settings->isValid())) {
return redirect()->back()->withInput()->withErrors($user->getErrors())->withErrors($settings->getErrors());
} else {
$user->save();
Auth::login($user, true);
$settings->save();
if ($request->input('email_creds') == '1') {
$data = [];
$data['email'] = $user->email;
$data['username'] = $user->username;
$data['first_name'] = $user->first_name;
$data['last_name'] = $user->last_name;
$data['password'] = $request->input('password');
$user->notify(new FirstAdminNotification($data));
}
return redirect()
->route('setup.done')
->with('section', trans('general.setup_create_admin'))
->with('icon', 'fa-solid fa-champagne-glasses')
->with('success', trans('admin/settings/general.create_admin_success'));
}
}
/**
* Return the admin user creation form in Setup.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v3.0]
*/
public function getSetupUser() : View
{
return view('setup/user')
->with('step', 3)
->with('section', trans('general.setup_create_admin'))
->with('icon', 'fa-solid fa-user-plus');
}
/**
* Return the view that tells the user that the Setup is done.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v3.0]
*/
public function getSetupDone() : View
{
return view('setup/done')
->with('success', trans('general.create_admin_success'))
->with('step', 4)
->with('icon', 'fa-solid fa-champagne-glasses fa-shake')
->with('section', trans('general.setup_done'));
}
/**
* Migrate the database tables, and return the output
* to a view for Setup.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v3.0]
*/
public function setupMigrate()
{
Artisan::call('migrate', ['--force' => true]);
$output = Artisan::output();
if ((! file_exists(storage_path().'/oauth-private.key')) || (! file_exists(storage_path().'/oauth-public.key'))) {
Artisan::call('migrate', ['--path' => 'vendor/laravel/passport/database/migrations', '--force' => true]);
Artisan::call('passport:install', ['--no-interaction' => true]);
}
return view('setup/migrate')
->with('success', trans('general.create_admin_success'))
->with('output', trim($output))
->with('step', 2)
->with('section', trans('general.setup_create_database'))
->with('icon', 'fa-solid fa-database');
}
}

View File

@ -28,7 +28,6 @@ class SetupUserRequest extends Request
'username' => 'required|string|min:2|unique:users,username,NULL,deleted_at',
'email' => 'email|unique:users,email',
'password' => 'required|min:8|confirmed',
'email_domain' => 'required|min:4',
];
}

View File

@ -366,13 +366,14 @@ return [
'employee_number' => 'Employee Number',
'create_admin_user' => 'Create a User ::',
'create_admin_success' => 'Success! Your admin user has been added!',
'create_admin_redirect' => 'Click here to go to your app login!',
'create_admin_redirect' => 'Go To Dashboard',
'setup_migrations' => 'Database Migrations ::',
'setup_no_migrations' => 'There was nothing to migrate. Your database tables were already set up!',
'setup_successful_migrations' => 'Your database tables have been created',
'setup_migration_output' => 'Migration output:',
'setup_migration_create_user' => 'Next: Create User',
'setup_migration_create_user' => 'Save User and Finish',
'ldap_settings_link' => 'LDAP Settings Page',
'setup_create_user_page_explanation' => 'Here you will create your first superadmin user and set some basic application setting defaults. (These can always be changed later in the Admin Settings section.) ',
'slack_test' => 'Test <i class="fab fa-slack"></i> Integration',
'status_label_name' => 'Status Label Name',
'super_admin_only' => 'Super Admin Only',

View File

@ -384,9 +384,10 @@ return [
'setup_step_3' => 'Step 3',
'setup_step_4' => 'Step 4',
'setup_config_check' => 'Configuration Check',
'setup_create_database' => 'Create database tables',
'setup_create_admin' => 'Create an admin user',
'setup_done' => 'Finished!',
'setup_create_database' => 'Create Database Tables',
'setup_create_admin' => 'Create an Admin User',
'setup_next' => 'Next',
'setup_done' => 'Setup Complete!',
'bulk_edit_about_to' => 'You are about to edit the following: ',
'checked_out' => 'Checked Out',
'checked_out_to' => 'Checked out to',

View File

@ -31,7 +31,10 @@
}
.preflight-error {
color: red;
color: #b60707;
}
.preflight-info {
font-size: 18px;
}
.preflight-warning {
@ -50,7 +53,26 @@
font-size: 16px;
}
body {
background-color: #ecf0f5;
}
.bs-wizard-info {
color: #959495 !important;
}
h4 {
line-height: 25px;
}
p, li {
font-size: 15px;
line-height: 25px;
}
li {
display: block;
}
</style>
</head>
@ -58,9 +80,9 @@
<div class="container">
<div class="row">
<div class="col-lg-10 col-lg-offset-1">
<h1 class="page-header">Snipe-IT {{ trans('general.pre_flight') }}</h1>
<h1 class="page-header"><img src="../img/logo.png" style="height: 65px;" alt="Snipe-IT logo"> {{ trans('general.pre_flight') }}</h1>
</div>
<div class="col-lg-11 col-lg-offset-1">
<div class="col-lg-12">
<div class="row bs-wizard" style="border-bottom:0;">
@ -68,13 +90,13 @@
<div class="text-center bs-wizard-stepnum">{{ trans('general.setup_step_1') }}</div>
<div class="progress"><div class="progress-bar"></div></div>
<a href="{{ route('setup') }}" class="bs-wizard-dot"></a>
<div class="bs-wizard-info text-center">{{ trans('general.setup_config_check') }}</div>
<div class="bs-wizard-info text-center" style="padding-left: 90px;">{{ trans('general.setup_config_check') }}</div>
</div>
<div class="col-xs-3 bs-wizard-step {{ ($step == 2) ? 'active': (($step < 2) ? 'disabled' : 'complete') }}"><!-- complete -->
<div class="text-center bs-wizard-stepnum">{{ trans('general.setup_step_2') }}</div>
<div class="progress"><div class="progress-bar"></div></div>
<a href="{{ route('setup.migrate') }}" class="bs-wizard-dot"></a>
<a href="#" class="bs-wizard-dot"></a>
<div class="bs-wizard-info text-center">{{ trans('general.setup_create_database') }}</div>
</div>
@ -94,25 +116,31 @@
</div>
</div>
<div class="col-lg-10 col-lg-offset-1" style="padding-top: 50px;">
<div class="panel panel-default">
<div class="panel-heading">
{{ $section }}
<div class="box box-default">
<div class="box-header with-border">
<h4><i class="{{ $icon ?? '' }}" style="--fa-animation-duration: 10s; --fa-animation-iteration-count: 3;"></i> {{ $section }}</h4>
</div>
<div class="panel-body">
<div class="box-body">
@include('notifications')
<!-- Content -->
@yield('content')
</div>
<div class="panel-footer text-right">
<div class="box-footer text-right">
@section('button')
@show
</div>
</div>
<strong>Snipe-IT {{ trans('general.version') }}</strong> {{ config('version.app_version') }} -
{{ trans('general.build') }} {{ config('version.build_version') }} ({{ config('version.branch') }})
</div>
</div>
</div>

View File

@ -7,13 +7,104 @@
{{-- Page content --}}
@section('content')
<div class="col-lg-12" style="padding-top: 20px;">
<div class="col-md-12">
<div class="alert alert-warning">
<i class="fas fa-check"></i>
{{ trans('general.create_admin_success') }}
<style>
.well-warning {
color: #8a6d3b;
background-color: #fcf8e3;
border-color: #faebcc;
}
</style>
<!-- Notifications -->
<div class="col-md-12">
<p>
If you're already familiar with Snipe-IT, you can get started right away by <strong><a href="{{ config('app.url') }}">heading right to your dashboard</a></strong>, or if it's your first time using Snipe-IT, you can check out some of the useful resources below:
</p>
<div class="well well-sm">
<div class="row">
<div class="col-md-6">
<ul>
<li><i class="fa-solid fa-book fa-fw"></i> <a href="https://snipe-it.readme.io/docs/overview#/" target="_blank">Overview <x-icon type="external-link" /></a></li>
<li><i class="fa-solid fa-book fa-fw"></i> <a href="https://snipe-it.readme.io/docs/getting-started#/" target="_blank">Getting Started <x-icon type="external-link" /></a></li>
<li><i class="fa-solid fa-book fa-fw"></i> <a href="https://snipe-it.readme.io/reference/api-overview#/" target="_blank">API Documentation <x-icon type="external-link" /></a></li>
<li><i class="fa-solid fa-book fa-fw"></i> <a href="https://snipe-it.readme.io/docs/importing-users#/" target="_blank">Importing Users <x-icon type="external-link" /></a></li>
<li><i class="fa-solid fa-book fa-fw"></i> <a href="https://snipe-it.readme.io/docs/importing-assets#/" target="_blank">Importing Assets <x-icon type="external-link" /></a></li>
</ul>
</div>
<div class="col-md-6">
<ul>
<li><i class="fa-solid fa-book fa-fw"></i> <a href="https://snipe-it.readme.io/reference/api-overview#/" target="_blank">API Documentation <x-icon type="external-link" /></a></li>
<li><i class="fa-solid fa-book fa-fw"></i> <a href="https://snipe-it.readme.io/docs/saml#/" target="_blank">SAML Authentication<x-icon type="external-link" /></a></li>
<li><i class="fa-solid fa-book fa-fw"></i> <a href="https://snipe-it.readme.io/docs/scim#/" target="_blank">SCIM <x-icon type="external-link" /></a></li>
<li><i class="fa-solid fa-book fa-fw"></i> <a href="https://snipe-it.readme.io/docs/ldap-sync-login#/" target="_blank">LDAP Sync &amp; Login <x-icon type="external-link" /></a></li>
<li><i class="fa-solid fa-book fa-fw"></i> <a href="https://snipe-it.readme.io/docs/webhook-integration#/" target="_blank">Webhooks <x-icon type="external-link" /></a></li>
</ul>
</div>
</div>
</div>
<div class="well well-sm well-warning">
<p>
<x-icon type="tip" /> <strong>Important Note Syncing Users via SCIM or LDAP</strong>
</p>
<p>
If you plan on using SCIM or LDAP syncing to keep your user lists up to date with your directory services,
make sure the username format for any users imported via CSV matches your directory service username format to avoid duplicating users in Snipe-IT.
</p>
</div>
<p>
Don't forget to join our communities! You can find us on:
</p>
<ul>
<li><i class="fa-brands fa-github fa-fw"></i> <a href="https://github.com/grokability/snipe-it" target="_blank">Github <x-icon type="external-link" /></a></li>
<li><i class="fa-brands fa-discord fa-fw"></i> <a href="https://discord.gg/yZFtShAcKk" target="_blank">Discord <x-icon type="external-link" /></a></li>
<li><i class="fa-brands fa-bluesky fa-fw"></i> <a href="https://bsky.app/profile/snipeitapp.com" target="_blank">Bluesky <x-icon type="external-link" /></a></li>
<li><i class="fa-brands fa-mastodon fa-fw"></i> <a href="https://hachyderm.io/@grokability" target="_blank">Mastodon <x-icon type="external-link" /></a></li>
<li><i class="fa-solid fa-square-rss fa-fw"></i> Our blog at <a href="https://grokstar.dev" target="_blank">Grokstar.Dev <x-icon type="external-link" /></a></li>
</ul>
<p>
Subscribe on Github for notifications about new releases. (We recommend selecting "Releases Only" for most users - the repo can get noisy.)
</p>
</div>
<p>{{ trans('general.create_admin_redirect') }} <a href="{{ config('app.url') }}">{{ config('app.url') }}</a></p>
</div>
@stop
@section('button')
<a class="btn btn-primary" href="{{ config('app.url') }}">{{ trans('admin/settings/general.create_admin_redirect') }}
<i class="fa-solid fa-angles-right"></i>
</a>
@parent
@stop
<script>
var duration = 2000;
var animationEnd = Date.now() + duration;
var defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };
function randomInRange(min, max) {
return Math.random() * (max - min) + min;
}
var interval = setInterval(function() {
var timeLeft = animationEnd - Date.now();
if (timeLeft <= 0) {
return clearInterval(interval);
}
var particleCount = 50 * (timeLeft / duration);
// since particles fall down, start a bit higher than random
confetti({ ...defaults, particleCount, origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 } });
confetti({ ...defaults, particleCount, origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 } });
}, 250);
</script>

View File

@ -9,7 +9,7 @@ Create a User ::
{{-- Page content --}}
@section('content')
<p>This page will do a system check to make sure your configuration looks correct. We'll add your first user on the next page. </p>
<h4> First let's do a quick system check to make sure your configuration looks correct. </h4>
<table class="table">
<thead>
@ -162,7 +162,7 @@ Create a User ::
@if (!$start_settings['debug_exposed'])
Awesomesauce. Debug is either turned off, or you're running this in a non-production environment. (Don't forget to turn it off when you're ready to go live.)
@else
Yikes! You should turn off debug mode unless you encounter any issues. Please update your <code>APP_DEBUG</code> settings in your <code>.env</code> file
<p>Yikes! You should turn off debug mode unless you encounter any issues. Please update your <code>APP_DEBUG</code> settings in your <code>.env</code> file</p>
@endif
</td>
</tr>
@ -178,29 +178,22 @@ Create a User ::
</td>
<td>
@if ($start_settings['gd'])
GD is installed. Go you!
<p>GD is installed. Go you!</p>
@else
The GD library isn't installed. While this won't prevent the system from working, you won't be able to generate labels or upload images.
<p>The GD library isn't installed. While this won't prevent the system from working, you won't be able to generate labels or upload images.</p>
@endif
</td>
</tr>
<tr id="mailtestrow" class="warning">
<tr id="mailtestrow" class="info">
<td>Email</td>
<td>
<a class="btn btn-default btn-sm pull-left" id="mailtest" style="margin-right: 10px;">
Send Test</a>
<span id="mailtesticon"></span>
</td>
<td>
<span id="mailtesticon"></span>
<span id="mailtestresult"></span>
<span id="mailteststatus"></span>
<div class="col-md-12">
<div id="mailteststatus-error" class="text-danger"></div>
</div>
<div class="col-md-12">
<p class="help-block">This will attempt to send a test mail to {{ config('mail.from.address') }}.</p>
</div>
<p>This will attempt to send a test mail to {{ config('mail.from.address') }}.</p>
<a class="btn btn-default btn-sm pull-left" id="mailtest" style="margin-right: 10px;">Send Test</a>
<div id="mailteststatus-text" class="text-danger"></div>
</td>
</tr>
</tbody>
@ -209,9 +202,14 @@ Create a User ::
@stop
@section('button')
<form action="{{ route('setup.migrate') }}" method="GET">
<button class="btn btn-primary">Next: Create Database Tables</button>
<form action="{{ route('setup.migrate') }}" method="POST">
@csrf
<button class="btn btn-primary">
{{ trans('general.setup_next') }}: {{ trans('general.setup_create_database') }}
<i class="fa-solid fa-angles-right"></i>
</button>
</form>
@parent
@stop
@ -223,12 +221,11 @@ Create a User ::
$("#mailtest").click(function(){
$("#mailtestrow").removeClass('success').removeClass('danger').removeClass('warning');
$("#mailtestrow").addClass('info');
$("#mailtestrow").removeClass('success').removeClass('danger').removeClass('warning').addClass('info');
$("#mailtesticon").html('');
$("#mailteststatus").html('');
$("#mailteststatus").html('Sending Test Email...');
$('#mailteststatus-error').html('');
$("#mailtesticon").html('<i class="fas fa-spinner spin"></i> Sending Test Email...');
$("#mailtesticon").html('<i class="fas fa-spinner fa-spin text-info"></i>');
$.ajax({
url: "{{ route('setup.mailtest') }}",
@ -237,12 +234,12 @@ Create a User ::
if (result.status == 'success') {
$("#mailtestrow").removeClass('info').removeClass('danger').removeClass('warning');
$("#mailtestrow").addClass('success');
$("#mailtesticon").html('');
$("#mailtesticon").html('<i class="fas fa-check preflight-success"></i>');
$("#mailteststatus").html('');
$('#mailteststatus-error').html('');
$("#mailteststatus").removeClass('text-danger');
$("#mailteststatus").addClass('text-success');
$("#mailteststatus").html('<i class="fas fa-check text-success"></i> Mail sent to {{ config('mail.from.address') }}!');
$("#mailteststatus-text").removeClass('text-danger');
$("#mailteststatus-text").addClass('text-success');
$("#mailteststatus-text").html('Mail sent to {{ config('mail.from.address') }}!');
} else {
$("#mailtestrow").removeClass('success').removeClass('info').removeClass('warning');
$("#mailtestrow").addClass('danger');
@ -257,12 +254,12 @@ Create a User ::
$("#mailtestrow").removeClass('success').removeClass('info').removeClass('warning');
$("#mailtestrow").addClass('danger');
$("#mailtesticon").html('');
$("#mailteststatus").html('');
$("#mailteststatus-text").html('');
$('#mailteststatus-error').html('');
$("#mailteststatus").removeClass('text-success');
$("#mailteststatus").addClass('text-danger');
$("#mailteststatus-text").removeClass('text-success');
$("#mailteststatus-text").addClass('text-danger');
$("#mailtesticon").html('<i class="fas fa-exclamation-triangle text-danger"></i>');
$('#mailteststatus').html('Mail could not be sent.');
$('#mailteststatus-text').html('Mail could not be sent.');
if (result.responseJSON) {
$('#mailteststatus-error').html('Error: ' + result.responseJSON.messages);
} else {

View File

@ -1,7 +1,7 @@
@extends('layouts/setup')
{{-- Page title --}}
@section('title')
{{ trans('general.setup_migrations') }}
{{ trans('admin/settings/general.setup_migrations') }}
@parent
@stop
@ -15,13 +15,6 @@
{{ trans('general.setup_no_migrations') }}
</div>
</div>
@else
<div class="col-md-12">
<div class="alert alert-success">
<i class="fas fa-check"></i>
{{ trans('general.setup_successful_migrations') }}
</div>
</div>
@endif
@ -31,8 +24,11 @@
@stop
@section('button')
<form action="{{ route('setup.user') }}" method="GET">
<button class="btn btn-primary">{{ trans('general.setup_migration_create_user') }}</button>
</form>
<a href="{{ route('setup.user') }}" class="btn btn-primary">
{{ trans('general.setup_next') }}:
{{ trans('general.setup_create_admin') }}
<i class="fa-solid fa-angles-right"></i>
</a>
@parent
@stop

View File

@ -8,7 +8,11 @@
{{-- Page content --}}
@section('content')
<p>{{ trans('admin/users/general.create_user_page_explanation') }}</p>
<div class="col-md-12">
<h4>{{ trans('admin/settings/general.setup_create_user_page_explanation') }}</h4>
</div>
<form action="{{ route('setup.user.save') }}" method="POST">
{{ csrf_field() }}
@ -27,7 +31,103 @@
</div>
</div>
<div class="row">
<!-- Name -->
<div class="row">
<!-- first name -->
<div class="form-group col-lg-6">
<label for="first_name">{{ trans('general.first_name') }}</label>
<input class="form-control" placeholder="Jane" required="" name="first_name" type="text" id="first_name" value="{{ old('first_name') }}">
{!! $errors->first('first_name', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
<!-- last name -->
<div class="form-group col-lg-6 required {{ $errors->has('last_name') ? 'error' : '' }}">
<label for="last_name">{{ trans('general.last_name') }}</label>
<input class="form-control" placeholder="Smith" required="" name="last_name" type="text" id="last_name" value="{{ old('last_name') }}">
{!! $errors->first('last_name', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>
<div class="row">
<!-- email-->
<div class="form-group col-lg-6{{ $errors->has('email') ? ' error' : '' }}">
<label for="email">{{ trans('admin/users/table.email') }}</label>
<input class="form-control" type="email" name="email" id="email" value="{{ old('email', config('mail.from.address')) }}" placeholder="you@example.com" required>
{!! $errors->first('email', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
<!-- username -->
<div class="form-group col-lg-6 {{ $errors->has('username') ? 'error' : '' }}">
<label for="username">{{ trans('admin/users/table.username') }}</label>
<input class="form-control" placeholder="jsmith" required="" name="username" type="text" id="username" value="{{ old('username') }}" required>
{!! $errors->first('username', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>
<div class="row">
<!-- password -->
<div class="form-group col-lg-6{{ (Helper::checkIfRequired(\App\Models\User::class, 'password')) ? ' required' : '' }} {{ $errors->has('password') ? 'error' : '' }}">
<label for="password">{{ trans('admin/users/table.password') }}</label>
<input class="form-control" type="password" name="password" id="password" value="" required>
{!! $errors->first('password', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
<!-- password confirm -->
<div class="form-group col-lg-6{{ (Helper::checkIfRequired(\App\Models\User::class, 'password')) ? ' required' : '' }} {{ $errors->has('password_confirm') ? 'error' : '' }}">
<label for="password_confirmation">{{ trans('admin/users/table.password_confirm') }}</label>
<input class="form-control" type="password" name="password_confirmation" id="password_confirmation" value="" required>
{!! $errors->first('password_confirmation', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
<!-- Email credentials -->
<div class="form-group col-lg-12">
<label class="form-control form-control">
<input type="checkbox" value="1" name="email_creds">{{ trans('admin/users/general.email_credentials_text') }}
</label>
</div>
</div>
<div class="row">
<div class="form-group col-lg-6{{ $errors->has('auto_increment_prefix') ? ' error' : '' }}">
<label for="auto_increment_prefix">{{ trans('admin/settings/general.auto_increment_prefix') }}</label>
<input class="form-control" name="auto_increment_prefix" type="text" id="auto_increment_prefix" value="{{ old('auto_increment_prefix') }}">
{!! $errors->first('auto_increment_prefix', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
<div class="form-group col-lg-6{{ $errors->has('zerofill_count') ? ' error' : '' }}">
<label for="zerofill_count">{{ trans('admin/settings/general.zerofill_count') }}</label>
<input class="form-control" name="zerofill_count" type="text" value="{{ old('zerofill_count', 5) }}" id="zerofill_count">
{!! $errors->first('zerofill_count', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>
<div class="row">
<div class="form-group col-lg-6">
<label class="form-control form-control">
<input type="checkbox" value="1" name="auto_increment_assets">{{trans('admin/settings/general.auto_increment_assets')}}
</label>
</div>
<!-- Multi Company Support -->
<div class="form-group col-lg-6">
<label class="form-control form-control">
<input type="checkbox" value="1" name="full_multiple_companies_support"> {{ trans('admin/settings/general.full_multiple_companies_support_text') }}
</label>
</div>
</div>
<div class="row">
<!-- Language -->
<div class="form-group col-lg-6{{$errors->has('default_language') ? ' error' : ''}}">
@ -48,122 +148,20 @@
</div>
<div class="row">
<div class="form-group col-lg-6">
<label class="form-control form-control">
<input type="checkbox" value="1" name="auto_increment_assets">{{trans('admin/settings/general.auto_increment_assets')}}
</label>
</div>
<!-- Multi Company Support -->
<div class="form-group col-lg-6">
<label class="form-control form-control">
<input type="checkbox" value="1" name="full_multiple_companies_support"> {{ trans('admin/settings/general.full_multiple_companies_support_text') }}
</label>
</div>
</div>
<div class="row">
<div class="form-group col-lg-6{{ $errors->has('auto_increment_prefix') ? ' error' : '' }}">
<label for="auto_increment_prefix">{{ trans('admin/settings/general.auto_increment_prefix') }}</label>
<input class="form-control" name="auto_increment_prefix" type="text" id="auto_increment_prefix" value="{{ old('auto_increment_prefix') }}">
{!! $errors->first('auto_increment_prefix', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
<div class="form-group col-lg-6{{ $errors->has('zerofill_count') ? ' error' : '' }}">
<label for="zerofill_count">{{ trans('admin/settings/general.zerofill_count') }}</label>
<input class="form-control" name="zerofill_count" type="text" value="{{ old('zerofill_count', 5) }}" id="zerofill_count">
{!! $errors->first('zerofill_count', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>
<!-- email domain -->
<div class="row">
<div class="form-group col-lg-6 required {{ $errors->has('email_domain') ? 'error' : '' }}">
<label for="email_domain">{{ trans('general.email_domain') }}</label>
<input class="form-control" placeholder="example.com" required="" name="email_domain" type="text" id="email_domain" value="{{ old('email_domain') }}">
<span class="help-block">{{ trans('general.email_domain_help') }}</span>
{!! $errors->first('email_domain', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
<!-- email format -->
<div class="form-group col-lg-6 {{ $errors->has('email_format') ? 'error' : '' }}">
<label for="email_format">{{ trans('admin/settings/general.email_formats.email_format') }}</label>
{!! Form::username_format('email_format', old('email_format', 'filastname'), 'select2') !!}
{!! $errors->first('email_format', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>
<!-- Name -->
<div class="row">
<!-- first name -->
<div class="form-group col-lg-6">
<label for="first_name">{{ trans('general.first_name') }}</label>
<input class="form-control" placeholder="Jane" required="" name="first_name" type="text" id="first_name" value="{{ old('first_name') }}">
{!! $errors->first('first_name', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
<!-- last name -->
<div class="form-group col-lg-6 required {{ $errors->has('last_name') ? 'error' : '' }}">
<label for="last_name">{{ trans('general.last_name') }}</label>
<input class="form-control" placeholder="Smith" required="" name="last_name" type="text" id="last_name" value="{{ old('last_name') }}">
{!! $errors->first('last_name', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>
<div class="row">
<!-- email-->
<div class="form-group col-lg-6{{ $errors->has('email') ? 'error' : '' }}">
<label for="email">{{ trans('admin/users/table.email') }}</label>
<input class="form-control" type="email" name="email" id="email" value="{{ old('email', config('mail.from.address')) }}" placeholder="you@example.com" required>
{!! $errors->first('email', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
<!-- username -->
<div class="form-group col-lg-6{{ (Helper::checkIfRequired(\App\Models\User::class, 'username')) ? ' required' : '' }} {{ $errors->has('username') ? 'error' : '' }}">
<label for="username">{{ trans('admin/users/table.username') }}</label>
<input class="form-control" placeholder="jsmith" required="" name="username" type="text" id="username" value="{{ old('username') }}">
{!! $errors->first('username', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>
<div class="row">
<!-- password -->
<div class="form-group col-lg-6{{ (Helper::checkIfRequired(\App\Models\User::class, 'password')) ? ' required' : '' }} {{ $errors->has('password') ? 'error' : '' }}">
<label for="password">{{ trans('admin/users/table.password') }}</label>
<input class="form-control" type="password" name="password" id="password" value="" required>
{!! $errors->first('password', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
<!-- password confirm -->
<div class="form-group col-lg-6{{ (Helper::checkIfRequired(\App\Models\User::class, 'password')) ? ' required' : '' }} {{ $errors->has('password_confirm') ? 'error' : '' }}">
<label for="password_confirmation">{{ trans('admin/users/table.password_confirm') }}</label>
<input class="form-control" type="password" name="password_confirmation" id="password_confirmation" value="" required>
{!! $errors->first('password_confirmation', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>
<!-- Email credentials -->
<div class="form-group col-lg-12">
<label class="form-control form-control">
<input type="checkbox" value="1" name="email_creds">{{ trans('admin/users/general.email_credentials_text') }}
</label>
</div>
</div> <!--/.COL-LG-12-->
@stop
@section('button')
<button class="btn btn-primary">{{ trans('admin/users/general.next_save_user') }}</button>
<button class="btn btn-primary">
{{ trans('general.setup_next') }}: {{ trans('admin/settings/general.setup_migration_create_user') }}
<i class="fa-solid fa-angles-right"></i>
</button>
</form>
@parent
@stop

View File

@ -24,6 +24,7 @@ use App\Http\Controllers\ProfileController;
use App\Http\Controllers\ReportTemplatesController;
use App\Http\Controllers\ReportsController;
use App\Http\Controllers\SettingsController;
use App\Http\Controllers\SetupController;
use App\Http\Controllers\StatuslabelsController;
use App\Http\Controllers\SuppliersController;
use App\Http\Controllers\ViewAssetsController;
@ -48,7 +49,7 @@ Route::group(['middleware' => 'auth'], function () {
]);
Route::post('categories/bulk/delete', [BulkCategoriesController::class, 'destroy'])->name('categories.bulk.delete');
/*
* Labels
*/
@ -285,15 +286,15 @@ Route::group(['prefix' => 'admin', 'middleware' => ['auth', 'authorize:superuser
Route::delete('delete/{filename}',
[SettingsController::class, 'deleteFile'])->name('settings.backups.destroy');
Route::post('/',
Route::post('/',
[SettingsController::class, 'postBackups']
)->name('settings.backups.create');
Route::post('/restore/{filename}',
Route::post('/restore/{filename}',
[SettingsController::class, 'postRestore']
)->name('settings.backups.restore');
Route::post('/upload',
Route::post('/upload',
[SettingsController::class, 'postUploadBackup']
)->name('settings.backups.upload');
@ -415,7 +416,7 @@ Route::group(['prefix' => 'account', 'middleware' => ['auth']], function () {
'display-sig/{filename}',
[ProfileController::class, 'displaySig']
)->name('profile.signature.view');
Route::get(
'stored-eula-file/{filename}',
[ProfileController::class, 'getStoredEula']
@ -604,23 +605,24 @@ Route::get(
Route::group(['prefix' => 'setup', 'middleware' => 'web'], function () {
Route::get(
'user',
[SettingsController::class, 'getSetupUser']
[SetupController::class, 'getSetupUser']
)->name('setup.user');
Route::post(
'user',
[SettingsController::class, 'postSaveFirstAdmin']
[SetupController::class, 'postSaveFirstAdmin']
)->name('setup.user.save');
Route::get(
Route::post(
'migrate',
[SettingsController::class, 'getSetupMigrate']
[SetupController::class, 'SetupMigrate']
)->name('setup.migrate');
Route::get(
'done',
[SettingsController::class, 'getSetupDone']
[SetupController::class, 'getSetupDone']
)->name('setup.done');
Route::get(
@ -630,7 +632,7 @@ Route::group(['prefix' => 'setup', 'middleware' => 'web'], function () {
Route::get(
'/',
[SettingsController::class, 'getSetupIndex']
[SetupController::class, 'getSetupIndex']
)->name('setup');
});

View File

@ -0,0 +1,85 @@
<?php
namespace Tests\Feature\Setup\Ui;
use App\Models\User;
use Tests\TestCase;
class IndexSetupTest extends TestCase
{
public function testPageRenders()
{
$this->get(route('setup'))
->assertOk();
}
public function testPageRedirectsIfNoRecordsFound()
{
$this->assertDatabaseEmpty('users');
$this->get(route('home'))
->assertStatus(302)
->assertRedirectToRoute('setup');
}
public function testPageRedirectsIfRecordsFound() {
$this->actingAs(User::factory()->superuser()->create())
->get(route('setup'))
->assertStatus(302)
->assertRedirectToRoute('home');
}
public function testMigrationPageRenders() {
$this->assertDatabaseEmpty('users');
$this->post(route('setup.migrate'))
->assertOk();
}
public function testCreateFirstUserPageRenders() {
$this->assertDatabaseEmpty('users');
$this->get(route('setup.user'))
->assertOk();
}
public function testCreateFirstUserValidation() {
$this->assertDatabaseEmpty('users');
$response = $this->post(route('setup.user.save'))
->assertStatus(302);
$this->followRedirects($response)->assertSee('error');
$this->assertDatabaseCount('users', 0);
}
public function testCreateFirstUserSaved() {
$this->assertDatabaseEmpty('users');
$this->post(route('setup.user.save'),
[
'site_name' => 'Snipe-IT',
'first_name' => 'First',
'last_name' => 'Admin',
'username' => 'AwesomeAdmin',
'password' => '0834529!!*423',
'password_confirmation' => '0834529!!*423',
'email_domain' => 'example.org',
])
->assertRedirectToRoute('setup.done')
->assertStatus(302)
->assertSessionHas('success');
$this->assertDatabaseCount('users', 1);
}
}

View File

@ -19,7 +19,7 @@ class Settings
return new self();
}
public function enableAlertEmail(string $email = 'notifications@afcrichmond.com'): Settings
public function enableAlertEmail(string $email = 'notifications@example.org'): Settings
{
return $this->update([
'alert_email' => $email,
@ -46,7 +46,7 @@ class Settings
]);
}
public function enableAdminCC(string $email = 'cc@example.co'): Settings
public function enableAdminCC(string $email = 'cc@example.org'): Settings
{
return $this->update([
'admin_cc_email' => $email,