admin-dashboard-web/src/Pages/UserProfile/index.js

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()}
/>
</>
)}
</>
);
}