Merge branch 'hotfixes/2fa_qr' into develop
# Conflicts: # .all-contributorsrc # Dockerfile # README.md # app/Console/Commands/LdapSync.php # app/Http/Controllers/Api/ImportController.php # app/Http/Controllers/AssetModelsController.php # app/Http/Controllers/Assets/AssetsController.php # app/Http/Controllers/Auth/LoginController.php # app/Http/Controllers/CategoriesController.php # app/Http/Controllers/CompaniesController.php # app/Http/Controllers/DepartmentsController.php # app/Http/Controllers/ImportsController.php # app/Http/Controllers/LocationsController.php # app/Http/Controllers/ManufacturersController.php # app/Http/Controllers/SuppliersController.php # app/Http/Requests/ItemImportRequest.php # app/Http/Transformers/ActionlogsTransformer.php # composer.json # composer.lock # config/app.php # config/version.php # docker/startup.sh # public/css/build/all.css # public/css/dist/all.css # public/js/build/all.js # public/js/build/vue.js # public/js/build/vue.js.map # public/js/dist/all.js # public/mix-manifest.json
This commit is contained in:
commit
bca82684a1
34 changed files with 1074 additions and 72 deletions
18
Dockerfile
18
Dockerfile
|
@ -26,6 +26,7 @@ vim \
|
||||||
git \
|
git \
|
||||||
cron \
|
cron \
|
||||||
mysql-client \
|
mysql-client \
|
||||||
|
supervisor \
|
||||||
cron \
|
cron \
|
||||||
gcc \
|
gcc \
|
||||||
make \
|
make \
|
||||||
|
@ -92,7 +93,9 @@ RUN \
|
||||||
&& rm -r "/var/www/html/storage/app/backups" && ln -fs "/var/lib/snipeit/dumps" "/var/www/html/storage/app/backups" \
|
&& rm -r "/var/www/html/storage/app/backups" && ln -fs "/var/lib/snipeit/dumps" "/var/www/html/storage/app/backups" \
|
||||||
&& mkdir -p "/var/lib/snipeit/keys" && ln -fs "/var/lib/snipeit/keys/oauth-private.key" "/var/www/html/storage/oauth-private.key" \
|
&& mkdir -p "/var/lib/snipeit/keys" && ln -fs "/var/lib/snipeit/keys/oauth-private.key" "/var/www/html/storage/oauth-private.key" \
|
||||||
&& ln -fs "/var/lib/snipeit/keys/oauth-public.key" "/var/www/html/storage/oauth-public.key" \
|
&& ln -fs "/var/lib/snipeit/keys/oauth-public.key" "/var/www/html/storage/oauth-public.key" \
|
||||||
&& chown docker "/var/lib/snipeit/keys/"
|
&& chown docker "/var/lib/snipeit/keys/" \
|
||||||
|
&& chmod +x /var/www/html/artisan \
|
||||||
|
&& echo "Finished setting up application in /var/www/html"
|
||||||
|
|
||||||
############## DEPENDENCIES via COMPOSER ###################
|
############## DEPENDENCIES via COMPOSER ###################
|
||||||
|
|
||||||
|
@ -119,16 +122,11 @@ VOLUME ["/var/lib/snipeit"]
|
||||||
|
|
||||||
##### START SERVER
|
##### START SERVER
|
||||||
|
|
||||||
COPY docker/entrypoint.sh /entrypoint.sh
|
COPY docker/startup.sh docker/supervisord.conf /
|
||||||
RUN chmod +x /entrypoint.sh
|
COPY docker/supervisor-exit-event-listener /usr/bin/supervisor-exit-event-listener
|
||||||
|
RUN chmod +x /startup.sh /usr/bin/supervisor-exit-event-listener
|
||||||
|
|
||||||
# Add Tini
|
CMD ["/startup.sh"]
|
||||||
ENV TINI_VERSION v0.14.0
|
|
||||||
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
|
|
||||||
RUN chmod +x /tini
|
|
||||||
ENTRYPOINT ["/tini", "--"]
|
|
||||||
|
|
||||||
CMD ["/entrypoint.sh"]
|
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
EXPOSE 443
|
EXPOSE 443
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
[](https://travis-ci.org/snipe/snipe-it) [](https://crowdin.com/project/snipe-it) [](https://gitter.im/snipe/snipe-it?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://hub.docker.com/r/snipe/snipe-it/) [](https://twitter.com/snipeitapp) [](https://www.codacy.com/app/snipe/snipe-it?utm_source=github.com&utm_medium=referral&utm_content=snipe/snipe-it&utm_campaign=Badge_Grade)
|
[](https://travis-ci.org/snipe/snipe-it) [](https://crowdin.com/project/snipe-it) [](https://gitter.im/snipe/snipe-it?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://hub.docker.com/r/snipe/snipe-it/) [](https://twitter.com/snipeitapp) [](https://www.codacy.com/app/snipe/snipe-it?utm_source=github.com&utm_medium=referral&utm_content=snipe/snipe-it&utm_campaign=Badge_Grade)
|
||||||
[](#contributors) [](https://www.codetriage.com/snipe/snipe-it)
|
[](#contributors) [](https://www.codetriage.com/snipe/snipe-it)
|
||||||
|
|
||||||
|
|
||||||
## Snipe-IT - Open Source Asset Management System
|
## Snipe-IT - Open Source Asset Management System
|
||||||
|
|
|
@ -179,7 +179,7 @@ class AssetModelsController extends Controller
|
||||||
try {
|
try {
|
||||||
Storage::disk('public')->delete('assetmodels/'.$assetmodel->image);
|
Storage::disk('public')->delete('assetmodels/'.$assetmodel->image);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
\Log::error($e);
|
\Log::info($e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ class ImportController extends Controller
|
||||||
*/
|
*/
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
//
|
$this->authorize('import');
|
||||||
$imports = Import::latest()->get();
|
$imports = Import::latest()->get();
|
||||||
return (new ImportsTransformer)->transformImports($imports);
|
return (new ImportsTransformer)->transformImports($imports);
|
||||||
|
|
||||||
|
@ -39,10 +39,8 @@ class ImportController extends Controller
|
||||||
*/
|
*/
|
||||||
public function store()
|
public function store()
|
||||||
{
|
{
|
||||||
//
|
$this->authorize('import');
|
||||||
if (!Company::isCurrentUserAuthorized()) {
|
if (!config('app.lock_passwords')) {
|
||||||
return redirect()->route('hardware.index')->with('error', trans('general.insufficient_permissions'));
|
|
||||||
} elseif (!config('app.lock_passwords')) {
|
|
||||||
$files = Input::file('files');
|
$files = Input::file('files');
|
||||||
$path = config('app.private_uploads').'/imports';
|
$path = config('app.private_uploads').'/imports';
|
||||||
$results = [];
|
$results = [];
|
||||||
|
@ -119,7 +117,7 @@ class ImportController extends Controller
|
||||||
*/
|
*/
|
||||||
public function process(ItemImportRequest $request, $import_id)
|
public function process(ItemImportRequest $request, $import_id)
|
||||||
{
|
{
|
||||||
$this->authorize('create', Asset::class);
|
$this->authorize('import');
|
||||||
// Run a backup immediately before processing
|
// Run a backup immediately before processing
|
||||||
Artisan::call('backup:run');
|
Artisan::call('backup:run');
|
||||||
$errors = $request->import(Import::find($import_id));
|
$errors = $request->import(Import::find($import_id));
|
||||||
|
|
|
@ -54,6 +54,7 @@ class UsersController extends Controller
|
||||||
'users.phone',
|
'users.phone',
|
||||||
'users.state',
|
'users.state',
|
||||||
'users.two_factor_enrolled',
|
'users.two_factor_enrolled',
|
||||||
|
'users.two_factor_optin',
|
||||||
'users.updated_at',
|
'users.updated_at',
|
||||||
'users.username',
|
'users.username',
|
||||||
'users.zip',
|
'users.zip',
|
||||||
|
|
|
@ -195,7 +195,7 @@ class AssetModelsController extends Controller
|
||||||
try {
|
try {
|
||||||
Storage::disk('public')->delete('models/'.$model->image);
|
Storage::disk('public')->delete('models/'.$model->image);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
\Log::error($e);
|
\Log::info($e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -308,7 +308,7 @@ class AssetsController extends Controller
|
||||||
unlink(public_path().'/uploads/assets/'.$asset->image);
|
unlink(public_path().'/uploads/assets/'.$asset->image);
|
||||||
$asset->image = '';
|
$asset->image = '';
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
\Log::error($e);
|
\Log::info($e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,11 @@ use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Session;
|
use Illuminate\Support\Facades\Session;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
use Redirect;
|
use Redirect;
|
||||||
|
use Log;
|
||||||
|
use View;
|
||||||
|
use Otp\Otp;
|
||||||
|
use Otp\GoogleAuthenticator;
|
||||||
|
use ParagonIE\ConstantTime\Encoding;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This controller handles authentication for the user, including local
|
* This controller handles authentication for the user, including local
|
||||||
|
@ -198,22 +203,24 @@ class LoginController extends Controller
|
||||||
return redirect()->route('login')->with('error', 'You must be logged in.');
|
return redirect()->route('login')->with('error', 'You must be logged in.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = Auth::user();
|
|
||||||
$google2fa = app()->make('pragmarx.google2fa');
|
|
||||||
|
|
||||||
if ($user->two_factor_secret=='') {
|
$settings = Setting::getSettings();
|
||||||
$user->two_factor_secret = $google2fa->generateSecretKey(32);
|
$user = Auth::user();
|
||||||
$user->save();
|
|
||||||
|
if (($user->two_factor_secret!='') && ($user->two_factor_enrolled==1)) {
|
||||||
|
return redirect()->route('two-factor')->with('error', 'Your device is already enrolled.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$google2fa_url = $google2fa->getQRCodeInline(
|
|
||||||
urlencode(Setting::getSettings()->site_name),
|
|
||||||
urlencode($user->username),
|
|
||||||
$user->two_factor_secret
|
|
||||||
);
|
|
||||||
|
|
||||||
return view('auth.two_factor_enroll')->with('google2fa_url', $google2fa_url);
|
new Otp();
|
||||||
|
$secret = GoogleAuthenticator::generateRandom();
|
||||||
|
$user->two_factor_secret = $secret;
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
$barcode = new \Com\Tecnick\Barcode\Barcode();
|
||||||
|
$barcode_obj = $barcode->getBarcodeObj('QRCODE', 'otpauth://totp/'.urlencode($settings->site_name).':'.urlencode($user->username).'?secret='.urlencode($secret).'&issuer=Snipe-IT&period=30', 300, 300, 'black', array(-2, -2, -2, -2));
|
||||||
|
return view('auth.two_factor_enroll')->with('barcode_obj', $barcode_obj);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,18 +249,23 @@ class LoginController extends Controller
|
||||||
return redirect()->route('login')->with('error', 'You must be logged in.');
|
return redirect()->route('login')->with('error', 'You must be logged in.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = Auth::user();
|
if (!$request->has('two_factor_secret')) {
|
||||||
$secret = $request->get('two_factor_secret');
|
return redirect()->route('two-factor')->with('error', 'Two-factor code is required.');
|
||||||
$google2fa = app()->make('pragmarx.google2fa');
|
}
|
||||||
$valid = $google2fa->verifyKey($user->two_factor_secret, $secret);
|
|
||||||
|
|
||||||
if ($valid) {
|
$user = Auth::user();
|
||||||
|
$otp = new Otp();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if ($otp->checkTotp(Encoding::base32DecodeUpper($user->two_factor_secret), $request->get('two_factor_secret'))) {
|
||||||
$user->two_factor_enrolled = 1;
|
$user->two_factor_enrolled = 1;
|
||||||
$user->save();
|
$user->save();
|
||||||
$request->session()->put('2fa_authed', 'true');
|
$request->session()->put('2fa_authed', 'true');
|
||||||
return redirect()->route('home')->with('success', 'You are logged in!');
|
return redirect()->route('home')->with('success', 'You are logged in!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
\Log::debug('Did not match');
|
||||||
return redirect()->route('two-factor')->with('error', 'Invalid two-factor code');
|
return redirect()->route('two-factor')->with('error', 'Invalid two-factor code');
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ class ImportsController extends Controller
|
||||||
*/
|
*/
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$this->authorize('create', Asset::class);
|
$this->authorize('import');
|
||||||
$imports = (new ImportsTransformer)->transformImports(Import::latest()->get());
|
$imports = (new ImportsTransformer)->transformImports(Import::latest()->get());
|
||||||
return view('importer/import')->with('imports', $imports);
|
return view('importer/import')->with('imports', $imports);
|
||||||
}
|
}
|
||||||
|
|
|
@ -164,7 +164,7 @@ class ManufacturersController extends Controller
|
||||||
try {
|
try {
|
||||||
Storage::disk('public')->delete('manufacturers/'.$manufacturer->image);
|
Storage::disk('public')->delete('manufacturers/'.$manufacturer->image);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
\Log::error($e);
|
\Log::info($e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,8 @@ class UsersTransformer
|
||||||
'permissions' => $user->decodePermissions(),
|
'permissions' => $user->decodePermissions(),
|
||||||
'activated' => ($user->activated =='1') ? true : false,
|
'activated' => ($user->activated =='1') ? true : false,
|
||||||
'two_factor_activated' => ($user->two_factor_active()) ? true : false,
|
'two_factor_activated' => ($user->two_factor_active()) ? true : false,
|
||||||
|
'two_factor_enrolled' => ($user->two_factor_active_and_enrolled()) ? true : false,
|
||||||
|
|
||||||
'assets_count' => (int) $user->assets_count,
|
'assets_count' => (int) $user->assets_count,
|
||||||
'licenses_count' => (int) $user->licenses_count,
|
'licenses_count' => (int) $user->licenses_count,
|
||||||
'accessories_count' => (int) $user->accessories_count,
|
'accessories_count' => (int) $user->accessories_count,
|
||||||
|
|
|
@ -525,7 +525,11 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether two-factor authorization is required and the user has activated it
|
* Check whether two-factor authorization is requiredfor this user
|
||||||
|
*
|
||||||
|
* 0 = 2FA disabled
|
||||||
|
* 1 = 2FA optional
|
||||||
|
* 2 = 2FA universally required
|
||||||
*
|
*
|
||||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||||
* @since [v4.0]
|
* @since [v4.0]
|
||||||
|
@ -534,10 +538,45 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
|
||||||
*/
|
*/
|
||||||
public function two_factor_active () {
|
public function two_factor_active () {
|
||||||
|
|
||||||
if (Setting::getSettings()->two_factor_enabled !='0') {
|
// If the 2FA is optional and the user has opted in
|
||||||
if (($this->two_factor_optin =='1') && ($this->two_factor_enrolled)) {
|
if ((Setting::getSettings()->two_factor_enabled =='1') && ($this->two_factor_optin =='1'))
|
||||||
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// If the 2FA is required for everyone so is implicitly active
|
||||||
|
elseif (Setting::getSettings()->two_factor_enabled =='2')
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether two-factor authorization is required and the user has activated it
|
||||||
|
* and enrolled a device
|
||||||
|
*
|
||||||
|
* 0 = 2FA disabled
|
||||||
|
* 1 = 2FA optional
|
||||||
|
* 2 = 2FA universally required
|
||||||
|
*
|
||||||
|
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||||
|
* @since [v4.6.14]
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function two_factor_active_and_enrolled () {
|
||||||
|
|
||||||
|
// If the 2FA is optional and the user has opted in and is enrolled
|
||||||
|
if ((Setting::getSettings()->two_factor_enabled =='1') && ($this->two_factor_optin =='1') && ($this->two_factor_enrolled =='1'))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// If the 2FA is required for everyone and the user has enrolled
|
||||||
|
elseif ((Setting::getSettings()->two_factor_enabled =='2') && ($this->two_factor_enrolled))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ abstract class SnipePermissionsPolicy
|
||||||
/**
|
/**
|
||||||
* Determine whether the user can view the accessory.
|
* Determine whether the user can view the accessory.
|
||||||
*
|
*
|
||||||
* @param \App\User $user
|
* @param \App\Models\User $user
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function view(User $user, $item = null)
|
public function view(User $user, $item = null)
|
||||||
|
@ -64,7 +64,7 @@ abstract class SnipePermissionsPolicy
|
||||||
/**
|
/**
|
||||||
* Determine whether the user can create accessories.
|
* Determine whether the user can create accessories.
|
||||||
*
|
*
|
||||||
* @param \App\User $user
|
* @param \App\Models\User $user
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function create(User $user)
|
public function create(User $user)
|
||||||
|
@ -75,7 +75,7 @@ abstract class SnipePermissionsPolicy
|
||||||
/**
|
/**
|
||||||
* Determine whether the user can update the accessory.
|
* Determine whether the user can update the accessory.
|
||||||
*
|
*
|
||||||
* @param \App\User $user
|
* @param \App\Models\User $user
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function update(User $user, $item = null)
|
public function update(User $user, $item = null)
|
||||||
|
@ -86,7 +86,7 @@ abstract class SnipePermissionsPolicy
|
||||||
/**
|
/**
|
||||||
* Determine whether the user can delete the accessory.
|
* Determine whether the user can delete the accessory.
|
||||||
*
|
*
|
||||||
* @param \App\User $user
|
* @param \App\Models\User $user
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function delete(User $user, $item = null)
|
public function delete(User $user, $item = null)
|
||||||
|
@ -97,11 +97,13 @@ abstract class SnipePermissionsPolicy
|
||||||
/**
|
/**
|
||||||
* Determine whether the user can manage the accessory.
|
* Determine whether the user can manage the accessory.
|
||||||
*
|
*
|
||||||
* @param \App\User $user
|
* @param \App\Models\User $user
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function manage(User $user, $item = null)
|
public function manage(User $user, $item = null)
|
||||||
{
|
{
|
||||||
return $user->hasAccess($this->columnName().'.edit');
|
return $user->hasAccess($this->columnName().'.edit');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -223,14 +223,14 @@ class UserPresenter extends Presenter
|
||||||
[
|
[
|
||||||
"field" => "two_factor_enrolled",
|
"field" => "two_factor_enrolled",
|
||||||
"searchable" => false,
|
"searchable" => false,
|
||||||
"sortable" => false,
|
"sortable" => true,
|
||||||
"switchable" => true,
|
"switchable" => true,
|
||||||
"title" => trans('admin/users/general.two_factor_enrolled'),
|
"title" => trans('admin/users/general.two_factor_enrolled'),
|
||||||
"visible" => false,
|
"visible" => false,
|
||||||
'formatter' => 'trueFalseFormatter'
|
'formatter' => 'trueFalseFormatter'
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"field" => "two_factor_active",
|
"field" => "two_factor_activated",
|
||||||
"searchable" => false,
|
"searchable" => false,
|
||||||
"sortable" => false,
|
"sortable" => false,
|
||||||
"switchable" => true,
|
"switchable" => true,
|
||||||
|
@ -243,7 +243,7 @@ class UserPresenter extends Presenter
|
||||||
"searchable" => false,
|
"searchable" => false,
|
||||||
"sortable" => true,
|
"sortable" => true,
|
||||||
"switchable" => true,
|
"switchable" => true,
|
||||||
"title" => trans('general.activated'),
|
"title" => trans('general.login_enabled'),
|
||||||
"visible" => true,
|
"visible" => true,
|
||||||
'formatter' => 'trueFalseFormatter'
|
'formatter' => 'trueFalseFormatter'
|
||||||
],
|
],
|
||||||
|
|
|
@ -113,6 +113,14 @@ class AuthServiceProvider extends ServiceProvider
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Can the user import CSVs?
|
||||||
|
Gate::define('import', function ($user) {
|
||||||
|
if ($user->hasAccess('import') ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------
|
# -----------------------------------------
|
||||||
# Reports
|
# Reports
|
||||||
# -----------------------------------------
|
# -----------------------------------------
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
"league/flysystem-sftp": "~1.0",
|
"league/flysystem-sftp": "~1.0",
|
||||||
"maknz/slack": "^1.7",
|
"maknz/slack": "^1.7",
|
||||||
"neitanod/forceutf8": "^2.0",
|
"neitanod/forceutf8": "^2.0",
|
||||||
|
"paragonie/constant_time_encoding": "^1.0",
|
||||||
"patchwork/utf8": "~1.2",
|
"patchwork/utf8": "~1.2",
|
||||||
"phpdocumentor/reflection-docblock": "3.2.2",
|
"phpdocumentor/reflection-docblock": "3.2.2",
|
||||||
"phpspec/prophecy": "1.7.5",
|
"phpspec/prophecy": "1.7.5",
|
||||||
|
|
|
@ -27,6 +27,15 @@ return array(
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
|
'CSV Import' => array(
|
||||||
|
array(
|
||||||
|
'permission' => 'import',
|
||||||
|
'label' => '',
|
||||||
|
'note' => 'This will allow users to import even if access to users, assets, etc is denied elsewhere.',
|
||||||
|
'display' => true,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
'Reports' => array(
|
'Reports' => array(
|
||||||
array(
|
array(
|
||||||
'permission' => 'reports.view',
|
'permission' => 'reports.view',
|
||||||
|
|
|
@ -5,7 +5,7 @@ if [ -z "$APP_KEY" ]
|
||||||
then
|
then
|
||||||
echo "Please re-run this container with an environment variable \$APP_KEY"
|
echo "Please re-run this container with an environment variable \$APP_KEY"
|
||||||
echo "An example APP_KEY you could use is: "
|
echo "An example APP_KEY you could use is: "
|
||||||
php artisan key:generate --show
|
/var/www/html/artisan key:generate --show
|
||||||
exit
|
exit
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@ -47,6 +47,8 @@ then
|
||||||
cp -ax /var/www/html/vendor/laravel/passport/database/migrations/* /var/www/html/database/migrations/
|
cp -ax /var/www/html/vendor/laravel/passport/database/migrations/* /var/www/html/database/migrations/
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
exec supervisord -c /supervisord.conf
|
||||||
|
|
||||||
php artisan migrate --force
|
php artisan migrate --force
|
||||||
php artisan config:clear
|
php artisan config:clear
|
||||||
php artisan config:cache
|
php artisan config:cache
|
19
docker/supervisor-exit-event-listener
Normal file
19
docker/supervisor-exit-event-listener
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# A supervisor event listener which terminates supervisord if any of its child
|
||||||
|
# processes enter the FATAL state.
|
||||||
|
# https://stackoverflow.com/a/37527488/119527
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
|
||||||
|
from supervisor import childutils
|
||||||
|
|
||||||
|
def main():
|
||||||
|
while True:
|
||||||
|
headers, payload = childutils.listener.wait()
|
||||||
|
childutils.listener.ok()
|
||||||
|
if headers['eventname'] != 'PROCESS_STATE_FATAL':
|
||||||
|
continue
|
||||||
|
os.kill(os.getppid(), signal.SIGTERM)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
27
docker/supervisord.conf
Normal file
27
docker/supervisord.conf
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
|
||||||
|
[program:apache]
|
||||||
|
; https://advancedweb.hu/2018/07/03/supervisor_docker/
|
||||||
|
command=apache2ctl -DFOREGROUND
|
||||||
|
killasgroup=true
|
||||||
|
stopasgroup=true
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
|
||||||
|
[program:run_schedule]
|
||||||
|
; Simply run the Laravel command scheduler every minute
|
||||||
|
command=/bin/bash -c "while true; do /var/www/html/artisan schedule:run; sleep 1m; done"
|
||||||
|
user=docker
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
|
||||||
|
|
||||||
|
; https://stackoverflow.com/a/37527488/119527
|
||||||
|
[eventlistener:exit_on_any_fatal]
|
||||||
|
command=supervisor-exit-event-listener
|
||||||
|
events=PROCESS_STATE_FATAL
|
617
public/css/build/all.css
Normal file
617
public/css/build/all.css
Normal file
File diff suppressed because one or more lines are too long
22
public/css/dist/all.css
vendored
Normal file
22
public/css/dist/all.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
29
public/js/dist/all.js
vendored
29
public/js/dist/all.js
vendored
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
|
<<<<<<< HEAD
|
||||||
"/js/app.js": "/js/app.js?id=be43e3109667a86c9a02",
|
"/js/app.js": "/js/app.js?id=be43e3109667a86c9a02",
|
||||||
"/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=f7a5d783fef321018f4c",
|
"/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=f7a5d783fef321018f4c",
|
||||||
"/css/build/app.css": "/css/build/app.css?id=0dfc05b0fe1dcc9b6e3d",
|
"/css/build/app.css": "/css/build/app.css?id=0dfc05b0fe1dcc9b6e3d",
|
||||||
|
@ -14,4 +15,18 @@
|
||||||
"/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=bc5e33610f678021cc48",
|
"/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=bc5e33610f678021cc48",
|
||||||
"/js/dist/bootstrap-table-simple-view.js": "/js/dist/bootstrap-table-simple-view.js?id=3926b8f4aaad6ca20d31",
|
"/js/dist/bootstrap-table-simple-view.js": "/js/dist/bootstrap-table-simple-view.js?id=3926b8f4aaad6ca20d31",
|
||||||
"/css/dist/bootstrap-table.css": "/css/dist/bootstrap-table.css?id=6b4ccfd094c065f065ae"
|
"/css/dist/bootstrap-table.css": "/css/dist/bootstrap-table.css?id=6b4ccfd094c065f065ae"
|
||||||
|
=======
|
||||||
|
"/js/build/vue.js": "/js/build/vue.js?id=96f90510b797ac27a94b",
|
||||||
|
"/css/AdminLTE.css": "/css/AdminLTE.css?id=5e72463a66acbcc740d5",
|
||||||
|
"/css/app.css": "/css/app.css?id=407edb63cc6b6dc62405",
|
||||||
|
"/css/overrides.css": "/css/overrides.css?id=2d81c3704393bac77011",
|
||||||
|
"/js/build/vue.js.map": "/js/build/vue.js.map?id=423f16f63b86abd6b196",
|
||||||
|
"/css/AdminLTE.css.map": "/css/AdminLTE.css.map?id=0be7790b84909dca6a0a",
|
||||||
|
"/css/app.css.map": "/css/app.css.map?id=96b5c985e860716e6a16",
|
||||||
|
"/css/overrides.css.map": "/css/overrides.css.map?id=f7ce9ca49027594ac402",
|
||||||
|
"/css/dist/all.css": "/css/dist/all.css?id=98db4e9b7650453c8b00",
|
||||||
|
"/js/dist/all.js": "/js/dist/all.js?id=114f1025a1b3e8975476",
|
||||||
|
"/css/build/all.css": "/css/build/all.css?id=98db4e9b7650453c8b00",
|
||||||
|
"/js/build/all.js": "/js/build/all.js?id=114f1025a1b3e8975476"
|
||||||
|
>>>>>>> hotfixes/2fa_qr
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,9 +40,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert col-md-12"
|
<div class="alert col-md-12" style="text-align:left"
|
||||||
:class="alertClass"
|
:class="alertClass"
|
||||||
style="text-align:left"
|
|
||||||
v-if="statusText">
|
v-if="statusText">
|
||||||
{{ this.statusText }}
|
{{ this.statusText }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -346,7 +346,18 @@ $(document).ready(function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDataSelection (datalist) {
|
function formatDataSelection (datalist) {
|
||||||
return datalist.text;
|
// This a heinous workaround for a known bug in Select2.
|
||||||
|
// Without this, the rich selectlists are vulnerable to XSS.
|
||||||
|
// Many thanks to @uberbrady for this fix. It ain't pretty,
|
||||||
|
// but it resolves the issue until Select2 addresses it on their end.
|
||||||
|
//
|
||||||
|
// Bug was reported in 2016 :{
|
||||||
|
// https://github.com/select2/select2/issues/4587
|
||||||
|
|
||||||
|
return datalist.text.replace(/>/g, '>')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
// This handles the radio button selectors for the checkout-to-foo options
|
// This handles the radio button selectors for the checkout-to-foo options
|
||||||
|
|
|
@ -226,4 +226,5 @@
|
||||||
'zip' => 'Zip',
|
'zip' => 'Zip',
|
||||||
'noimage' => 'No image uploaded or image not found.',
|
'noimage' => 'No image uploaded or image not found.',
|
||||||
'token_expired' => 'Your form session has expired. Please try again.',
|
'token_expired' => 'Your form session has expired. Please try again.',
|
||||||
|
'login_enabled' => 'Login Enabled',
|
||||||
];
|
];
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-12 text-center">
|
<div class="col-md-12 text-center">
|
||||||
<img src="{{ $google2fa_url }}" style="padding: 15px 0px 15px 0px">
|
{!! $barcode_obj->getHtmlDiv() !!}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -509,7 +509,7 @@
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@endcan
|
@endcan
|
||||||
@can('create', \App\Models\Asset::class)
|
@can('import')
|
||||||
<li{!! (Request::is('import/*') ? ' class="active"' : '') !!}>
|
<li{!! (Request::is('import/*') ? ' class="active"' : '') !!}>
|
||||||
<a href="{{ route('imports.index') }}">
|
<a href="{{ route('imports.index') }}">
|
||||||
<i class="fa fa-cloud-download"></i>
|
<i class="fa fa-cloud-download"></i>
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
<!-- Require signature for acceptance -->
|
<!-- Require signature for acceptance -->
|
||||||
<div class="form-group {{ $errors->has('require_accept_signature') ? 'error' : '' }}">
|
<div class="form-group {{ $errors->has('require_accept_signature') ? 'error' : '' }}">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{{ Form::label('full_multiple_companies_support',
|
{{ Form::label('require_accept_signature',
|
||||||
trans('admin/settings/general.require_accept_signature')) }}
|
trans('admin/settings/general.require_accept_signature')) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
|
@ -166,7 +166,7 @@
|
||||||
<!-- Default EULA -->
|
<!-- Default EULA -->
|
||||||
<div class="form-group {{ $errors->has('default_eula_text') ? 'error' : '' }}">
|
<div class="form-group {{ $errors->has('default_eula_text') ? 'error' : '' }}">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{{ Form::label('per_page', trans('admin/settings/general.default_eula_text')) }}
|
{{ Form::label('default_eula_text', trans('admin/settings/general.default_eula_text')) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
{{ Form::textarea('default_eula_text', Input::old('default_eula_text', $setting->default_eula_text), array('class' => 'form-control','placeholder' => 'Add your default EULA text')) }}
|
{{ Form::textarea('default_eula_text', Input::old('default_eula_text', $setting->default_eula_text), array('class' => 'form-control','placeholder' => 'Add your default EULA text')) }}
|
||||||
|
|
|
@ -123,22 +123,22 @@
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
@if (!is_null($user->company))
|
@if (!is_null($user->company))
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ trans('general.company') }}</td>
|
<td class="text-nowrap">{{ trans('general.company') }}</td>
|
||||||
<td>{{ $user->company->name }}</td>
|
<td>{{ $user->company->name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ trans('admin/users/table.name') }}</td>
|
<td class="text-nowrap">{{ trans('admin/users/table.name') }}</td>
|
||||||
<td>{{ $user->present()->fullName() }}</td>
|
<td>{{ $user->present()->fullName() }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ trans('admin/users/table.username') }}</td>
|
<td class="text-nowrap">{{ trans('admin/users/table.username') }}</td>
|
||||||
<td>{{ $user->username }}</td>
|
<td>{{ $user->username }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ trans('general.groups') }}</td>
|
<td class="text-nowrap">{{ trans('general.groups') }}</td>
|
||||||
<td>
|
<td>
|
||||||
@if ($user->groups->count() > 0)
|
@if ($user->groups->count() > 0)
|
||||||
@foreach ($user->groups as $group)
|
@foreach ($user->groups as $group)
|
||||||
|
@ -160,21 +160,21 @@
|
||||||
|
|
||||||
@if ($user->jobtitle)
|
@if ($user->jobtitle)
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ trans('admin/users/table.job') }}</td>
|
<td class="text-nowrap">{{ trans('admin/users/table.job') }}</td>
|
||||||
<td>{{ $user->jobtitle }}</td>
|
<td>{{ $user->jobtitle }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if ($user->employee_num)
|
@if ($user->employee_num)
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ trans('admin/users/table.employee_num') }}</td>
|
<td class="text-nowrap">{{ trans('admin/users/table.employee_num') }}</td>
|
||||||
<td>{{ $user->employee_num }}</td>
|
<td>{{ $user->employee_num }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if ($user->manager)
|
@if ($user->manager)
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ trans('admin/users/table.manager') }}</td>
|
<td class="text-nowrap">{{ trans('admin/users/table.manager') }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ route('users.show', $user->manager->id) }}">{{ $user->manager->getFullNameAttribute() }}</a>
|
<a href="{{ route('users.show', $user->manager->id) }}">{{ $user->manager->getFullNameAttribute() }}</a>
|
||||||
|
|
||||||
|
@ -184,21 +184,21 @@
|
||||||
|
|
||||||
@if ($user->email)
|
@if ($user->email)
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ trans('admin/users/table.email') }}</td>
|
<td class="text-nowrap">{{ trans('admin/users/table.email') }}</td>
|
||||||
<td><a href="mailto:{{ $user->email }}">{{ $user->email }}</a></td>
|
<td><a href="mailto:{{ $user->email }}">{{ $user->email }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if ($user->phone)
|
@if ($user->phone)
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ trans('admin/users/table.phone') }}</td>
|
<td class="text-nowrap">{{ trans('admin/users/table.phone') }}</td>
|
||||||
<td><a href="tel:{{ $user->phone }}">{{ $user->phone }}</a></td>
|
<td><a href="tel:{{ $user->phone }}">{{ $user->phone }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if ($user->userloc)
|
@if ($user->userloc)
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ trans('admin/users/table.location') }}</td>
|
<td class="text-nowrap">{{ trans('admin/users/table.location') }}</td>
|
||||||
<td>{{ link_to_route('locations.show', $user->userloc->name, [$user->userloc->id]) }}</td>
|
<td>{{ link_to_route('locations.show', $user->userloc->name, [$user->userloc->id]) }}</td>
|
||||||
|
|
||||||
|
|
||||||
|
@ -206,14 +206,14 @@
|
||||||
@endif
|
@endif
|
||||||
@if ($user->last_login)
|
@if ($user->last_login)
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ trans('general.last_login') }}</td>
|
<td class="text-nowrap">{{ trans('general.last_login') }}</td>
|
||||||
<td>{{ \App\Helpers\Helper::getFormattedDateObject($user->last_login, 'datetime', false) }}</td>
|
<td>{{ \App\Helpers\Helper::getFormattedDateObject($user->last_login, 'datetime', false) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if (!is_null($user->department))
|
@if (!is_null($user->department))
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ trans('general.department') }}</td>
|
<td class="text-nowrap">{{ trans('general.department') }}</td>
|
||||||
<td><a href="{{ route('departments.show', $user->department) }}">{{ $user->department->name }}</a></td>
|
<td><a href="{{ route('departments.show', $user->department) }}">{{ $user->department->name }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
@endif
|
@endif
|
||||||
|
@ -223,6 +223,45 @@
|
||||||
<td>{{ $user->created_at->format('F j, Y h:iA') }}</td>
|
<td>{{ $user->created_at->format('F j, Y h:iA') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endif
|
@endif
|
||||||
|
<tr>
|
||||||
|
<td class="text-nowrap">{{ trans('general.login_enabled') }}</td>
|
||||||
|
<td>{{ ($user->activated=='1') ? trans('general.yes') : trans('general.no') }}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
@if ($user->activated=='1')
|
||||||
|
<tr>
|
||||||
|
<td class="text-nowrap">{{ trans('admin/users/general.two_factor_active') }}</td>
|
||||||
|
<td>{{ ($user->two_factor_active()) ? trans('general.yes') : trans('general.no') }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-nowrap">{{ trans('admin/users/general.two_factor_enrolled') }}</td>
|
||||||
|
<td class="two_factor_resetrow">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-1" id="two_factor_reset_toggle">
|
||||||
|
{{ ($user->two_factor_active_and_enrolled()) ? trans('general.yes') : trans('general.no') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ((Auth::user()->isSuperUser()) && ($snipeSettings->two_factor_enabled!='0'))
|
||||||
|
<div class="col-md-11">
|
||||||
|
<a class="btn btn-default btn-sm pull-left" id="two_factor_reset" style="margin-right: 10px;"> {{ trans('admin/settings/general.two_factor_reset') }}</a>
|
||||||
|
<span id="two_factor_reseticon">
|
||||||
|
</span>
|
||||||
|
<span id="two_factor_resetresult">
|
||||||
|
</span>
|
||||||
|
<span id="two_factor_resetstatus">
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<br><br><p class="help-block">{{ trans('admin/settings/general.two_factor_reset_help') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
@endif
|
||||||
|
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div> <!--/col-md-8-->
|
</div> <!--/col-md-8-->
|
||||||
|
@ -532,6 +571,40 @@
|
||||||
@include ('partials.bootstrap-table', ['simple_view' => true])
|
@include ('partials.bootstrap-table', ['simple_view' => true])
|
||||||
<script nonce="{{ csrf_token() }}">
|
<script nonce="{{ csrf_token() }}">
|
||||||
$(function () {
|
$(function () {
|
||||||
|
|
||||||
|
$("#two_factor_reset").click(function(){
|
||||||
|
$("#two_factor_resetrow").removeClass('success');
|
||||||
|
$("#two_factor_resetrow").removeClass('danger');
|
||||||
|
$("#two_factor_resetstatus").html('');
|
||||||
|
$("#two_factor_reseticon").html('<i class="fa fa-spinner spin"></i>');
|
||||||
|
$.ajax({
|
||||||
|
url: '{{ route('api.users.two_factor_reset', ['id'=> $user->id]) }}',
|
||||||
|
type: 'POST',
|
||||||
|
data: {},
|
||||||
|
headers: {
|
||||||
|
"X-Requested-With": 'XMLHttpRequest',
|
||||||
|
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr('content')
|
||||||
|
},
|
||||||
|
dataType: 'json',
|
||||||
|
|
||||||
|
success: function (data) {
|
||||||
|
$("#two_factor_reset_toggle").html('').html('{{ trans('general.no') }}');
|
||||||
|
$("#two_factor_reseticon").html('');
|
||||||
|
$("#two_factor_resetstatus").html('<i class="fa fa-check text-success"></i>' + data.message);
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
error: function (data) {
|
||||||
|
$("#two_factor_reseticon").html('');
|
||||||
|
$("#two_factor_reseticon").html('<i class="fa fa-exclamation-triangle text-danger"></i>');
|
||||||
|
$('#two_factor_resetstatus').text(data.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
//binds to onchange event of your input field
|
//binds to onchange event of your input field
|
||||||
var uploadedFileSize = 0;
|
var uploadedFileSize = 0;
|
||||||
$('#fileupload').bind('change', function() {
|
$('#fileupload').bind('change', function() {
|
||||||
|
|
Loading…
Add table
Reference in a new issue