526 lines
21 KiB
TypeScript
526 lines
21 KiB
TypeScript
/*
|
|
* Copyright 2018 Red Hat, Inc. and/or its affiliates.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import * as React from "react";
|
|
|
|
import { withRouter, RouteComponentProps } from "react-router-dom";
|
|
import {
|
|
Alert,
|
|
Button,
|
|
DataList,
|
|
DataListAction,
|
|
DataListItemCells,
|
|
DataListCell,
|
|
DataListItem,
|
|
DataListItemRow,
|
|
EmptyState,
|
|
EmptyStateVariant,
|
|
EmptyStateBody,
|
|
Split,
|
|
SplitItem,
|
|
Title,
|
|
Dropdown,
|
|
DropdownPosition,
|
|
KebabToggle,
|
|
PageSection,
|
|
PageSectionVariants
|
|
} from "@patternfly/react-core";
|
|
|
|
import { AIACommand } from "../../util/AIACommand";
|
|
import TimeUtil from "../../util/TimeUtil";
|
|
import {
|
|
HttpResponse,
|
|
AccountServiceClient,
|
|
} from "../../account-service/account.service";
|
|
import { AccountServiceContext } from "../../account-service/AccountServiceContext";
|
|
import { ContinueCancelModal } from "../../widgets/ContinueCancelModal";
|
|
import { Features } from "../../widgets/features";
|
|
import { Msg } from "../../widgets/Msg";
|
|
import { ContentPage } from "../ContentPage";
|
|
import { ContentAlert } from "../ContentAlert";
|
|
import { KeycloakContext } from "../../keycloak-service/KeycloakContext";
|
|
import { KeycloakService } from "../../keycloak-service/keycloak.service";
|
|
import { css } from "@patternfly/react-styles";
|
|
|
|
declare const features: Features;
|
|
|
|
interface PasswordDetails {
|
|
registered: boolean;
|
|
lastUpdate: number;
|
|
}
|
|
|
|
type CredCategory = "password" | "two-factor" | "passwordless";
|
|
type CredType = string;
|
|
type CredTypeMap = Map<CredType, CredentialContainer>;
|
|
type CredContainerMap = Map<CredCategory, CredTypeMap>;
|
|
|
|
interface CredMetadata {
|
|
infoMessage?: string;
|
|
warningMessageTitle?: string;
|
|
warningMessageDescription?: string;
|
|
credential: UserCredential;
|
|
}
|
|
|
|
interface UserCredential {
|
|
id: string;
|
|
type: string;
|
|
userLabel: string;
|
|
createdDate?: number;
|
|
strCreatedDate?: string;
|
|
credentialData?: string;
|
|
}
|
|
|
|
// A CredentialContainer is unique by combo of credential type and credential category
|
|
interface CredentialContainer {
|
|
category: CredCategory;
|
|
type: CredType;
|
|
displayName: string;
|
|
helptext?: string;
|
|
createAction?: string;
|
|
updateAction?: string;
|
|
removeable: boolean;
|
|
userCredentialMetadatas: CredMetadata[];
|
|
open: boolean;
|
|
}
|
|
|
|
interface SigningInPageProps extends RouteComponentProps {}
|
|
|
|
interface SigningInPageState {
|
|
// Credential containers organized by category then type
|
|
credentialContainers: CredContainerMap;
|
|
}
|
|
|
|
/**
|
|
* @author Stan Silvert ssilvert@redhat.com (C) 2018 Red Hat Inc.
|
|
*/
|
|
class SigningInPage extends React.Component<
|
|
SigningInPageProps,
|
|
SigningInPageState
|
|
> {
|
|
static contextType = AccountServiceContext;
|
|
context: React.ContextType<typeof AccountServiceContext>;
|
|
|
|
public constructor(
|
|
props: SigningInPageProps,
|
|
context: React.ContextType<typeof AccountServiceContext>
|
|
) {
|
|
super(props);
|
|
this.context = context;
|
|
|
|
this.state = {
|
|
credentialContainers: new Map(),
|
|
};
|
|
|
|
this.getCredentialContainers();
|
|
}
|
|
|
|
private getCredentialContainers(): void {
|
|
this.context!.doGet("/credentials").then(
|
|
(response: HttpResponse<CredentialContainer[]>) => {
|
|
const allContainers: CredContainerMap = new Map();
|
|
const containers: CredentialContainer[] = response.data || [];
|
|
containers.forEach((container) => {
|
|
let categoryMap = allContainers.get(container.category);
|
|
if (!categoryMap) {
|
|
categoryMap = new Map();
|
|
allContainers.set(container.category, categoryMap);
|
|
}
|
|
categoryMap.set(container.type, container);
|
|
});
|
|
|
|
this.setState({ credentialContainers: allContainers });
|
|
}
|
|
);
|
|
}
|
|
|
|
private handleRemove = (credentialId: string, userLabel: string) => {
|
|
this.context!.doDelete("/credentials/" + encodeURIComponent(credentialId)).then(() => {
|
|
this.getCredentialContainers();
|
|
ContentAlert.success("successRemovedMessage", [userLabel]);
|
|
});
|
|
};
|
|
|
|
public static credElementId(
|
|
credType: CredType,
|
|
credId: string,
|
|
item: string
|
|
): string {
|
|
return `${credType}-${item}-${credId.substring(0, 8)}`;
|
|
}
|
|
|
|
public render(): React.ReactNode {
|
|
return (
|
|
<ContentPage title="signingIn" introMessage="signingInSubMessage">
|
|
{this.renderCategories()}
|
|
</ContentPage>
|
|
);
|
|
}
|
|
|
|
private renderCategories(): React.ReactNode {
|
|
return Array.from(this.state.credentialContainers.keys()).map(
|
|
(category) => (
|
|
<PageSection key={category} variant={PageSectionVariants.light}>
|
|
<Title
|
|
id={`${category}-categ-title`}
|
|
headingLevel="h2"
|
|
size="xl"
|
|
>
|
|
<Msg msgKey={category} />
|
|
</Title>
|
|
{this.renderTypes(category!)}
|
|
</PageSection>
|
|
)
|
|
)
|
|
}
|
|
|
|
private renderTypes(category: CredCategory): React.ReactNode {
|
|
let credTypeMap: CredTypeMap = this.state.credentialContainers.get(
|
|
category
|
|
)!;
|
|
|
|
return (
|
|
<KeycloakContext.Consumer>
|
|
{(keycloak) => (
|
|
<>
|
|
{Array.from(
|
|
credTypeMap.keys()
|
|
).map(
|
|
(
|
|
credType: CredType,
|
|
index: number,
|
|
typeArray: string[]
|
|
) => [
|
|
this.renderCredTypeTitle(
|
|
credTypeMap.get(credType)!,
|
|
keycloak!,
|
|
category
|
|
),
|
|
this.renderUserCredentials(
|
|
credTypeMap,
|
|
credType,
|
|
keycloak!
|
|
),
|
|
]
|
|
)}
|
|
</>
|
|
)}
|
|
</KeycloakContext.Consumer>
|
|
);
|
|
}
|
|
|
|
private renderEmptyRow(type: string, isLast: boolean): React.ReactNode {
|
|
if (isLast) return; // don't put empty row at the end
|
|
|
|
return (
|
|
<DataListItem aria-labelledby={"empty-list-item-" + type}>
|
|
<DataListItemRow key={"empty-row-" + type}>
|
|
<DataListItemCells
|
|
dataListCells={[<DataListCell></DataListCell>]}
|
|
/>
|
|
</DataListItemRow>
|
|
</DataListItem>
|
|
);
|
|
}
|
|
|
|
private renderUserCredentials(
|
|
credTypeMap: CredTypeMap,
|
|
credType: CredType,
|
|
keycloak: KeycloakService
|
|
): React.ReactNode {
|
|
const credContainer: CredentialContainer = credTypeMap.get(credType)!;
|
|
const userCredentialMetadatas: CredMetadata[] = credContainer.userCredentialMetadatas;
|
|
const removeable: boolean = credContainer.removeable;
|
|
const type: string = credContainer.type;
|
|
const displayName: string = credContainer.displayName;
|
|
|
|
if (!userCredentialMetadatas || userCredentialMetadatas.length === 0) {
|
|
const localizedDisplayName = Msg.localize(displayName);
|
|
return (
|
|
<DataList aria-label={Msg.localize('notSetUp', [localizedDisplayName])} className="pf-u-mb-xl">
|
|
<DataListItem key='no-credentials-list-item' aria-labelledby={Msg.localize('notSetUp', [localizedDisplayName])}>
|
|
<DataListItemRow key='no-credentials-list-item-row' className="pf-u-align-items-center">
|
|
<DataListItemCells
|
|
dataListCells={[
|
|
<DataListCell key={'no-credentials-cell-0'}/>,
|
|
<EmptyState id={`${type}-not-set-up`} key={'no-credentials-cell-1'} variant={EmptyStateVariant.xs}>
|
|
<EmptyStateBody>
|
|
<Msg msgKey='notSetUp' params={[localizedDisplayName]}/>
|
|
</EmptyStateBody>
|
|
</EmptyState>,
|
|
<DataListCell key={'no-credentials-cell-2'}/>
|
|
]}/>
|
|
</DataListItemRow>
|
|
</DataListItem>
|
|
</DataList>
|
|
);
|
|
}
|
|
|
|
userCredentialMetadatas.forEach(credentialMetadata => {
|
|
let credential = credentialMetadata.credential;
|
|
if (!credential.userLabel) credential.userLabel = Msg.localize(credential.type);
|
|
if (credential.hasOwnProperty('createdDate') && credential.createdDate && credential.createdDate! > 0) {
|
|
credential.strCreatedDate = TimeUtil.format(credential.createdDate as number);
|
|
}
|
|
});
|
|
|
|
let updateAIA: AIACommand;
|
|
if (credContainer.updateAction) {
|
|
updateAIA = new AIACommand(keycloak, credContainer.updateAction);
|
|
}
|
|
|
|
let maxWidth = { maxWidth: 689 } as React.CSSProperties;
|
|
|
|
return (
|
|
<React.Fragment key='userCredentialMetadatas'> {
|
|
userCredentialMetadatas.map(credentialMetadata => (
|
|
<>
|
|
{(credentialMetadata.infoMessage && !credentialMetadata.warningMessageTitle && !credentialMetadata.warningMessageDescription) &&
|
|
<Alert variant="default" className="pf-u-mb-md" isInline isPlain title={Msg.localize(JSON.parse(credentialMetadata.infoMessage).key, JSON.parse(credentialMetadata.infoMessage).parameters)} />
|
|
}
|
|
{(credentialMetadata.warningMessageTitle && credentialMetadata.warningMessageDescription) &&
|
|
<Alert variant="warning" className="pf-u-mb-md" isInline title={Msg.localize(JSON.parse(credentialMetadata.warningMessageTitle).key, JSON.parse(credentialMetadata.warningMessageTitle).parameters)} style={maxWidth}>
|
|
|
|
<p>{Msg.localize(JSON.parse(credentialMetadata.warningMessageDescription).key, JSON.parse(credentialMetadata.warningMessageDescription).parameters)}</p>
|
|
</Alert>
|
|
}
|
|
<DataList aria-label="user credential" className="pf-u-mb-xl">
|
|
<DataListItem id={`${SigningInPage.credElementId(type, credentialMetadata.credential.id, 'row')}`} key={'credential-list-item-' + credentialMetadata.credential.id} aria-labelledby={'credential-list-item-' + credentialMetadata.credential.userLabel}>
|
|
<DataListItemRow key={'userCredentialRow-' + credentialMetadata.credential.id} className="pf-u-align-items-center">
|
|
<DataListItemCells dataListCells={this.credentialRowCells(credentialMetadata, type)}/>
|
|
<CredentialAction
|
|
credential={credentialMetadata.credential}
|
|
removeable={removeable}
|
|
updateAction={updateAIA}
|
|
credRemover={this.handleRemove}
|
|
/>
|
|
</DataListItemRow>
|
|
</DataListItem>
|
|
</DataList>
|
|
</>
|
|
))
|
|
} </React.Fragment>
|
|
)
|
|
}
|
|
|
|
private credentialRowCells(credMetadata: CredMetadata, type: string): React.ReactNode[] {
|
|
const credRowCells: React.ReactNode[] = [];
|
|
const credential = credMetadata.credential;
|
|
let maxWidth = { "--pf-u-max-width--MaxWidth": "300px" } as React.CSSProperties;
|
|
credRowCells.push(
|
|
<DataListCell id={`${SigningInPage.credElementId(type, credential.id, 'label')}`} key={'userLabel-' + credential.id} className="pf-u-max-width" style={maxWidth}>
|
|
{credential.userLabel}
|
|
</DataListCell>
|
|
);
|
|
if (credential.strCreatedDate) {
|
|
credRowCells.push(
|
|
<DataListCell
|
|
id={`${SigningInPage.credElementId(
|
|
type,
|
|
credential.id,
|
|
"created-at"
|
|
)}`}
|
|
key={"created-" + credential.id}
|
|
>
|
|
<strong className="pf-u-mr-md">
|
|
<Msg msgKey="credentialCreatedAt" />{" "}
|
|
</strong>
|
|
{credential.strCreatedDate}
|
|
</DataListCell>
|
|
);
|
|
credRowCells.push(<DataListCell key={"spacer-" + credential.id} />);
|
|
}
|
|
|
|
return credRowCells;
|
|
}
|
|
|
|
private renderCredTypeTitle(
|
|
credContainer: CredentialContainer,
|
|
keycloak: KeycloakService,
|
|
category: CredCategory
|
|
): React.ReactNode {
|
|
|
|
if (
|
|
!credContainer.hasOwnProperty("helptext") &&
|
|
!credContainer.hasOwnProperty("createAction")
|
|
)
|
|
return;
|
|
|
|
let setupAction: AIACommand;
|
|
if (credContainer.createAction) {
|
|
setupAction = new AIACommand(keycloak, credContainer.createAction);
|
|
}
|
|
|
|
const credContainerDisplayName: string = Msg.localize(
|
|
credContainer.displayName
|
|
);
|
|
return (
|
|
<React.Fragment key={"credTypeTitle-" + credContainer.type}>
|
|
<Split className="pf-u-mt-lg pf-u-mb-lg">
|
|
<SplitItem>
|
|
<Title
|
|
headingLevel="h3"
|
|
size="md"
|
|
className="pf-u-mb-md"
|
|
>
|
|
<span className="cred-title pf-u-display-block" id={`${credContainer.type}-cred-title`}>
|
|
<Msg msgKey={credContainer.displayName} />
|
|
</span>
|
|
</Title>
|
|
<span id={`${credContainer.type}-cred-help`}>
|
|
{credContainer.helptext && (
|
|
<Msg msgKey={credContainer.helptext} />
|
|
)}
|
|
</span>
|
|
</SplitItem>
|
|
|
|
<SplitItem isFilled>
|
|
{credContainer.createAction && (
|
|
<div
|
|
id={"mob-setUpAction-" + credContainer.type}
|
|
className="pf-u-display-none-on-lg pf-u-float-right"
|
|
>
|
|
<Dropdown
|
|
isPlain
|
|
position={DropdownPosition.right}
|
|
toggle={
|
|
<KebabToggle
|
|
onToggle={(isOpen) => {
|
|
credContainer.open = isOpen;
|
|
this.setState({
|
|
credentialContainers: new Map(
|
|
this.state.credentialContainers
|
|
),
|
|
});
|
|
}}
|
|
/>
|
|
}
|
|
isOpen={credContainer.open}
|
|
dropdownItems={[
|
|
<button
|
|
id={`mob-${credContainer.type}-set-up`}
|
|
className="pf-c-button pf-m-link"
|
|
type="button"
|
|
onClick={() =>
|
|
setupAction.execute()
|
|
}
|
|
>
|
|
<span className="pf-c-button__icon">
|
|
<i
|
|
className="fa fa-plus-circle"
|
|
aria-hidden="true"
|
|
></i>
|
|
</span>
|
|
<Msg
|
|
msgKey="setUpNew"
|
|
params={[
|
|
credContainerDisplayName,
|
|
]}
|
|
/>
|
|
</button>,
|
|
]}
|
|
/>
|
|
</div>
|
|
)}
|
|
{credContainer.createAction && (
|
|
<div
|
|
id={"setUpAction-" + credContainer.type}
|
|
className="pf-u-display-none pf-u-display-inline-flex-on-lg pf-u-float-right"
|
|
>
|
|
<button
|
|
id={`${credContainer.type}-set-up`}
|
|
className="pf-c-button pf-m-link"
|
|
type="button"
|
|
onClick={() => setupAction.execute()}
|
|
>
|
|
<span className="pf-c-button__icon">
|
|
<i
|
|
className="fa fa-plus-circle"
|
|
aria-hidden="true"
|
|
></i>
|
|
</span>
|
|
<Msg
|
|
msgKey="setUpNew"
|
|
params={[credContainerDisplayName]}
|
|
/>
|
|
</button>
|
|
</div>
|
|
)}
|
|
</SplitItem>
|
|
</Split>
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
}
|
|
|
|
type CredRemover = (credentialId: string, userLabel: string) => void;
|
|
interface CredentialActionProps {
|
|
credential: UserCredential;
|
|
removeable: boolean;
|
|
updateAction: AIACommand;
|
|
credRemover: CredRemover;
|
|
};
|
|
|
|
class CredentialAction extends React.Component<CredentialActionProps> {
|
|
render(): React.ReactNode {
|
|
if (this.props.updateAction) {
|
|
return (
|
|
<DataListAction
|
|
aria-labelledby={Msg.localize('updateCredAriaLabel')}
|
|
aria-label={Msg.localize('updateCredAriaLabel')}
|
|
id={"updateAction-" + this.props.credential.id}
|
|
>
|
|
<Button
|
|
variant="secondary"
|
|
id={`${SigningInPage.credElementId(
|
|
this.props.credential.type,
|
|
this.props.credential.id,
|
|
"update"
|
|
)}`}
|
|
onClick={() => this.props.updateAction.execute()}
|
|
>
|
|
<Msg msgKey="update" />
|
|
</Button>
|
|
</DataListAction>
|
|
);
|
|
}
|
|
|
|
if (this.props.removeable) {
|
|
const userLabel: string = this.props.credential.userLabel;
|
|
return (
|
|
<DataListAction
|
|
aria-label={Msg.localize('removeCredAriaLabel')}
|
|
aria-labelledby={Msg.localize('removeCredAriaLabel')}
|
|
id={'removeAction-' + this.props.credential.id }
|
|
>
|
|
<ContinueCancelModal
|
|
buttonTitle='remove'
|
|
buttonVariant='danger'
|
|
buttonId={`${SigningInPage.credElementId(this.props.credential.type, this.props.credential.id, 'remove')}`}
|
|
modalTitle={Msg.localize('removeCred', [userLabel])}
|
|
modalMessage={Msg.localize('stopUsingCred', [userLabel])}
|
|
onContinue={() => this.props.credRemover(this.props.credential.id, userLabel)}
|
|
/>
|
|
</DataListAction>
|
|
);
|
|
}
|
|
|
|
return <></>;
|
|
}
|
|
}
|
|
|
|
const SigningInPageWithRouter = withRouter(SigningInPage);
|
|
export { SigningInPageWithRouter as SigningInPage };
|