Add UI for api keys

This commit is contained in:
René Pfeuffer
2020-10-01 13:20:00 +02:00
parent 20345c895f
commit ec57dc0731
10 changed files with 435 additions and 4 deletions

View File

@@ -38,7 +38,8 @@
"generalNavLink": "Generell", "generalNavLink": "Generell",
"setPasswordNavLink": "Passwort", "setPasswordNavLink": "Passwort",
"setPermissionsNavLink": "Berechtigungen", "setPermissionsNavLink": "Berechtigungen",
"setPublicKeyNavLink": "Öffentliche Schlüssel" "setPublicKeyNavLink": "Öffentliche Schlüssel",
"setApiKeyNavLink": "API Schlüssel"
} }
}, },
"createUser": { "createUser": {
@@ -70,5 +71,17 @@
"addKey": "Schlüssel hinzufügen", "addKey": "Schlüssel hinzufügen",
"delete": "Löschen", "delete": "Löschen",
"download": "Herunterladen" "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"
} }
} }

View File

@@ -38,7 +38,8 @@
"generalNavLink": "General", "generalNavLink": "General",
"setPasswordNavLink": "Password", "setPasswordNavLink": "Password",
"setPermissionsNavLink": "Permissions", "setPermissionsNavLink": "Permissions",
"setPublicKeyNavLink": "Public Keys" "setPublicKeyNavLink": "Public Keys",
"setApiKeyNavLink": "API keys"
} }
}, },
"createUser": { "createUser": {
@@ -70,5 +71,17 @@
"addKey": "Add key", "addKey": "Add key",
"delete": "Delete", "delete": "Delete",
"download": "Download" "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"
} }
} }

View File

@@ -44,6 +44,8 @@ import ProfileInfo from "./ProfileInfo";
import { ExtensionPoint } from "@scm-manager/ui-extensions"; import { ExtensionPoint } from "@scm-manager/ui-extensions";
import SetPublicKeys from "../users/components/publicKeys/SetPublicKeys"; import SetPublicKeys from "../users/components/publicKeys/SetPublicKeys";
import SetPublicKeyNavLink from "../users/components/navLinks/SetPublicKeysNavLink"; 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"; import { urls } from "@scm-manager/ui-components";
type Props = RouteComponentProps & type Props = RouteComponentProps &
@@ -65,6 +67,11 @@ class Profile extends React.Component<Props> {
return !!me?._links?.publicKeys; return !!me?._links?.publicKeys;
}; };
canManageApiKeys = () => {
const { me } = this.props;
return !!me?._links?.apiKeys;
};
render() { render() {
const url = urls.matchedUrl(this.props); const url = urls.matchedUrl(this.props);
@@ -100,6 +107,9 @@ class Profile extends React.Component<Props> {
{this.canManagePublicKeys() && ( {this.canManagePublicKeys() && (
<Route path={`${url}/settings/publicKeys`} render={() => <SetPublicKeys user={me} />} /> <Route path={`${url}/settings/publicKeys`} render={() => <SetPublicKeys user={me} />} />
)} )}
{this.canManageApiKeys() && (
<Route path={`${url}/settings/apiKeys`} render={() => <SetApiKeys user={me} />} />
)}
<ExtensionPoint name="profile.route" props={extensionProps} renderAll={true} /> <ExtensionPoint name="profile.route" props={extensionProps} renderAll={true} />
</PrimaryContentColumn> </PrimaryContentColumn>
<SecondaryNavigationColumn> <SecondaryNavigationColumn>
@@ -118,6 +128,7 @@ class Profile extends React.Component<Props> {
> >
<NavLink to={`${url}/settings/password`} label={t("profile.changePasswordNavLink")} /> <NavLink to={`${url}/settings/password`} label={t("profile.changePasswordNavLink")} />
<SetPublicKeyNavLink user={me} publicKeyUrl={`${url}/settings/publicKeys`} /> <SetPublicKeyNavLink user={me} publicKeyUrl={`${url}/settings/publicKeys`} />
<SetApiKeyNavLink user={me} apiKeyUrl={`${url}/settings/apiKeys`} />
<ExtensionPoint name="profile.setting" props={extensionProps} renderAll={true} /> <ExtensionPoint name="profile.setting" props={extensionProps} renderAll={true} />
</SubNavigation> </SubNavigation>
)} )}

View File

@@ -26,7 +26,7 @@ import { WithTranslation, withTranslation } from "react-i18next";
import { Select } from "@scm-manager/ui-components"; import { Select } from "@scm-manager/ui-components";
type Props = WithTranslation & { type Props = WithTranslation & {
availableRoles: string[]; availableRoles?: string[];
handleRoleChange: (p: string) => void; handleRoleChange: (p: string) => void;
role: string; role: string;
label?: string; label?: string;

View File

@@ -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<Props> = ({
createLink,
refresh,
fetchAvailablePermissionsIfNeeded,
repositoryRolesLink,
repositoryVerbsLink,
availableRepositoryRoles
}) => {
const [t] = useTranslation("users");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<undefined | Error>();
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 <ErrorNotification error={error} />;
}
if (loading) {
return <Loading />;
}
const availableRoleNames = availableRepositoryRoles? availableRepositoryRoles.map(r => r.name): [];
return (
<>
<InputField label={t("apiKey.displayName")} value={displayName} onChange={setDisplayName} />
<RoleSelector
loading={!availableRoleNames}
availableRoles={availableRoleNames}
label={t("apiKey.permissionRole.label")}
helpText={t("apiKey.permissionRole.help")}
handleRoleChange={setPermissionRole}
role={permissionRole}
/>
<Level
right={<SubmitButton label={t("apiKey.addKey")} loading={loading} disabled={!isValid()} action={addKey} />}
/>
</>
);
};
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);

View File

@@ -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<Props> = ({ apiKey, onDelete }) => {
const [t] = useTranslation("users");
let deleteButton;
if (apiKey?._links?.delete) {
deleteButton = (
<DeleteButton label={t("apiKey.delete")} action={() => onDelete((apiKey._links.delete as Link).href)} />
);
}
return (
<>
<tr>
<td>{apiKey.displayName}</td>
<td>{apiKey.permissionRole}</td>
<td className="is-hidden-mobile">
<DateFromNow date={apiKey.created}/>
</td>
<td>{deleteButton}</td>
</tr>
</>
);
};
export default ApiKeyEntry;

View File

@@ -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<Props> = ({ apiKeys, onDelete }) => {
const [t] = useTranslation("users");
if (apiKeys?._embedded?.keys?.length === 0) {
return <Notification type="info">{t("apiKey.noStoredKeys")}</Notification>;
}
return (
<table className="card-table table is-hoverable is-fullwidth">
<thead>
<tr>
<th>{t("apiKey.displayName")}</th>
<th>{t("apiKey.permissionRole")}</th>
<th>{t("apiKey.created")}</th>
<th />
</tr>
</thead>
<tbody>
{apiKeys?._embedded?.keys?.map((apiKey: ApiKey, index: number) => {
return <ApiKeyEntry key={index} onDelete={onDelete} apiKey={apiKey} />;
})}
</tbody>
</table>
);
};
export default ApiKeyTable;

View File

@@ -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<Props> = ({ user }) => {
const [error, setError] = useState<undefined | Error>();
const [loading, setLoading] = useState(false);
const [apiKeys, setApiKeys] = useState<ApiKeysCollection | undefined>(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 <ErrorNotification error={error} />;
}
if (loading) {
return <Loading />;
}
return (
<>
<ApiKeyTable apiKeys={apiKeys} onDelete={onDelete} />
{createLink && <AddApiKey createLink={createLink} refresh={fetchApiKeys} />}
</>
);
};
export default SetApiKeys;

View File

@@ -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<Props> = ({ user, apiKeyUrl }) => {
const [t] = useTranslation("users");
if ((user?._links?.apiKeys as Link)?.href) {
return <NavLink to={apiKeyUrl} label={t("singleUser.menu.setApiKeyNavLink")} />;
}
return null;
};
export default SetApiKeyNavLink;

View File

@@ -52,6 +52,6 @@ public class ApiKeyCollectionToDtoMapper {
final Links.Builder links = Links.linkingTo() final Links.Builder links = Links.linkingTo()
.self(resourceLinks.apiKeyCollection().self()) .self(resourceLinks.apiKeyCollection().self())
.single(link("create", resourceLinks.apiKeyCollection().create())); .single(link("create", resourceLinks.apiKeyCollection().create()));
return new HalRepresentation(links.build(), Embedded.embedded("apiKeys", dtos)); return new HalRepresentation(links.build(), Embedded.embedded("keys", dtos));
} }
} }