Compare commits

..
4 Commits
Author SHA1 Message Date
Misha Bragin 331dd2b429 Make it more visible that user can be blocked (#184) 2023-05-19 12:40:08 +02:00
Misha Bragin 3a8106c1e7 Track block/unblock user activity (#181) 2023-05-17 09:53:46 +02:00
Misha Bragin b595e0d6a8 Switch to activate/deactivate a user (#179)
This PR adds a switch to deactivate (or block) a user.
Only admins can block/unblock users.
Users can't block themselves.
2023-05-15 16:57:35 +02:00
pascal-fischer 33621cae5d remove href- from breadcrumbs (#180) 2023-05-15 14:28:14 +02:00
7 changed files with 173 additions and 90 deletions

No files matched your search

+52 -42
View File
@@ -10,7 +10,7 @@ import {
Row,
Select,
Skeleton,
Space, Table,
Space, Switch, Table,
Tag, Typography
} from "antd";
import {useDispatch, useSelector} from "react-redux";
@@ -63,6 +63,7 @@ const UserEdit = () => {
const [formUser, setFormUser] = useState({} as FormUser)
const [form] = Form.useForm()
const [isAdmin, setIsAdmin] = useState(false);
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
@@ -87,10 +88,20 @@ const UserEdit = () => {
name: values.name,
groupsToCreate: groupsToCreate,
auto_groups: autoGroups,
is_service_user: isServiceUser
is_service_user: isServiceUser,
is_blocked: values.is_blocked
} as UserToSave
}
useEffect(() => {
if(users) {
let currentUser = users.find((user) => user.is_current)
if(currentUser) {
setIsAdmin(currentUser.role === "admin");
}
}
}, [users])
const handleFormSubmit = () => {
form.validateFields()
.then((values) => {
@@ -274,29 +285,12 @@ const UserEdit = () => {
name: user.name,
role: user.role,
email: user.email,
is_blocked: user.is_blocked,
autoGroupsNames: currentGroups,
})
}
}, [form, user, currentGroups])
const menuItems = [
{
key: '1',
label: (
<Text onClick={() => onBreadcrumbUsersClick("Users")}>
Users
</Text>
),
},
{
key: '2',
label: (
<Text onClick={() => onBreadcrumbUsersClick("Service Users")}>
Service Users
</Text>
),
},
];
const isUserAdmin = (userId: string): boolean => {
return users.find(u => u.id === userId)?.role === "admin"
@@ -308,10 +302,10 @@ const UserEdit = () => {
<Breadcrumb style={{marginBottom: "30px"}}
items={[
{
title: <a href = "" onClick={() => onBreadcrumbUsersClick("Users")}>All Users</a>,
title: <a onClick={() => onBreadcrumbUsersClick("Users")}>All Users</a>,
},
{
title: <a href= "" onClick={() => onBreadcrumbUsersClick(tab)}>{tab}</a>,
title: <a onClick={() => onBreadcrumbUsersClick(tab)}>{tab}</a>,
// menu: { items: menuItems },
},
{
@@ -332,6 +326,7 @@ const UserEdit = () => {
name: formUser.name,
role: formUser.role,
email: formUser.email,
is_blocked: formUser.is_blocked,
autoGroupsNames: formUser.autoGroupsNames,
}}
>
@@ -364,30 +359,45 @@ const UserEdit = () => {
</Form.Item>
</Col>
</Row>
{!user.is_service_user && <Row style={{paddingBottom: "15px"}}>
<Col span={9}>
<Form.Item
name="autoGroupsNames"
label={<Text style={{}}>Auto-assigned groups</Text>}
tooltip="Every peer enrolled with this user will be automatically added to these groups"
rules={[{validator: selectValidator}]}
>
<Select mode="tags"
style={{width: '100%'}}
placeholder="Associate groups with the user"
tagRender={tagRender}
dropdownRender={dropDownRender}
disabled={oidcUser && !isUserAdmin(oidcUser.sub)}
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
<Form.Item
name="autoGroupsNames"
label={<Text style={{}}>Auto-assigned groups</Text>}
tooltip="Every peer enrolled with this user will be automatically added to these groups"
rules={[{validator: selectValidator}]}
style={{marginRight: "70px"}}
>
{
tagGroups.map(m =>
<Option key={m}>{optionRender(m)}</Option>
)
}
</Select>
<Select mode="tags"
placeholder="Associate groups with the user"
tagRender={tagRender}
dropdownRender={dropDownRender}
disabled={oidcUser && !isUserAdmin(oidcUser.sub)}
>
{
tagGroups.map(m =>
<Option key={m}>{optionRender(m)}</Option>
)
}
</Select>
</Form.Item>
</Col>
{!user.is_current && isAdmin && (
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
<Form.Item
valuePropName="checked"
name="is_blocked"
label="Block user"
style={{marginRight: "50px"}}
>
<Switch/>
</Form.Item>
</Col>
</Col>)}
</Row>}
<Space style={{display: 'flex', justifyContent: 'start'}}>
<Button disabled={loading} onClick={onCancel}>Cancel</Button>
<Button type="primary"
+2 -3
View File
@@ -5,8 +5,6 @@ import service from './service';
import actions from './actions';
import serviceGroup from "../group/service";
import {Group} from "../group/types";
import {actions as groupActions} from "../group";
import {PersonalAccessToken} from "../personal-access-token/types";
export function* getUsers(action: ReturnType<typeof actions.getUsers.request>): Generator {
try {
@@ -74,7 +72,8 @@ export function* saveUser(action: ReturnType<typeof actions.saveUser.request>):
email: userToSave.email,
role: userToSave.role,
auto_groups: newGroups,
is_service_user: userToSave.is_service_user
is_service_user: userToSave.is_service_user,
is_blocked: userToSave.is_blocked
} as UserToSave
let effect
+2
View File
@@ -7,10 +7,12 @@ export interface User {
auto_groups: string[];
is_current?: boolean;
is_service_user?: boolean;
is_blocked?: boolean;
}
export interface FormUser extends User {
autoGroupsNames: string[]
is_active?: boolean
}
export interface UserToSave extends User {
+2
View File
@@ -212,6 +212,8 @@ export const Activity = () => {
case "service.user.delete":
return renderMultiRowSpan(event.meta.name,"Service User")
case "user.invite":
case "user.block":
case "user.unblock":
if (user) {
return renderMultiRowSpan(user.name ? user.name : user.id,user.email ? user.email : "User")
}
+111 -40
View File
@@ -1,4 +1,4 @@
import React, {useEffect, useState} from 'react';
import React, {useEffect, useState} from 'react';
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "typesafe-actions";
import {actions as userActions} from '../store/user';
@@ -8,29 +8,33 @@ import {
Button,
Card,
Col,
Dropdown,
Dropdown, Empty,
Input,
Menu,
message, Modal,
Popover,
Row,
Select,
Space,
Space, Switch,
Table,
Tag,
Tag, Tooltip,
Typography,
} from "antd";
import {User} from "../store/user/types";
import {User, UserToSave} from "../store/user/types";
import {filter} from "lodash";
import tableSpin from "../components/Spin";
import {useGetTokenSilently} from "../utils/token";
import {actions as groupActions} from "../store/group";
import {Group} from "../store/group/types";
import {TooltipPlacement} from "antd/es/tooltip";
import {isLocalDev, isNetBirdHosted} from "../utils/common";
import {capitalize, isLocalDev, isNetBirdHosted} from "../utils/common";
import {usePageSizeHelpers} from "../utils/pageSize";
import AddServiceUserPopup from "../components/popups/AddServiceUserPopup";
import InviteUserPopup from "../components/popups/InviteUserPopup";
import {Peer, PeerDataTable} from "../store/peer/types";
import {ExclamationCircleOutlined, MinusOutlined} from "@ant-design/icons";
import {actions as peerActions} from "../store/peer";
import {useOidcUser} from "@axa-fr/react-oidc";
const {Title, Paragraph, Text} = Typography;
const {Column} = Table;
@@ -42,10 +46,12 @@ interface UserDataTable extends User {
const styleNotification = {marginTop: 85}
export const RegularUsers = () => {
const {onChangePageSize,pageSizeOptions,pageSize} = usePageSizeHelpers()
const {onChangePageSize, pageSizeOptions, pageSize} = usePageSizeHelpers()
const {getTokenSilently} = useGetTokenSilently()
const dispatch = useDispatch()
const [isAdmin, setIsAdmin] = useState(false);
const groups = useSelector((state: RootState) => state.group.data)
const users = useSelector((state: RootState) => state.user.regularUsers);
const failed = useSelector((state: RootState) => state.user.failed);
@@ -114,6 +120,7 @@ export const RegularUsers = () => {
auto_groups: userToAction?.auto_groups ? userToAction?.auto_groups : [],
name: userToAction?.name,
role: userToAction?.role,
is_blocked: userToAction?.is_blocked,
} as User));
}
@@ -122,6 +129,15 @@ export const RegularUsers = () => {
dispatch(userActions.setUser(null as unknown as User));
}
useEffect(() => {
if(users) {
let currentUser = users.find((user) => user.is_current)
if(currentUser) {
setIsAdmin(currentUser.role === "admin");
}
}
}, [users])
const renderPopoverGroups = (label: string, rowGroups: string[] | string[] | null, userToAction: UserDataTable) => {
let groupsMap = new Map<string, Group>();
@@ -196,10 +212,10 @@ export const RegularUsers = () => {
let errorMsg = "Failed to update user"
switch (savedUser.error.statusCode) {
case 412:
errorMsg = savedUser.error.data
break
case 403:
errorMsg = "Failed to update user. You might not have enough permissions."
if (savedUser.error.data) {
errorMsg = capitalize(savedUser.error.data.message)
}
break
}
message.error({
@@ -243,16 +259,59 @@ export const RegularUsers = () => {
name: user.name,
is_current: user.is_current,
is_service_user: user.is_service_user,
is_blocked: user.is_blocked
} as User));
dispatch(userActions.setEditUserPopupVisible(true));
}
const handleBlockUser = (block: boolean, user: UserDataTable) => {
if (block) {
confirmModal.confirm({
icon: <ExclamationCircleOutlined/>,
title: "Are you sure you want to block " + user.name + "?",
width: 600,
content: <Space direction="vertical" size="small">
<Paragraph>Blocking this user will disconnect their devices and disable dashboard access.</Paragraph>
</Space>,
onOk() {
let userToSave = createUserToSave(user, block)
dispatch(userActions.saveUser.request({
getAccessTokenSilently: getTokenSilently,
payload: userToSave
}));
},
onCancel() {
// noop
},
});
} else {
let userToSave = createUserToSave(user, block)
dispatch(userActions.saveUser.request({
getAccessTokenSilently: getTokenSilently,
payload: userToSave
}));
}
}
const createUserToSave = (values: UserDataTable, block: boolean): UserToSave => {
return {
id: values.id,
role: values.role,
name: values.name,
groupsToCreate: Array.of(),
auto_groups: values.auto_groups,
is_service_user: values.is_service_user,
is_blocked: block
} as UserToSave
}
return (
<>
<Container style={{padding: "0px"}}>
<Row>
<Col span={24}>
<Paragraph>Manage users and their permissions.{(window.location.hostname == "app.netbird.io") ? "Same-domain email users are added automatically on first sign-in." : ""}</Paragraph>
<Paragraph>Manage users and their
permissions.{(window.location.hostname == "app.netbird.io") ? "Same-domain email users are added automatically on first sign-in." : ""}</Paragraph>
<Space direction="vertical" size="large" style={{display: 'flex'}}>
<Row gutter={[16, 24]}>
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
@@ -272,15 +331,16 @@ export const RegularUsers = () => {
xl={5}
xxl={5} span={5}>
{(isNetBirdHosted() || isLocalDev()) &&
<Row justify="end">
<Col>
<Button type="primary" onClick={onClickInviteUser}>Invite User</Button>
</Col>
</Row>}
<Row justify="end">
<Col>
<Button type="primary" onClick={onClickInviteUser}>Invite user</Button>
</Col>
</Row>}
</Col>
</Row>
{failed &&
<Alert message={failed.message} description={failed.data ? failed.data.message : " "} type="error" showIcon
<Alert message={failed.message} description={failed.data ? failed.data.message : " "}
type="error" showIcon
closable/>
}
<Card bodyStyle={{padding: 0}}>
@@ -305,6 +365,7 @@ export const RegularUsers = () => {
className="tooltip-label">
<Text
strong>{(text && text.trim() !== "") ? text : (record as User).id}</Text>
</Button>
if ((record as User).is_current) {
@@ -313,25 +374,24 @@ export const RegularUsers = () => {
</div>
}
if ((record as User).status === "invited") {
return <div>{btn}
<Tag color="gold">invited</Tag>
</div>
}
if ((record as User).status === "blocked") {
return <div>{btn}
<Tag color="red">blocked</Tag>
</div>
}
return btn
}}
/>
<Column title="Name" dataIndex="name"
onFilter={(value: string | number | boolean, record) => (record as any).name.includes(value)}
sorter={(a, b) => ((a as any).name.localeCompare((b as any).name))}/>
<Column title="Status" dataIndex="status"
align="center"
onFilter={(value: string | number | boolean, record) => (record as any).status.includes(value)}
sorter={(a, b) => ((a as any).status.localeCompare((b as any).status))}
render={(text, record, index) => {
if (text == "active") {
return <Tag color="green">{text}</Tag>
} else if (text === "invited"){
return <Tag color="gold">{text}</Tag>
}
return <Tag color="red">{text}</Tag>
}}
/>
<Column title="Groups" dataIndex="groupsCount" align="center"
render={(text, record: UserDataTable, index) => {
return renderPopoverGroups(text, record.auto_groups, record)
@@ -340,16 +400,27 @@ export const RegularUsers = () => {
<Column title="Role" dataIndex="role"
onFilter={(value: string | number | boolean, record) => (record as any).role.includes(value)}
sorter={(a, b) => ((a as any).role.localeCompare((b as any).role))}/>
{/*<Column title="" align="center" width="30px"*/}
{/* render={(text, record, index) => {*/}
{/* return (*/}
{/* <Dropdown.Button type="text" overlay={actionsMenu}*/}
{/* trigger={["click"]}*/}
{/* onVisibleChange={visible => {*/}
{/* if (visible) setUserToAction(record as UserDataTable)*/}
{/* }}></Dropdown.Button>)*/}
{/* }}*/}
{/*/>*/}
{isAdmin && (
<Column title="Block user" align="center" width="150px" dataIndex="is_blocked"
render={(e, record: UserDataTable, index) => {
let witch = <Switch size={"small"} checked={e}
disabled={record.is_current}
onClick={(active: boolean) => {
handleBlockUser(active, record)
}}
/>
if (record.is_current) {
return <Tooltip
title="You can't block or unblock yourself">
<Empty image={""} description={""} style={{height: "1px", width: "auto"}}/>
</Tooltip>
}
return witch
}}
/>
)}
</Table>
</Card>
</Space>
+4 -4
View File
@@ -22,7 +22,7 @@ import {filter} from "lodash";
import tableSpin from "../components/Spin";
import {useGetTokenSilently} from "../utils/token";
import {actions as groupActions} from "../store/group";
import {isLocalDev, isNetBirdHosted} from "../utils/common";
import {capitalize, isLocalDev, isNetBirdHosted} from "../utils/common";
import {usePageSizeHelpers} from "../utils/pageSize";
import AddServiceUserPopup from "../components/popups/AddServiceUserPopup";
import {ExclamationCircleOutlined} from "@ant-design/icons";
@@ -111,10 +111,10 @@ export const ServiceUsers = () => {
let errorMsg = "Failed to update user"
switch (savedUser.error.statusCode) {
case 412:
errorMsg = savedUser.error.data
break
case 403:
errorMsg = "Failed to update user. You might not have enough permissions."
if (savedUser.error.data) {
errorMsg = capitalize(savedUser.error.data.message)
}
break
}
message.error({
-1
View File
@@ -2,7 +2,6 @@ import React, {useEffect, useState} from 'react';
import {Col, Row, Tabs, TabsProps} from "antd";
import {Container} from "../components/Container";
import {useOidcUser} from "@axa-fr/react-oidc";
import {actions as userActions} from "../store/user";
import {useGetTokenSilently} from "../utils/token";
import {useDispatch, useSelector} from "react-redux";