Skip to content

Commit

Permalink
Multi-users in Gladys Assistant (#1050)
Browse files Browse the repository at this point in the history
* First views working

* improve create user page

* Improve design of user list

* Fix bug when setting a new picture

* disableRole in user own profile edit

* Add get user by selector + delete user by selector

* Add PATCH user route

* Clean response of getBySelector/updateBySelector

* edit user with get

* Fix password update

* make reset password work for admin user

* Now possible to edit preferences

* Dashboard are now private

* It's now possible to delete a user

* user shouldn't be able to delete his own account

* Get user should be searchable & orderable

* Fix edit user page

* Fix tests & add more retrictions on routes (admin)

* Fix tests

* Hide settings for non-admin users

* Add search and order to user list

* Add FR translations

* Fix eslint front

* fix ui bugs

* filter integrations for user with non admin role + hide scene

* fix integration list bug

* Hide weather + improve telegram integration

* non admin user cannot create, update and delete scene

* Fix demo mode

* Save preferences in same form as user

* Update dashboard selector in dashboard migration

* UI improvements

* Explain better what are roles

* Set temperature/distance unit in create user too

* Renforce admin securities on services

* when creating account, role is not shown

* Adapt Gladys plus to multi-users

* Add loading bar for user screen
  • Loading branch information
Pierre-Gilles authored Feb 19, 2021
1 parent 4afbc88 commit 24b6b69
Show file tree
Hide file tree
Showing 63 changed files with 1,922 additions and 193 deletions.
49 changes: 29 additions & 20 deletions front/src/actions/dashboard/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,6 @@ import update from 'immutability-helper';
import get from 'get-value';
import { DASHBOARD_TYPE } from '../../../../server/utils/constants';

const EMPTY_DASHBOARD = {
name: 'Home',
selector: 'home',
type: DASHBOARD_TYPE.MAIN,
boxes: [[], [], []]
};

function createActions(store) {
const actions = {
editDashboard(state) {
Expand Down Expand Up @@ -120,24 +113,32 @@ function createActions(store) {
DashboardGetBoxesStatus: RequestStatus.Getting
});
try {
const homeDashboard = await state.httpClient.get('/api/v1/dashboard/home');
store.setState({
gatewayInstanceNotFound: false,
homeDashboard,
DashboardGetBoxesStatus: RequestStatus.Success
});
const dashboards = await state.httpClient.get('/api/v1/dashboard');
if (dashboards.length) {
const homeDashboard = await state.httpClient.get(`/api/v1/dashboard/${dashboards[0].selector}`);
store.setState({
gatewayInstanceNotFound: false,
homeDashboard,
DashboardGetBoxesStatus: RequestStatus.Success
});
} else {
store.setState({
dashboardNotConfigured: true,
homeDashboard: {
name: `${state.user.firstname} home`,
selector: `${state.user.selector}-home`,
type: DASHBOARD_TYPE.MAIN,
boxes: [[], [], []]
}
});
}
} catch (e) {
const status = get(e, 'response.status');
const errorMessage = get(e, 'response.error_message');
if (status === 404 && errorMessage === 'NO_INSTANCE_FOUND') {
store.setState({
gatewayInstanceNotFound: true
});
} else if (status === 404) {
store.setState({
dashboardNotConfigured: true,
homeDashboard: EMPTY_DASHBOARD
});
} else {
store.setState({
DashboardGetBoxesStatus: RequestStatus.Error
Expand Down Expand Up @@ -168,9 +169,17 @@ function createActions(store) {
try {
let homeDashboard;
if (state.homeDashboard.id) {
homeDashboard = await state.httpClient.patch('/api/v1/dashboard/home', state.homeDashboard);
homeDashboard = await state.httpClient.patch(
`/api/v1/dashboard/${state.homeDashboard.selector}`,
state.homeDashboard
);
} else {
homeDashboard = await state.httpClient.post('/api/v1/dashboard', state.homeDashboard);
const homeDashboardToCreate = {
...state.homeDashboard,
name: `${state.user.firstname} home`,
selector: `${state.user.selector}-home`
};
homeDashboard = await state.httpClient.post('/api/v1/dashboard', homeDashboardToCreate);
}
store.setState({
homeDashboard,
Expand Down
21 changes: 18 additions & 3 deletions front/src/actions/integration.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import get from 'get-value';
import update from 'immutability-helper';

import { USER_ROLE } from '../../../server/utils/constants';
import { integrations, integrationsByType, categories } from '../config/integrations';

const HIDDEN_CATEGORIES_FOR_NON_ADMIN_USERS = ['device', 'weather'];

const actions = store => ({
getIntegrations(state, category = null) {
const selectedIntegrations = integrationsByType[category] || integrations;
let selectedIntegrations = integrationsByType[category] || integrations;
let categoriesFiltered = categories;
if (state.user && state.user.role !== USER_ROLE.ADMIN) {
selectedIntegrations = selectedIntegrations.filter(
i => HIDDEN_CATEGORIES_FOR_NON_ADMIN_USERS.indexOf(i.type) === -1
);
categoriesFiltered = categoriesFiltered.filter(i => HIDDEN_CATEGORIES_FOR_NON_ADMIN_USERS.indexOf(i.type) === -1);
}
store.setState({
integrations: selectedIntegrations,
totalSize: selectedIntegrations.length,
integrationCategories: categories,
integrationCategories: categoriesFiltered,
searchKeyword: ''
});
},
Expand Down Expand Up @@ -62,7 +72,12 @@ const actions = store => ({
}
},
getIntegrationByCategory(state, category) {
const selectedIntegrations = category ? integrationsByType[category] || [] : integrations;
let selectedIntegrations = category ? integrationsByType[category] || [] : integrations;
if (state.user && state.user.role !== USER_ROLE.ADMIN) {
selectedIntegrations = selectedIntegrations.filter(
i => HIDDEN_CATEGORIES_FOR_NON_ADMIN_USERS.indexOf(i.type) === -1
);
}
store.setState({
integrations: selectedIntegrations,
searchKeyword: ''
Expand Down
115 changes: 115 additions & 0 deletions front/src/actions/profile.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { RequestStatus } from '../utils/consts';
import validateEmail from '../utils/validateEmail';
import update from 'immutability-helper';
import get from 'get-value';
import { route } from 'preact-router';
import { fileToBase64, getCropperBase64Image } from '../utils/picture';
import { getYearsMonthsAndDays } from '../utils/date';

Expand Down Expand Up @@ -31,6 +32,28 @@ function createActions(store) {
});
}
},
async getUser(state, selector) {
store.setState({
ProfileGetStatus: RequestStatus.Getting,
cropper: null,
newProfilePicture: null,
newProfilePictureFormValue: null
});
try {
const user = await state.httpClient.get(`/api/v1/user/${selector}`);
user.birthdateDay = parseInt(user.birthdate.substr(8, 2), 10);
user.birthdateMonth = parseInt(user.birthdate.substr(5, 2), 10);
user.birthdateYear = parseInt(user.birthdate.substr(0, 4), 10);
store.setState({
newUser: user,
ProfileGetStatus: RequestStatus.Success
});
} catch (e) {
store.setState({
ProfileGetStatus: RequestStatus.Error
});
}
},
validateUser(state) {
let errored = false;
const errors = {};
Expand Down Expand Up @@ -93,6 +116,16 @@ function createActions(store) {
});
store.setState(newState);
},
initNewUser(state, newUser) {
store.setState({
newUser,
cropper: null,
profileUpdateErrors: null,
newProfilePicture: null,
newProfilePictureFormValue: null,
ProfilePatchStatus: null
});
},
validatePassword(state) {
store.setState({
validPassword: state.newUser.password.length >= MIN_PASSWORD_LENGTH
Expand All @@ -114,6 +147,48 @@ function createActions(store) {
years
});
},
async createUser(state, e) {
e.preventDefault();
store.setState({
createUserError: null,
createUserStatus: RequestStatus.Getting
});
try {
const data = Object.assign({}, state.newUser);
const errored = actions.validateUser(state);
if (errored) {
throw new Error();
}
data.birthdate = new Date(data.birthdateYear, data.birthdateMonth - 1, data.birthdateDay);
delete data.birthdateYear;
delete data.birthdateMonth;
delete data.birthdateDay;
if (state.cropper) {
const profilePicture = await getCropperBase64Image(state.cropper);
if (profilePicture) {
data.picture = profilePicture;
}
}
await state.httpClient.post('/api/v1/user', data);
store.setState({
createUserStatus: RequestStatus.Success
});
route('/dashboard/settings/user');
} catch (e) {
console.log(e);
const status = get(e, 'response.status');
if (status === 409) {
store.setState({
createUserError: e.response.data,
createUserStatus: RequestStatus.ConflictError
});
} else {
store.setState({
createUserStatus: RequestStatus.Error
});
}
}
},
async saveProfile(state, e) {
e.preventDefault();
store.setState({
Expand Down Expand Up @@ -155,6 +230,46 @@ function createActions(store) {
ProfilePatchStatus: RequestStatus.Error
});
}
},
async updateUser(state, e) {
e.preventDefault();
store.setState({
ProfilePatchStatus: RequestStatus.Getting
});
try {
const data = Object.assign({}, state.newUser);
const errored = actions.validateUser(state);
if (errored) {
throw new Error();
}
data.birthdate = new Date(data.birthdateYear, data.birthdateMonth - 1, data.birthdateDay);
delete data.birthdateYear;
delete data.birthdateMonth;
delete data.birthdateDay;
if (state.cropper) {
const profilePicture = await getCropperBase64Image(state.cropper);
if (profilePicture) {
data.picture = profilePicture;
}
}
await state.httpClient.patch(`/api/v1/user/${data.selector}`, data);
store.setState({
ProfilePatchStatus: RequestStatus.Success
});
} catch (e) {
console.error(e);
const status = get(e, 'response.status');
if (status === 409) {
store.setState({
ProfilePatchError: e.response.data,
ProfilePatchStatus: RequestStatus.ConflictError
});
} else {
store.setState({
ProfilePatchStatus: RequestStatus.Error
});
}
}
}
};
return actions;
Expand Down
6 changes: 6 additions & 0 deletions front/src/components/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ import TriggerPage from '../routes/trigger';
import ProfilePage from '../routes/profile';
import SettingsSessionPage from '../routes/settings/settings-session';
import SettingsHousePage from '../routes/settings/settings-house';
import SettingsUserPage from '../routes/settings/settings-users';
import SettingsEditUserPage from '../routes/settings/settings-users/edit-user';
import SettingsCreateUserPage from '../routes/settings/settings-users/create-user';
import SettingsSystemPage from '../routes/settings/settings-system';
import SettingsServicePage from '../routes/settings/settings-service';
import SettingsGateway from '../routes/settings/settings-gateway';
Expand Down Expand Up @@ -213,6 +216,9 @@ const AppRouter = connect(
<ProfilePage path="/dashboard/profile" />
<SettingsSessionPage path="/dashboard/settings/session" />
<SettingsHousePage path="/dashboard/settings/house" />
<SettingsUserPage path="/dashboard/settings/user" />
<SettingsEditUserPage path="/dashboard/settings/user/edit/:user_selector" />
<SettingsCreateUserPage path="/dashboard/settings/user/new" />
<SettingsSystemPage path="/dashboard/settings/system" />
<SettingsGateway path="/dashboard/settings/gateway" />
<SettingsServicePage path="/dashboard/settings/service" />
Expand Down
31 changes: 18 additions & 13 deletions front/src/components/header/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Text, Localizer } from 'preact-i18n';
import cx from 'classnames';
import { Link } from 'preact-router/match';
import { isUrlInArray } from '../../utils/url';
import { USER_ROLE } from '../../../../server/utils/constants';

const PAGES_WITHOUT_HEADER = [
'/login',
Expand Down Expand Up @@ -48,8 +49,8 @@ const Header = ({ ...props }) => {
<span class="ml-2 d-none d-lg-block">
<span class="text-default">{props.user.firstname}</span>
<small class="text-muted d-block mt-1">
{props.user.role === 'admin' && <Text id="profile.adminRole" />}
{props.user.role !== 'admin' && <Text id="profile.userRole" />}
{props.user.role === USER_ROLE.ADMIN && <Text id="profile.adminRole" />}
{props.user.role !== USER_ROLE.ADMIN && <Text id="profile.userRole" />}
</small>
</span>
</a>
Expand All @@ -61,9 +62,11 @@ const Header = ({ ...props }) => {
<a class="dropdown-item" href="/dashboard/profile">
<i class="dropdown-icon fe fe-user" /> <Text id="header.profile" />
</a>
<a class="dropdown-item" href="/dashboard/settings/house">
<i class="dropdown-icon fe fe-settings" /> <Text id="header.settings" />
</a>
{props.user.role === USER_ROLE.ADMIN && (
<a class="dropdown-item" href="/dashboard/settings/house">
<i class="dropdown-icon fe fe-settings" /> <Text id="header.settings" />
</a>
)}
<div class="dropdown-divider" />
<a
class="dropdown-item"
Expand Down Expand Up @@ -147,14 +150,16 @@ const Header = ({ ...props }) => {
<i class="fe fe-map" /> <Text id="header.maps" />
</Link>
</li>
<li class="nav-item">
<Link
href="/dashboard/scene"
class={props.currentUrl.startsWith('/dashboard/scene') ? 'active nav-link' : 'nav-link'}
>
<i class="fe fe-play" /> <Text id="header.scenes" />
</Link>
</li>
{props.user.role === USER_ROLE.ADMIN && (
<li class="nav-item">
<Link
href="/dashboard/scene"
class={props.currentUrl.startsWith('/dashboard/scene') ? 'active nav-link' : 'nav-link'}
>
<i class="fe fe-play" /> <Text id="header.scenes" />
</Link>
</li>
)}
</ul>
</div>
</div>
Expand Down
Loading

0 comments on commit 24b6b69

Please sign in to comment.