diff --git a/scm-ui/ui-webapp/public/locales/de/users.json b/scm-ui/ui-webapp/public/locales/de/users.json index 825876ec47..101ea3371d 100644 --- a/scm-ui/ui-webapp/public/locales/de/users.json +++ b/scm-ui/ui-webapp/public/locales/de/users.json @@ -38,7 +38,8 @@ "generalNavLink": "Generell", "setPasswordNavLink": "Passwort", "setPermissionsNavLink": "Berechtigungen", - "setPublicKeyNavLink": "Öffentliche Schlüssel" + "setPublicKeyNavLink": "Öffentliche Schlüssel", + "setApiKeyNavLink": "API Schlüssel" } }, "createUser": { @@ -70,5 +71,17 @@ "addKey": "Schlüssel hinzufügen", "delete": "Löschen", "download": "Herunterladen" + }, + "apiKey": { + "noStoredKeys": "Es wurden keine Schlüssel gefunden.", + "displayName": "Anzeigename", + "permissionRole": { + "label": "Berechtigte Rolle", + "help": "Mit der Rolle können Sie die Berechtigung für diesen Schlüssel einschränken" + }, + "created": "Eingetragen an", + "addKey": "Schlüssel hinzufügen", + "delete": "Löschen", + "download": "Herunterladen" } } diff --git a/scm-ui/ui-webapp/public/locales/en/users.json b/scm-ui/ui-webapp/public/locales/en/users.json index d50618bcf0..f3756ab46f 100644 --- a/scm-ui/ui-webapp/public/locales/en/users.json +++ b/scm-ui/ui-webapp/public/locales/en/users.json @@ -38,7 +38,8 @@ "generalNavLink": "General", "setPasswordNavLink": "Password", "setPermissionsNavLink": "Permissions", - "setPublicKeyNavLink": "Public Keys" + "setPublicKeyNavLink": "Public Keys", + "setApiKeyNavLink": "API keys" } }, "createUser": { @@ -70,5 +71,17 @@ "addKey": "Add key", "delete": "Delete", "download": "Download" + }, + "apiKey": { + "noStoredKeys": "No keys found.", + "displayName": "Display Name", + "permissionRole": { + "label": "Permitted Role", + "help": "The api key will be restricted to permissions of this role" + }, + "created": "Created on", + "addKey": "Add key", + "delete": "Delete", + "download": "Download" } } diff --git a/scm-ui/ui-webapp/src/containers/Profile.tsx b/scm-ui/ui-webapp/src/containers/Profile.tsx index d1d8a065f3..0828f71ec5 100644 --- a/scm-ui/ui-webapp/src/containers/Profile.tsx +++ b/scm-ui/ui-webapp/src/containers/Profile.tsx @@ -44,6 +44,8 @@ import ProfileInfo from "./ProfileInfo"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; import SetPublicKeys from "../users/components/publicKeys/SetPublicKeys"; import SetPublicKeyNavLink from "../users/components/navLinks/SetPublicKeysNavLink"; +import SetApiKeys from "../users/components/apiKeys/SetApiKeys"; +import SetApiKeyNavLink from "../users/components/navLinks/SetApiKeysNavLink"; import { urls } from "@scm-manager/ui-components"; type Props = RouteComponentProps & @@ -65,6 +67,11 @@ class Profile extends React.Component { return !!me?._links?.publicKeys; }; + canManageApiKeys = () => { + const { me } = this.props; + return !!me?._links?.apiKeys; + }; + render() { const url = urls.matchedUrl(this.props); @@ -100,6 +107,9 @@ class Profile extends React.Component { {this.canManagePublicKeys() && ( } /> )} + {this.canManageApiKeys() && ( + } /> + )} @@ -118,6 +128,7 @@ class Profile extends React.Component { > + )} diff --git a/scm-ui/ui-webapp/src/repos/permissions/components/RoleSelector.tsx b/scm-ui/ui-webapp/src/repos/permissions/components/RoleSelector.tsx index 536c824914..941ddc88ee 100644 --- a/scm-ui/ui-webapp/src/repos/permissions/components/RoleSelector.tsx +++ b/scm-ui/ui-webapp/src/repos/permissions/components/RoleSelector.tsx @@ -26,7 +26,7 @@ import { WithTranslation, withTranslation } from "react-i18next"; import { Select } from "@scm-manager/ui-components"; type Props = WithTranslation & { - availableRoles: string[]; + availableRoles?: string[]; handleRoleChange: (p: string) => void; role: string; label?: string; diff --git a/scm-ui/ui-webapp/src/users/components/apiKeys/AddApiKey.tsx b/scm-ui/ui-webapp/src/users/components/apiKeys/AddApiKey.tsx new file mode 100644 index 0000000000..8b69a8daf6 --- /dev/null +++ b/scm-ui/ui-webapp/src/users/components/apiKeys/AddApiKey.tsx @@ -0,0 +1,134 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React, { FC, useEffect, useState } from "react"; +import { apiClient, ErrorNotification, InputField, Level, Loading, SubmitButton } from "@scm-manager/ui-components"; +import { useTranslation } from "react-i18next"; +import { CONTENT_TYPE_API_KEY } from "./SetApiKeys"; +import { connect } from "react-redux"; +import { + fetchAvailablePermissionsIfNeeded, + getAvailableRepositoryRoles +} from "../../../repos/permissions/modules/permissions"; +import { RepositoryRole } from "@scm-manager/ui-types"; +import { getRepositoryRolesLink, getRepositoryVerbsLink } from "../../../modules/indexResource"; +import RoleSelector from "../../../repos/permissions/components/RoleSelector"; + +type Props = { + createLink: string; + refresh: () => void; + repositoryRolesLink: string; + repositoryVerbsLink: string; + fetchAvailablePermissionsIfNeeded: (repositoryRolesLink: string, repositoryVerbsLink: string) => void; + availableRepositoryRoles?: RepositoryRole[]; +}; + +const AddApiKey: FC = ({ + createLink, + refresh, + fetchAvailablePermissionsIfNeeded, + repositoryRolesLink, + repositoryVerbsLink, + availableRepositoryRoles +}) => { + const [t] = useTranslation("users"); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(); + const [displayName, setDisplayName] = useState(""); + const [permissionRole, setPermissionRole] = useState(""); + + useEffect(() => { + if (!availableRepositoryRoles) { + fetchAvailablePermissionsIfNeeded(repositoryRolesLink, repositoryVerbsLink); + } + }); + + const isValid = () => { + return !!displayName && !!permissionRole; + }; + + const resetForm = () => { + setDisplayName(""); + setPermissionRole(""); + }; + + const addKey = () => { + setLoading(true); + apiClient + .post(createLink, { displayName: displayName, permissionRole: permissionRole }, CONTENT_TYPE_API_KEY) + .then(resetForm) + .then(refresh) + .then(() => setLoading(false)) + .catch(setError); + }; + + if (error) { + return ; + } + + if (loading) { + return ; + } + + const availableRoleNames = availableRepositoryRoles? availableRepositoryRoles.map(r => r.name): []; + + return ( + <> + + + } + /> + + ); +}; + +const mapStateToProps = (state: any, ownProps: Props) => { + const availableRepositoryRoles = getAvailableRepositoryRoles(state); + const repositoryRolesLink = getRepositoryRolesLink(state); + const repositoryVerbsLink = getRepositoryVerbsLink(state); + + return { + availableRepositoryRoles, + repositoryRolesLink, + repositoryVerbsLink + }; +}; + +const mapDispatchToProps = (dispatch: any) => { + return { + fetchAvailablePermissionsIfNeeded: (repositoryRolesLink: string, repositoryVerbsLink: string) => { + dispatch(fetchAvailablePermissionsIfNeeded(repositoryRolesLink, repositoryVerbsLink)); + } + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(AddApiKey); diff --git a/scm-ui/ui-webapp/src/users/components/apiKeys/ApiKeyEntry.tsx b/scm-ui/ui-webapp/src/users/components/apiKeys/ApiKeyEntry.tsx new file mode 100644 index 0000000000..ca2bd7d949 --- /dev/null +++ b/scm-ui/ui-webapp/src/users/components/apiKeys/ApiKeyEntry.tsx @@ -0,0 +1,60 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React, { FC } from "react"; +import { DateFromNow, DeleteButton } from "@scm-manager/ui-components"; +import { ApiKey } from "./SetApiKeys"; +import { useTranslation } from "react-i18next"; +import { Link } from "@scm-manager/ui-types"; + +type Props = { + apiKey: ApiKey; + onDelete: (link: string) => void; +}; + +export const ApiKeyEntry: FC = ({ apiKey, onDelete }) => { + const [t] = useTranslation("users"); + + let deleteButton; + if (apiKey?._links?.delete) { + deleteButton = ( + onDelete((apiKey._links.delete as Link).href)} /> + ); + } + + return ( + <> + + {apiKey.displayName} + {apiKey.permissionRole} + + + + {deleteButton} + + + ); +}; + +export default ApiKeyEntry; diff --git a/scm-ui/ui-webapp/src/users/components/apiKeys/ApiKeyTable.tsx b/scm-ui/ui-webapp/src/users/components/apiKeys/ApiKeyTable.tsx new file mode 100644 index 0000000000..51e8b7033c --- /dev/null +++ b/scm-ui/ui-webapp/src/users/components/apiKeys/ApiKeyTable.tsx @@ -0,0 +1,62 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React, { FC } from "react"; +import { useTranslation } from "react-i18next"; +import { ApiKey, ApiKeysCollection } from "./SetApiKeys"; +import ApiKeyEntry from "./ApiKeyEntry"; +import { Notification } from "@scm-manager/ui-components"; + +type Props = { + apiKeys?: ApiKeysCollection; + onDelete: (link: string) => void; +}; + +const ApiKeyTable: FC = ({ apiKeys, onDelete }) => { + const [t] = useTranslation("users"); + + if (apiKeys?._embedded?.keys?.length === 0) { + return {t("apiKey.noStoredKeys")}; + } + + return ( + + + + + + + + + + {apiKeys?._embedded?.keys?.map((apiKey: ApiKey, index: number) => { + return ; + })} + +
{t("apiKey.displayName")}{t("apiKey.permissionRole")}{t("apiKey.created")} +
+ ); +}; + +export default ApiKeyTable; diff --git a/scm-ui/ui-webapp/src/users/components/apiKeys/SetApiKeys.tsx b/scm-ui/ui-webapp/src/users/components/apiKeys/SetApiKeys.tsx new file mode 100644 index 0000000000..b7c8d8aace --- /dev/null +++ b/scm-ui/ui-webapp/src/users/components/apiKeys/SetApiKeys.tsx @@ -0,0 +1,95 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { Collection, Link, Links, User, Me } from "@scm-manager/ui-types"; +import React, { FC, useEffect, useState } from "react"; +import { apiClient, ErrorNotification, Loading } from "@scm-manager/ui-components"; +import ApiKeyTable from "./ApiKeyTable"; +import AddApiKey from "./AddApiKey"; + +export type ApiKeysCollection = Collection & { + _embedded: { + keys: ApiKey[]; + }; +}; + +export type ApiKey = { + id: string; + displayName: string; + permissionRole: string; + created: string; + _links: Links; +}; + +export const CONTENT_TYPE_API_KEY = "application/vnd.scmm-apiKey+json;v=2"; + +type Props = { + user: User | Me; +}; + +const SetApiKeys: FC = ({ user }) => { + const [error, setError] = useState(); + const [loading, setLoading] = useState(false); + const [apiKeys, setApiKeys] = useState(undefined); + + useEffect(() => { + fetchApiKeys(); + }, [user]); + + const fetchApiKeys = () => { + setLoading(true); + apiClient + .get((user._links.apiKeys as Link).href) + .then(r => r.json()) + .then(setApiKeys) + .then(() => setLoading(false)) + .catch(setError); + }; + + const onDelete = (link: string) => { + apiClient + .delete(link) + .then(fetchApiKeys) + .catch(setError); + }; + + const createLink = (apiKeys?._links?.create as Link)?.href; + + if (error) { + return ; + } + + if (loading) { + return ; + } + + return ( + <> + + {createLink && } + + ); +}; + +export default SetApiKeys; diff --git a/scm-ui/ui-webapp/src/users/components/navLinks/SetApiKeysNavLink.tsx b/scm-ui/ui-webapp/src/users/components/navLinks/SetApiKeysNavLink.tsx new file mode 100644 index 0000000000..189d9f287a --- /dev/null +++ b/scm-ui/ui-webapp/src/users/components/navLinks/SetApiKeysNavLink.tsx @@ -0,0 +1,43 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import React, { FC } from "react"; +import { Link, User, Me } from "@scm-manager/ui-types"; +import { NavLink } from "@scm-manager/ui-components"; +import { useTranslation } from "react-i18next"; + +type Props = { + user: User | Me; + apiKeyUrl: string; +}; + +const SetApiKeyNavLink: FC = ({ user, apiKeyUrl }) => { + const [t] = useTranslation("users"); + + if ((user?._links?.apiKeys as Link)?.href) { + return ; + } + return null; +}; + +export default SetApiKeyNavLink; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ApiKeyCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ApiKeyCollectionToDtoMapper.java index 0ff3df4f0f..29c2a83177 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ApiKeyCollectionToDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ApiKeyCollectionToDtoMapper.java @@ -52,6 +52,6 @@ public class ApiKeyCollectionToDtoMapper { final Links.Builder links = Links.linkingTo() .self(resourceLinks.apiKeyCollection().self()) .single(link("create", resourceLinks.apiKeyCollection().create())); - return new HalRepresentation(links.build(), Embedded.embedded("apiKeys", dtos)); + return new HalRepresentation(links.build(), Embedded.embedded("keys", dtos)); } }