Compare commits

...

54 Commits

Author SHA1 Message Date
snipe
1abd669de5 Refactor custom fields handling for storage
Signed-off-by: snipe <snipe@snipe.net>
2024-07-08 14:00:15 +01:00
snipe
ac9df2fc08 Merge pull request #15043 from snipe/fixes/custom_fields_on_audit
Fixed #15037 - Removed custom fieldsets on auditing - it’s not used (yet)
2024-07-08 13:20:59 +01:00
snipe
eba24b9242 Removed custom fieldsets on auditing - it’s not used
Signed-off-by: snipe <snipe@snipe.net>
2024-07-08 13:19:05 +01:00
snipe
edd61705dc Merge pull request #15027 from snipe/feature/sc-26112/allow_default_avatar
Added #15015 - ability for admins to select default avatar
2024-07-04 17:10:10 +01:00
snipe
3f20e29901 Moved gthe settings for remote loading to the branding section
Signed-off-by: snipe <snipe@snipe.net>
2024-07-04 16:55:47 +01:00
snipe
36ae162626 Changed my mind :)
Signed-off-by: snipe <snipe@snipe.net>
2024-07-04 16:48:05 +01:00
snipe
00f7cb9dbb Reverse the order for default gravatar
Signed-off-by: snipe <snipe@snipe.net>
2024-07-04 16:46:12 +01:00
snipe
1ca3dc26eb Removed extra debugging
Signed-off-by: snipe <snipe@snipe.net>
2024-07-04 16:36:48 +01:00
snipe
2f3be267b3 Added some logic around deleting images
Signed-off-by: snipe <snipe@snipe.net>
2024-07-04 16:33:22 +01:00
snipe
5b8f6910fb Marked tests as incomplete :(
Signed-off-by: snipe <snipe@snipe.net>
2024-07-04 16:33:08 +01:00
snipe
9fe26ba814 Reverted the validation - might have an impact on quickstart setup
Signed-off-by: snipe <snipe@snipe.net>
2024-07-04 13:25:39 +01:00
snipe
5e97ed1c7e Added migration
Signed-off-by: snipe <snipe@snipe.net>
2024-07-04 13:24:11 +01:00
snipe
947fb7af7a Added tests
Signed-off-by: snipe <snipe@snipe.net>
2024-07-04 13:24:03 +01:00
snipe
44bcc157e5 Updated icon
Signed-off-by: snipe <snipe@snipe.net>
2024-07-04 13:23:46 +01:00
snipe
278bf3da13 Updated language
Signed-off-by: snipe <snipe@snipe.net>
2024-07-04 13:23:38 +01:00
snipe
446bc81d3a Updated presenter to use new avatar
Signed-off-by: snipe <snipe@snipe.net>
2024-07-04 13:23:31 +01:00
snipe
9527aac242 Make site name required at the model
Signed-off-by: snipe <snipe@snipe.net>
2024-07-04 13:23:17 +01:00
snipe
c57f1f9d7d Use null coalescence
Signed-off-by: snipe <snipe@snipe.net>
2024-07-04 13:22:56 +01:00
snipe
e372527d13 Added default_avatar to settings
Signed-off-by: snipe <snipe@snipe.net>
2024-07-04 13:22:36 +01:00
snipe
96be1e1275 Removed duplicate locale directive
Signed-off-by: snipe <snipe@snipe.net>
2024-07-04 02:14:10 +01:00
snipe
8ce17d0585 Merge pull request #15025 from snipe/security/upgrade_webpack
Upgrade webpack from 5.91.0 to 5.92.0 #15008
2024-07-04 01:08:17 +01:00
snipe
6af1eaa4e4 Updated webpack to 5.92
Signed-off-by: snipe <snipe@snipe.net>
2024-07-04 01:07:26 +01:00
snipe
da01487301 Merge pull request #15023 from snipe/improvements/optimize_queries_for_user_bulk_actions
Added files column to bulk user delete, optimized queries
2024-07-03 23:45:09 +01:00
snipe
708d7b5fc5 Use icons for consistency
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 23:43:44 +01:00
snipe
62b5a159a9 Added files column
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 23:31:09 +01:00
snipe
a6d04509a5 Merge pull request #15016 from snipe/improved_user_merge
Fixed #15005 - Improvements  on user merge
2024-07-03 23:19:50 +01:00
snipe
0071596274 Added upload count
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 23:18:43 +01:00
snipe
59f66051f8 More eager loading
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 23:18:37 +01:00
snipe
9f1e59cf78 Marcus’ nitpicks
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 23:01:41 +01:00
snipe
f1f68b8ef6 Merge pull request #15022 from spencerrlongg/livewire-delete-button
Resolved Potential Issue when Deleting Personal Access Tokens
2024-07-03 22:24:25 +01:00
snipe
f22c3cdda9 Merge pull request #15021 from snipe/fixes/removed_non_counts
Removed non-counts from allowed array
2024-07-03 22:21:17 +01:00
snipe
5e15cc3bbe Added markIncompleteIfSqlite() method
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 22:18:31 +01:00
spencerrlongg
a6690493b0 resolved 2024-07-03 16:12:55 -05:00
snipe
17a6335d13 Added test
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 22:12:06 +01:00
akemidx
ca57f6de85 adding in item=>asset, missed in first commit 2024-07-03 17:07:50 -04:00
snipe
aefaabdb1a Removed non-counts from allowed array
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 21:40:43 +01:00
snipe
9211c8d3b1 Fixed test
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 20:57:21 +01:00
snipe
eff1980df5 Added console test
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 20:52:22 +01:00
snipe
1553ba5630 Added null coalescence for admin id in case via cli
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 20:52:12 +01:00
snipe
ec24120d2a Allow admin to be nullable (for cli)
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 20:50:35 +01:00
snipe
50df750202 Add merge event
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 20:50:23 +01:00
snipe
dab4aced48 Renamed test
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 20:50:09 +01:00
snipe
1774952312 Added additional assertions
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 20:19:12 +01:00
snipe
d66d6e70a6 Added checkedOutToUser factory for consumables
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 20:18:53 +01:00
snipe
4635e9269d Added user update log factory
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 20:18:34 +01:00
snipe
574867536d Standardize query for merging
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 19:41:34 +01:00
snipe
5488a4d118 One more test
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 14:36:27 +01:00
snipe
e34f3c7c2c Fixed typo
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 14:32:41 +01:00
snipe
ceaff7b645 Added tests
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 14:30:11 +01:00
snipe
d27a025347 Added factories
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 14:30:06 +01:00
snipe
2b2853a183 Added acceptance model
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 14:29:58 +01:00
snipe
a25263f868 Transfer files and acceptances on merge
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 14:29:49 +01:00
snipe
4b9727067b Merge pull request #15010 from snipe/fixes/translation_strings
Added missing translations
2024-07-03 12:43:50 +01:00
snipe
00fc392a12 Added missing traslations
Signed-off-by: snipe <snipe@snipe.net>
2024-07-03 12:41:04 +01:00
36 changed files with 970 additions and 230 deletions

View File

@@ -2,6 +2,7 @@
namespace App\Console\Commands;
use App\Events\UserMerged;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Console\Command;
@@ -51,7 +52,7 @@ class MergeUsersByUsername extends Command
$bad_users = User::where('username', '=', trim($parts[0]))
->whereNull('deleted_at')
->with('assets', 'manager', 'userlog', 'licenses', 'consumables', 'accessories', 'managedLocations')
->with('assets', 'manager', 'userlog', 'licenses', 'consumables', 'accessories', 'managedLocations','uploads', 'acceptances')
->get();
@@ -105,10 +106,26 @@ class MergeUsersByUsername extends Command
$managedLocation->save();
}
foreach ($bad_user->uploads as $upload) {
$this->info('Updating upload log record '.$upload->id.' to user '.$user->id);
$upload->item_id = $user->id;
$upload->save();
}
foreach ($bad_user->acceptances as $acceptance) {
$this->info('Updating acceptance log record '.$acceptance->id.' to user '.$user->id);
$acceptance->item_id = $user->id;
$acceptance->save();
}
// Mark the user as deleted
$this->info('Marking the user as deleted');
$bad_user->deleted_at = Carbon::now()->timestamp;
$bad_user->save();
event(new UserMerged($bad_user, $user, null));
}
}
}

View File

@@ -15,7 +15,7 @@ class UserMerged
*
* @return void
*/
public function __construct(User $from_user, User $to_user, User $admin)
public function __construct(User $from_user, User $to_user, ?User $admin)
{
$this->merged_from = $from_user;
$this->merged_to = $to_user;

View File

@@ -585,49 +585,8 @@ class AssetsController extends Controller
}
$asset = $request->handleImages($asset);
$asset = $asset->handleCustomFieldsForStoring($request);
// Update custom fields in the database.
$model = AssetModel::find($request->input('model_id'));
// Check that it's an object and not a collection
// (Sometimes people send arrays here and they shouldn't
if (($model) && ($model instanceof AssetModel) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field) {
// Set the field value based on what was sent in the request
$field_val = $request->input($field->db_column, null);
// If input value is null, use custom field's default value
if ($field_val == null) {
Log::debug('Field value for '.$field->db_column.' is null');
$field_val = $field->defaultValue($request->get('model_id'));
Log::debug('Use the default fieldset value of '.$field->defaultValue($request->get('model_id')));
}
// if the field is set to encrypted, make sure we encrypt the value
if ($field->field_encrypted == '1') {
Log::debug('This model field is encrypted in this fieldset.');
if (Gate::allows('admin')) {
// If input value is null, use custom field's default value
if (($field_val == null) && ($request->has('model_id') != '')) {
$field_val = Crypt::encrypt($field->defaultValue($request->get('model_id')));
} else {
$field_val = Crypt::encrypt($request->input($field->db_column));
}
}
}
if ($field->element == 'checkbox') {
if(is_array($field_val)) {
$field_val = implode(',', $field_val);
}
}
$asset->{$field->db_column} = $field_val;
}
}
if ($asset->save()) {
if ($request->get('assigned_user')) {
@@ -688,32 +647,7 @@ class AssetsController extends Controller
}
$asset = $request->handleImages($asset);
$model = AssetModel::find($asset->model_id);
// Update custom fields
$problems_updating_encrypted_custom_fields = false;
if (($model) && (isset($model->fieldset))) {
foreach ($model->fieldset->fields as $field) {
$field_val = $request->input($field->db_column, null);
if ($request->has($field->db_column)) {
if ($field->element == 'checkbox') {
if(is_array($field_val)) {
$field_val = implode(',', $field_val);
}
}
if ($field->field_encrypted == '1') {
if (Gate::allows('admin')) {
$field_val = Crypt::encrypt($field_val);
} else {
$problems_updating_encrypted_custom_fields = true;
continue;
}
}
$asset->{$field->db_column} = $field_val;
}
}
}
$asset = $asset->handleCustomFieldsForStoring($request);
if ($asset->save()) {

View File

@@ -247,10 +247,6 @@ class UsersController extends Controller
'jobtitle',
'username',
'employee_num',
'assets',
'accessories',
'consumables',
'licenses',
'groups',
'activated',
'created_at',

View File

@@ -160,29 +160,7 @@ class AssetsController extends Controller
$asset = $request->handleImages($asset);
}
// Update custom fields in the database.
// Validation for these fields is handled through the AssetRequest form request
$model = AssetModel::find($request->get('model_id'));
if (($model) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field) {
if ($field->field_encrypted == '1') {
if (Gate::allows('admin')) {
if (is_array($request->input($field->db_column))) {
$asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column)));
} else {
$asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column));
}
}
} else {
if (is_array($request->input($field->db_column))) {
$asset->{$field->db_column} = implode(', ', $request->input($field->db_column));
} else {
$asset->{$field->db_column} = $request->input($field->db_column);
}
}
}
}
$asset = $asset->handleCustomFieldsForStoring($request);
// Validate the asset before saving
if ($asset->isValid() && $asset->save()) {
@@ -370,32 +348,7 @@ class AssetsController extends Controller
$asset->notes = $request->input('notes');
$asset = $request->handleImages($asset);
// Update custom fields in the database.
// Validation for these fields is handlded through the AssetRequest form request
// FIXME: No idea why this is returning a Builder error on db_column_name.
// Need to investigate and fix. Using static method for now.
$model = AssetModel::find($request->get('model_id'));
if (($model) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field) {
if ($field->field_encrypted == '1') {
if (Gate::allows('admin')) {
if (is_array($request->input($field->db_column))) {
$asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column)));
} else {
$asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column));
}
}
} else {
if (is_array($request->input($field->db_column))) {
$asset->{$field->db_column} = implode(', ', $request->input($field->db_column));
} else {
$asset->{$field->db_column} = $request->input($field->db_column);
}
}
}
}
$asset = $asset->handleCustomFieldsForStoring($request);
if ($asset->save()) {
return redirect()->route('hardware.show', $assetId)
@@ -905,7 +858,7 @@ class AssetsController extends Controller
if ($request->input('update_location') == '1') {
$asset->location_id = $request->input('location_id');
}
/**
* Invoke Watson Validating to check the asset itself and check to make sure it saved correctly.

View File

@@ -348,7 +348,6 @@ class SettingsController extends Controller
}
$setting->default_eula_text = $request->input('default_eula_text');
$setting->load_remote = $request->input('load_remote', 0);
$setting->thumbnail_max_h = $request->input('thumbnail_max_h');
$setting->privacy_policy_link = $request->input('privacy_policy_link');
$setting->depreciation_method = $request->input('depreciation_method');
@@ -393,10 +392,11 @@ class SettingsController extends Controller
*
* @since [v1.0]
*
* @return View
* @return \Illuminate\Contracts\View\View | \Illuminate\Http\RedirectResponse
*/
public function postBranding(ImageUploadRequest $request)
{
// Something has gone horribly wrong - no settings record exists!
if (is_null($setting = Setting::getSettings())) {
return redirect()->to('admin')->with('error', trans('admin/settings/message.update.error'));
}
@@ -407,51 +407,75 @@ class SettingsController extends Controller
$setting->version_footer = $request->input('version_footer');
$setting->footer_text = $request->input('footer_text');
$setting->skin = $request->input('skin');
$setting->allow_user_skin = $request->input('allow_user_skin');
$setting->allow_user_skin = $request->input('allow_user_skin', '0');
$setting->show_url_in_emails = $request->input('show_url_in_emails', '0');
$setting->logo_print_assets = $request->input('logo_print_assets', '0');
$setting->load_remote = $request->input('load_remote', 0);
// Only allow the site name and CSS to be changed if lock_passwords is false
// Only allow the site name, images, and CSS to be changed if lock_passwords is false
// Because public demos make people act like dicks
if (! config('app.lock_passwords')) {
$request->validate(['site_name' => 'required']);
$setting->site_name = $request->input('site_name');
if (!config('app.lock_passwords')) {
if ($request->has('site_name')) {
$request->validate(['site_name' => 'required']);
}
$setting->site_name = $request->input('site_name', 'Snipe-IT');
$setting->custom_css = $request->input('custom_css');
// Logo upload
$setting = $request->handleImages($setting, 600, 'logo', '', 'logo');
if ('1' == $request->input('clear_logo')) {
Storage::disk('public')->delete($setting->logo);
if ($request->input('clear_logo') == '1') {
if (($setting->logo) && (Storage::exists($setting->logo))) {
Storage::disk('public')->delete($setting->logo);
}
$setting->logo = null;
$setting->brand = 1;
}
// Email logo upload
$setting = $request->handleImages($setting, 600, 'email_logo', '', 'email_logo');
if ($request->input('clear_email_logo') == '1') {
if ('1' == $request->input('clear_email_logo')) {
Storage::disk('public')->delete($setting->email_logo);
if (($setting->email_logo) && (Storage::exists($setting->email_logo))) {
Storage::disk('public')->delete($setting->email_logo);
}
$setting->email_logo = null;
// If they are uploading an image, validate it and upload it
}
// Label logo upload
$setting = $request->handleImages($setting, 600, 'label_logo', '', 'label_logo');
if ($request->input('clear_label_logo') == '1') {
if ('1' == $request->input('clear_label_logo')) {
Storage::disk('public')->delete($setting->label_logo);
if (($setting->label_logo) && (Storage::exists($setting->label_logo))) {
Storage::disk('public')->delete($setting->label_logo);
}
$setting->label_logo = null;
}
$setting = $request->handleImages($setting, 600, 'favicon', '', 'favicon');
// If the user wants to clear the favicon...
// Favicon upload
$setting = $request->handleImages($setting, 100, 'favicon', '', 'favicon');
if ('1' == $request->input('clear_favicon')) {
Storage::disk('public')->delete($setting->favicon);
if (($setting->favicon) && (Storage::exists($setting->favicon))) {
Storage::disk('public')->delete($setting->favicon);
}
$setting->favicon = null;
}
// Default avatar upload
$setting = $request->handleImages($setting, 500, 'default_avatar', 'avatars', 'default_avatar');
if ($request->input('clear_default_avatar') == '1') {
if (($setting->default_avatar) && (Storage::exists('avatars/'.$setting->default_avatar))) {
Storage::disk('public')->delete('avatars/'.$setting->default_avatar);
}
$setting->default_avatar = null;
}
}
if ($setting->save()) {

View File

@@ -42,7 +42,7 @@ class BulkUsersController extends Controller
// Get the list of affected users
$user_raw_array = request('ids');
$users = User::whereIn('id', $user_raw_array)
->with('groups', 'assets', 'licenses', 'accessories')->get();
->with('assets', 'manager', 'userlog', 'licenses', 'consumables', 'accessories', 'managedLocations','uploads', 'acceptances')->get();
// bulk edit, display the bulk edit form
if ($request->input('bulk_actions') == 'edit') {
@@ -317,7 +317,7 @@ class BulkUsersController extends Controller
// Get the users
$merge_into_user = User::find($request->input('merge_into_id'));
$users_to_merge = User::whereIn('id', $user_ids_to_merge)->with('assets', 'licenses', 'consumables','accessories')->get();
$users_to_merge = User::whereIn('id', $user_ids_to_merge)->with('assets', 'manager', 'userlog', 'licenses', 'consumables', 'accessories', 'managedLocations','uploads', 'acceptances')->get();
$admin = User::find(Auth::user()->id);
// Walk users
@@ -344,10 +344,20 @@ class BulkUsersController extends Controller
}
foreach ($user_to_merge->userlog as $log) {
$log->target_id = $user_to_merge->id;
$log->target_id = $merge_into_user->id;
$log->save();
}
foreach ($user_to_merge->uploads as $upload) {
$upload->item_id = $merge_into_user->id;
$upload->save();
}
foreach ($user_to_merge->acceptances as $acceptance) {
$acceptance->item_id = $merge_into_user->id;
$acceptance->save();
}
User::where('manager_id', '=', $user_to_merge->id)->update(['manager_id' => $merge_into_user->id]);
foreach ($user_to_merge->managedLocations as $managedLocation) {
@@ -356,7 +366,6 @@ class BulkUsersController extends Controller
}
$user_to_merge->delete();
//$user_to_merge->save();
event(new UserMerged($user_to_merge, $merge_into_user, $admin));

View File

@@ -96,10 +96,7 @@ class ImageUploadRequest extends Request
$ext = $image->guessExtension();
$file_name = $type.'-'.$form_fieldname.'-'.$item->id.'-'.str_random(10).'.'.$ext;
Log::info('File name will be: '.$file_name);
Log::debug('File extension is: '.$ext);
if (($image->getMimeType() == 'image/vnd.microsoft.icon') || ($image->getMimeType() == 'image/x-icon') || ($image->getMimeType() == 'image/avif') || ($image->getMimeType() == 'image/webp')) {
// If the file is an icon, webp or avif, we need to just move it since gd doesn't support resizing
// icons or avif, and webp support and needs to be compiled into gd for resizing to be available

View File

@@ -24,7 +24,7 @@ class UsersTransformer
$array = [
'id' => (int) $user->id,
'avatar' => e($user->present()->gravatar),
'avatar' => e($user->present()->gravatar) ?? null,
'name' => e($user->getFullNameAttribute()),
'first_name' => e($user->first_name),
'last_name' => e($user->last_name),

View File

@@ -111,7 +111,7 @@ class LogListener
$logaction->target_type = User::class;
$logaction->action_type = 'merged';
$logaction->note = trans('general.merged_log_this_user_from', $to_from_array);
$logaction->user_id = $event->admin->id;
$logaction->user_id = $event->admin->id ?? null;
$logaction->save();
// Add a record to the users being merged TO
@@ -122,7 +122,7 @@ class LogListener
$logaction->item_type = User::class;
$logaction->action_type = 'merged';
$logaction->note = trans('general.merged_log_this_user_into', $to_from_array);
$logaction->user_id = $event->admin->id;
$logaction->user_id = $event->admin->id ?? null;
$logaction->save();

View File

@@ -49,6 +49,6 @@ class PersonalAccessTokens extends Component
{
//this needs safety (though the scope of auth::user might kind of do it...)
//seems like it does, test more
Auth::user()->tokens()->find($tokenId)->delete();
Auth::user()->tokens()->find($tokenId)?->delete();
}
}

View File

@@ -13,10 +13,12 @@ use App\Presenters\Presentable;
use App\Presenters\AssetPresenter;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Watson\Validating\ValidatingTrait;
use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -966,6 +968,46 @@ class Asset extends Depreciable
return $cost;
}
public function handleCustomFieldsForStoring($request) : Asset
{
$model = AssetModel::find($this->model_id);
if (($model) && ($model instanceof AssetModel) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field) {
/*
* Check if the decrypted existing value is different from one we just submitted
* and if not, pull it out of the object since it shouldn't really be updating at all.
* If we don't do this, it will try to re-encrypt it, and the same value encrypted two
* different times will have different values, so it will *look* like it was updated
* but it wasn't.
*/
if ($request->input($field->db_column)!='') {
if ($field->field_encrypted == '1') {
if (Gate::allows('admin')) {
if (is_array($request->input($field->db_column))) {
$this->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column)));
} else {
$this->{$field->db_column} = Crypt::encrypt($request->input($field->db_column));
}
}
} else {
if (is_array($request->input($field->db_column))) {
$this->{$field->db_column} = implode(', ', $request->input($field->db_column));
} else {
$this->{$field->db_column} = $request->input($field->db_column);
}
}
}
}
}
return $this;
}
/**
* -----------------------------------------------
* BEGIN MUTATORS

View File

@@ -481,8 +481,6 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
/**
* Establishes the user -> uploads relationship
*
* @todo I don't think we use this?
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
@@ -496,6 +494,21 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
->orderBy('created_at', 'desc');
}
/**
* Establishes the user -> acceptances relationship
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v7.0.7]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function acceptances()
{
return $this->hasMany(\App\Models\Actionlog::class, 'target_id')
->where('target_type', self::class)
->where('action_type', '=', 'accepted')
->orderBy('created_at', 'desc');
}
/**
* Establishes the user -> requested assets relationship
*

View File

@@ -432,6 +432,8 @@ class UserPresenter extends Presenter
*/
public function gravatar()
{
// User's specific avatar
if ($this->avatar) {
// Check if it's a google avatar or some external avatar
@@ -443,6 +445,12 @@ class UserPresenter extends Presenter
return Storage::disk('public')->url('avatars/'.e($this->avatar));
}
// If there is a default avatar
if (Setting::getSettings()->default_avatar!= '') {
return Storage::disk('public')->url('avatars/'.e(Setting::getSettings()->default_avatar));
}
// Fall back to Gravatar if the settings allow loading remote scripts
if (Setting::getSettings()->load_remote == '1') {
if ($this->model->gravatar != '') {
@@ -456,8 +464,8 @@ class UserPresenter extends Presenter
}
}
// Set a fun, gender-neutral default icon
return config('app.url').'/img/default-sm.png';
return false;
}
/**

View File

@@ -105,4 +105,64 @@ class ActionlogFactory extends Factory
];
});
}
public function filesUploaded()
{
return $this->state(function () {
return [
'created_at' => $this->faker->dateTimeBetween('-1 years', 'now', date_default_timezone_get()),
'action_type' => 'uploaded',
'item_type' => User::class,
'filename' => $this->faker->unixTime('now'),
];
});
}
public function acceptedSignature()
{
return $this->state(function () {
$asset = Asset::factory()->create();
return [
'created_at' => $this->faker->dateTimeBetween('-1 years', 'now', date_default_timezone_get()),
'action_type' => 'accepted',
'item_id' => $asset->id,
'item_type' => Asset::class,
'target_type' => User::class,
'accept_signature' => $this->faker->unixTime('now'),
];
});
}
public function acceptedEula()
{
return $this->state(function () {
$asset = Asset::factory()->create();
return [
'created_at' => $this->faker->dateTimeBetween('-1 years', 'now', date_default_timezone_get()),
'action_type' => 'accepted',
'item_id' => $asset->id,
'item_type' => Asset::class,
'target_type' => User::class,
'filename' => $this->faker->unixTime('now'),
];
});
}
public function userUpdated()
{
return $this->state(function () {
return [
'created_at' => $this->faker->dateTimeBetween('-1 years', 'now', date_default_timezone_get()),
'action_type' => 'update',
'target_type' => User::class,
'item_type' => User::class,
];
});
}
}

View File

@@ -7,6 +7,7 @@ use App\Models\Company;
use App\Models\Consumable;
use App\Models\Manufacturer;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\Factory;
use App\Models\Supplier;
@@ -116,4 +117,16 @@ class ConsumableFactory extends Factory
$consumable->category->update(['require_acceptance' => 1]);
});
}
public function checkedOutToUser(User $user = null)
{
return $this->afterCreating(function (Consumable $consumable) use ($user) {
$consumable->users()->attach($consumable->id, [
'consumable_id' => $consumable->id,
'created_at' => Carbon::now(),
'user_id' => User::factory()->create()->id,
'assigned_to' => $user->id ?? User::factory()->create()->id,
]);
});
}
}

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('settings', function (Blueprint $table) {
$table->string('default_avatar')->after('favicon')->default('default.png')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('settings', function (Blueprint $table) {
$table->dropColumn('default_avatar');
});
}
};

27
package-lock.json generated
View File

@@ -36,7 +36,7 @@
"signature_pad": "^4.2.0",
"tableexport.jquery.plugin": "1.30.0",
"tether": "^1.4.0",
"webpack": "^5.90.2"
"webpack": "^5.92.0"
},
"devDependencies": {
"all-contributors-cli": "^6.26.1",
@@ -2497,6 +2497,14 @@
"acorn": "^8"
}
},
"node_modules/acorn-import-attributes": {
"version": "1.9.5",
"resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
"integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==",
"peerDependencies": {
"acorn": "^8"
}
},
"node_modules/acorn-node": {
"version": "1.8.2",
"license": "Apache-2.0",
@@ -5291,8 +5299,9 @@
}
},
"node_modules/enhanced-resolve": {
"version": "5.16.0",
"license": "MIT",
"version": "5.17.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz",
"integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
@@ -10394,7 +10403,8 @@
},
"node_modules/tapable": {
"version": "2.2.1",
"license": "MIT",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
"engines": {
"node": ">=6"
}
@@ -10857,8 +10867,9 @@
"license": "BSD-2-Clause"
},
"node_modules/webpack": {
"version": "5.91.0",
"license": "MIT",
"version": "5.92.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz",
"integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==",
"dependencies": {
"@types/eslint-scope": "^3.7.3",
"@types/estree": "^1.0.5",
@@ -10866,10 +10877,10 @@
"@webassemblyjs/wasm-edit": "^1.12.1",
"@webassemblyjs/wasm-parser": "^1.12.1",
"acorn": "^8.7.1",
"acorn-import-assertions": "^1.9.0",
"acorn-import-attributes": "^1.9.5",
"browserslist": "^4.21.10",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.16.0",
"enhanced-resolve": "^5.17.0",
"es-module-lexer": "^1.2.1",
"eslint-scope": "5.1.1",
"events": "^3.2.0",

View File

@@ -56,6 +56,6 @@
"signature_pad": "^4.2.0",
"tableexport.jquery.plugin": "1.30.0",
"tether": "^1.4.0",
"webpack": "^5.90.2"
"webpack": "^5.92.0"
}
}

View File

@@ -122,8 +122,8 @@ return [
'ldap_test' => 'Test LDAP',
'ldap_test_sync' => 'Test LDAP Synchronization',
'license' => 'Software License',
'load_remote' => 'Use Gravatar',
'load_remote_help_text' => 'Uncheck this box if your install cannot load scripts from the outside internet. This will prevent Snipe-IT from trying load images from Gravatar.',
'load_remote' => 'Load Remote Avatars',
'load_remote_help_text' => 'Uncheck this box if your install cannot load scripts from the outside internet. This will prevent Snipe-IT from trying load avatars from Gravatar or other outside sources.',
'login' => 'Login Attempts',
'login_attempt' => 'Login Attempt',
'login_ip' => 'IP Address',
@@ -375,5 +375,6 @@ return [
'timezone' => 'Timezone',
'profile_edit' => 'Edit Profile',
'profile_edit_help' => 'Allow users to edit their own profiles.',
'default_avatar' => 'Upload default avatar',
];

View File

@@ -549,6 +549,8 @@ return [
'license_seats' => ':count License Seat|:count License Seats',
'consumables' => ':count Consumable|:count Consumables',
'components' => ':count Component|:count Components',
]
],
'more_info' => 'More Info',
'quickscan_bulk_help' => 'Checking this box will edit the asset record to reflect this new location. Leaving it unchecked will simply note the location in the audit log. Note that if this asset is checked out, it will not change the location of the person, asset or location it is checked out to.',
];

View File

@@ -52,7 +52,7 @@
<input type="checkbox" value="1" name="update_location" {{ old('update_location') == '1' ? ' checked="checked"' : '' }}> {{ trans('admin/hardware/form.asset_location') }}
</label>
<a href="#" class="text-dark-gray" tabindex="0" role="button" data-toggle="popover" data-trigger="focus" title="<i class='far fa-life-ring'></i> More Info" data-html="true" data-content="Checking this box will edit the asset record to reflect this new location. Leaving it unchecked will simply note the location in the audit log.<br><br>Note that if this asset is checked out, it will not change the location of the person, asset or location it is checked out to."><i class="far fa-life-ring"></i></a>
<a href="#" class="text-dark-gray" tabindex="0" role="button" data-toggle="popover" data-trigger="focus" title="<i class='far fa-life-ring'></i>"{{ trans('general.more_info') }} data-html="true" data-content="{{ trans('general.quickscan_bulk_help') }}"><i class="far fa-life-ring"></i></a>
</div>
</div>

View File

@@ -324,7 +324,7 @@ dir="{{ in_array(app()->getLocale(),['ar-SA','fa-IR', 'he-IL']) ? 'rtl' : 'ltr'
<img src="{{ Auth::user()->present()->gravatar() }}" class="user-image"
alt="">
@else
<i class="fas fa-users" aria-hidden="true"></i>
<i class="fas fa-user" aria-hidden="true"></i>
@endif
<span class="hidden-xs">{{ Auth::user()->getFullNameAttribute() }} <strong

View File

@@ -48,7 +48,8 @@
</td>
<!-- Delete Button -->
<td style="vertical-align: middle;" class="text-right">
<a class="action-link btn btn-danger btn-sm" wire:click="deleteToken('{{ $token->id }}')">
<a class="action-link btn btn-danger btn-sm" wire:click="deleteToken('{{ $token->id }}')"
wire:loading.attr="disabled">
<i class="fas fa-trash"></i>
</a>
</td>

View File

@@ -9,7 +9,7 @@
<script nonce="{{ csrf_token() }}">
$(function () {
var locale = '{{ config('app.locale') }}';
var locale = '{{ app()->getLocale() }}';
var blockedFields = "searchable,sortable,switchable,title,visible,formatter,class".split(",");
var keyBlocked = function(key) {
@@ -46,7 +46,6 @@
stickyHeader: true,
stickyHeaderOffsetLeft: parseInt($('body').css('padding-left'), 10),
stickyHeaderOffsetRight: parseInt($('body').css('padding-right'), 10),
locale: '{{ app()->getLocale() }}',
undefinedText: '',
iconsPrefix: 'fa',
cookieStorage: '{{ config('session.bs_table_storage') }}',

View File

@@ -9,7 +9,7 @@
<div class="col-md-9">
<label class="btn btn-default{{ (config('app.lock_passwords')) ? ' disabled' : '' }}">
{{ trans('button.select_file') }}
<input type="file" name="{{ $logoVariable }}" class="js-uploadFile" id="{{ $logoId }}" accept="{{ (isset($allowedTypes) ? $allowedTypes : "image/gif,image/jpeg,image/webp,image/png,image/svg,image/svg+xml") }}" data-maxsize="{{ $maxSize ?? Helper::file_upload_max_size() }}"
<input type="file" name="{{ $logoVariable }}" class="js-uploadFile" id="{{ $logoId }}" accept="{{ $allowedTypes ?? "image/gif,image/jpeg,image/webp,image/png,image/svg,image/svg+xml" }}" data-maxsize="{{ $maxSize ?? Helper::file_upload_max_size() }}"
style="display:none; max-width: 90%"{{ (config('app.lock_passwords')) ? ' disabled' : '' }}>
</label>
@@ -28,13 +28,12 @@
</div>
<div class="col-md-9 col-md-offset-3">
@if (($setting->$logoVariable!='') && (Storage::disk('public')->exists(e($snipeSettings->$logoVariable))))
<div class="pull-left" style="padding-right: 20px;">
<a href="{{ Storage::disk('public')->url(e($snipeSettings->$logoVariable)) }}"{!! ($logoVariable!='favicon') ? ' data-toggle="lightbox"' : '' !!}>
<img id="{{ $logoId }}-imagePreview" style="height: 80px; padding-bottom: 5px;" alt="" src="{{ Storage::disk('public')->url(e($snipeSettings->$logoVariable)) }}">
</a>
</div>
@if (($setting->$logoVariable!='') && (Storage::disk('public')->exists(($logoPath ?? ''). $snipeSettings->$logoVariable)))
<div class="pull-left" style="padding-right: 20px;">
<a href="{{ Storage::disk('public')->url(e(($logoPath ?? '').$snipeSettings->$logoVariable)) }}"{!! ($logoVariable!='favicon') ? ' data-toggle="lightbox"' : '' !!}>
<img id="{{ $logoId }}-imagePreview" style="height: 80px; padding-bottom: 5px;" alt="" src="{{ Storage::disk('public')->url(e(($logoPath ?? ''). $snipeSettings->$logoVariable)) }}">
</a>
</div>
@endif
<div id="{{ $logoId }}-previewContainer" style="display: none;">
@@ -44,7 +43,7 @@
</div>
@if (($setting->$logoVariable!='') && (Storage::disk('public')->exists(e($snipeSettings->$logoVariable))))
@if (($setting->$logoVariable!='') && (Storage::disk('public')->exists(($logoPath ?? '').$snipeSettings->$logoVariable)))
<div class="col-md-9 col-md-offset-3">
<label id="{{ $logoId }}-deleteCheckbox" for="{{ $logoClearVariable }}" style="font-weight: normal" class="form-control">

View File

@@ -1,3 +1,3 @@
<a style="padding-left: 10px; font-size: 18px;" class="text-dark-gray" data-trigger="focus" tabindex="0" role="button" data-toggle="popover" title="More Info" data-placement="right" data-html="true" data-content="{{ (isset($helpText)) ? $helpText : 'Help Info Missing' }}">
<a style="padding-left: 10px; font-size: 18px;" class="text-dark-gray" data-trigger="focus" tabindex="0" role="button" data-toggle="popover" title="{{ trans('general.more_info') }}" data-placement="right" data-html="true" data-content="{{ (isset($helpText)) ? $helpText : 'Help Info Missing' }}">
<i class="far fa-life-ring" aria-hidden="true"><span class="sr-only">{{ trans('general.moreinfo') }}</span></i>
</a>

View File

@@ -109,7 +109,37 @@
"maxSize" => 20000
])
<!-- Include logo in print assets -->
<!-- Default Avatar -->
@include('partials/forms/edit/uploadLogo', [
"logoVariable" => "default_avatar",
"logoId" => "defaultAvatar",
"logoLabel" => trans('admin/settings/general.default_avatar'),
"logoClearVariable" => "clear_default_avatar",
"logoPath" => "avatars/",
"helpBlock" => trans('general.image_filetypes_help', ['size' => Helper::file_upload_max_size_readable()]),
])
<!-- Load gravatar -->
<div class="form-group {{ $errors->has('load_remote') ? 'error' : '' }}">
<div class="col-md-3">
<strong>{{ trans('admin/settings/general.load_remote') }}</strong>
</div>
<div class="col-md-9">
<label class="form-control">
{{ Form::checkbox('load_remote', '1', old('load_remote', $setting->load_remote)) }}
{{ trans('general.yes') }}
{!! $errors->first('load_remote', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</label>
<p class="help-block">
{{ trans('admin/settings/general.load_remote_help_text') }}
</p>
</div>
</div>
<!-- Include logo in print assets -->
<div class="form-group">
<div class="col-md-3">
<strong>{{ trans('admin/settings/general.logo_print_assets') }}</strong>

View File

@@ -144,24 +144,6 @@
</div>
</div>
<!-- Load gravatar -->
<div class="form-group {{ $errors->has('load_remote') ? 'error' : '' }}">
<div class="col-md-3">
<strong>{{ trans('admin/settings/general.load_remote') }}</strong>
</div>
<div class="col-md-9">
<label class="form-control">
{{ Form::checkbox('load_remote', '1', old('load_remote', $setting->load_remote)) }}
{{ trans('general.yes') }}
{!! $errors->first('load_remote', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</label>
<p class="help-block">
{{ trans('admin/settings/general.load_remote_help_text') }}
</p>
</div>
</div>
<!-- unique serial -->
<div class="form-group">

View File

@@ -39,12 +39,28 @@
<th class="col-md-1">
<!-- <input type="checkbox" id="checkAll"> -->
</th>
<th class="col-md-6">{{ trans('general.name') }}</th>
<th class="col-md-5">{{ trans('general.groups') }}</th>
<th class="col-md-5">{{ trans('general.assets') }}</th>
<th class="col-md-5">{{ trans('general.accessories') }}</th>
<th class="col-md-5">{{ trans('general.licenses') }}</th>
<th class="col-md-5">{{ trans('general.consumables') }}</th>
<th class="col-md-3">{{ trans('general.name') }}</th>
<th class="col-md-3">{{ trans('general.groups') }}</th>
<th class="text-right">
<i class="fas fa-barcode fa-fw" aria-hidden="true" style="font-size: 17px;"></i>
<span class="sr-only">{{ trans('general.assets') }}</span>
</th>
<th class="text-right">
<i class="far fa-keyboard fa-fw" aria-hidden="true" style="font-size: 17px;"></i>
<span class="sr-only">{{ trans('general.accessories') }}</span>
</th>
<th class="text-right">
<i class="far fa-save fa-fw" aria-hidden="true" style="font-size: 17px;"></i>
<span class="sr-only">{{ trans('general.licenses') }}</span>
</th>
<th class="text-right">
<i class="fas fa-tint fa-fw" aria-hidden="true" style="font-size: 17px;"></i>
<span class="sr-only">{{ trans('general.consumables') }}</span>
</th>
<th class="text-right">
<i class="fas fa-paperclip fa-fw" aria-hidden="true" style="font-size: 17px;"></i>
<span class="sr-only">{{ trans('general.files') }}</span>
</th>
</tr>
</thead>
<tbody>
@@ -71,17 +87,20 @@
</a>&nbsp;
@endforeach
</td>
<td>
{{ number_format($user->assets()->count()) }}
<td class="text-right">
{{ number_format($user->assets->count()) }}
</td>
<td>
{{ number_format($user->accessories()->count()) }}
<td class="text-right">
{{ number_format($user->accessories->count()) }}
</td>
<td>
{{ number_format($user->licenses()->count()) }}
<td class="text-right">
{{ number_format($user->licenses->count()) }}
</td>
<td>
{{ number_format($user->consumables()->count()) }}
<td class="text-right">
{{ number_format($user->consumables->count()) }}
</td>
<td class="text-right">
{{ number_format($user->uploads->count()) }}
</td>
</tr>
@endforeach
@@ -89,13 +108,13 @@
<tfoot>
<tr>
<td colspan="7">
<td colspan="8">
{{ Form::select('status_id', $statuslabel_list , old('status_id'), array('class'=>'select2', 'style'=>'width:250px')) }}
<label>{{ trans('admin/users/general.update_user_assets_status') }}</label>
</td>
</tr>
<tr>
<td colspan="7" class="col-md-12 alert-danger">
<td colspan="8" class="col-md-12 alert-danger">
<label class="form-control">
<input type="checkbox" name="delete_user" value="1">
<span><i class="fa fa-warning fa-2x"></i> {{ trans('general.bulk_soft_delete') }}</span>

View File

@@ -66,6 +66,10 @@
<i class="fas fa-tint fa-fw" aria-hidden="true" style="font-size: 17px;"></i>
<span class="sr-only">{{ trans('general.consumables') }}</span>
</th>
<th class="col-md-1 text-right">
<i class="fas fa-paperclip fa-fw" aria-hidden="true" style="font-size: 17px;"></i>
<span class="sr-only">{{ trans('general.files') }}</span>
</th>
</tr>
</thead>
<tbody>
@@ -93,16 +97,19 @@
@endforeach
</td>
<td class="text-right">
{{ number_format($user->assets()->count()) }}
{{ number_format($user->assets->count()) }}
</td>
<td class="text-right">
{{ number_format($user->accessories()->count()) }}
{{ number_format($user->accessories->count()) }}
</td>
<td class="text-right">
{{ number_format($user->licenses()->count()) }}
{{ number_format($user->licenses->count()) }}
</td>
<td class="text-right">
{{ number_format($user->consumables()->count()) }}
{{ number_format($user->consumables->count()) }}
</td>
<td class="text-right">
{{ number_format($user->uploads->count()) }}
</td>
</tr>
@endforeach

View File

@@ -0,0 +1,135 @@
<?php
namespace Tests\Feature\Console;
use App\Models\Accessory;
use App\Models\Asset;
use App\Models\Consumable;
use App\Models\LicenseSeat;
use App\Models\User;
use App\Models\Actionlog;
use Tests\TestCase;
class MergeUsersTest extends TestCase
{
public function testAssetsAreTransferredOnUserMerge()
{
$user1 = User::factory()->create(['username' => 'user1']);
$user_to_merge_into = User::factory()->create(['username' => 'user1@example.com']);
Asset::factory()->count(3)->assignedToUser($user1)->create();
Asset::factory()->count(3)->assignedToUser($user_to_merge_into)->create();
$this->artisan('snipeit:merge-users')->assertExitCode(0);
$this->assertEquals(6, $user_to_merge_into->refresh()->assets->count());
$this->assertEquals(0, $user1->refresh()->assets->count());
}
public function testLicensesAreTransferredOnUserMerge(): void
{
$user1 = User::factory()->create(['username' => 'user1']);
$user_to_merge_into = User::factory()->create(['username' => 'user1@example.com']);
LicenseSeat::factory()->count(3)->create(['assigned_to' => $user1->id]);
LicenseSeat::factory()->count(3)->create(['assigned_to' => $user_to_merge_into->id]);
$this->assertEquals(3, $user_to_merge_into->refresh()->licenses->count());
$this->artisan('snipeit:merge-users')->assertExitCode(0);
$this->assertEquals(6, $user_to_merge_into->refresh()->licenses->count());
$this->assertEquals(0, $user1->refresh()->licenses->count());
}
public function testAccessoriesTransferredOnUserMerge(): void
{
$user1 = User::factory()->create(['username' => 'user1']);
$user_to_merge_into = User::factory()->create(['username' => 'user1@example.com']);
Accessory::factory()->count(3)->checkedOutToUser($user1)->create();
Accessory::factory()->count(3)->checkedOutToUser($user_to_merge_into)->create();
$this->assertEquals(3, $user_to_merge_into->refresh()->accessories->count());
$this->artisan('snipeit:merge-users')->assertExitCode(0);
$this->assertEquals(6, $user_to_merge_into->refresh()->accessories->count());
$this->assertEquals(0, $user1->refresh()->accessories->count());
}
public function testConsumablesTransferredOnUserMerge(): void
{
$user1 = User::factory()->create(['username' => 'user1']);
$user_to_merge_into = User::factory()->create(['username' => 'user1@example.com']);
Consumable::factory()->count(3)->checkedOutToUser($user1)->create();
Consumable::factory()->count(3)->checkedOutToUser($user_to_merge_into)->create();
$this->assertEquals(3, $user_to_merge_into->refresh()->consumables->count());
$this->artisan('snipeit:merge-users')->assertExitCode(0);
$this->assertEquals(6, $user_to_merge_into->refresh()->consumables->count());
$this->assertEquals(0, $user1->refresh()->consumables->count());
}
public function testFilesAreTransferredOnUserMerge(): void
{
$user1 = User::factory()->create(['username' => 'user1']);
$user_to_merge_into = User::factory()->create(['username' => 'user1@example.com']);
Actionlog::factory()->count(3)->filesUploaded()->create(['item_id' => $user1->id]);
Actionlog::factory()->count(3)->filesUploaded()->create(['item_id' => $user_to_merge_into->id]);
$this->assertEquals(3, $user_to_merge_into->refresh()->uploads->count());
$this->artisan('snipeit:merge-users')->assertExitCode(0);
$this->assertEquals(6, $user_to_merge_into->refresh()->uploads->count());
$this->assertEquals(0, $user1->refresh()->uploads->count());
}
public function testAcceptancesAreTransferredOnUserMerge(): void
{
$user1 = User::factory()->create(['username' => 'user1']);
$user_to_merge_into = User::factory()->create(['username' => 'user1@example.com']);
Actionlog::factory()->count(3)->acceptedSignature()->create(['target_id' => $user1->id]);
Actionlog::factory()->count(3)->acceptedSignature()->create(['target_id' => $user_to_merge_into->id]);
$this->assertEquals(3, $user_to_merge_into->refresh()->acceptances->count());
$this->artisan('snipeit:merge-users')->assertExitCode(0);
$this->assertEquals(6, $user_to_merge_into->refresh()->acceptances->count());
$this->assertEquals(0, $user1->refresh()->acceptances->count());
}
public function testUserUpdateHistoryIsTransferredOnUserMerge(): void
{
$user1 = User::factory()->create(['username' => 'user1']);
$user_to_merge_into = User::factory()->create(['username' => 'user1@example.com']);
Actionlog::factory()->count(3)->userUpdated()->create(['target_id' => $user1->id, 'item_id' => $user1->id]);
Actionlog::factory()->count(3)->userUpdated()->create(['target_id' => $user_to_merge_into->id, 'item_id' => $user_to_merge_into->id]);
$this->assertEquals(3, $user_to_merge_into->refresh()->userlog->count());
$this->artisan('snipeit:merge-users')->assertExitCode(0);
// This needs to be more than the otherwise expected because the merge action itself is logged for the two merging users
$this->assertEquals(7, $user_to_merge_into->refresh()->userlog->count());
$this->assertEquals(1, $user1->refresh()->userlog->count());
}
}

View File

@@ -2,15 +2,242 @@
namespace Tests\Feature\Settings;
use App\Models\User;
use Tests\TestCase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use App\Models\User;
use App\Models\Setting;
class BrandingSettingsTest extends TestCase
{
public function testSiteNameIsRequired()
{
$this->actingAs(User::factory()->superuser()->create())
$response = $this->actingAs(User::factory()->superuser()->create())
->from(route('settings.branding.index'))
->post(route('settings.branding.save', ['site_name' => '']))
->assertInvalid('site_name');
->assertSessionHasErrors(['site_name'])
->assertInvalid(['site_name'])
->assertStatus(302)
->assertRedirect(route('settings.branding.index'));
$this->followRedirects($response)->assertSee(trans('general.error'));
}
public function testSiteNameCanBeSaved()
{
$response = $this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save', ['site_name' => 'My Awesome Site']))
->assertStatus(302)
->assertValid('site_name')
->assertRedirect(route('settings.index'))
->assertSessionHasNoErrors();
$this->followRedirects($response)->assertSee('Success');
}
public function testLogoCanBeUploaded()
{
$this->markTestIncomplete('This test fails because of how we handle image uploads in the ImageUploadRequest.');
Storage::fake('public');
$this->actingAs(User::factory()->superuser()->create())
->post(
route('settings.branding.save',
['logo' => UploadedFile::fake()->image('logo.jpg')])
)->assertValid('logo')
->assertStatus(302)
->assertRedirect(route('settings.index'))
->assertSessionHasNoErrors();
$setting = Setting::first();
$this->assertNotNull($setting->logo);
$this->assertDatabaseHas('settings', ['logo' => $setting->logo]);
Storage::disk('public')->assertExists($setting->logo);
}
public function testLogoCanBeDeleted()
{
$this->markTestIncomplete('This test fails because of how we handle image uploads in the ImageUploadRequest.');
Storage::fake('testdisk');
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',
['logo' => UploadedFile::fake()->image('logo.jpg')]
));
$setting = Setting::getSettings()->first();
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',['clear_logo' => '1']));
Storage::disk('testdisk')->assertMissing('logo.jpg');
$setting->refresh();
$this->assertNull($setting->logo);
}
public function testEmailLogoCanBeUploaded()
{
$this->markTestIncomplete('This test fails because of how we handle image uploads in the ImageUploadRequest.');
Storage::fake('testdisk');
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',
['email_logo' => UploadedFile::fake()->image('email-logo.jpg')]
))
->assertValid('email_logo')
->assertStatus(302)
->assertRedirect(route('settings.index'))
->assertSessionHasNoErrors();
$setting = Setting::getSettings()->first();
\Log::error($setting->toArray());
Storage::disk('testdisk')->assertExists($setting->email_logo);
}
public function testEmailLogoCanBeDeleted()
{
$this->markTestIncomplete('This test fails because of how we handle image uploads in the ImageUploadRequest.');
Storage::fake('testdisk');
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',
['email_logo' => UploadedFile::fake()->image('email-logo.jpg')]
));
$setting = Setting::getSettings()->first();
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',['clear_email_logo' => '1']));
Storage::disk('testdisk')->assertMissing('email-logo.jpg');
$setting->refresh();
$this->assertNull($setting->email_logo);
}
public function testLabelLogoCanBeUploaded()
{
$this->markTestIncomplete('This test fails because of how we handle image uploads in the ImageUploadRequest.');
Storage::fake('testdisk');
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',
['label_logo' => UploadedFile::fake()->image('label-logo.jpg')]
))
->assertValid('label_logo')
->assertStatus(302)
->assertRedirect(route('settings.index'))
->assertSessionHasNoErrors();
$setting = Setting::getSettings()->first();
Storage::disk('testdisk')->assertExists($setting->label_logo);
}
public function testLabelLogoCanBeDeleted()
{
$this->markTestIncomplete('This test fails because of how we handle image uploads in the ImageUploadRequest.');
Storage::fake('testdisk');
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',
['label_logo' => UploadedFile::fake()->image('label-logo.jpg')]
));
$setting = Setting::getSettings()->first();
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',['clear_label_logo' => '1']));
Storage::disk('testdisk')->assertMissing('label-logo.jpg');
$setting->refresh();
$this->assertNull($setting->label_logo);
}
public function testDefaultAvatarCanBeUploaded()
{
$this->markTestIncomplete('This test fails because of how we handle image uploads in the ImageUploadRequest.');
$setting = Setting::getSettings()->first();
Storage::fake('testdisk');
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',
['default_avatar' => UploadedFile::fake()->image('default-avatar.jpg')]
))
->assertValid('default_avatar')
->assertStatus(302)
->assertRedirect(route('settings.index'))
->assertSessionHasNoErrors();
$setting->refresh();
Storage::disk('testdisk')->assertExists($setting->default_avatar);
}
public function testDefaultAvatarCanBeDeleted()
{
$this->markTestIncomplete('This test fails because of how we handle image uploads in the ImageUploadRequest.');
Storage::fake('testdisk');
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',
['default_avatar' => UploadedFile::fake()->image('default-avatar.jpg')]
));
$setting = Setting::getSettings()->first();
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',['clear_default_avatar' => '1']));
Storage::disk('testdisk')->assertMissing('default-avatar.jpg');
$setting->refresh();
$this->assertNull($setting->default_avatar);
}
public function testFaviconCanBeUploaded()
{
$this->markTestIncomplete('This fails mimetype validation on the mock');
Storage::fake('testdisk');
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',
['favicon' => UploadedFile::fake()->image('favicon.svg')]
))
->assertValid('favicon')
->assertStatus(302)
->assertRedirect(route('settings.index'))
->assertSessionHasNoErrors();
$setting = Setting::getSettings()->first();
Storage::disk('testdisk')->assertExists($setting->favicon);
}
public function testFaviconCanBeDeleted()
{
$this->markTestIncomplete('This fails mimetype validation on the mock');
Storage::fake('testdisk');
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',
['favicon' => UploadedFile::fake()->image('favicon.ico')->mimeType('image/x-icon')]
));
$setting = Setting::getSettings()->first();
$this->actingAs(User::factory()->superuser()->create())
->post(route('settings.branding.save',['clear_favicon' => '1']));
Storage::disk('testdisk')->assertMissing('favicon.ico');
$setting->refresh();
$this->assertNull($setting->favicon);
}
}

View File

@@ -144,4 +144,17 @@ class UserSearchTest extends TestCase
'User index contains unexpected user from another company'
);
}
public function testUsersIndexWhenInvalidSortFieldIsPassed()
{
$this->markIncompleteIfSqlite('This test is not compatible with SQLite');
$this->actingAsForApi(User::factory()->viewUsers()->create())
->getJson(route('api.users.index', [
'sort' => 'assets',
]))
->assertOk()
->assertStatus(200)
->json();
}
}

View File

@@ -0,0 +1,213 @@
<?php
namespace Tests\Feature\Users\Ui;
use App\Models\Accessory;
use App\Models\Asset;
use App\Models\Consumable;
use App\Models\LicenseSeat;
use App\Models\User;
use App\Models\Actionlog;
use Tests\TestCase;
class MergeUsersTest extends TestCase
{
public function testAssetsAreTransferredOnUserMerge()
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$user_to_merge_into = User::factory()->create();
Asset::factory()->count(3)->assignedToUser($user1)->create();
Asset::factory()->count(3)->assignedToUser($user2)->create();
Asset::factory()->count(3)->assignedToUser($user_to_merge_into)->create();
$response = $this->actingAs(User::factory()->editUsers()->viewUsers()->create())
->post(route('users.merge.save', $user1->id),
[
'ids_to_merge' => [$user1->id, $user2->id],
'merge_into_id' => $user_to_merge_into->id
])
->assertStatus(302)
->assertRedirect(route('users.index'));
$this->followRedirects($response)->assertSee('Success');
$this->assertEquals(9, $user_to_merge_into->refresh()->assets->count());
$this->assertEquals(0, $user1->refresh()->assets->count());
$this->assertEquals(0, $user2->refresh()->assets->count());
}
public function testLicensesAreTransferredOnUserMerge()
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$user_to_merge_into = User::factory()->create();
LicenseSeat::factory()->count(3)->create(['assigned_to' => $user1->id]);
LicenseSeat::factory()->count(3)->create(['assigned_to' => $user2->id]);
LicenseSeat::factory()->count(3)->create(['assigned_to' => $user_to_merge_into->id]);
$this->assertEquals(3, $user_to_merge_into->refresh()->licenses->count());
$response = $this->actingAs(User::factory()->editUsers()->viewUsers()->create())
->post(route('users.merge.save', $user1->id),
[
'ids_to_merge' => [$user1->id, $user2->id],
'merge_into_id' => $user_to_merge_into->id
])
->assertStatus(302)
->assertRedirect(route('users.index'));
$this->followRedirects($response)->assertSee('Success');
$this->assertEquals(9, $user_to_merge_into->refresh()->licenses->count());
$this->assertEquals(0, $user1->refresh()->licenses->count());
$this->assertEquals(0, $user2->refresh()->licenses->count());
}
public function testAccessoriesTransferredOnUserMerge()
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$user_to_merge_into = User::factory()->create();
Accessory::factory()->count(3)->checkedOutToUser($user1)->create();
Accessory::factory()->count(3)->checkedOutToUser($user2)->create();
Accessory::factory()->count(3)->checkedOutToUser($user_to_merge_into)->create();
$this->assertEquals(3, $user_to_merge_into->refresh()->accessories->count());
$response = $this->actingAs(User::factory()->editUsers()->viewUsers()->create())
->post(route('users.merge.save', $user1->id),
[
'ids_to_merge' => [$user1->id, $user2->id],
'merge_into_id' => $user_to_merge_into->id
])
->assertStatus(302)
->assertRedirect(route('users.index'));
$this->followRedirects($response)->assertSee('Success');
$this->assertEquals(9, $user_to_merge_into->refresh()->accessories->count());
$this->assertEquals(0, $user1->refresh()->accessories->count());
$this->assertEquals(0, $user2->refresh()->accessories->count());
}
public function testConsumablesTransferredOnUserMerge()
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$user_to_merge_into = User::factory()->create();
Consumable::factory()->count(3)->checkedOutToUser($user1)->create();
Consumable::factory()->count(3)->checkedOutToUser($user2)->create();
Consumable::factory()->count(3)->checkedOutToUser($user_to_merge_into)->create();
$this->assertEquals(3, $user_to_merge_into->refresh()->consumables->count());
$response = $this->actingAs(User::factory()->editUsers()->viewUsers()->create())
->post(route('users.merge.save', $user1->id),
[
'ids_to_merge' => [$user1->id, $user2->id],
'merge_into_id' => $user_to_merge_into->id
])
->assertStatus(302)
->assertRedirect(route('users.index'));
$this->followRedirects($response)->assertSee('Success');
$this->assertEquals(9, $user_to_merge_into->refresh()->consumables->count());
$this->assertEquals(0, $user1->refresh()->consumables->count());
$this->assertEquals(0, $user2->refresh()->consumables->count());
}
public function testFilesAreTransferredOnUserMerge()
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$user_to_merge_into = User::factory()->create();
Actionlog::factory()->count(3)->filesUploaded()->create(['item_id' => $user1->id]);
Actionlog::factory()->count(3)->filesUploaded()->create(['item_id' => $user2->id]);
Actionlog::factory()->count(3)->filesUploaded()->create(['item_id' => $user_to_merge_into->id]);
$this->assertEquals(3, $user_to_merge_into->refresh()->uploads->count());
$response = $this->actingAs(User::factory()->editUsers()->viewUsers()->create())
->post(route('users.merge.save', $user1->id),
[
'ids_to_merge' => [$user1->id, $user2->id],
'merge_into_id' => $user_to_merge_into->id
])
->assertStatus(302)
->assertRedirect(route('users.index'));
$this->followRedirects($response)->assertSee('Success');
$this->assertEquals(9, $user_to_merge_into->refresh()->uploads->count());
$this->assertEquals(0, $user1->refresh()->uploads->count());
$this->assertEquals(0, $user2->refresh()->uploads->count());
}
public function testAcceptancesAreTransferredOnUserMerge()
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$user_to_merge_into = User::factory()->create();
Actionlog::factory()->count(3)->acceptedSignature()->create(['target_id' => $user1->id]);
Actionlog::factory()->count(3)->acceptedSignature()->create(['target_id' => $user2->id]);
Actionlog::factory()->count(3)->acceptedSignature()->create(['target_id' => $user_to_merge_into->id]);
$this->assertEquals(3, $user_to_merge_into->refresh()->acceptances->count());
$response = $this->actingAs(User::factory()->editUsers()->viewUsers()->create())
->post(route('users.merge.save', $user1->id),
[
'ids_to_merge' => [$user1->id, $user2->id],
'merge_into_id' => $user_to_merge_into->id
])
->assertStatus(302)
->assertRedirect(route('users.index'));
$this->followRedirects($response)->assertSee('Success');
$this->assertEquals(9, $user_to_merge_into->refresh()->acceptances->count());
$this->assertEquals(0, $user1->refresh()->acceptances->count());
$this->assertEquals(0, $user2->refresh()->acceptances->count());
}
public function testUserUpdateHistoryIsTransferredOnUserMerge()
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$user_to_merge_into = User::factory()->create();
Actionlog::factory()->count(3)->userUpdated()->create(['target_id' => $user1->id, 'item_id' => $user1->id]);
Actionlog::factory()->count(3)->userUpdated()->create(['target_id' => $user2->id, 'item_id' => $user2->id]);
Actionlog::factory()->count(3)->userUpdated()->create(['target_id' => $user_to_merge_into->id, 'item_id' => $user_to_merge_into->id]);
$this->assertEquals(3, $user_to_merge_into->refresh()->userlog->count());
$response = $this->actingAs(User::factory()->editUsers()->viewUsers()->create())
->post(route('users.merge.save', $user1->id),
[
'ids_to_merge' => [$user1->id, $user2->id],
'merge_into_id' => $user_to_merge_into->id
])
->assertStatus(302)
->assertRedirect(route('users.index'));
$this->followRedirects($response)->assertSee('Success');
// This needs to be 2 more than the otherwise expected because the merge action itself is logged for the two merging users
$this->assertEquals(11, $user_to_merge_into->refresh()->userlog->count());
$this->assertEquals(2, $user1->refresh()->userlog->count());
$this->assertEquals(2, $user2->refresh()->userlog->count());
}
}

View File

@@ -10,4 +10,11 @@ trait CanSkipTests
$this->markTestIncomplete($message);
}
}
public function markIncompleteIfSqlite($message = 'Test skipped due to database driver being sqlite.')
{
if (config('database.default') === 'sqlite') {
$this->markTestIncomplete($message);
}
}
}