Merge branch 'feat/user-roles' into 'main'

Added user roles functionality

See merge request stackspin/dashboard!24
This commit is contained in:
Maarten de Waard 2022-04-20 12:20:24 +00:00
commit f1bf0279cb
11 changed files with 117 additions and 92 deletions

View file

@ -10,8 +10,6 @@ export const Select = ({ control, name, label, options }: SelectProps) => {
} = useController({ } = useController({
name, name,
control, control,
rules: { required: true },
defaultValue: '',
}); });
return ( return (
@ -25,14 +23,14 @@ export const Select = ({ control, name, label, options }: SelectProps) => {
id={name} id={name}
onChange={field.onChange} // send value to hook form onChange={field.onChange} // send value to hook form
onBlur={field.onBlur} // notify when input is touched/blur onBlur={field.onBlur} // notify when input is touched/blur
value={field.value ? field.value.toString() : ''} // input value value={field.value ? field.value : ''} // input value
name={name} // send down the input name name={name} // send down the input name
ref={field.ref} // send input ref, so we can focus on input when error appear ref={field.ref} // send input ref, so we can focus on input when error appear
className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md" className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md"
> >
{options?.map((value) => ( {options?.map((option) => (
<option key={value} value={value}> <option key={option.value} value={option.value}>
{value} {option.name}
</option> </option>
))} ))}
</select> </select>

View file

@ -1,21 +1,21 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { Modal, Banner } from 'src/components'; import { Modal, Banner } from 'src/components';
import { Input } from 'src/components/Form'; import { Input, Select } from 'src/components/Form';
import { useUsers } from 'src/services/users'; import { User, UserRole, useUsers } from 'src/services/users';
import { CurrentUser } from 'src/services/auth';
import { appAccessList } from './consts'; import { appAccessList } from './consts';
import { UserModalProps } from './types'; import { UserModalProps } from './types';
export const CurrentUserModal = ({ open, onClose, user }: UserModalProps) => { export const CurrentUserModal = ({ open, onClose, user }: UserModalProps) => {
const { editUserById, userModalLoading } = useUsers(); const { editUserById, userModalLoading } = useUsers();
const { control, reset, handleSubmit } = useForm<CurrentUser>({ const { control, reset, handleSubmit } = useForm<User>({
defaultValues: { defaultValues: {
name: null, name: null,
email: null, email: null,
id: null, id: null,
preferredUsername: null, role_id: null,
status: null,
}, },
}); });
@ -70,11 +70,23 @@ export const CurrentUserModal = ({ open, onClose, user }: UserModalProps) => {
</div> </div>
<div className="sm:col-span-6"> <div className="sm:col-span-6">
<Banner title="Editing user status, roles and app access coming soon." titleSm="Comming soon!" /> <Select
control={control}
name="role_id"
label="Role"
options={[
{ value: UserRole.Admin, name: 'Admin' },
{ value: UserRole.User, name: 'User' },
]}
/>
</div>
<div className="sm:col-span-6">
<Banner title="Editing user status and app access coming soon." titleSm="Comming soon!" />
</div> </div>
<div className="sm:col-span-3 opacity-40 cursor-default pointer-events-none select-none"> <div className="sm:col-span-3 opacity-40 cursor-default pointer-events-none select-none">
{/* <Select control={control} name="status" label="Status" options={['Active', 'Inactive']} /> */} {/* <Select control={control} name="status" label="Status" options={['Admin', 'Inactive']} /> */}
<label htmlFor="status" className="block text-sm font-medium text-gray-700"> <label htmlFor="status" className="block text-sm font-medium text-gray-700">
Status Status
</label> </label>
@ -90,23 +102,6 @@ export const CurrentUserModal = ({ open, onClose, user }: UserModalProps) => {
</select> </select>
</div> </div>
</div> </div>
<div className="sm:col-span-3 opacity-40 cursor-default pointer-events-none select-none">
<label htmlFor="status" className="block text-sm font-medium text-gray-700">
Role
</label>
<div className="mt-1">
<select
id="status"
name="status"
className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md"
>
<option>User</option>
<option>Admin</option>
<option>Super Admin</option>
</select>
</div>
</div>
</div> </div>
</div> </div>

View file

@ -1,7 +1,7 @@
import { CurrentUser } from 'src/services/auth'; import { User } from 'src/services/users';
export type UserModalProps = { export type UserModalProps = {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
user: CurrentUser; user: User;
}; };

View file

@ -2,9 +2,8 @@ import React, { useEffect, useState } from 'react';
import { TrashIcon } from '@heroicons/react/outline'; import { TrashIcon } from '@heroicons/react/outline';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { Modal, Banner, ConfirmationModal } from 'src/components'; import { Modal, Banner, ConfirmationModal } from 'src/components';
import { Input } from 'src/components/Form'; import { Input, Select } from 'src/components/Form';
import { useUsers } from 'src/services/users'; import { User, UserRole, useUsers } from 'src/services/users';
import { CurrentUserState } from 'src/services/users/redux';
import { useAuth } from 'src/services/auth'; import { useAuth } from 'src/services/auth';
import { appAccessList } from './consts'; import { appAccessList } from './consts';
import { UserModalProps } from './types'; import { UserModalProps } from './types';
@ -14,11 +13,12 @@ export const UserModal = ({ open, onClose, userId }: UserModalProps) => {
const { user, loadUser, editUserById, createNewUser, userModalLoading, deleteUserById } = useUsers(); const { user, loadUser, editUserById, createNewUser, userModalLoading, deleteUserById } = useUsers();
const { currentUser } = useAuth(); const { currentUser } = useAuth();
const { control, reset, handleSubmit } = useForm<CurrentUserState>({ const { control, reset, handleSubmit } = useForm<User>({
defaultValues: { defaultValues: {
name: null, name: null,
email: null, email: null,
id: null, id: null,
role_id: null,
status: null, status: null,
}, },
}); });
@ -117,7 +117,19 @@ export const UserModal = ({ open, onClose, userId }: UserModalProps) => {
</div> </div>
<div className="sm:col-span-6"> <div className="sm:col-span-6">
<Banner title="Editing user status, roles and app access coming soon." titleSm="Comming soon!" /> <Select
control={control}
name="role_id"
label="Role"
options={[
{ value: UserRole.Admin, name: 'Admin' },
{ value: UserRole.User, name: 'User' },
]}
/>
</div>
<div className="sm:col-span-6">
<Banner title="Editing user status and app access coming soon." titleSm="Comming soon!" />
</div> </div>
<div className="sm:col-span-3 opacity-40 cursor-default pointer-events-none select-none"> <div className="sm:col-span-3 opacity-40 cursor-default pointer-events-none select-none">
@ -137,23 +149,6 @@ export const UserModal = ({ open, onClose, userId }: UserModalProps) => {
</select> </select>
</div> </div>
</div> </div>
<div className="sm:col-span-3 opacity-40 cursor-default pointer-events-none select-none">
<label htmlFor="status" className="block text-sm font-medium text-gray-700">
Role
</label>
<div className="mt-1">
<select
id="status"
name="status"
className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md"
>
<option>User</option>
<option>Admin</option>
<option>Super Admin</option>
</select>
</div>
</div>
</div> </div>
</div> </div>

View file

@ -6,6 +6,7 @@ export enum AuthActionTypes {
SIGN_IN_SUCCESS = 'auth/sign_in_success', SIGN_IN_SUCCESS = 'auth/sign_in_success',
SIGN_IN_FAILURE = 'auth/sign_in_failure', SIGN_IN_FAILURE = 'auth/sign_in_failure',
SIGN_OUT = 'auth/SIGN_OUT', SIGN_OUT = 'auth/SIGN_OUT',
UPDATE_AUTH_USER = 'auth/update_auth_user',
REGISTRATION_START = 'auth/registration_start', REGISTRATION_START = 'auth/registration_start',
REGISTRATION_FAILURE = 'auth/registration_failure', REGISTRATION_FAILURE = 'auth/registration_failure',
} }

View file

@ -1,14 +1,17 @@
import { createApiReducer, chainReducers, INITIAL_API_STATUS } from 'src/services/api'; import { createApiReducer, chainReducers, INITIAL_API_STATUS } from 'src/services/api';
import { User } from 'src/services/users';
import { AuthState } from './types'; import { AuthState } from './types';
import { AuthActionTypes } from './actions'; import { AuthActionTypes } from './actions';
import { CurrentUser } from '../types'; import { transformAuthUser } from '../transformations';
const initialCurrentUserState: CurrentUser = { const initialCurrentUserState: User = {
email: null, email: null,
name: null, name: null,
preferredUsername: null,
id: null, id: null,
role_id: null,
status: null,
preferredUsername: null,
}; };
const initialState: AuthState = { const initialState: AuthState = {
@ -17,9 +20,21 @@ const initialState: AuthState = {
_status: INITIAL_API_STATUS, _status: INITIAL_API_STATUS,
}; };
const authLocalReducer = (state: any = initialState, action: any) => {
switch (action.type) {
case AuthActionTypes.UPDATE_AUTH_USER:
return {
...state,
userInfo: action.payload,
};
default:
return state;
}
};
const auth = createApiReducer( const auth = createApiReducer(
[AuthActionTypes.SIGN_IN_START, AuthActionTypes.SIGN_IN_SUCCESS, AuthActionTypes.SIGN_IN_FAILURE], [AuthActionTypes.SIGN_IN_START, AuthActionTypes.SIGN_IN_SUCCESS, AuthActionTypes.SIGN_IN_FAILURE],
(data) => ({ token: data.accessToken, userInfo: data.userInfo }), (data) => transformAuthUser(data),
(data) => data.error.message, (data) => data.error.message,
); );
@ -29,4 +44,4 @@ const signOut = createApiReducer(
() => initialState, () => initialState,
); );
export default chainReducers(initialState, auth, signOut); export default chainReducers(initialState, auth, signOut, authLocalReducer);

View file

@ -0,0 +1,18 @@
import { UserRole } from '../users';
import { Auth } from './types';
export const transformAuthUser = (response: any): Auth => {
const resolvedUserRole = !response.userInfo.role_id ? UserRole.User : response.userInfo.role_id;
return {
token: response.accessToken,
userInfo: {
id: response.userInfo.id,
role_id: resolvedUserRole,
email: response.userInfo.email ?? null,
name: response.userInfo.name ?? null,
preferredUsername: response.userInfo.preferredUsername,
status: response.userInfo.state ?? null,
},
};
};

View file

@ -1,11 +1,6 @@
import { User } from '../users';
export interface Auth { export interface Auth {
token: string | null; token: string | null;
userInfo: CurrentUser; userInfo: User;
}
export interface CurrentUser {
email: string | null;
name: string | null;
preferredUsername: string | null;
id: string | null;
} }

View file

@ -1,7 +1,8 @@
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import { showToast, ToastType } from 'src/common/util/show-toast'; import { showToast, ToastType } from 'src/common/util/show-toast';
import { performApiCall } from 'src/services/api'; import { performApiCall } from 'src/services/api';
import { transformRequestUser, transformResponseUser } from '../transformations'; import { AuthActionTypes } from 'src/services/auth';
import { transformRequestUser, transformUser } from '../transformations';
export enum UserActionTypes { export enum UserActionTypes {
FETCH_USERS = 'users/fetch_users', FETCH_USERS = 'users/fetch_users',
@ -38,7 +39,7 @@ export const fetchUsers = () => async (dispatch: Dispatch<any>) => {
dispatch({ dispatch({
type: UserActionTypes.FETCH_USERS, type: UserActionTypes.FETCH_USERS,
payload: data.map(transformResponseUser), payload: data.map(transformUser),
}); });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -58,7 +59,7 @@ export const fetchUserById = (id: string) => async (dispatch: Dispatch<any>) =>
dispatch({ dispatch({
type: UserActionTypes.FETCH_USER, type: UserActionTypes.FETCH_USER,
payload: transformResponseUser(data), payload: transformUser(data),
}); });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -79,7 +80,12 @@ export const updateUserById = (user: any) => async (dispatch: Dispatch<any>) =>
dispatch({ dispatch({
type: UserActionTypes.UPDATE_USER, type: UserActionTypes.UPDATE_USER,
payload: transformResponseUser(data), payload: transformUser(data),
});
dispatch({
type: AuthActionTypes.UPDATE_AUTH_USER,
payload: transformUser(data),
}); });
showToast('User updated successfully.', ToastType.Success); showToast('User updated successfully.', ToastType.Success);
@ -104,7 +110,7 @@ export const createUser = (user: any) => async (dispatch: Dispatch<any>) => {
dispatch({ dispatch({
type: UserActionTypes.CREATE_USER, type: UserActionTypes.CREATE_USER,
payload: transformResponseUser(data), payload: transformUser(data),
}); });
showToast('User created successfully.', ToastType.Success); showToast('User created successfully.', ToastType.Success);

View file

@ -1,31 +1,32 @@
import _ from 'lodash'; import _ from 'lodash';
import { User } from './types'; import { User, UserRole } from './types';
export const transformResponseUser = (response: any): User => {
const userResponse = _.get(response, 'user', response);
return {
id: userResponse.id,
email: userResponse.traits.email,
name: userResponse.traits.name ?? null,
status: userResponse.state,
};
};
export const transformUser = (response: any): User => { export const transformUser = (response: any): User => {
const userResponse = _.get(response, 'user', response); const userResponse = _.get(response, 'user', response);
const resolvedUserRole = !userResponse.traits.role_id ? UserRole.User : userResponse.traits.role_id;
return { return {
id: userResponse.id, id: userResponse.id,
email: userResponse.email, role_id: resolvedUserRole,
name: userResponse.name, email: userResponse.traits.email,
status: userResponse.status, name: userResponse.traits.name ?? null,
preferredUsername: userResponse.preferredUsername,
status: userResponse.state,
}; };
}; };
export const transformRequestUser = (data: any) => { export const transformRequestUser = (data: Pick<User, 'role_id' | 'name' | 'email'>) => {
if (data.role_id === UserRole.User) {
return {
email: data.email,
name: data.name,
};
}
return { return {
role_id: Number(data.role_id),
email: data.email, email: data.email,
name: data.name, name: data.name,
}; };

View file

@ -1,7 +1,9 @@
export interface User { export interface User {
id: number | null; id: number | null;
role_id: UserRole | null;
email: string | null; email: string | null;
name: string | null; name: string | null;
preferredUsername: string | null;
status: string | null; status: string | null;
} }
@ -10,10 +12,9 @@ export interface FormUser extends User {
confirmPassword?: string; confirmPassword?: string;
} }
export interface UserRole { export enum UserRole {
id?: number; Admin = '1',
name: string; User = '2',
isAdministrator: boolean;
} }
export interface UserApiRequest { export interface UserApiRequest {