1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-19 13:54:25 +01:00

(ui) Option to prevent users from changing their group

This commit is contained in:
Sylvain 2022-05-11 15:45:49 +02:00
parent 1d2b814d6f
commit e45872956c
16 changed files with 123 additions and 107 deletions

View File

@ -3,9 +3,13 @@
## next deploy
- Option to disable the 'machines' module
- Option to prevent users from changing their group
- Ability to define social networks for the FabLab "about page"
- Improved security when changing passwords
- Support for OpenID Connect in Sign-Sign-On authentication providers
- ICS file attached to the reservation notification email
- Refactored the user profile edition form
- Improved the profile completion page
- No longer needed to recompile the assets when switching the authentication provider
- Updated the documentation about the minimum docker version
- Updated nodejs version to 16.13.2 for dev environment, to reflect production version

View File

@ -10,6 +10,9 @@ import { useTranslation } from 'react-i18next';
import { useForm } from 'react-hook-form';
import { FormSelect } from '../form/form-select';
import MemberAPI from '../../api/member';
import SettingAPI from '../../api/setting';
import { SettingName } from '../../models/setting';
import UserLib from '../../lib/user';
declare const Application: IApplication;
@ -18,6 +21,7 @@ interface ChangeGroupProps {
onSuccess: (message: string, user: User) => void,
onError: (message: string) => void,
allowChange?: boolean,
className?: string,
}
/**
@ -26,18 +30,22 @@ interface ChangeGroupProps {
*/
type selectOption = { value: number, label: string };
export const ChangeGroup: React.FC<ChangeGroupProps> = ({ user, onSuccess, onError, allowChange }) => {
export const ChangeGroup: React.FC<ChangeGroupProps> = ({ user, onSuccess, onError, allowChange, className }) => {
const { t } = useTranslation('shared');
const [groups, setGroups] = useState<Array<Group>>([]);
const [changeRequested, setChangeRequested] = useState<boolean>(false);
const [operator, setOperator] = useState<User>(null);
const [allowedUserChangeGoup, setAllowedUserChangeGoup] = useState<boolean>(false);
const { handleSubmit, control } = useForm();
useEffect(() => {
GroupAPI.index({ disabled: false, admins: false }).then(setGroups).catch(onError);
MemberAPI.current().then(setOperator).catch(onError);
SettingAPI.get(SettingName.UserChangeGroup).then((setting) => {
setAllowedUserChangeGoup(setting.value === 'true');
}).catch(onError);
}, []);
useEffect(() => {
@ -55,7 +63,8 @@ export const ChangeGroup: React.FC<ChangeGroupProps> = ({ user, onSuccess, onErr
* Check if the group changing is currently allowed.
*/
const canChangeGroup = (): boolean => {
return allowChange;
const userLib = new UserLib(operator);
return allowChange && (allowedUserChangeGoup || userLib.isPrivileged(user));
};
/**
@ -81,7 +90,7 @@ export const ChangeGroup: React.FC<ChangeGroupProps> = ({ user, onSuccess, onErr
if (!user) return null;
return (
<div className="change-group">
<div className={`change-group ${className || ''}`}>
<h3>{t('app.shared.change_group.title', { OPERATOR: operator?.id === user.id ? 'self' : 'admin' })}</h3>
{!changeRequested && <div className="display">
<div className="current-group">
@ -114,4 +123,4 @@ const ChangeGroupWrapper: React.FC<ChangeGroupProps> = (props) => {
);
};
Application.Components.component('changeGroup', react2angular(ChangeGroupWrapper, ['user', 'onSuccess', 'onError', 'allowChange']));
Application.Components.component('changeGroup', react2angular(ChangeGroupWrapper, ['user', 'onSuccess', 'onError', 'allowChange', 'className']));

View File

@ -140,20 +140,24 @@ Application.Controllers.controller('EditProfileController', ['$scope', '$rootSco
};
/**
* Change the group of the current user to the one set in $scope.userGroup
* Callback triggered when the user has successfully changed his group
*/
$scope.selectGroup = () =>
Member.update({ id: $scope.user.id }, { user: { group_id: $scope.userGroup } }, function (user) {
$scope.user = user;
$rootScope.currentUser = user;
Auth._currentUser.group_id = user.group_id;
$scope.group.change = false;
return growl.success(_t('app.logged.dashboard.settings.your_group_has_been_successfully_changed'));
}
, function (err) {
growl.error(_t('app.logged.dashboard.settings.an_unexpected_error_prevented_your_group_from_being_changed'));
return console.error(err);
});
$scope.onGroupUpdateSuccess = function (message, user) {
growl.success(message);
setTimeout(() => {
$scope.user = _.cloneDeep(user);
$scope.$apply();
}, 50);
$rootScope.currentUser.group_id = user.group_id;
Auth._currentUser.group_id = user.group_id;
};
/**
* Check if it is allowed the change the group of the current user
*/
$scope.isAllowedChangingGroup = function () {
return !$scope.user.subscribed_plan?.name && $scope.user.role !== 'admin';
};
/**
* Callback to diplay the datepicker as a dropdown when clicking on the input field

View File

@ -16,16 +16,6 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
function ($scope, $rootScope, $state, $uibModal, Auth, AuthService, dialogs, growl, groupsPromise, Subscription, Member, subscriptionExplicationsPromise, _t, Wallet, helpers, settingsPromise, Price) {
/* PUBLIC SCOPE */
// list of groups
$scope.groups = groupsPromise.filter(function (g) { return (g.slug !== 'admins') & !g.disabled; });
// default : do not show the group changing form
// group ID of the current/selected user
$scope.group = {
change: false,
id: null
};
// user to deal with
$scope.ctrl = {
member: null,
@ -59,10 +49,8 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
$scope.updateMember = function () {
$scope.selectedPlan = null;
$scope.paid.plan = null;
$scope.group.change = false;
Member.get({ id: $scope.ctrl.member.id }, function (member) {
$scope.ctrl.member = member;
$scope.group.id = $scope.ctrl.member.group_id;
});
};
@ -104,56 +92,6 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
growl.error(message);
};
/**
* Return the group object, identified by the ID set in $scope.group.id
*/
$scope.getUserGroup = function () {
for (const group of Array.from($scope.groups)) {
if (group.id === $scope.group.id) {
return group;
}
}
};
/**
* Change the group of the current/selected user to the one set in $scope.group.id
*/
$scope.selectGroup = function () {
Member.update({ id: $scope.ctrl.member.id }, { user: { group_id: $scope.group.id } }, function (user) {
$scope.ctrl.member = user;
$scope.group.change = false;
$scope.selectedPlan = null;
if (AuthService.isAuthorized('member') ||
(AuthService.isAuthorized('manager') && $scope.currentUser.id !== $scope.ctrl.member.id)) {
$rootScope.currentUser = user;
Auth._currentUser.group_id = user.group_id;
growl.success(_t('app.public.plans.your_group_was_successfully_changed'));
} else {
growl.success(_t('app.public.plans.the_user_s_group_was_successfully_changed'));
}
}
, function (err) {
if (AuthService.isAuthorized('member') ||
(AuthService.isAuthorized('manager') && $scope.currentUser.id !== $scope.ctrl.member.id)) {
growl.error(_t('app.public.plans.an_error_prevented_your_group_from_being_changed'));
} else {
growl.error(_t('app.public.plans.an_error_prevented_to_change_the_user_s_group'));
}
console.error(err);
});
};
/**
* Return an enumerable meaninful string for the gender of the provider user
* @param user {Object} Database user record
* @return {string} 'male' or 'female'
*/
$scope.getGender = function (user) {
if (user && user.statistic_profile_attributes) {
if (user.statistic_profile_attributes.gender === 'true') { return 'male'; } else { return 'female'; }
} else { return 'other'; }
};
/**
* Test if the provided date is in the future
* @param dateTime {Date}
@ -199,7 +137,7 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
};
/**
* Check if it is allowed the change the group of teh selected user
* Check if it is allowed the change the group of the selected user
*/
$scope.isAllowedChangingGroup = function () {
return $scope.ctrl.member && !$scope.selectedPlan && !$scope.paid.plan;
@ -215,7 +153,6 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
if (!AuthService.isAuthorized('admin')) {
$scope.ctrl.member = $scope.currentUser;
$scope.paid.plan = $scope.currentUser.subscribed_plan;
$scope.group.id = $scope.currentUser.group_id;
}
}

View File

@ -13,10 +13,10 @@ export default class UserLib {
* Check if the current user has privileged access for resources concerning the provided customer
*/
isPrivileged = (customer: User): boolean => {
if (this.user.role === 'admin') return true;
if (this.user?.role === 'admin') return true;
if (this.user.role === 'manager') {
return (this.user.id !== customer.id);
if (this.user?.role === 'manager') {
return (this.user?.id !== customer.id);
}
return false;

View File

@ -134,6 +134,7 @@ export enum SettingName {
SocialsLastfm = 'lastfm',
SocialsFlickr = 'flickr',
MachinesModule = 'machines_module',
UserChangeGroup = 'user_change_group',
}
export type SettingValue = string|boolean|number;

View File

@ -525,7 +525,6 @@ angular.module('application.router', ['ui.router'])
},
resolve: {
subscriptionExplicationsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'subscription_explications_alert' }).$promise; }],
groupsPromise: ['Group', function (Group) { return Group.query().$promise; }],
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['online_payment_module', 'payment_gateway', 'overlapping_categories']" }).$promise; }]
}
})
@ -1072,7 +1071,8 @@ angular.module('application.router', ['ui.router'])
"'renew_pack_threshold', 'pack_only_for_subscription', 'overlapping_categories', 'public_registrations'," +
"'extended_prices_in_same_day', 'recaptcha_site_key', 'recaptcha_secret_key', 'user_validation_required', 'user_validation_required_machine', " +
"'user_validation_required_training', 'user_validation_required_subscription', 'user_validation_required_space'," +
"'user_validation_required_event', 'user_validation_required_pack', 'user_validation_required_list', 'machines_module']"
"'user_validation_required_event', 'user_validation_required_pack', 'user_validation_required_list'," +
"'machines_module', 'user_change_group']"
}).$promise;
}],
privacyDraftsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'privacy_draft', history: true }).$promise; }],

View File

@ -85,6 +85,7 @@
@import "modules/abuses";
@import "modules/cookies";
@import "modules/dashboard";
@import "modules/document-filters";
@import "modules/event-themes";
@import "modules/icalendar";

View File

@ -0,0 +1,51 @@
.dashboard {
.dashboard-change-group {
border: 0;
margin: 0;
h3 {
padding: 0;
text-transform: uppercase;
border: 0;
}
.display,
.change-group-form {
padding: 10px 0 0;
margin-bottom: 10px;
}
.display {
.current-group {
margin-bottom: 10px;
background-color: #fcf8e3;
border-color: #faebcc;
color: #8a6d3b;
}
.request-change-btn {
width: auto;
text-transform: uppercase;
border-radius: 3px;
font-size: 12px;
line-height: 18px;
padding: 5px 10px;
height: 30px;
background-color: var(--secondary);
border-color: var(--secondary);
&:hover {
background-color: white;
}
}
}
.actions {
.fab-button {
font-size: 12px;
line-height: 18px;
padding: 5px 10px;
height: 30px;
}
}
}
}

View File

@ -480,6 +480,18 @@
</boolean-setting>
</div>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.change_group' }}</h3>
<p class="alert alert-warning m-h-md" translate>
{{ 'app.admin.settings.change_group_info' }}
</p>
<div class="col-md-10 col-md-offset-1">
<boolean-setting name="user_change_group"
settings="allSettings"
label="app.admin.settings.allow_group_change">
</boolean-setting>
</div>
</div>
</div>
</div>

View File

@ -4,7 +4,7 @@
<div class="row no-gutter wrapper">
<div class="row no-gutter wrapper dashboard">
<div class="col-sm-12 col-md-12 col-lg-3">
<div class="widget panel b-a m m-t-lg">
<div class="panel-heading b-b small text-center">
@ -16,24 +16,12 @@
<div class="text-xs" ng-show="user.last_sign_in_at"><i>{{ 'app.logged.dashboard.settings.last_activity_on_' | translate:{DATE:(user.last_sign_in_at | amDateFormat: 'LL')} }}</i></div>
</div>
<div class="widget-content no-bg b-b auto wrapper">
<div class="m-b-md">
<h3 class="text-u-c" translate>{{ 'app.logged.dashboard.settings.group' }}</h3>
<div ng-show="!group.change">
<uib-alert type="warning">
<span class="text-black font-sbold">{{getUserGroup().name}}</span>
</uib-alert>
<button class="btn text-black btn-warning-full btn-sm m-t-n-sm"
ng-click="group.change = !group.change"
ng-hide="user.subscribed_plan.name || user.role === 'admin'"
translate>
{{ 'app.logged.dashboard.settings.i_want_to_change_group' }}
</button>
</div>
<div ng-show="group.change">
<select class="form-control" ng-options="g.id as g.name for g in groups" ng-model="userGroup"></select>
<button class="btn btn-success m-t" ng-click="selectGroup()">Changer mon groupe</button>
</div>
</div>
<change-group user="user"
on-success="onGroupUpdateSuccess"
on-error="onError"
class-name="'dashboard-change-group'"
allow-change="isAllowedChangingGroup()">
</change-group>
<div ng-show="$root.modules.plans">
<h3 class="text-u-c" translate>{{ 'app.logged.dashboard.settings.subscription' }}</h3>
<div ng-show="user.subscribed_plan">

View File

@ -144,7 +144,8 @@ class Setting < ApplicationRecord
pinterest
lastfm
flickr
machines_module] }
machines_module
user_change_group] }
# WARNING: when adding a new key, you may also want to add it in:
# - config/locales/en.yml#settings
# - app/frontend/src/javascript/models/setting.ts#SettingName

View File

@ -40,7 +40,8 @@ class SettingPolicy < ApplicationPolicy
recaptcha_site_key feature_tour_display disqus_shortname allowed_cad_extensions openlab_app_id openlab_default
online_payment_module stripe_public_key confirmation_required wallet_module trainings_module address_required
payment_gateway payzen_endpoint payzen_public_key public_agenda_module renew_pack_threshold statistics_module
pack_only_for_subscription overlapping_categories public_registrations socials_facebook socials_twitter socials_viadeo socials_linkedin socials_instagram socials_youtube socials_vimeo socials_dailymotion socials_github socials_echosciences socials_pinterest socials_lastfm socials_flickr]
pack_only_for_subscription overlapping_categories public_registrations facebook twitter viadeo linkedin instagram
youtube vimeo dailymotion github echosciences pinterest lastfm flickr machines_module user_change_group]
end
##

View File

@ -1408,6 +1408,10 @@ en:
account_confirmation: "Account confirmation"
confirmation_required_info: "Optionally, you can force the users to confirm their email address before being able to access Fab-manager."
confirmation_is_required: "Confirmation required"
change_group: "Group change"
change_group_info: "After an user has created his account, you can restrict him from changing his group. In that case, only managers and administrators will be able to change the user's group."
allow_group_change: "Allow group change"
user_change_group: "users can change their group"
wallet_module: "wallet module"
public_agenda_module: "public agenda module"
statistics_module: "statistics module"

View File

@ -582,3 +582,4 @@ en:
lastfm: "lastfm"
flickr: "flickr"
machines_module: "Machines module"
user_change_group: "Allow users to change their group"

View File

@ -905,6 +905,8 @@ Setting.set('pack_only_for_subscription', true) unless Setting.find_by(name: 'pa
Setting.set('public_registrations', true) unless Setting.find_by(name: 'public_registrations').try(:value)
Setting.set('user_change_group', true) unless Setting.find_by(name: 'user_change_group').try(:value)
unless Setting.find_by(name: 'overlapping_categories').try(:value)
Setting.set('overlapping_categories', 'training_reservations,machine_reservations,space_reservations,events_reservations')
end