508 lines
14 KiB
JavaScript
508 lines
14 KiB
JavaScript
import {
|
|
Button,
|
|
Card,
|
|
Col,
|
|
Form,
|
|
Input,
|
|
Popconfirm,
|
|
Row,
|
|
Select,
|
|
Space,
|
|
Table,
|
|
Tooltip,
|
|
Typography,
|
|
Upload,
|
|
notification,
|
|
} from "antd";
|
|
import { useEffect, useState } from "react";
|
|
import {
|
|
Constants,
|
|
EncodeStringToBase64,
|
|
FormatDatetime,
|
|
getConnectionStatusItem,
|
|
getUserSessionFromLocalStorage,
|
|
hasPermission,
|
|
isEmailValid,
|
|
myFetch,
|
|
} from "../../utils";
|
|
import { Link } from "react-router-dom";
|
|
import { useTranslation } from "react-i18next";
|
|
import {
|
|
CopyOutlined,
|
|
EyeInvisibleOutlined,
|
|
EyeOutlined,
|
|
FileTextOutlined,
|
|
KeyOutlined,
|
|
} from "@ant-design/icons";
|
|
import { MyUserAvatar } from "../../Components/MyAvatar";
|
|
import { useUserProfileContext } from "../../Contexts/UserProfileContext";
|
|
import { useWebSocketContext } from "../../Contexts/WebSocketContext";
|
|
import { useAppContext } from "../../Contexts/AppContext";
|
|
import { useSideBarContext } from "../../Contexts/SideBarContext";
|
|
import { SentMessagesCommands } from "../../Handlers/WebSocketMessageHandler";
|
|
|
|
export default function UserProfile() {
|
|
const webSocketContext = useWebSocketContext();
|
|
const appContext = useAppContext();
|
|
const sideBarContext = useSideBarContext();
|
|
const userProfileContext = useUserProfileContext();
|
|
const [notificationApi, notificationContextHolder] =
|
|
notification.useNotification();
|
|
const { t, i18n } = useTranslation();
|
|
|
|
const [username, setUsername] = useState(Constants.LOADING);
|
|
const [email, setEmail] = useState(Constants.LOADING);
|
|
const [oldPassword, setOldPassword] = useState("");
|
|
const [newPassword, setNewPassword] = useState("");
|
|
const [repeatedNewPassword, setRepeatedNewPassword] = useState("");
|
|
const [newApiKeyName, setNewApikeyName] = useState("");
|
|
const [showApiKeyPassword, setShowApiKeyPassword] = useState(false);
|
|
|
|
const getSessionTableColumns = () => {
|
|
return [
|
|
{
|
|
title: t("userProfile.column.userAgent"),
|
|
dataIndex: "userAgent",
|
|
key: "userAgent",
|
|
},
|
|
{
|
|
title: t("userProfile.column.connectionStatus"),
|
|
dataIndex: "connectionStatus",
|
|
key: "connectionStatus",
|
|
},
|
|
{
|
|
title: t("userProfile.column.lastUsed"),
|
|
dataIndex: "lastUsed",
|
|
key: "lastUsed",
|
|
},
|
|
{
|
|
title: t("userProfile.column.expiresAt"),
|
|
dataIndex: "expiresAt",
|
|
key: "expiresAt",
|
|
},
|
|
{
|
|
title: t("userProfile.column.action.title"),
|
|
dataIndex: "action",
|
|
key: "action",
|
|
render: (_, record) => {
|
|
return (
|
|
<Space size="middle">
|
|
<Link
|
|
to="#"
|
|
onClick={() => myFetch(`/user/session/${record.key}`, "DELETE")}
|
|
>
|
|
{t("userProfile.column.action.signOut")}
|
|
</Link>
|
|
</Space>
|
|
);
|
|
},
|
|
},
|
|
];
|
|
};
|
|
|
|
const getApiKeyTableColumns = () => {
|
|
return [
|
|
{
|
|
title: t("userProfile.column.name"),
|
|
dataIndex: "name",
|
|
key: "name",
|
|
},
|
|
{
|
|
title: t("userProfile.column.token"),
|
|
dataIndex: "token",
|
|
key: "token",
|
|
render: (text) => (
|
|
<Space>
|
|
{showApiKeyPassword ? text : "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"}
|
|
{showApiKeyPassword ? (
|
|
<EyeInvisibleOutlined
|
|
onClick={() => setShowApiKeyPassword(false)}
|
|
/>
|
|
) : (
|
|
<EyeOutlined onClick={() => setShowApiKeyPassword(true)} />
|
|
)}
|
|
{showApiKeyPassword && (
|
|
<CopyOutlined
|
|
onClick={() => {
|
|
navigator.clipboard.writeText(text);
|
|
setShowApiKeyPassword(false);
|
|
|
|
notificationApi["info"]({
|
|
message: t(
|
|
"userProfile.button.copyToClipboard.notification"
|
|
),
|
|
});
|
|
}}
|
|
/>
|
|
)}
|
|
</Space>
|
|
),
|
|
},
|
|
{
|
|
title: t("userProfile.column.usageCount"),
|
|
dataIndex: "usageCount",
|
|
key: "usageCount",
|
|
},
|
|
|
|
{
|
|
title: t("userProfile.column.lastUsed"),
|
|
dataIndex: "lastUsed",
|
|
key: "lastUsed",
|
|
},
|
|
{
|
|
title: t("userProfile.column.createdAt"),
|
|
dataIndex: "createdAt",
|
|
key: "createdAt",
|
|
},
|
|
{
|
|
title: t("userProfile.column.action.title"),
|
|
dataIndex: "action",
|
|
key: "action",
|
|
render: (_, record) => (
|
|
<Space size="middle">
|
|
<Popconfirm
|
|
placement="left"
|
|
title={t("userProfile.column.deleteApiKey.popconfirm.title")}
|
|
description={t(
|
|
"userProfile.column.deleteApiKey.popconfirm.description"
|
|
)}
|
|
okText={t("common.button.delete")}
|
|
cancelText={t("common.button.cancel")}
|
|
onConfirm={() =>
|
|
webSocketContext.SendSocketMessage(
|
|
SentMessagesCommands.DeleteUserApiKey,
|
|
{ Id: record.key }
|
|
)
|
|
}
|
|
>
|
|
<Link to="#">{t("userProfile.column.deleteApiKey.link")}</Link>
|
|
</Popconfirm>
|
|
</Space>
|
|
),
|
|
},
|
|
];
|
|
};
|
|
|
|
const getApiKeyTableItems = () => {
|
|
let items = [];
|
|
|
|
userProfileContext.apiKeys.forEach((apiKey) => {
|
|
items.push({
|
|
key: apiKey.Id,
|
|
name: apiKey.Name,
|
|
token: apiKey.Token,
|
|
usageCount: apiKey.UsageCount,
|
|
createdAt: FormatDatetime(apiKey.CreatedAt),
|
|
lastUsed: FormatDatetime(apiKey.LastUsed),
|
|
});
|
|
});
|
|
|
|
return items;
|
|
};
|
|
|
|
const getSessionTableItems = () => {
|
|
let items = [];
|
|
|
|
userProfileContext.sessions.sort(
|
|
(a, b) => b.ConnectionStatus - a.ConnectionStatus
|
|
);
|
|
|
|
userProfileContext.sessions.forEach((session) => {
|
|
items.push({
|
|
key: session.IdForDeletion,
|
|
userAgent: session.UserAgent,
|
|
connectionStatus: getConnectionStatusItem(session.ConnectionStatus),
|
|
lastUsed: FormatDatetime(session.LastUsed),
|
|
expiresAt: FormatDatetime(session.ExpiresAt),
|
|
});
|
|
});
|
|
|
|
return items;
|
|
};
|
|
|
|
const beforeUpload = (file) => {
|
|
if (file.size > Constants.MAX_AVATAR_SIZE) {
|
|
notificationApi["error"]({
|
|
message: t("userProfile.changeAvatarError.notification.message"),
|
|
description: t(
|
|
"userProfile.changeAvatarError.notification.description",
|
|
{ MAX_AVATAR_SIZE: Constants.MAX_AVATAR_SIZE / 1024 / 1024 }
|
|
),
|
|
});
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
const isButtonDisabled = () => {
|
|
if (
|
|
!isEmailValid(email) ||
|
|
sideBarContext.username.length < Constants.GLOBALS.MIN_USERNAME_LENGTH
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
if (
|
|
username !== sideBarContext.username ||
|
|
email !== userProfileContext.email ||
|
|
(oldPassword !== "" &&
|
|
newPassword !== "" &&
|
|
newPassword === repeatedNewPassword)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
const handleOnSubmit = () => {
|
|
const changes = {};
|
|
|
|
if (sideBarContext.username !== username) {
|
|
changes.username = username;
|
|
}
|
|
|
|
if (userProfileContext.email !== email) {
|
|
changes.email = email;
|
|
}
|
|
|
|
if (
|
|
oldPassword !== "" &&
|
|
newPassword !== "" &&
|
|
newPassword === repeatedNewPassword
|
|
) {
|
|
changes.oldPassword = EncodeStringToBase64(oldPassword);
|
|
changes.newPassword = EncodeStringToBase64(newPassword);
|
|
}
|
|
|
|
webSocketContext.SendSocketMessage(SentMessagesCommands.UpdateUserProfile, {
|
|
changes: changes,
|
|
});
|
|
};
|
|
|
|
const onCreateNewApiKeyConfirm = () => {
|
|
webSocketContext.SendSocketMessage(
|
|
SentMessagesCommands.CreateNewUserApiKey,
|
|
{ Name: newApiKeyName }
|
|
);
|
|
|
|
setNewApikeyName("");
|
|
};
|
|
|
|
useEffect(() => {
|
|
myFetch("/user/profile", "GET").then((data) => {
|
|
userProfileContext.setEmail(data.Email);
|
|
userProfileContext.setSessions(data.Sessions);
|
|
userProfileContext.setApiKeys(data.ApiKeys);
|
|
|
|
setEmail(data.Email);
|
|
});
|
|
}, []);
|
|
|
|
useEffect(
|
|
() => setUsername(sideBarContext.username),
|
|
[sideBarContext.username]
|
|
);
|
|
|
|
useEffect(
|
|
() => setEmail(userProfileContext.email),
|
|
[userProfileContext.email]
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{notificationContextHolder}
|
|
|
|
<Typography.Title level={4}>
|
|
{t("userProfile.header.yourProfile")}
|
|
</Typography.Title>
|
|
|
|
<Card>
|
|
<Form layout="vertical">
|
|
<Row>
|
|
<Col xs={24} sm={4}>
|
|
<Upload
|
|
accept={Constants.ACCEPTED_AVATAR_FILE_TYPES.join(",")}
|
|
action={Constants.API_ADDRESS + "/user/avatar"}
|
|
maxCount={1}
|
|
showUploadList={false}
|
|
beforeUpload={beforeUpload}
|
|
headers={{
|
|
"X-Authorization": getUserSessionFromLocalStorage(),
|
|
}}
|
|
>
|
|
<MyUserAvatar avatar={sideBarContext.avatar} size={256} />
|
|
</Upload>
|
|
</Col>
|
|
|
|
<Col xs={24} sm={{ span: 4, offset: 16 }}>
|
|
<Form.Item label={t("userProfile.form.language")}>
|
|
<Select
|
|
style={{ width: "100%" }}
|
|
defaultValue={i18n.language}
|
|
options={[
|
|
{
|
|
value: "en",
|
|
label: "English",
|
|
},
|
|
{
|
|
value: "de",
|
|
label: "Deutsch",
|
|
},
|
|
]}
|
|
onChange={(e) => i18n.changeLanguage(e)}
|
|
/>
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Form.Item
|
|
label={t("userProfile.form.username")}
|
|
hasFeedback
|
|
validateStatus={
|
|
username < Constants.GLOBALS.MIN_USERNAME_LENGTH && "error"
|
|
}
|
|
>
|
|
<Input
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
minLength={Constants.GLOBALS.MIN_USERNAME_LENGTH}
|
|
maxLength={Constants.GLOBALS.MAX_USERNAME_LENGTH}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item
|
|
label={t("userProfile.form.email")}
|
|
hasFeedback
|
|
validateStatus={!isEmailValid(userProfileContext.email) && "error"}
|
|
>
|
|
<Input
|
|
type="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item
|
|
label={t("userProfile.form.oldPassword")}
|
|
hasFeedback
|
|
validateStatus={
|
|
oldPassword !== "" &&
|
|
oldPassword.length < Constants.GLOBALS.MIN_PASSWORD_LENGTH &&
|
|
"error"
|
|
}
|
|
>
|
|
<Input.Password
|
|
value={oldPassword}
|
|
onChange={(e) => setOldPassword(e.target.value)}
|
|
minLength={Constants.GLOBALS.MIN_PASSWORD_LENGTH}
|
|
maxLength={Constants.GLOBALS.MAX_PASSWORD_LENGTH}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item
|
|
label={t("userProfile.form.newPassword")}
|
|
hasFeedback
|
|
validateStatus={
|
|
newPassword !== "" &&
|
|
newPassword.length < Constants.GLOBALS.MIN_PASSWORD_LENGTH &&
|
|
"error"
|
|
}
|
|
>
|
|
<Input.Password
|
|
value={newPassword}
|
|
onChange={(e) => setNewPassword(e.target.value)}
|
|
minLength={Constants.GLOBALS.MIN_PASSWORD_LENGTH}
|
|
maxLength={Constants.GLOBALS.MAX_PASSWORD_LENGTH}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item
|
|
label={t("userProfile.form.repeatNewPassword")}
|
|
hasFeedback
|
|
validateStatus={newPassword !== repeatedNewPassword && "error"}
|
|
>
|
|
<Input.Password
|
|
value={repeatedNewPassword}
|
|
onChange={(e) => setRepeatedNewPassword(e.target.value)}
|
|
minLength={Constants.GLOBALS.MIN_PASSWORD_LENGTH}
|
|
maxLength={Constants.GLOBALS.MAX_PASSWORD_LENGTH}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item style={{ margin: 0 }}>
|
|
<Button
|
|
type="primary"
|
|
htmlType="submit"
|
|
onClick={() => handleOnSubmit()}
|
|
disabled={isButtonDisabled()}
|
|
>
|
|
{t("common.button.save")}
|
|
</Button>
|
|
</Form.Item>
|
|
</Form>
|
|
</Card>
|
|
|
|
<br />
|
|
|
|
<Typography.Title level={4}>
|
|
{t("userProfile.header.yourSessions")} (
|
|
{userProfileContext.sessions.length})
|
|
</Typography.Title>
|
|
|
|
<Table
|
|
scroll={{ x: "max-content" }}
|
|
columns={getSessionTableColumns()}
|
|
dataSource={getSessionTableItems()}
|
|
/>
|
|
|
|
{hasPermission(
|
|
appContext.userPermissions,
|
|
Constants.PERMISSIONS.USER_PROFILE.API_KEYS
|
|
) && (
|
|
<>
|
|
{" "}
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
justifyContent: "space-between",
|
|
}}
|
|
>
|
|
<Typography.Title level={4}>
|
|
{t("userProfile.header.yourApiKeys")} (
|
|
{userProfileContext.apiKeys.length}){" "}
|
|
<Tooltip title={t("userProfile.icon.viewApiDoc")}>
|
|
<Link
|
|
target="_blank"
|
|
to={Constants.STATIC_CONTENT_ADDRESS + "swagger/index.html"}
|
|
>
|
|
<FileTextOutlined style={{ fontSize: 16 }} />
|
|
</Link>
|
|
</Tooltip>
|
|
</Typography.Title>
|
|
|
|
<Popconfirm
|
|
placement="topRight"
|
|
title={t("userProfile.button.createApiKey.popconfirm.title")}
|
|
description={
|
|
<Input
|
|
placeholder="Name"
|
|
value={newApiKeyName}
|
|
onChange={(e) => setNewApikeyName(e.target.value)}
|
|
/>
|
|
}
|
|
okText={t("userProfile.button.createApiKey.popconfirm.okText")}
|
|
cancelText={t("common.button.cancel")}
|
|
onConfirm={() => onCreateNewApiKeyConfirm()}
|
|
>
|
|
<Button icon={<KeyOutlined />}>
|
|
{t("userProfile.button.createApiKey.title")}
|
|
</Button>
|
|
</Popconfirm>
|
|
</div>
|
|
<Table
|
|
scroll={{ x: "max-content" }}
|
|
columns={getApiKeyTableColumns()}
|
|
dataSource={getApiKeyTableItems()}
|
|
/>
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
}
|