Skip to content

Commit

Permalink
WIP for API tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
oharsta committed Nov 16, 2023
1 parent 2147528 commit 509e564
Show file tree
Hide file tree
Showing 32 changed files with 787 additions and 55 deletions.
2 changes: 1 addition & 1 deletion client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>org.openconext</groupId>
<artifactId>access</artifactId>
<version>0.0.3</version>
<version>0.0.4-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
17 changes: 17 additions & 0 deletions client/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,23 @@ export function updateUserRoleEndData(userRoleId, endDate) {
export function deleteUserRole(userRoleId) {
return fetchDelete(`/api/v1/user_roles/${userRoleId}`);
}
//API tokens
export function apiTokens() {
return fetchJson("/api/v1/tokens");
}

export function generateToken() {
return fetchJson("/api/v1/tokens/generate-token");
}

export function createToken(description) {
return postPutJson("/api/v1/tokens", {description: description}, "POST");
}

export function deleteToken(token) {
return fetchDelete(`/api/v1/tokens/${token.id}`);
}


//Validations
export function validate(type, value) {
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/Page.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React from "react";
import "./Page.scss";

export const Page = ({children}) => {
export const Page = ({children, className="page"}) => {

return (
<div className="page">
<div className={className}>
{children}
</div>
);
Expand Down
25 changes: 24 additions & 1 deletion client/src/locale/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ const en = {
userRoles: "Role managers & inviters",
guestRoles: "Guests with this role",
cron: "Cron",
invite: "Invite"
invite: "Invite",
tokens: "API Tokens"
},
home: {
access: "SURFconext Invite",
Expand Down Expand Up @@ -294,6 +295,28 @@ const en = {
welcome: "Welcome Institution administrator of {{name}}! You can start with creating your first role and subsequently invite managers.",
create: "Create access role"
},
tokens: {
title: "API tokens",
new: "Add API token",
searchPlaceHolder: "Search for API tokens...",
noEntities: "No API tokens",
backToApiKeys: "Back to all API tokens",
titleNew: "Create an API token for {{institutions}}",
backToOverview: "Back to all API tokens",
createdAt: "Created at",
secretDisclaimer: "You can view this API token only once. Copy it and store it somewhere safe.<br><br>If the token is lost, delete it and create a new one.",
secret: "API token",
secretValue: "One-way hashed secret",
secretTooltip: "The value to use in the X-API-TOKEN header",
description: "Description",
descriptionPlaceHolder: "Description for this API token",
descriptionTooltip: "A description explaining the use of this API token",
deleteFlash: "API Token has been deleted",
deleteConfirmation: "Are you sure you want to delete this API token?",
createFlash: "API Token has been created",
submit: "Submit",
required: "The description is required for an API token",
},
tooltips: {
userIcon: "User {{name}} provisioned at {{createdAt}} with last activity on {{lastActivity}}",
impersonateIcon: "Impersonate user {{name}}",
Expand Down
25 changes: 24 additions & 1 deletion client/src/locale/nl.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ const nl = {
userRoles: "Rol managers en uitnodigers",
guestRoles: "Gasten",
cron: "Cron",
invite: "Uitnodiging"
invite: "Uitnodiging",
tokens: "API Tokens"
},
home: {
access: "SURFconext Invite",
Expand Down Expand Up @@ -294,6 +295,28 @@ const nl = {
welcome: "Welkom Instellings admin van {{name}}! Je kan nu je eerste rol aanmaken en managers daarvoor uitnodigen.",
create: "Create access role"
},
tokens: {
title: "API tokens",
new: "Add API token",
searchPlaceHolder: "Search for API tokens...",
noEntities: "No API tokens",
backToApiKeys: "Back to all API tokens",
titleNew: "Create an API token for {{institutions}}",
backToOverview: "Back to all API tokens",
createdAt: "Created at",
secretDisclaimer: "You can view this API token only once. Copy it and store it somewhere safe.<br><br>If the token is lost, delete it and create a new one.",
secret: "API token",
secretValue: "One-way hashed secret",
secretTooltip: "The value to use in the X-API-TOKEN header",
description: "Description",
descriptionPlaceHolder: "Description for this API token",
descriptionTooltip: "A description explaining the use of this API token",
deleteFlash: "API Token has been deleted",
deleteConfirmation: "Are you sure you want to delete this API token?",
createFlash: "API Token has been created",
submit: "Submit",
required: "The description is required for an API token",
},
tooltips: {
userIcon: "Gebruiker {{name}} geprovisioned op {{createdAt}} laatst actief op {{lastActivity}}",
impersonateIcon: "Doe gebruiker {{name}} na",
Expand Down
11 changes: 11 additions & 0 deletions client/src/pages/Home.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {ReactComponent as Logo} from "../icons/Owl_Emblem.svg";
import {ReactComponent as RoleLogo} from "@surfnet/sds/icons/illustrative-icons/hierarchy-2.svg";
import {ReactComponent as ApplicationLogo} from "@surfnet/sds/icons/illustrative-icons/database-refresh.svg";
import {ReactComponent as UserLogo} from "@surfnet/sds/icons/functional-icons/id-2.svg";
import {ReactComponent as TokenLogo} from "@surfnet/sds/icons/illustrative-icons/database-hand.svg";
import Tabs from "../components/Tabs";
import "./Home.scss";
import {UnitHeader} from "../components/UnitHeader";
Expand All @@ -15,6 +16,7 @@ import {Roles} from "../tabs/Roles";
import Applications from "../tabs/Applications";
import {AUTHORITIES, highestAuthority} from "../utils/UserRole";
import {Loader} from "@surfnet/sds";
import {Tokens} from "../tabs/Tokens";

export const Home = () => {
const {tab = "roles"} = useParams();
Expand Down Expand Up @@ -64,6 +66,15 @@ export const Home = () => {
</Page>
);
}
if (user && user.institutionAdmin && user.organizationGUID) {
newTabs.push(
<Page key="tokens"
name="tokens"
label={I18n.t("tabs.tokens")}
Icon={TokenLogo}>
<Tokens/>
</Page>);
}
setTabs(newTabs);
setLoading(false);
}, [currentTab, user]);// eslint-disable-line react-hooks/exhaustive-deps
Expand Down
10 changes: 10 additions & 0 deletions client/src/styles/forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,13 @@
button.sds--btn span {
text-align: center;
}

section.actions {
border-top: 2px solid #e0e0df;
display: flex;
gap: 25px;
justify-content: flex-end;
margin-top: 30px;
padding: 30px 0;
position: relative;
}
190 changes: 190 additions & 0 deletions client/src/tabs/Tokens.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import "./Tokens.scss";
import {useAppStore} from "../stores/AppStore";
import React, {useCallback, useEffect, useState} from "react";
import {Entities} from "../components/Entities";
import I18n from "../locale/I18n";
import {Button, ButtonType, Loader} from "@surfnet/sds";
import {useNavigate} from "react-router-dom";
import {apiTokens, createToken, deleteToken, generateToken} from "../api";
import {dateFromEpoch} from "../utils/Date";
import {ReactComponent as TrashIcon} from "@surfnet/sds/icons/functional-icons/bin.svg";
import {ReactComponent as ChevronLeft} from "@surfnet/sds/icons/functional-icons/arrow-left-2.svg";
import ConfirmationDialog from "../components/ConfirmationDialog";
import DOMPurify from "dompurify";
import InputField from "../components/InputField";
import ErrorIndicator from "../components/ErrorIndicator";
import {isEmpty, stopEvent} from "../utils/Utils";
import {Page} from "../components/Page";

export const Tokens = () => {
const {user, setFlash} = useAppStore(state => state);
const navigate = useNavigate();
const [tokens, setTokens] = useState(true);
const [tokenValue, setTokenValue] = useState(null);
const [description, setDescription] = useState("");
const [newToken, setNewToken] = useState(false);
const [loading, setLoading] = useState(true);
const [initial, setInitial] = useState(true);
const [confirmation, setConfirmation] = useState({});
const [confirmationOpen, setConfirmationOpen] = useState(false);

const fetchTokens = useCallback(() => {
apiTokens()
.then(res => {
setTokens(res);
setLoading(false);
setNewToken(false);
setDescription("");
setTokenValue(null);
setConfirmationOpen(false);
});
}, []);

useEffect(() => {
if (user.institutionAdmin) {
fetchTokens();
} else {
navigate("/404");
}
}, [user])// eslint-disable-line react-hooks/exhaustive-deps

const removeAPIToken = (token, showConfirmation) => {
if (showConfirmation) {
setConfirmation({
cancel: () => setConfirmationOpen(false),
action: () => removeAPIToken(token, false),
warning: true,
question: I18n.t("tokens.deleteConfirmation"),
});
setConfirmationOpen(true);
} else {
setLoading(true);
deleteToken(token).then(() => fetchTokens())
}
};

const submitNewToken = () => {
if (isEmpty(description)) {
setInitial(false);
} else {
setLoading(true);
createToken(description).then(() => {
setFlash(I18n.t("tokens.createFlash"));
fetchTokens();
});
}

}

const cancelSideScreen = e => {
stopEvent(e);
setNewToken(false);
}

const createNewToken = () => {
setLoading(true);
generateToken().then(res => {
setNewToken(true);
setTokenValue(res.token);
setLoading(false);
});
}

const renderNewToken = () => {
return (
<Page className={"page new-token"}>
<div>
<a href={"/cancel"}
className={"back-to-tokens"}
onClick={cancelSideScreen}>
<ChevronLeft/>{I18n.t("tokens.backToOverview")}
</a>
</div>
<div className="new-token">
<p dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(I18n.t("tokens.secretDisclaimer"))}}/>
<InputField value={tokenValue}
name={I18n.t("tokens.secret")}
toolTip={I18n.t("tokens.secretTooltip")}
disabled={true}
copyClipBoard={true}/>

<InputField value={description}
onChange={e => setDescription(e.target.value)}
placeholder={I18n.t("tokens.descriptionPlaceHolder")}
name={I18n.t("tokens.description")}
error={(!initial && isEmpty(description))}
toolTip={I18n.t("tokens.descriptionTooltip")}
/>
{(!initial && isEmpty(description)) && <ErrorIndicator
msg={I18n.t("tokens.required")}/>}

<section className="actions">
<Button type={ButtonType.Secondary}
txt={I18n.t("forms.cancel")}
onClick={() => setNewToken(false)}/>
<Button txt={I18n.t("forms.save")}
onClick={() => submitNewToken()}/>
</section>
</div>
</Page>
);

}

const columns = [
{
key: "secret",
header: I18n.t("tokens.secret"),
mapper: () => I18n.t("tokens.secretValue"),
},
{
key: "description",
header: I18n.t("tokens.description"),
mapper: token => <span className={"cut-of-lines"}>{token.description}</span>
},
{
key: "created_at",
header: I18n.t("tokens.createdAt"),
mapper: token => dateFromEpoch(token.createdAt)
},
{
nonSortable: true,
key: "trash",
header: "",
mapper: token =>
<span onClick={() => removeAPIToken(token, true)}>
<TrashIcon/>
</span>
},
]

if (loading) {
return <Loader/>
}

return (
<div className={"mod-tokens"}>
{newToken && renderNewToken()}
{confirmationOpen && <ConfirmationDialog isOpen={confirmationOpen}
cancel={confirmation.cancel}
confirm={confirmation.action}
isWarning={confirmation.warning}
question={confirmation.question}/>}
{!newToken && <Entities
entities={tokens}
modelName="tokens"
showNew={true}
newLabel={I18n.t("tokens.new")}
newEntityFunc={() => createNewToken()}
defaultSort="description"
columns={columns}
searchAttributes={["description"]}
customNoEntities={I18n.t(`tokens.noEntities`)}
loading={false}
inputFocus={true}
hideTitle={false}
/>}
</div>
);

}
Loading

0 comments on commit 509e564

Please sign in to comment.