diff --git a/.env.example b/.env.example index ce5f17913124ad997e6af938e3ac9a0e5bc6a56d..f3afcc4b0abf91b564fb6abbf1126507d060a191 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,2 @@ REACT_APP_API_URL=http://stackspin_proxy:8081/api/v1 -REACT_APP_HYDRA_PUBLIC_URL=https://sso.init.stackspin.net +REACT_APP_HYDRA_PUBLIC_URL=https://sso.init.stackspin.net \ No newline at end of file diff --git a/deployment/helmchart/CHANGELOG.md b/deployment/helmchart/CHANGELOG.md index 7cfd647692d1e2b1ad6c3c322f3241c39318fec5..72b6183c333a7fa50fbac5a798ee8b47d81f3a48 100644 --- a/deployment/helmchart/CHANGELOG.md +++ b/deployment/helmchart/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [1.1.0] + +### Bug fixes + +* Logging out of dashboard now calls SSO signout URL based on current domain + +### Features + +* Dashboard admin users automatically have admin rights in all apps +* App-specific rights for dashboard admin users are not editable + ## [1.0.5] ### Bug fixes diff --git a/deployment/helmchart/Chart.yaml b/deployment/helmchart/Chart.yaml index 4cefaa8c38c3706602c058e561481728ce7da2b7..a0ba170b15a3eec697155252a508cc75d872dbfc 100644 --- a/deployment/helmchart/Chart.yaml +++ b/deployment/helmchart/Chart.yaml @@ -23,4 +23,4 @@ name: stackspin-dashboard sources: - https://open.greenhost.net/stackspin/dashboard/ - https://open.greenhost.net/stackspin/dashboard-backend/ -version: 1.0.5 +version: 1.1.0 diff --git a/deployment/helmchart/values.yaml b/deployment/helmchart/values.yaml index fff5256e54ff8647df5a75f04dad3059982ef25b..01c48fb9eade293417c5fa024368d143620a7ed8 100644 --- a/deployment/helmchart/values.yaml +++ b/deployment/helmchart/values.yaml @@ -68,7 +68,7 @@ dashboard: image: registry: open.greenhost.net:4567 repository: stackspin/dashboard/dashboard - tag: 0-2-6 + tag: 0-2-7 ## Optionally specify an array of imagePullSecrets. ## Secrets must be manually created in the namespace. ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ @@ -235,7 +235,7 @@ backend: image: registry: open.greenhost.net:4567 repository: stackspin/dashboard-backend/dashboard-backend - tag: 0-2-7 + tag: 0-2-8 ## Optionally specify an array of imagePullSecrets. ## Secrets must be manually created in the namespace. ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 29cc3c5c6849c1a5562b74ef3e4bab4c0fc09b60..97a24eda56729ac74454a0b75d12385be3c0d293 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, useState } from 'react'; +import React, { Fragment, useMemo, useState } from 'react'; import { Disclosure, Menu, Transition } from '@headlessui/react'; import { MenuIcon, XIcon } from '@heroicons/react/outline'; import { useAuth } from 'src/services/auth'; @@ -9,6 +9,8 @@ import _ from 'lodash'; import { UserModal } from '../UserModal'; +const HYDRA_LOGOUT_URL = `${process.env.REACT_APP_HYDRA_PUBLIC_URL}/oauth2/sessions/logout`; + const navigation = [ { name: 'Dashboard', to: '/dashboard', requiresAdmin: false }, { name: 'Users', to: '/users', requiresAdmin: true }, @@ -26,8 +28,6 @@ function filterNavigationByDashboardRole(isAdmin: boolean) { return navigation.filter((item) => !item.requiresAdmin); } -const HYDRA_URL = process.env.REACT_APP_HYDRA_PUBLIC_URL; - // eslint-disable-next-line @typescript-eslint/no-empty-interface interface HeaderProps {} @@ -50,7 +50,14 @@ const Header: React.FC<HeaderProps> = () => { const navigationItems = filterNavigationByDashboardRole(isAdmin); - const signOutUrl = `${HYDRA_URL}/oauth2/sessions/logout`; + const signOutUrl = useMemo(() => { + const { hostname } = window.location; + // If we are developing locally, we need to use the init cluster's public URL + if (hostname === 'localhost') { + return HYDRA_LOGOUT_URL; + } + return `https://${hostname.replace(/^dashboard/, 'sso')}/oauth2/sessions/logout`; + }, []); return ( <> diff --git a/src/components/UserModal/UserModal.tsx b/src/components/UserModal/UserModal.tsx index ced538a7ecb077a95d550dfd9ede5a3b25a30901..0993b94fbeb3473526f26b9a83e05ebdf635c6fe 100644 --- a/src/components/UserModal/UserModal.tsx +++ b/src/components/UserModal/UserModal.tsx @@ -11,8 +11,19 @@ import { UserModalProps } from './types'; export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps) => { const [deleteModal, setDeleteModal] = useState(false); - const { user, loadUser, editUserById, createNewUser, userModalLoading, deleteUserById, clearSelectedUser } = - useUsers(); + const [isAdminRoleSelected, setAdminRoleSelected] = useState(true); + const [isPersonalModal, setPersonalModal] = useState(false); + const { + user, + loadUser, + loadPersonalInfo, + editUserById, + editPersonalInfo, + createNewUser, + userModalLoading, + deleteUserById, + clearSelectedUser, + } = useUsers(); const { currentUser, isAdmin } = useAuth(); const { control, reset, handleSubmit } = useForm<User>({ @@ -26,7 +37,13 @@ export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps) useEffect(() => { if (userId) { - loadUser(userId); + const currentUserId = currentUser?.id; + if (currentUserId === userId) { + setPersonalModal(true); + loadPersonalInfo(); + } else { + loadUser(userId); + } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [userId, open]); @@ -47,7 +64,9 @@ export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps) }); useEffect(() => { - if (dashboardRole === UserRole.Admin) { + const isAdminDashboardRoleSelected = dashboardRole === UserRole.Admin; + setAdminRoleSelected(isAdminDashboardRoleSelected); + if (isAdminDashboardRoleSelected) { fields.forEach((field, index) => update(index, { name: field.name, role: UserRole.Admin })); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -55,7 +74,9 @@ export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps) const handleSave = async () => { try { - if (userId) { + if (isPersonalModal) { + await handleSubmit((data) => editPersonalInfo(data))(); + } else if (userId) { await handleSubmit((data) => editUserById(data))(); } else { await handleSubmit((data) => createNewUser(data))(); @@ -178,13 +199,13 @@ export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps) </div> </div> - {isAdmin && ( + {isAdmin && !userModalLoading && ( <div> <div className="mt-8"> <h3 className="text-lg leading-6 font-medium text-gray-900">App Access</h3> </div> - {dashboardRole === UserRole.Admin && ( + {isAdminRoleSelected && ( <div className="sm:col-span-6"> <Banner title="Admin users automatically have admin-level access to all apps." @@ -193,47 +214,49 @@ export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps) </div> )} - <div> - <div className="flow-root mt-6"> - <ul className="-my-5 divide-y divide-gray-200 "> - {fields.map((item, index) => { - if (item.name === 'dashboard') { - return null; - } - - return ( - <li className="py-4" key={item.name}> - <div className="flex items-center space-x-4"> - <div className="flex-shrink-0 flex-1 flex items-center"> - <img - className="h-10 w-10 rounded-md overflow-hidden" - src={_.find(appAccessList, ['name', item.name!])?.image} - alt={item.name ?? 'Image'} - /> - <h3 className="ml-4 text-md leading-6 font-medium text-gray-900"> - {_.find(appAccessList, ['name', item.name!])?.label} - </h3> - </div> - <div> - <Select - key={item.id} - control={control} - name={`app_roles.${index}.role`} - disabled={dashboardRole === UserRole.Admin} - options={[ - { value: UserRole.NoAccess, name: 'No Access' }, - { value: UserRole.User, name: 'User' }, - { value: UserRole.Admin, name: 'Admin' }, - ]} - /> + {!isAdminRoleSelected && ( + <div> + <div className="flow-root mt-6"> + <ul className="-my-5 divide-y divide-gray-200"> + {fields.map((item, index) => { + if (item.name === 'dashboard') { + return null; + } + + return ( + <li className="py-4" key={item.name}> + <div className="flex items-center space-x-4"> + <div className="flex-shrink-0 flex-1 flex items-center"> + <img + className="h-10 w-10 rounded-md overflow-hidden" + src={_.find(appAccessList, ['name', item.name!])?.image} + alt={item.name ?? 'Image'} + /> + <h3 className="ml-4 text-md leading-6 font-medium text-gray-900"> + {_.find(appAccessList, ['name', item.name!])?.label} + </h3> + </div> + <div> + <Select + key={item.id} + control={control} + name={`app_roles.${index}.role`} + disabled={isAdminRoleSelected} + options={[ + { value: UserRole.NoAccess, name: 'No Access' }, + { value: UserRole.User, name: 'User' }, + { value: UserRole.Admin, name: 'Admin' }, + ]} + /> + </div> </div> - </div> - </li> - ); - })} - </ul> + </li> + ); + })} + </ul> + </div> </div> - </div> + )} </div> )} </div> diff --git a/src/services/users/hooks/use-users.ts b/src/services/users/hooks/use-users.ts index f5079b831e7877f9242e1602f52730b975b99a28..2d0381de36faf88771aab5af1e9e033f455a68fc 100644 --- a/src/services/users/hooks/use-users.ts +++ b/src/services/users/hooks/use-users.ts @@ -3,7 +3,9 @@ import { getUsers, fetchUsers, fetchUserById, + fetchPersonalInfo, updateUserById, + updatePersonalInfo, createUser, deleteUser, clearCurrentUser, @@ -26,6 +28,10 @@ export function useUsers() { return dispatch(fetchUserById(id)); } + function loadPersonalInfo() { + return dispatch(fetchPersonalInfo()); + } + function clearSelectedUser() { return dispatch(clearCurrentUser()); } @@ -34,6 +40,10 @@ export function useUsers() { return dispatch(updateUserById(data)); } + function editPersonalInfo(data: any) { + return dispatch(updatePersonalInfo(data)); + } + function createNewUser(data: any) { return dispatch(createUser(data)); } @@ -51,7 +61,9 @@ export function useUsers() { user, loadUser, loadUsers, + loadPersonalInfo, editUserById, + editPersonalInfo, userModalLoading, userTableLoading, createNewUser, diff --git a/src/services/users/redux/actions.ts b/src/services/users/redux/actions.ts index 4ac3932f115353c04ee84271190760dab228d0f9..ac5adc98902b9d480a183786508ac2fda3e29dee 100644 --- a/src/services/users/redux/actions.ts +++ b/src/services/users/redux/actions.ts @@ -76,6 +76,26 @@ export const fetchUserById = (id: string) => async (dispatch: Dispatch<any>) => dispatch(setUserModalLoading(false)); }; +export const fetchPersonalInfo = () => async (dispatch: Dispatch<any>) => { + dispatch(setUserModalLoading(true)); + + try { + const { data } = await performApiCall({ + path: '/me', + method: 'GET', + }); + + dispatch({ + type: UserActionTypes.FETCH_USER, + payload: transformUser(data), + }); + } catch (err) { + console.error(err); + } + + dispatch(setUserModalLoading(false)); +}; + export const updateUserById = (user: any) => async (dispatch: Dispatch<any>, getState: any) => { dispatch(setUserModalLoading(true)); @@ -110,6 +130,34 @@ export const updateUserById = (user: any) => async (dispatch: Dispatch<any>, get dispatch(setUserModalLoading(false)); }; +export const updatePersonalInfo = (user: any) => async (dispatch: Dispatch<any>) => { + dispatch(setUserModalLoading(true)); + + try { + const { data } = await performApiCall({ + path: '/me', + method: 'PUT', + body: transformRequestUser(user), + }); + + dispatch({ + type: UserActionTypes.UPDATE_USER, + payload: transformUser(data), + }); + + dispatch({ + type: AuthActionTypes.UPDATE_AUTH_USER, + payload: transformUser(data), + }); + + showToast('Personal information updated successfully.', ToastType.Success); + } catch (err) { + console.error(err); + } + + dispatch(setUserModalLoading(false)); +}; + export const createUser = (user: any) => async (dispatch: Dispatch<any>) => { dispatch(setUserModalLoading(true));