diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json index 39feb74..e5bd8fd 100644 --- a/public/locales/de/translation.json +++ b/public/locales/de/translation.json @@ -140,6 +140,10 @@ "userProfile.column.expiresAt": "Läuft ab am", "userProfile.column.action": "Maßnahme", "userProfile.column.action.signOut": "Abmelden", + "userProfile.column.createdAt": "Erstellt am", + "userProfile.column.usageCount": "Anzahl der Nutzungen", + "userProfile.column.name": "Name", + "userProfile.column.token": "Token", "userProfile.changeAvatarError.notification.message": "Ihr Avatar konnte nicht geändert werden", "userProfile.changeAvatarError.notification.description": "Avatar muss kleiner sein als {{MAX_AVATAR_SIZE}} MB sein", "userProfile.form.username": "Benutzername", @@ -149,6 +153,11 @@ "userProfile.form.repeatNewPassword": "Neues Passwort wiederholen", "userProfile.form.language": "Sprache", "userProfile.header.yourSessions": "Ihre Sitzungen", + "userProfile.header.yourApiKeys": "Ihre API Schlüssel", + "userProfile.button.createApiKey": "Neuen API Schlüssel erstellen", + "userProfile.button.createApiKey.popconfirm.title": "Name für den neuen API Schlüssel", + "userProfile.button.createApiKey.popconfirm.okText": "Erstellen", + "userProfile.button.copyToClipboard.notification": "API Token in die Zwischenablage kopiert", "scanners.column.name": "Name", "scanners.column.usedBy": "Verwendet von", "scanners.column.lastUsed": "Zuletzt verwendet", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index e130080..f8f4117 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -140,6 +140,10 @@ "userProfile.column.expiresAt": "Expires at", "userProfile.column.action": "Action", "userProfile.column.action.signOut": "Sign out", + "userProfile.column.createdAt": "Created at", + "userProfile.column.usageCount": "Usage count", + "userProfile.column.name": "Name", + "userProfile.column.token": "Token", "userProfile.changeAvatarError.notification.message": "Your avatar could not be changed", "userProfile.changeAvatarError.notification.description": "Avatar must be smaller than {{MAX_AVATAR_SIZE}} MB", "userProfile.form.username": "Username", @@ -149,6 +153,11 @@ "userProfile.form.repeatNewPassword": "Repeat new password", "userProfile.form.language": "Language", "userProfile.header.yourSessions": "Your sessions", + "userProfile.header.yourApiKeys": "Your API keys", + "userProfile.button.createApiKey": "Create new API key", + "userProfile.button.createApiKey.popconfirm.title": "Name for the new API key", + "userProfile.button.createApiKey.popconfirm.okText": "Create", + "userProfile.button.copyToClipboard.notification": "API token copied to clipboard", "scanners.column.name": "Name", "scanners.column.usedBy": "Used by", "scanners.column.lastUsed": "Last used", diff --git a/src/Pages/UserProfile/index.js b/src/Pages/UserProfile/index.js index fa2ffb6..251c49f 100644 --- a/src/Pages/UserProfile/index.js +++ b/src/Pages/UserProfile/index.js @@ -4,6 +4,7 @@ import { Col, Form, Input, + Popconfirm, Row, Select, Space, @@ -26,6 +27,12 @@ import { } from "../../utils"; import { Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; +import { + CopyOutlined, + EyeInvisibleOutlined, + EyeOutlined, + KeyOutlined, +} from "@ant-design/icons"; export default function UserProfile() { const webSocketContext = useContext(WebSocketContext); @@ -36,8 +43,10 @@ export default function UserProfile() { const [oldPassword, setOldPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [repeatedNewPassword, setRepeatedNewPassword] = useState(""); + const [newApiKeyName, setNewApikeyName] = useState(""); + const [showApiKeyPassword, setShowApiKeyPassword] = useState(false); - const getTableColumns = () => { + const getSessionTableColumns = () => { return [ { title: t("userProfile.column.userAgent"), @@ -91,7 +100,81 @@ export default function UserProfile() { ]; }; - const getTableItems = () => { + const getApiKeyTableColumns = () => { + return [ + { + title: t("userProfile.column.name"), + dataIndex: "name", + key: "name", + }, + { + title: t("userProfile.column.token"), + dataIndex: "token", + key: "token", + render: (text) => ( + + {showApiKeyPassword ? text : "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"} + {showApiKeyPassword ? ( + setShowApiKeyPassword(false)} + /> + ) : ( + setShowApiKeyPassword(true)} /> + )} + {showApiKeyPassword && ( + { + navigator.clipboard.writeText(text); + setShowApiKeyPassword(false); + + notificationApi["info"]({ + message: t( + "userProfile.button.copyToClipboard.notification" + ), + }); + }} + /> + )} + + ), + }, + { + 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", + }, + ]; + }; + + const getApiKeyTableItems = () => { + let items = []; + + webSocketContext.User.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 = []; webSocketContext.User.Sessions.sort( @@ -178,6 +261,15 @@ export default function UserProfile() { }); }; + const onCreateNewApiKeyConfirm = () => { + webSocketContext.SendSocketMessage( + SentMessagesCommands.CreateNewUserApiKey, + { Name: newApiKeyName } + ); + + setNewApikeyName(""); + }; + return ( <> {notificationContextHolder} @@ -325,7 +417,46 @@ export default function UserProfile() { {webSocketContext.User.Sessions.length}) - +
+ +
+

+ {t("userProfile.header.yourApiKeys")} ( + {webSocketContext.User.ApiKeys.length}) +

+ + setNewApikeyName(e.target.value)} + /> + } + okText={t("userProfile.button.createApiKey.popconfirm.okText")} + cancelText={t("buttonCancel")} + onConfirm={() => onCreateNewApiKeyConfirm()} + > + + +
+ +
); } diff --git a/src/utils.js b/src/utils.js index bdb8147..e74b6e2 100644 --- a/src/utils.js +++ b/src/utils.js @@ -144,6 +144,7 @@ let webSocketContextPreview = { Sessions: [], Permissions: [], RoleId: "", + ApiKeys: [], }, AllUsers: [], AllRoles: [], @@ -191,6 +192,7 @@ const ReceivedMessagesCommands = { AllUsersUserDeleted: 27, AllUsersUserDeactivation: 28, GroupTasksCategoryGroupChanges: 29, + NewUserApiKeyCreated: 30, }; // commands sent to the backend server @@ -213,6 +215,7 @@ export const SentMessagesCommands = { ScannersDisconnectScanner: 16, GroupTasksCheckingForCategoryGroupChanges: 17, HandleUserActionTaskStep: 18, + CreateNewUserApiKey: 19, }; export function WebSocketProvider({ @@ -951,7 +954,15 @@ export function WebSocketProvider({ return newArr; }); } + break; + case ReceivedMessagesCommands.NewUserApiKeyCreated: + setUser((user) => { + const updatedUser = { ...user }; + updatedUser.ApiKeys.push(body); + + return updatedUser; + }); break; default: