Compare commits

...

19 Commits

Author SHA1 Message Date
snipe
47d25da371 Use upated $request->request stuff 2026-03-11 19:00:05 +00:00
snipe
2cc6109959 Fixed create 2026-03-11 18:56:07 +00:00
snipe
a186f54478 Added more checks 2026-03-11 18:55:58 +00:00
snipe
17bbe7736a Added negative tests 2026-03-11 18:50:41 +00:00
snipe
7beabb3a9c Work on tests 2026-03-11 17:36:21 +00:00
snipe
aac3b7b372 Updated string so the Oxford comma sticklers don’t kill me 2026-03-11 15:54:21 +00:00
snipe
fea4a3d53e Use assets_count 2026-03-11 15:54:04 +00:00
snipe
0c3f551dde Clarify the senstitive fields 2026-03-11 15:51:32 +00:00
snipe
64780e338b Disallow updating address info in controller 2026-03-11 15:06:20 +00:00
snipe
2dc2e6328f Removed $user from gate check 2026-03-11 15:05:39 +00:00
snipe
5851e2cd68 Removed $user from gate check 2026-03-11 15:05:26 +00:00
snipe
b90f2d719c Added fields gto SimpleNameSearch scope 2026-03-11 14:59:13 +00:00
snipe
d7fdb71554 Hide email as field in selectlist search unless authorized 2026-03-11 14:55:06 +00:00
snipe
646de9a074 Moved sensitive fields 2026-03-10 15:14:30 +00:00
snipe
34acd827be Added fields to export 2026-03-10 15:11:54 +00:00
snipe
fbce94f591 Set policy 2026-03-10 15:11:45 +00:00
snipe
0b30f9eae6 Added getters 2026-03-10 15:11:21 +00:00
snipe
8f6782bdfa Added permissions 2026-03-10 15:08:40 +00:00
snipe
bbe7393a61 Normalized breadcrumbs 2026-03-10 15:06:47 +00:00
18 changed files with 611 additions and 279 deletions

View File

@@ -109,7 +109,6 @@ class UsersController extends Controller
'last_name',
'first_name',
'display_name',
'email',
'jobtitle',
'username',
'employee_num',
@@ -126,13 +125,6 @@ class UsersController extends Controller
'accessories_count',
'manages_users_count',
'manages_locations_count',
'phone',
'mobile',
'address',
'city',
'state',
'country',
'zip',
'id',
'ldap_import',
'two_factor_optin',
@@ -142,7 +134,6 @@ class UsersController extends Controller
'start_date',
'end_date',
'autoassign_licenses',
'website',
'locale',
'notes',
'employee_num',
@@ -159,6 +150,21 @@ class UsersController extends Controller
];
// Do not even request these fields if the requesting user cannot manage user contact info
if (auth()->user()->can('manageContactInfo')) {
array_push($allowed_columns,
'address',
'city',
'country',
'email',
'mobile',
'phone',
'state',
'website',
'zip',
);
}
$filter = [];
if ($request->filled('filter')) {
@@ -196,13 +202,39 @@ class UsersController extends Controller
$users = $users->where('users.company_id', '=', $request->input('company_id'));
}
if ($request->filled('phone')) {
$users = $users->where('users.phone', '=', $request->input('phone'));
// Check that the user can view contact info
if (auth()->user()->can('manageContactInfo')) {
if ($request->filled('phone')) {
$users = $users->where('users.phone', '=', $request->input('phone'));
}
if ($request->filled('mobile')) {
$users = $users->where('users.mobile', '=', $request->input('mobile'));
}
if ($request->filled('email')) {
$users = $users->where('users.email', '=', $request->input('email'));
}
if ($request->filled('state')) {
$users = $users->where('users.state', '=', $request->input('state'));
}
if ($request->filled('country')) {
$users = $users->where('users.country', '=', $request->input('country'));
}
if ($request->filled('website')) {
$users = $users->where('users.website', '=', $request->input('website'));
}
if ($request->filled('zip')) {
$users = $users->where('users.zip', '=', $request->input('zip'));
}
}
if ($request->filled('mobile')) {
$users = $users->where('users.mobile', '=', $request->input('mobile'));
}
if ($request->filled('location_id')) {
$users = $users->where('users.location_id', '=', $request->input('location_id'));
@@ -212,10 +244,6 @@ class UsersController extends Controller
$users = $users->where('users.created_by', '=', $request->input('created_by'));
}
if ($request->filled('email')) {
$users = $users->where('users.email', '=', $request->input('email'));
}
if ($request->filled('username')) {
$users = $users->where('users.username', '=', $request->input('username'));
}
@@ -236,21 +264,6 @@ class UsersController extends Controller
$users = $users->where('users.employee_num', '=', $request->input('employee_num'));
}
if ($request->filled('state')) {
$users = $users->where('users.state', '=', $request->input('state'));
}
if ($request->filled('country')) {
$users = $users->where('users.country', '=', $request->input('country'));
}
if ($request->filled('website')) {
$users = $users->where('users.website', '=', $request->input('website'));
}
if ($request->filled('zip')) {
$users = $users->where('users.zip', '=', $request->input('zip'));
}
if ($request->filled('group_id')) {
$users = $users->ByGroup($request->input('group_id'));
@@ -384,27 +397,34 @@ class UsersController extends Controller
*/
public function selectlist(Request $request) : array
{
$users = User::select(
[
'users.id',
'users.username',
'users.employee_num',
'users.first_name',
'users.last_name',
'users.display_name',
'users.gravatar',
'users.avatar',
'users.email',
]
)->where('show_in_list', '=', '1');
$select_array = [
'users.id',
'users.username',
'users.employee_num',
'users.first_name',
'users.last_name',
'users.display_name',
'users.gravatar',
'users.avatar',
];
if (auth()->user()->can('manageContactInfo')) {
array_push($select_array, 'users.email');
}
$users = User::select($select_array)->where('show_in_list', '=', '1');
if ($request->filled('search')) {
$users = $users->where(function ($query) use ($request) {
$query->SimpleNameSearch($request->input('search'))
->orWhere('username', 'LIKE', '%'.$request->input('search').'%')
->orWhere('display_name', 'LIKE', '%'.$request->input('search').'%')
->orWhere('email', 'LIKE', '%'.$request->input('search').'%')
->orWhere('employee_num', 'LIKE', '%'.$request->input('search').'%');
$query->SimpleNameSearch($request->input('search'));
// Check that the requesting user can search against the email field
if (auth()->user()->can('manageContactInfo')) {
$query->orWhere('users.email', 'LIKE', '%'.$request->input('search').'%');
}
});
}
@@ -555,6 +575,7 @@ class UsersController extends Controller
$user->fill($request->except(['password', 'username', 'email', 'activated', 'permissions', 'activation_code', 'remember_token', 'two_factor_secret', 'two_factor_enrolled', 'two_factor_optin']));
if (auth()->user()->can('canEditAuthFields', $user) && auth()->user()->can('editableOnDemo')) {
if ($request->filled('password')) {
@@ -611,6 +632,18 @@ class UsersController extends Controller
}
if (auth()->user()->cannot('manageContactInfo')) {
$request->request->remove('phone');
$request->request->remove('mobile');
$request->request->remove('address');
$request->request->remove('city');
$request->request->remove('state');
$request->request->remove('country');
$request->request->remove('zip');
$request->request->remove('website');
}
if ($request->filled('display_name')) {
$user->display_name = $request->input('display_name');
}

View File

@@ -148,7 +148,7 @@ class BulkUsersController extends Controller
{
$this->authorize('update', User::class);
if ((! $request->filled('ids')) || $request->input('ids') <= 0) {
if ((!$request->filled('ids')) || $request->input('ids') <= 0) {
return redirect()->back()->with('error', trans('general.no_users_selected'));
}
$user_raw_array = $request->input('ids');
@@ -172,9 +172,16 @@ class BulkUsersController extends Controller
->conditionallyAddItem('display_name')
->conditionallyAddItem('start_date')
->conditionallyAddItem('end_date')
->conditionallyAddItem('city')
->conditionallyAddItem('autoassign_licenses');
// Check that the user can manage contact info for users
if (auth()->user()->can('manageContactInfo')) {
$this->conditionallyAddItem('city')
->conditionallyAddItem('state')
->conditionallyAddItem('country')
->conditionallyAddItem('zip');
}
// If the manager_id is one of the users being updated, generate a warning.
if (array_search($request->input('manager_id'), $user_raw_array)) {

View File

@@ -14,6 +14,7 @@ use App\Models\Group;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\WelcomeNotification;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Password;
@@ -88,7 +89,19 @@ class UsersController extends Controller
$this->authorize('create', User::class);
$user = new User;
//Username, email, and password need to be handled specially because the need to respect config values on an edit.
$user->email = trim($request->input('email'));
if (auth()->user()->can('manageContactInfo')) {
$user->email = trim($request->input('email'));
$user->phone = $request->input('phone');
$user->mobile = $request->input('mobile');
$user->address = $request->input('address', null);
$user->city = $request->input('city', null);
$user->state = $request->input('state', null);
$user->country = $request->input('country', null);
$user->zip = $request->input('zip', null);
$user->website = $request->input('website', null);
}
$user->username = trim($request->input('username'));
$user->display_name = $request->input('display_name');
if ($request->filled('password')) {
@@ -100,20 +113,15 @@ class UsersController extends Controller
$user->employee_num = $request->input('employee_num');
$user->activated = $request->input('activated', 0);
$user->jobtitle = $request->input('jobtitle');
$user->phone = $request->input('phone');
$user->mobile = $request->input('mobile');
$user->location_id = $request->input('location_id', null);
$user->department_id = $request->input('department_id', null);
$user->company_id = Company::getIdForUser($request->input('company_id', null));
$user->manager_id = $request->input('manager_id', null);
$user->notes = $request->input('notes');
$user->address = $request->input('address', null);
$user->city = $request->input('city', null);
$user->state = $request->input('state', null);
$user->country = $request->input('country', null);
$user->zip = $request->input('zip', null);
$user->remote = $request->input('remote', 0);
$user->website = $request->input('website', null);
$user->created_by = auth()->id();
$user->start_date = $request->input('start_date', null);
$user->end_date = $request->input('end_date', null);
@@ -268,6 +276,19 @@ class UsersController extends Controller
// Update the user fields
if (auth()->user()->can('manageContactInfo')) {
$user->email = trim($request->input('email'));
$user->phone = $request->input('phone');
$user->mobile = $request->input('mobile');
$user->address = $request->input('address', null);
$user->city = $request->input('city', null);
$user->state = $request->input('state', null);
$user->country = $request->input('country', null);
$user->zip = $request->input('zip', null);
$user->website = $request->input('website', null);
}
$user->first_name = $request->input('first_name');
$user->last_name = $request->input('last_name');
$user->display_name = $request->input('display_name');
@@ -275,21 +296,13 @@ class UsersController extends Controller
$user->locale = $request->input('locale');
$user->employee_num = $request->input('employee_num');
$user->jobtitle = $request->input('jobtitle', null);
$user->phone = $request->input('phone');
$user->mobile = $request->input('mobile');
$user->location_id = $request->input('location_id', null);
$user->company_id = Company::getIdForUser($request->input('company_id', null));
$user->manager_id = $request->input('manager_id', null);
$user->notes = $request->input('notes');
$user->department_id = $request->input('department_id', null);
$user->address = $request->input('address', null);
$user->city = $request->input('city', null);
$user->state = $request->input('state', null);
$user->country = $request->input('country', null);
$user->zip = $request->input('zip', null);
$user->remote = $request->input('remote', 0);
$user->vip = $request->input('vip', 0);
$user->website = $request->input('website', null);
$user->start_date = $request->input('start_date', null);
$user->end_date = $request->input('end_date', null);
$user->autoassign_licenses = $request->input('autoassign_licenses', 0);
@@ -486,11 +499,15 @@ class UsersController extends Controller
// Blank out some fields
$user->first_name = '';
$user->last_name = '';
$user->email = substr($user->email, ($pos = strpos($user->email, '@')) !== false ? $pos : 0);
$user->id = null;
$user->username = null;
$user->avatar = null;
if (auth()->user()->can('manageContactInfo')) {
$user->email = substr($user->email, ($pos = strpos($user->email, '@')) !== false ? $pos : 0);
}
// Get this user's groups
$userGroups = $user_to_clone->groups()->pluck('name', 'id');
@@ -530,6 +547,65 @@ class UsersController extends Controller
// Open output stream
$handle = fopen('php://output', 'w');
$headers = [
// strtolower to prevent Excel from trying to open it as a SYLK file
strtolower(trans('general.id')),
trans('admin/companies/table.title'),
trans('admin/users/table.title'),
trans('general.employee_number'),
trans('admin/users/table.first_name'),
trans('admin/users/table.last_name'),
trans('admin/users/table.name'),
trans('admin/users/table.username'),
trans('admin/users/table.display_name'),
];
if (auth()->user()->can('manageContactInfo')) {
array_push($headers,
trans('admin/users/table.email'),
trans('admin/users/table.phone'),
trans('admin/users/table.mobile'),
trans('general.address'),
trans('general.city'),
trans('general.state'),
trans('general.country'),
trans('general.zip'),
trans('general.website'));
}
array_push($headers,
trans('admin/users/table.manager'),
trans('admin/users/table.location'),
trans('general.department'),
trans('admin/users/general.department_manager'),
trans('general.assets'),
trans('general.licenses'),
trans('general.accessories'),
trans('general.consumables'),
trans('admin/users/table.managed_users'),
trans('admin/users/table.managed_locations'),
trans('general.groups'),
trans('general.permissions'),
trans('general.notes'),
trans('admin/users/table.activated'),
trans('admin/settings/general.ldap_enabled'),
trans('admin/users/general.two_factor_enrolled'),
trans('admin/users/general.two_factor_active'),
trans('general.autoassign_licenses'),
trans('admin/users/general.remote'),
trans('admin/users/general.vip_label'),
trans('general.language'),
trans('general.start_date'),
trans('general.end_date'),
trans('general.last_login'),
trans('general.updated_at'),
trans('general.created_at'),
trans('general.created_by'),
);
$users = User::with(
'assets',
'accessories',
@@ -540,32 +616,18 @@ class UsersController extends Controller
'groups',
'userloc',
'company'
)->orderBy('created_at', 'DESC')
->chunk(500, function ($users) use ($handle) {
$headers = [
// strtolower to prevent Excel from trying to open it as a SYLK file
strtolower(trans('general.id')),
trans('admin/companies/table.title'),
trans('admin/users/table.title'),
trans('general.employee_number'),
trans('admin/users/table.first_name'),
trans('admin/users/table.last_name'),
trans('admin/users/table.name'),
trans('admin/users/table.username'),
trans('admin/users/table.email'),
trans('admin/users/table.manager'),
trans('admin/users/table.location'),
trans('general.department'),
trans('general.assets'),
trans('general.licenses'),
trans('general.accessories'),
trans('general.consumables'),
trans('general.groups'),
trans('general.permissions'),
trans('general.notes'),
trans('admin/users/table.activated'),
trans('general.created_at'),
];
) ->withCount([
'assets as assets_count' => function(Builder $query) {
$query->withoutTrashed();
},
'licenses as licenses_count',
'accessories as accessories_count',
'consumables as consumables_count',
'managesUsers as manages_users_count',
'managedLocations as manages_locations_count'
])->orderBy('created_at', 'DESC')
->chunk(500, function ($users) use ($handle, $headers) {
fputcsv($handle, $headers);
@@ -599,20 +661,53 @@ class UsersController extends Controller
$user->last_name,
$user->display_name,
$user->username,
$user->email,
$user->getRawOriginal('display_name'),
];
if (auth()->user()->can('manageContactInfo')) {
array_push($values,
$user->email,
$user->phone,
$user->mobile,
$user->address,
$user->city,
$user->state,
$user->country,
$user->zip,
$user->website,
);
}
array_push($values,
($user->manager) ? $user->manager->display_name : '',
($user->userloc) ? $user->userloc->name : '',
($user->department) ? $user->department->name : '',
$user->assets->count(),
$user->licenses->count(),
$user->accessories->count(),
$user->consumables->count(),
(($user->department) && ($user->department->manager)) ? $user->department->manager->display_name : '',
$user->assets_count,
$user->licenses_count,
$user->accessories_count,
$user->consumables_count,
$user->manages_users_count,
$user->manages_locations_count,
$user_groups,
$permissionstring,
$user->notes,
($user->activated == '1') ? trans('general.yes') : trans('general.no'),
($user->ldap_import == '1') ? trans('general.yes') : trans('general.no'),
($user->two_factor_active_and_enrolled()) ? trans('general.yes') : trans('general.no'),
($user->two_factor_active()) ? trans('general.yes') : trans('general.no'),
($user->autoassign_licenses == '1') ? trans('general.yes') : trans('general.no'),
($user->remote == '1') ? trans('general.yes') : trans('general.no'),
($user->vip == '1') ? trans('general.yes') : trans('general.no'),
$user->locale,
$user->start_date,
$user->end_date,
$user->last_login,
$user->updated_at,
$user->created_at,
];
$user->createdBy?->display_name,
);
fputcsv($handle, $values);
}

View File

@@ -28,6 +28,19 @@ class UsersTransformer
} elseif ($user->isAdmin()) {
$role = 'admin';
}
$sensitive_fields = [
'email' => ($user->email) ? e($user->email) : null,
'phone' => ($user->phone) ? e($user->phone) : null,
'mobile' => ($user->mobile) ? e($user->mobile) : null,
'website' => ($user->website) ? e($user->website) : null,
'address' => ($user->address) ? e($user->address) : null,
'city' => ($user->city) ? e($user->city) : null,
'state' => ($user->state) ? e($user->state) : null,
'country' => ($user->country) ? e($user->country) : null,
'zip' => ($user->zip) ? e($user->zip) : null,
];
$array = [
'id' => (int) $user->id,
'avatar' => e($user->present()->gravatar) ?? null,
@@ -45,15 +58,15 @@ class UsersTransformer
] : null,
'jobtitle' => ($user->jobtitle) ? e($user->jobtitle) : null,
'vip' => ($user->vip == '1') ? true : false,
'phone' => ($user->phone) ? e($user->phone) : null,
'mobile' => ($user->mobile) ? e($user->mobile) : null,
'website' => ($user->website) ? e($user->website) : null,
'address' => ($user->address) ? e($user->address) : null,
'city' => ($user->city) ? e($user->city) : null,
'state' => ($user->state) ? e($user->state) : null,
'country' => ($user->country) ? e($user->country) : null,
'zip' => ($user->zip) ? e($user->zip) : null,
'email' => ($user->email) ? e($user->email) : null,
];
if (auth()->user()->can('manageContactInfo')) {
$array += $sensitive_fields;
}
$array += [
'department' => ($user->department) ? [
'id' => (int) $user->department->id,
'name'=> e($user->department->name),

View File

@@ -208,6 +208,68 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
* @return Attribute
*/
/**
* These fields should be hidden if the requesting user cannot view contact info
* @return Attribute
*/
protected function address(): Attribute
{
return Attribute:: make(
get: fn(mixed $value) => (auth()->user() && auth()->user()->can('manageContactInfo')) ? $value : null,
);
}
protected function city(): Attribute
{
return Attribute:: make(
get: fn(mixed $value) => (auth()->user() && auth()->user()->can('manageContactInfo')) ? $value : null,
);
}
protected function state(): Attribute
{
return Attribute:: make(
get: fn(mixed $value) => (auth()->user() && auth()->user()->can('manageContactInfo')) ? $value : null,
);
}
protected function country(): Attribute
{
return Attribute:: make(
get: fn(mixed $value) => (auth()->user() && auth()->user()->can('manageContactInfo')) ? $value : null,
);
}
protected function zip(): Attribute
{
return Attribute:: make(
get: fn(mixed $value) => (auth()->user() && auth()->user()->can('manageContactInfo')) ? $value : null,
);
}
protected function phone(): Attribute
{
return Attribute:: make(
get: fn(mixed $value) => (auth()->user() && auth()->user()->can('manageContactInfo')) ? $value : null,
);
}
protected function mobile(): Attribute
{
return Attribute:: make(
get: fn(mixed $value) => (auth()->user() && auth()->user()->can('manageContactInfo')) ? $value : null,
);
}
protected function website(): Attribute
{
return Attribute:: make(
get: fn(mixed $value) => (auth()->user() && auth()->user()->can('manageContactInfo')) ? $value : null,
);
}
protected function displayName(): Attribute
{
return Attribute:: make(
@@ -1033,9 +1095,11 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
*/
public function scopeSimpleNameSearch($query, $search)
{
return $query->where('first_name', 'LIKE', '%' . $search . '%')
->orWhere('last_name', 'LIKE', '%' . $search . '%')
->orWhere('display_name', 'LIKE', '%' . $search . '%')
return $query->where('users.first_name', 'LIKE', '%' . $search . '%')
->orWhere('users.last_name', 'LIKE', '%' . $search . '%')
->orWhere('users.username', 'LIKE', '%' . $search . '%')
->orWhere('users.display_name', 'LIKE', '%' . $search . '%')
->orWhere('users.employee_num', 'LIKE', '%' . $search . '%')
->orWhereMultipleColumns(
[
'users.first_name',

View File

@@ -2,10 +2,17 @@
namespace App\Policies;
use App\Models\User;
class UserPolicy extends SnipePermissionsPolicy
{
protected function columnName()
{
return 'users';
}
public function manageContactInfo(User $user)
{
return $user->hasAccess('users.contact');
}
}

View File

@@ -128,7 +128,11 @@ class UserPresenter extends Presenter
'title' => trans('admin/users/general.remote'),
'visible' => false,
'formatter' => 'trueFalseFormatter',
],
]
];
$sensitive_fields = [
[
'field' => 'email',
'searchable' => true,
@@ -204,8 +208,17 @@ class UserPresenter extends Presenter
'switchable' => true,
'title' => trans('general.zip'),
'visible' => false,
],
]
];
// Add the sensitive fields in if the user can see them
if (auth()->user()->can('manageContactInfo')) {
foreach ($sensitive_fields as $sensitive_field) {
array_push($layout, $sensitive_field);
}
}
array_push($layout,
[
'field' => 'locale',
'searchable' => true,
@@ -434,8 +447,8 @@ class UserPresenter extends Presenter
'formatter' => 'usersActionsFormatter',
'printIgnore' => true,
'class' => 'hidden-print',
],
];
]
);
return json_encode($layout);
}
@@ -449,31 +462,6 @@ class UserPresenter extends Presenter
return '';
}
/**
* Returns the user full name, it simply concatenates
* the user first and last name.
*
* @return string
*/
// public function fullName()
// {
// if ($this->display_name) {
// return 'kjdfh'.html_entity_decode($this->display_name, ENT_QUOTES | ENT_XML1, 'UTF-8');
// }
// return 'roieuoe'.html_entity_decode($this->first_name.' '.$this->last_name, ENT_QUOTES | ENT_XML1, 'UTF-8');
// }
// /**
// * Standard accessor.
// * @TODO Remove presenter::fullName() entirely?
// * @return string
// */
// public function name()
// {
// return $this->fullName();
// }
/**
* Returns the user Gravatar image url.
@@ -558,4 +546,6 @@ class UserPresenter extends Presenter
return '<span class="'. (($this->deleted_at!='') ? 'deleted' : '').'">'.e($this->display_name).'</span>';
}
}

View File

@@ -162,6 +162,12 @@ class AuthServiceProvider extends ServiceProvider
return true;
});
Gate::define('manageContactInfo', function ($user) {
if ($user->hasAccess('users.contact')) {
return true;
}
});
Gate::define('admin', function ($user) {
if ($user->hasAccess('admin')) {
return true;

View File

@@ -607,7 +607,8 @@ class BreadcrumbsServiceProvider extends ServiceProvider
Breadcrumbs::for('users.edit', fn (Trail $trail, User $user) =>
$trail->parent('users.index', route('users.index'))
->push(trans('general.breadcrumb_button_actions.edit_item', ['name' => $user->name]), route('users.edit', $user))
->push($user->display_name, route('users.show', $user))
->push(trans('general.update'), route('users.edit', $user))
);

View File

@@ -251,6 +251,14 @@ return [
'permission' => 'users.delete',
'display' => true,
],
[
'permission' => 'users.files',
'display' => true,
],
[
'permission' => 'users.contact',
'display' => true,
],
],

View File

@@ -111,6 +111,11 @@ class UserFactory extends Factory
];
});
}
public function manageContactInfo()
{
return $this->appendPermission(['users.contact' => '1']);
}
public function viewAssets()
{

View File

@@ -226,6 +226,16 @@ return array(
'usersdelete' => [
'name' => 'Delete Users',
],
'usersfiles' => [
'name' => 'Manage User Files',
'note' => 'Allows the user to view, upload, download, and delete files associated with users.',
],
'userscontact' => [
'name' => 'View/Edit User Contact Info',
'note' => 'Allows the user to view and edit personal contact information about the user. This includes: address, city, state/province, country, postal code, phone number, mobile number, email address, and website.',
],
'models' => [
'name' => 'Models',
'note' => 'Grants access to the Models section of the application.',

View File

@@ -19,8 +19,7 @@
margin-left: -20px;
}
</style>
<div class="row">
<div class="col-md-8 col-md-offset-2">
<x-container class="col-md-6 col-md-offset-3">
<p>{{ trans('admin/users/general.bulk_update_help') }}</p>
@@ -108,6 +107,7 @@
</div>
</div>
@can('manageContactInfo')
<!-- City -->
<div class="form-group{{ $errors->has('city') ? ' has-error' : '' }}">
<label class="col-md-3 control-label" for="city">{{ trans('general.city') }}</label>
@@ -117,6 +117,37 @@
</div>
</div>
<div class="form-group{{ $errors->has('state') ? ' has-error' : '' }}">
<label class="col-md-3 control-label" for="state">{{ trans('general.state') }}</label>
<div class="col-md-4">
<input class="form-control" type="text" name="state" id="state" aria-label="state" />
{!! $errors->first('state', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>
<!-- Country -->
<div class="form-group{{ $errors->has('country') ? ' has-error' : '' }}">
<label class="col-md-3 control-label" for="country">{{ trans('general.country') }}</label>
<div class="col-md-6">
<x-input.country-select
name="country"
class="col-md-12"
/>
<p class="help-block">{{ trans('general.countries_manually_entered_help') }}</p>
{!! $errors->first('country', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>
<div class="form-group{{ $errors->has('zip') ? ' has-error' : '' }}">
<label class="col-md-3 control-label" for="zip">{{ trans('general.zip') }}</label>
<div class="col-md-4">
<input class="form-control" type="text" name="zip" id="zip" aria-label="zip" />
{!! $errors->first('zip', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>
@endcan
<!-- remote -->
<div class="form-group">
<div class="col-sm-3 control-label">
@@ -162,7 +193,7 @@
</div>
</div> <!--/form-group-->
<!-- activated -->
<!-- autoassign -->
<div class="form-group">
<div class="col-sm-3 control-label">
{{ trans('general.autoassign_licenses') }}
@@ -303,5 +334,5 @@
</div> <!--/.box.box-default-->
</form>
</div> <!--/.col-md-8-->
</div>
</x-container>
@stop

View File

@@ -39,8 +39,8 @@
</style>
<div class="row">
<div class="col-md-8 col-md-offset-2">
<x-container class="col-md-6 col-md-offset-3">
<form class="form-horizontal" method="post" autocomplete="off"
action="{{ (isset($user->id)) ? route('users.update', ['user' => $user->id]) : route('users.store') }}"
enctype="multipart/form-data" id="userForm">
@@ -237,57 +237,58 @@
</div>
</div>
<!-- Email -->
<div class="form-group {{ $errors->has('email') ? 'has-error' : '' }}">
<label class="col-md-3 control-label" for="email">{{ trans('admin/users/table.email') }} </label>
<div class="col-md-6">
<input class="form-control" type="email" name="email" id="email" maxlength="191" value="{{ old('email', $user->email) }}" autocomplete="off"
readonly onfocus="this.removeAttribute('readonly');" {{ (Helper::checkIfRequired($user, 'email')) ? ' required' : '' }}{!! (!Gate::allows('canEditAuthFields', $user)) || ((!Gate::allows('editableOnDemo')) && ($user->id)) ? ' style="cursor: not-allowed" disabled ' : '' !!}>
@can('manageContactInfo')
<!-- Email -->
<div class="form-group {{ $errors->has('email') ? 'has-error' : '' }}">
<label class="col-md-3 control-label" for="email">{{ trans('admin/users/table.email') }} </label>
<div class="col-md-6">
<input class="form-control" type="email" name="email" id="email" maxlength="191" value="{{ old('email', $user->email) }}" autocomplete="off"
readonly onfocus="this.removeAttribute('readonly');" {{ (Helper::checkIfRequired($user, 'email')) ? ' required' : '' }}{!! (!Gate::allows('canEditAuthFields', $user)) || ((!Gate::allows('editableOnDemo')) && ($user->id)) ? ' style="cursor: not-allowed" disabled ' : '' !!}>
@cannot('canEditAuthFields', $user)
<!-- authed user is an admin or regular user and is trying to edit someone higher -->
<p class="help-block">
<x-icon type="locked" />
{{ trans('general.action_permission_generic', ['action' => trans('general.edit'), 'item_type' => trans('general.email')]) }}
</p>
@endcannot
@cannot('canEditAuthFields', $user)
<!-- authed user is an admin or regular user and is trying to edit someone higher -->
<p class="help-block">
<x-icon type="locked" />
{{ trans('general.action_permission_generic', ['action' => trans('general.edit'), 'item_type' => trans('general.email')]) }}
</p>
@endcannot
@if (!Gate::allows('editableOnDemo') && ($user->id))
<p class="text-warning">
<x-icon type="locked" />
{{ trans('admin/users/table.lock_passwords') }}
</p>
@endif
@if (!Gate::allows('editableOnDemo') && ($user->id))
<p class="text-warning">
<x-icon type="locked" />
{{ trans('admin/users/table.lock_passwords') }}
</p>
@endif
{!! $errors->first('email', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
{!! $errors->first('email', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>
</div>
</div>
<!-- Send welcome email to user -->
@if (!$user->id)
<div class="form-group" id="email_user_row">
<!-- Send welcome email to user -->
@if (!$user->id)
<div class="form-group" id="email_user_row">
<div class="col-md-8 col-md-offset-3">
<label class="form-control form-control--disabled">
<input
type="checkbox"
name="send_welcome"
id="email_user_checkbox"
value="1"
aria-label="send_welcome"
@checked(old('send_welcome'))
/>
{{ trans('general.send_welcome_email_to_users') }}
</label>
<div class="col-md-8 col-md-offset-3">
<label class="form-control form-control--disabled">
<input
type="checkbox"
name="send_welcome"
id="email_user_checkbox"
value="1"
aria-label="send_welcome"
@checked(old('send_welcome'))
/>
{{ trans('general.send_welcome_email_to_users') }}
</label>
<p class="help-block"> {{ trans('general.send_welcome_email_help') }}</p>
<p class="help-block"> {{ trans('general.send_welcome_email_help') }}</p>
</div>
</div> <!--/form-group-->
@endif
</div>
</div> <!--/form-group-->
@endif
@endcan
@include ('partials.forms.edit.image-upload', ['fieldname' => 'avatar', 'image_path' => app('users_upload_path')])
@@ -444,92 +445,94 @@
<!-- Location -->
@include ('partials.forms.edit.location-select', ['translated_name' => trans('general.location'), 'fieldname' => 'location_id'])
<!-- Phone -->
<div class="form-group {{ $errors->has('phone') ? 'has-error' : '' }}">
<label class="col-md-3 control-label" for="phone">{{ trans('admin/users/table.phone') }}</label>
<div class="col-md-6">
<input class="form-control" type="text" name="phone" id="phone" value="{{ old('phone', $user->phone) }}" maxlength="191" />
{!! $errors->first('phone', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
@can('manageContactInfo')
<!-- Phone -->
<div class="form-group {{ $errors->has('phone') ? 'has-error' : '' }}">
<label class="col-md-3 control-label" for="phone">{{ trans('admin/users/table.phone') }}</label>
<div class="col-md-6">
<input class="form-control" type="text" name="phone" id="phone" value="{{ old('phone', $user->phone) }}" maxlength="191" />
{!! $errors->first('phone', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>
</div>
<!-- Mobile -->
<div class="form-group {{ $errors->has('mobile') ? 'has-error' : '' }}">
<label class="col-md-3 control-label" for="phone">{{ trans('admin/users/table.mobile') }}</label>
<div class="col-md-6">
<input class="form-control" type="text" name="mobile" id="mobile" value="{{ old('mobile', $user->mobile) }}" maxlength="191" />
{!! $errors->first('mobile', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
<!-- Mobile -->
<div class="form-group {{ $errors->has('mobile') ? 'has-error' : '' }}">
<label class="col-md-3 control-label" for="phone">{{ trans('admin/users/table.mobile') }}</label>
<div class="col-md-6">
<input class="form-control" type="text" name="mobile" id="mobile" value="{{ old('mobile', $user->mobile) }}" maxlength="191" />
{!! $errors->first('mobile', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>
</div>
<!-- Website URL -->
<div class="form-group {{ $errors->has('website') ? ' has-error' : '' }}">
<label for="website" class="col-md-3 control-label">{{ trans('general.website') }}</label>
<div class="col-md-6">
<input class="form-control" type="url" name="website" id="website" value="{{ old('website', $user->website) }}" maxlength="191" />
{!! $errors->first('website', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
<!-- Website URL -->
<div class="form-group {{ $errors->has('website') ? ' has-error' : '' }}">
<label for="website" class="col-md-3 control-label">{{ trans('general.website') }}</label>
<div class="col-md-6">
<input class="form-control" type="url" name="website" id="website" value="{{ old('website', $user->website) }}" maxlength="191" />
{!! $errors->first('website', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
</div>
</div>
<!-- Address -->
<div class="form-group{{ $errors->has('address') ? ' has-error' : '' }}">
<label class="col-md-3 control-label" for="address">{{ trans('general.address') }}</label>
<div class="col-md-6">
<input class="form-control" type="text" name="address" id="address" value="{{ old('address', $user->address) }}" maxlength="191" />
{!! $errors->first('address', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
<!-- Address -->
<div class="form-group{{ $errors->has('address') ? ' has-error' : '' }}">
<label class="col-md-3 control-label" for="address">{{ trans('general.address') }}</label>
<div class="col-md-6">
<input class="form-control" type="text" name="address" id="address" value="{{ old('address', $user->address) }}" maxlength="191" />
{!! $errors->first('address', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>
</div>
<!-- City -->
<div class="form-group{{ $errors->has('city') ? ' has-error' : '' }}">
<label class="col-md-3 control-label" for="city">{{ trans('general.city') }}</label>
<div class="col-md-6">
<input class="form-control" type="text" name="city" id="city" aria-label="city" value="{{ old('city', $user->city) }}" maxlength="191" />
{!! $errors->first('city', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
<!-- City -->
<div class="form-group{{ $errors->has('city') ? ' has-error' : '' }}">
<label class="col-md-3 control-label" for="city">{{ trans('general.city') }}</label>
<div class="col-md-6">
<input class="form-control" type="text" name="city" id="city" aria-label="city" value="{{ old('city', $user->city) }}" maxlength="191" />
{!! $errors->first('city', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>
</div>
<!-- State -->
<div class="form-group{{ $errors->has('state') ? ' has-error' : '' }}">
<label class="col-md-3 control-label" for="state">{{ trans('general.state') }}</label>
<div class="col-md-6">
<input class="form-control" type="text" name="state" id="state" value="{{ old('state', $user->state) }}" maxlength="191" />
{!! $errors->first('state', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
<!-- State -->
<div class="form-group{{ $errors->has('state') ? ' has-error' : '' }}">
<label class="col-md-3 control-label" for="state">{{ trans('general.state') }}</label>
<div class="col-md-6">
<input class="form-control" type="text" name="state" id="state" value="{{ old('state', $user->state) }}" maxlength="191" />
{!! $errors->first('state', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>
</div>
<!-- Country -->
<div class="form-group{{ $errors->has('country') ? ' has-error' : '' }}">
<label class="col-md-3 control-label" for="country">{{ trans('general.country') }}</label>
<div class="col-md-6">
<x-input.country-select
name="country"
:selected="old('country', $user->country)"
class="col-md-12"
/>
<!-- Country -->
<div class="form-group{{ $errors->has('country') ? ' has-error' : '' }}">
<label class="col-md-3 control-label" for="country">{{ trans('general.country') }}</label>
<div class="col-md-6">
<x-input.country-select
name="country"
:selected="old('country', $user->country)"
class="col-md-12"
/>
<p class="help-block">{{ trans('general.countries_manually_entered_help') }}</p>
{!! $errors->first('country', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
<p class="help-block">{{ trans('general.countries_manually_entered_help') }}</p>
{!! $errors->first('country', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>
</div>
<!-- Zip -->
<div class="form-group{{ $errors->has('zip') ? ' has-error' : '' }}">
<label class="col-md-3 control-label" for="zip">{{ trans('general.zip') }}</label>
<div class="col-md-3 text-right">
<input class="form-control" type="text" name="zip" id="zip" value="{{ old('zip', $user->zip) }}" maxlength="10" />
{!! $errors->first('zip', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
<!-- Zip -->
<div class="form-group{{ $errors->has('zip') ? ' has-error' : '' }}">
<label class="col-md-3 control-label" for="zip">{{ trans('general.zip') }}</label>
<div class="col-md-3 text-right">
<input class="form-control" type="text" name="zip" id="zip" value="{{ old('zip', $user->zip) }}" maxlength="10" />
{!! $errors->first('zip', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>
</div>
@endcan
<!-- Notes -->
<div class="form-group{!! $errors->has('notes') ? ' has-error' : '' !!}">
<label for="notes" class="col-md-3 control-label">{{ trans('admin/users/table.notes') }}</label>
<div class="col-md-6">
<textarea class="form-control" rows="5" id="notes" name="notes">{{ old('notes', $user->notes) }}</textarea>
{!! $errors->first('notes', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
</div>
<!-- Notes -->
<x-form.row
:label="trans('general.notes')"
:item="$user"
name="notes"
type="textarea"
placeholder="{{ trans('general.placeholders.notes') }}"
/>
@if ($snipeSettings->two_factor_enabled!='')
@if ($snipeSettings->two_factor_enabled=='1')
@@ -583,7 +586,7 @@
<label class="col-md-3 control-label" for="groups[]">
{{ trans('general.groups') }}
</label>
<div class="col-md-6">
<div class="col-md-8">
@if ($groups->count())
@if ((!Gate::allows('editableOnDemo') || (!Auth::user()->isSuperUser())))
@@ -676,7 +679,7 @@
</div><!-- nav-tabs-custom -->
</form>
</div> <!--/col-md-8-->
</div><!--/row-->
</x-container>
@stop
@section('moar_scripts')

View File

@@ -83,16 +83,18 @@
</a>
</li>
<li>
<a href="#files" data-toggle="tab">
<span class="hidden-lg hidden-md">
<x-icon type="files" class="fa-2x" />
</span>
<span class="hidden-xs hidden-sm">{{ trans('general.file_uploads') }}
{!! ($user->uploads->count() > 0 ) ? '<span class="badge badge-secondary">'.number_format($user->uploads->count()).'</span>' : '' !!}
</span>
</a>
</li>
@can('update', $user)
<li>
<a href="#files" data-toggle="tab">
<span class="hidden-lg hidden-md">
<x-icon type="files" class="fa-2x" />
</span>
<span class="hidden-xs hidden-sm">{{ trans('general.file_uploads') }}
{!! ($user->uploads->count() > 0 ) ? '<span class="badge badge-secondary">'.number_format($user->uploads->count()).'</span>' : '' !!}
</span>
</a>
</li>
@endcan
<li>
<a href="#eulas" data-toggle="tab">
@@ -160,7 +162,7 @@
</li>
@endcan
@can('update', \App\Models\User::class)
@can('manageFiles', $user)
<li class="pull-right">
<a href="#" data-toggle="modal" data-target="#uploadFileModal">
<span class="hidden-xs"><x-icon type="paperclip" /></span>
@@ -375,6 +377,7 @@
</div>
@endif
@can('manageContactInfo')
<!-- address -->
@if (($user->address) || ($user->city) || ($user->state) || ($user->country))
<div class="row">
@@ -402,6 +405,7 @@
</div>
</div>
@endif
@endcan
<!-- company -->
@if (!is_null($user->company))
@@ -512,6 +516,7 @@
@endif
@can('manageContactInfo')
@if ($user->email)
<!-- email -->
<div class="row">
@@ -526,6 +531,7 @@
</div>
@endif
@if ($user->website)
<!-- website -->
<div class="row">
@@ -565,6 +571,7 @@
</div>
</div>
@endif
@endcan
@if ($user->userloc)
<!-- location -->
<div class="row">
@@ -997,6 +1004,7 @@
</table>
</div><!-- /consumables-tab -->
@can('manageFiles', $user)
<div class="tab-pane" id="files">
<div class="row">
@@ -1005,6 +1013,7 @@
</div>
</div> <!--/ROW-->
</div><!--/FILES-->
@endcan
<div class="tab-pane" id="eulas">
<table
@@ -1114,7 +1123,7 @@
@stop
@section('moar_scripts')
@include ('partials.bootstrap-table', ['simple_view' => true])
@include ('partials.bootstrap-table', ['simple_view' => true])
<script nonce="{{ csrf_token() }}">
$(function () {

View File

@@ -44,7 +44,7 @@ class UsersForSelectListTest extends TestCase
{
User::factory()->create(['first_name' => 'Luke', 'last_name' => 'Skywalker', 'email' => 'luke@jedis.org']);
Passport::actingAs(User::factory()->create());
Passport::actingAs(User::factory()->manageContactInfo()->create());
$response = $this->getJson(route('api.users.selectlist', ['search' => 'luke@jedis']))->assertOk();
$results = collect($response->json('results'));
@@ -53,6 +53,18 @@ class UsersForSelectListTest extends TestCase
$this->assertTrue($results->pluck('text')->contains(fn($text) => str_contains($text, 'Luke')));
}
public function testUsersCannotBeSearchedByEmailWithoutPermission()
{
User::factory()->create(['first_name' => 'Luke', 'last_name' => 'Skywalker', 'email' => 'luke@jedis.org']);
Passport::actingAs(User::factory()->create());
$response = $this->getJson(route('api.users.selectlist', ['search' => 'luke@jedis']))->assertOk();
$results = collect($response->json('results'));
$this->assertEquals(0, $results->count());
}
public function testUsersScopedToCompanyWhenMultipleFullCompanySupportEnabled()
{
$this->settings->enableMultipleFullCompanySupport();

View File

@@ -29,7 +29,7 @@ class CreateUserTest extends TestCase
{
Notification::fake();
$response = $this->actingAs(User::factory()->createUsers()->viewUsers()->create())
$response = $this->actingAs(User::factory()->createUsers()->viewUsers()->manageContactInfo()->create())
->from(route('users.index'))
->post(route('users.store'), [
'first_name' => 'Test First Name',
@@ -59,12 +59,49 @@ class CreateUserTest extends TestCase
}
public function testCannotUpdateContactWithoutPermission()
{
Notification::fake();
$response = $this->actingAs(User::factory()->createUsers()->viewUsers()->create())
->from(route('users.index'))
->post(route('users.store'), [
'first_name' => 'Test First Name',
'last_name' => 'Test Last Name',
'username' => 'testuser',
'password' => 'testpassword1235!!',
'password_confirmation' => 'testpassword1235!!',
'activated' => '1',
'email' => 'foo@example.org',
'notes' => 'Test Note',
])
->assertSessionHasNoErrors()
->assertStatus(302)
->assertRedirect(route('users.index'));
$this->assertDatabaseHas('users', [
'first_name' => 'Test First Name',
'last_name' => 'Test Last Name',
'username' => 'testuser',
'activated' => '1',
'email' => null,
'notes' => 'Test Note',
]);
Notification::assertNothingSent();
$this->followRedirects($response)->assertSee('Success');
}
public function testCanCreateAndNotifyUser()
{
Notification::fake();
$admin = User::factory()->createUsers()->viewUsers()->manageContactInfo()->create();
$response = $this->actingAs(User::factory()->createUsers()->viewUsers()->create())
// dd($admin);
$response = $this->actingAs($admin)
->from(route('users.index'))
->post(route('users.store'), [
'first_name' => 'Test First Name',

View File

@@ -38,4 +38,5 @@ class ViewUserTest extends TestCase
->get(route('users.show', $user))
->assertStatus(302);
}
}