KEYCLOAK-11550: Signing In page
This commit is contained in:
parent
812b69af13
commit
210fd92d23
9 changed files with 401 additions and 139 deletions
|
@ -57,6 +57,145 @@ public class AccountCredentialResource {
|
||||||
// models.forEach(c -> c.setSecretData(null));
|
// models.forEach(c -> c.setSecretData(null));
|
||||||
// return models.stream().map(ModelToRepresentation::toRepresentation).collect(Collectors.toList());
|
// return models.stream().map(ModelToRepresentation::toRepresentation).collect(Collectors.toList());
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
private static class CredentialContainer {
|
||||||
|
// ** These first three attributes can be ordinary UI text or a key into
|
||||||
|
// a localized message bundle. Typically, it will be a key, but
|
||||||
|
// the UI will work just fine if you don't care about localization
|
||||||
|
// and you want to just send UI text.
|
||||||
|
//
|
||||||
|
// Also, the ${} shown in Apicurio is not needed.
|
||||||
|
private String category; // **
|
||||||
|
private String type; // **
|
||||||
|
private String helptext; // **
|
||||||
|
private boolean enabled;
|
||||||
|
private String createAction;
|
||||||
|
private String updateAction;
|
||||||
|
private boolean removeable;
|
||||||
|
private List<CredentialModel> userCredentials;
|
||||||
|
|
||||||
|
public CredentialContainer(String category, String type, String helptext, boolean enabled, String createAction, String updateAction, boolean removeable,List<CredentialModel> userCredentials) {
|
||||||
|
this.category = category;
|
||||||
|
this.type = type;
|
||||||
|
this.helptext = helptext;
|
||||||
|
this.enabled = enabled;
|
||||||
|
this.createAction = createAction;
|
||||||
|
this.updateAction = updateAction;
|
||||||
|
this.removeable = removeable;
|
||||||
|
this.userCredentials = userCredentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCategory() {
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHelptext() {
|
||||||
|
return helptext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCreateAction() {
|
||||||
|
return createAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUpdateAction() {
|
||||||
|
return updateAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRemoveable() {
|
||||||
|
return removeable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CredentialModel> getUserCredentials() {
|
||||||
|
return userCredentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@NoCache
|
||||||
|
@Produces(javax.ws.rs.core.MediaType.APPLICATION_JSON)
|
||||||
|
public List<CredentialContainer> dummyCredentialTypes(){
|
||||||
|
auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
|
||||||
|
List<CredentialModel> models = session.userCredentialManager().getStoredCredentials(realm, user);
|
||||||
|
|
||||||
|
List<CredentialModel> passwordUserCreds = new java.util.ArrayList<>();
|
||||||
|
passwordUserCreds.add(models.get(0));
|
||||||
|
|
||||||
|
List<CredentialModel> otpUserCreds = new java.util.ArrayList<>();
|
||||||
|
if (models.size() > 1) otpUserCreds.add(models.get(1));
|
||||||
|
if (models.size() > 2) otpUserCreds.add(models.get(2));
|
||||||
|
|
||||||
|
List<CredentialModel> webauthnUserCreds = new java.util.ArrayList<>();
|
||||||
|
CredentialModel webauthnCred = new CredentialModel();
|
||||||
|
webauthnCred.setId("bogus-id");
|
||||||
|
webauthnCred.setUserLabel("yubikey key");
|
||||||
|
webauthnCred.setCreatedDate(1579122652382L);
|
||||||
|
webauthnUserCreds.add(webauthnCred);
|
||||||
|
|
||||||
|
List<CredentialModel> webauthnStrongUserCreds = new java.util.ArrayList<>();
|
||||||
|
CredentialModel webauthnStrongCred = new CredentialModel();
|
||||||
|
webauthnStrongCred.setId("bogus-id-for-webauthnStrong");
|
||||||
|
webauthnStrongCred.setUserLabel("My very strong key with required PIN");
|
||||||
|
webauthnStrongCred.setCreatedDate(1579122652382L);
|
||||||
|
webauthnUserCreds.add(webauthnStrongCred);
|
||||||
|
|
||||||
|
CredentialContainer password = new CredentialContainer(
|
||||||
|
"password",
|
||||||
|
"password",
|
||||||
|
"passwordHelptext",
|
||||||
|
true,
|
||||||
|
null, // no create action
|
||||||
|
"UPDATE_PASSWORD",
|
||||||
|
false,
|
||||||
|
passwordUserCreds
|
||||||
|
);
|
||||||
|
CredentialContainer otp = new CredentialContainer(
|
||||||
|
"two-factor",
|
||||||
|
"otp",
|
||||||
|
"otpHelptext",
|
||||||
|
true,
|
||||||
|
"CONFIGURE_TOTP",
|
||||||
|
null, // no update action
|
||||||
|
true,
|
||||||
|
otpUserCreds
|
||||||
|
);
|
||||||
|
CredentialContainer webAuthn = new CredentialContainer(
|
||||||
|
"two-factor",
|
||||||
|
"webauthn",
|
||||||
|
"webauthnHelptext",
|
||||||
|
true,
|
||||||
|
"CONFIGURE_WEBAUTHN",
|
||||||
|
null, // no update action
|
||||||
|
true,
|
||||||
|
webauthnUserCreds
|
||||||
|
);
|
||||||
|
CredentialContainer passwordless = new CredentialContainer(
|
||||||
|
"passwordless",
|
||||||
|
"webauthn-passwordless",
|
||||||
|
"webauthn-passwordlessHelptext",
|
||||||
|
true,
|
||||||
|
"CONFIGURE_WEBAUTHN_STRONG",
|
||||||
|
null, // no update action
|
||||||
|
true,
|
||||||
|
webauthnStrongUserCreds
|
||||||
|
);
|
||||||
|
|
||||||
|
List<CredentialContainer> dummyCreds = new java.util.ArrayList<>();
|
||||||
|
dummyCreds.add(password);
|
||||||
|
dummyCreds.add(otp);
|
||||||
|
dummyCreds.add(webAuthn);
|
||||||
|
dummyCreds.add(passwordless);
|
||||||
|
|
||||||
|
return dummyCreds;
|
||||||
|
}
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
// @GET
|
// @GET
|
||||||
|
@ -72,17 +211,17 @@ public class AccountCredentialResource {
|
||||||
// .collect(Collectors.toList());
|
// .collect(Collectors.toList());
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// /**
|
/**
|
||||||
// * Remove a credential for a user
|
* Remove a credential for a user
|
||||||
// *
|
*
|
||||||
// */
|
*/
|
||||||
// @Path("{credentialId}")
|
@Path("{credentialId}")
|
||||||
// @DELETE
|
@DELETE
|
||||||
// @NoCache
|
@NoCache
|
||||||
// public void removeCredential(final @PathParam("credentialId") String credentialId) {
|
public void removeCredential(final @PathParam("credentialId") String credentialId) {
|
||||||
// auth.require(AccountRoles.MANAGE_ACCOUNT);
|
auth.require(AccountRoles.MANAGE_ACCOUNT);
|
||||||
// session.userCredentialManager().removeStoredCredential(realm, user, credentialId);
|
session.userCredentialManager().removeStoredCredential(realm, user, credentialId);
|
||||||
// }
|
}
|
||||||
//
|
//
|
||||||
// /**
|
// /**
|
||||||
// * Update a credential label for a user
|
// * Update a credential label for a user
|
||||||
|
|
|
@ -59,16 +59,21 @@ unLink=Unlink Account
|
||||||
# Signing In Page
|
# Signing In Page
|
||||||
signingIn=Signing In
|
signingIn=Signing In
|
||||||
signingInSubMessage=Configure ways to sign in.
|
signingInSubMessage=Configure ways to sign in.
|
||||||
twoFactorEnabled=Two-factor authentication is enabled.
|
credentialCreatedAt=Created
|
||||||
twoFactorDisabled=Two-factor authentication is disabled.
|
successRemovedMessage={0} was removed.
|
||||||
twoFactorAuth=Two-Factor Authentication
|
stopUsingCred=Stop using {0}?
|
||||||
mobileAuthDefault=Mobile Authenticator (Default)
|
removeCred=Remove {0}
|
||||||
removeMobileAuth=Remove Mobile Authenticator
|
setUpNew=Set up {0}
|
||||||
stopMobileAuth=Stop using Mobile Authenticator?
|
notSetUp={0} is not set up.
|
||||||
setUp=Set Up
|
two-factor=Two-Factor Authentication
|
||||||
passwordless=Passwordless
|
passwordless=Passwordless
|
||||||
lastUpdate=Last Update
|
|
||||||
unknown=Unknown
|
unknown=Unknown
|
||||||
|
otp=One Time Password
|
||||||
|
webauthn=WebAuthn
|
||||||
|
webauthn-passwordless=WebAuthn Passwordless
|
||||||
|
otpHelptext=A one-time password (OTP), is a password that is valid for only one login session.
|
||||||
|
webauthnHelptext=WebAuthn lets web applications authenticate users without storing their passwords on servers.
|
||||||
|
webauthn-passwordlessHelptext=I need help with this help text. Any suggestions?
|
||||||
|
|
||||||
# Applications page
|
# Applications page
|
||||||
applicationsPageTitle=Applications
|
applicationsPageTitle=Applications
|
||||||
|
|
|
@ -104,7 +104,7 @@ export class AccountServiceClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
private makeConfig(endpoint: string, config: AxiosRequestConfig = {}): Promise<AxiosRequestConfig> {
|
private makeConfig(endpoint: string, config: AxiosRequestConfig = {}): Promise<AxiosRequestConfig> {
|
||||||
return new Promise( (resolve: ConfigResolve, reject: ErrorReject) => {
|
return new Promise( (resolve: ConfigResolve) => {
|
||||||
this.kcSvc.getToken()
|
this.kcSvc.getToken()
|
||||||
.then( (token: string) => {
|
.then( (token: string) => {
|
||||||
resolve( {
|
resolve( {
|
||||||
|
|
|
@ -39,38 +39,38 @@ export class ContentAlert extends React.Component<ContentAlertProps, ContentAler
|
||||||
/**
|
/**
|
||||||
* @param message A literal text message or localization key.
|
* @param message A literal text message or localization key.
|
||||||
*/
|
*/
|
||||||
public static success(message: string): void {
|
public static success(message: string, params?: string[]): void {
|
||||||
ContentAlert.instance.postAlert(message, 'success');
|
ContentAlert.instance.postAlert('success', message, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param message A literal text message or localization key.
|
* @param message A literal text message or localization key.
|
||||||
*/
|
*/
|
||||||
public static danger(message: string): void {
|
public static danger(message: string, params?: string[]): void {
|
||||||
ContentAlert.instance.postAlert(message, 'danger');
|
ContentAlert.instance.postAlert('danger', message, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param message A literal text message or localization key.
|
* @param message A literal text message or localization key.
|
||||||
*/
|
*/
|
||||||
public static warning(message: string): void {
|
public static warning(message: string, params?: string[]): void {
|
||||||
ContentAlert.instance.postAlert(message, 'warning');
|
ContentAlert.instance.postAlert('warning', message, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param message A literal text message or localization key.
|
* @param message A literal text message or localization key.
|
||||||
*/
|
*/
|
||||||
public static info(message: string): void {
|
public static info(message: string, params?: string[]): void {
|
||||||
ContentAlert.instance.postAlert(message, 'info');
|
ContentAlert.instance.postAlert('info', message, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
private hideAlert = () => {
|
private hideAlert = () => {
|
||||||
this.setState({isVisible: false});
|
this.setState({isVisible: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
private postAlert = (message: string, variant: AlertVariant) => {
|
private postAlert = (variant: AlertVariant, message: string, params?: string[]) => {
|
||||||
this.setState({isVisible: true,
|
this.setState({isVisible: true,
|
||||||
message: Msg.localize(message),
|
message: Msg.localize(message, params),
|
||||||
variant});
|
variant});
|
||||||
|
|
||||||
if (variant !== 'danger') {
|
if (variant !== 'danger') {
|
||||||
|
|
|
@ -29,7 +29,7 @@ import {
|
||||||
GridItem,
|
GridItem,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
import { InfoAltIcon, CheckIcon, LinkIcon, BuilderImageIcon } from '@patternfly/react-icons';
|
import { InfoAltIcon, CheckIcon, BuilderImageIcon } from '@patternfly/react-icons';
|
||||||
import { ContentPage } from '../ContentPage';
|
import { ContentPage } from '../ContentPage';
|
||||||
import { ContinueCancelModal } from '../../widgets/ContinueCancelModal';
|
import { ContinueCancelModal } from '../../widgets/ContinueCancelModal';
|
||||||
import { AccountServiceClient } from '../../account-service/account.service';
|
import { AccountServiceClient } from '../../account-service/account.service';
|
||||||
|
@ -110,6 +110,8 @@ export class ApplicationsPage extends React.Component<ApplicationsPageProps, App
|
||||||
<ContentPage title={Msg.localize('applicationsPageTitle')}>
|
<ContentPage title={Msg.localize('applicationsPageTitle')}>
|
||||||
<DataList id="applications-list" aria-label={Msg.localize('applicationsPageTitle')}>
|
<DataList id="applications-list" aria-label={Msg.localize('applicationsPageTitle')}>
|
||||||
{this.state.applications.map((application: Application, appIndex: number) => {
|
{this.state.applications.map((application: Application, appIndex: number) => {
|
||||||
|
const appUrl: string = application.userConsentRequired ? application.baseUrl : '/auth' + application.baseUrl;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataListItem key={'application-' + appIndex} aria-labelledby="applications-list" isExpanded={this.state.isRowOpen[appIndex]}>
|
<DataListItem key={'application-' + appIndex} aria-labelledby="applications-list" isExpanded={this.state.isRowOpen[appIndex]}>
|
||||||
<DataListItemRow>
|
<DataListItemRow>
|
||||||
|
@ -132,7 +134,10 @@ export class ApplicationsPage extends React.Component<ApplicationsPageProps, App
|
||||||
{application.inUse ? Msg.localize('inUse') : Msg.localize('notInUse')}
|
{application.inUse ? Msg.localize('inUse') : Msg.localize('notInUse')}
|
||||||
</DataListCell>,
|
</DataListCell>,
|
||||||
<DataListCell width={4} key={'baseUrl-' + appIndex}>
|
<DataListCell width={4} key={'baseUrl-' + appIndex}>
|
||||||
<a href={application.userConsentRequired ? application.baseUrl : '/auth' + application.baseUrl} target="_blank"><LinkIcon /> {application.baseUrl}</a>
|
<button className="pf-c-button pf-m-link" type="button" onClick={() => window.open(appUrl)}>
|
||||||
|
<span className="pf-c-button__icon">
|
||||||
|
<i className="fas fa-link" aria-hidden="true"></i>
|
||||||
|
</span>{application.baseUrl}</button>
|
||||||
</DataListCell>,
|
</DataListCell>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -158,7 +158,7 @@ export class DeviceActivityPage extends React.Component<DeviceActivityPageProps,
|
||||||
return moment(time * 1000).format('LLLL');
|
return moment(time * 1000).format('LLLL');
|
||||||
}
|
}
|
||||||
|
|
||||||
private elementId(item: string, session: Session) : string {
|
private elementId(item: string, session: Session): string {
|
||||||
return `session-${session.id.substring(0,7)}-${item}`;
|
return `session-${session.id.substring(0,7)}-${item}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,12 +25,12 @@ import {
|
||||||
DataListAction,
|
DataListAction,
|
||||||
DataListItemCells,
|
DataListItemCells,
|
||||||
DataListCell,
|
DataListCell,
|
||||||
|
DataListItem,
|
||||||
DataListItemRow,
|
DataListItemRow,
|
||||||
Stack,
|
Stack,
|
||||||
StackItem,
|
StackItem,
|
||||||
Switch,
|
|
||||||
Title,
|
Title,
|
||||||
TitleLevel
|
TitleLevel,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
import {AIACommand} from '../../util/AIACommand';
|
import {AIACommand} from '../../util/AIACommand';
|
||||||
|
@ -48,14 +48,36 @@ interface PasswordDetails {
|
||||||
lastUpdate: number;
|
lastUpdate: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CredCategory = 'password' | 'two-factor' | 'passwordless';
|
||||||
|
type CredType = string;
|
||||||
|
type CredTypeMap = Map<CredType, CredentialContainer>;
|
||||||
|
type CredContainerMap = Map<CredCategory, CredTypeMap>;
|
||||||
|
|
||||||
|
interface UserCredential {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
userLabel: string;
|
||||||
|
createdDate: number;
|
||||||
|
strCreatedDate?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A CredentialContainer is unique by combo of credential type and credential category
|
||||||
|
interface CredentialContainer {
|
||||||
|
category: CredCategory;
|
||||||
|
type: CredType;
|
||||||
|
helptext?: string;
|
||||||
|
createAction: string;
|
||||||
|
updateAction: string;
|
||||||
|
removeable: boolean;
|
||||||
|
userCredentials: UserCredential[];
|
||||||
|
}
|
||||||
|
|
||||||
interface SigningInPageProps extends RouteComponentProps {
|
interface SigningInPageProps extends RouteComponentProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SigningInPageState {
|
interface SigningInPageState {
|
||||||
twoFactorEnabled: boolean;
|
// Credential containers organized by category then type
|
||||||
twoFactorEnabledText: string;
|
credentialContainers: CredContainerMap;
|
||||||
isTotpConfigured: boolean;
|
|
||||||
lastPasswordUpdate?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -68,116 +90,205 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
|
||||||
public constructor(props: SigningInPageProps) {
|
public constructor(props: SigningInPageProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
twoFactorEnabled: true,
|
credentialContainers: new Map()
|
||||||
twoFactorEnabledText: Msg.localize('twoFactorEnabled'),
|
|
||||||
isTotpConfigured: features.isTotpConfigured,
|
|
||||||
}
|
}
|
||||||
this.setLastPwdUpdate();
|
|
||||||
|
this.getCredentialContainers();
|
||||||
}
|
}
|
||||||
|
|
||||||
private setLastPwdUpdate(): void {
|
private getCredentialContainers(): void {
|
||||||
AccountServiceClient.Instance.doGet("/credentials/password")
|
AccountServiceClient.Instance.doGet("/credentials")
|
||||||
.then((response: AxiosResponse<PasswordDetails>) => {
|
.then((response: AxiosResponse<CredentialContainer[]>) => {
|
||||||
if (response.data.lastUpdate) {
|
|
||||||
const lastUpdate: number = response.data.lastUpdate;
|
const allContainers: CredContainerMap = new Map();
|
||||||
this.setState({lastPasswordUpdate: lastUpdate});
|
response.data.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});
|
||||||
|
console.log({allContainers})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleTwoFactorSwitch = () => {
|
private handleRemove = (credentialId: string, userLabel: string) => {
|
||||||
if (this.state.twoFactorEnabled) {
|
AccountServiceClient.Instance.doDelete("/credentials/" + credentialId)
|
||||||
this.setState({twoFactorEnabled: false, twoFactorEnabledText: Msg.localize('twoFactorDisabled')})
|
|
||||||
} else {
|
|
||||||
this.setState({twoFactorEnabled: true, twoFactorEnabledText: Msg.localize('twoFactorEnabled')})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleRemoveTOTP = () => {
|
|
||||||
AccountServiceClient.Instance.doDelete("/totp/remove")
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.setState({isTotpConfigured: false});
|
this.getCredentialContainers();
|
||||||
ContentAlert.success('successTotpRemovedMessage');
|
ContentAlert.success('successRemovedMessage', [userLabel]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
let lastPwdUpdate: string = Msg.localize('unknown');
|
|
||||||
if (this.state.lastPasswordUpdate) {
|
|
||||||
lastPwdUpdate = moment(this.state.lastPasswordUpdate).format('LLL');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContentPage title="signingIn"
|
<ContentPage title="signingIn"
|
||||||
introMessage="signingInSubMessage">
|
introMessage="signingInSubMessage">
|
||||||
<Stack gutter='md'>
|
<Stack gutter='md'>
|
||||||
<StackItem isFilled>
|
{this.renderCategories()}
|
||||||
<Title headingLevel={TitleLevel.h2} size='2xl'>
|
|
||||||
<strong><Msg msgKey='password'/></strong>
|
|
||||||
</Title>
|
|
||||||
<DataList aria-label='foo'>
|
|
||||||
<DataListItemRow>
|
|
||||||
<DataListItemCells
|
|
||||||
dataListCells={[
|
|
||||||
<DataListCell key='password'><Msg msgKey='password'/></DataListCell>,
|
|
||||||
<DataListCell key='lastPwdUpdate'><strong><Msg msgKey='lastUpdate'/>: </strong>{lastPwdUpdate}</DataListCell>,
|
|
||||||
<DataListCell key='spacer'/>
|
|
||||||
]}/>
|
|
||||||
<DataListAction aria-labelledby='foo' aria-label='foo action' id='setPasswordAction'>
|
|
||||||
<Button variant='primary'onClick={()=> this.updatePassword.execute()}><Msg msgKey='update'/></Button>
|
|
||||||
</DataListAction>
|
|
||||||
</DataListItemRow>
|
|
||||||
</DataList>
|
|
||||||
</StackItem>
|
|
||||||
<StackItem isFilled>
|
|
||||||
<Title headingLevel={TitleLevel.h2} size='2xl'>
|
|
||||||
<strong><Msg msgKey='twoFactorAuth'/></strong>
|
|
||||||
</Title>
|
|
||||||
<DataList aria-label='foo'>
|
|
||||||
<DataListItemRow>
|
|
||||||
<DataListAction aria-labelledby='foo' aria-label='foo action' id='twoFactorOnOff'>
|
|
||||||
<Switch
|
|
||||||
aria-label='twoFactorSwitch'
|
|
||||||
label={this.state.twoFactorEnabledText}
|
|
||||||
isChecked={this.state.twoFactorEnabled}
|
|
||||||
onClick={this.handleTwoFactorSwitch}
|
|
||||||
/>
|
|
||||||
</DataListAction>
|
|
||||||
</DataListItemRow>
|
|
||||||
<DataListItemRow>
|
|
||||||
<DataListItemCells
|
|
||||||
dataListCells={[
|
|
||||||
<DataListCell key='mobileAuth'><Msg msgKey='mobileAuthDefault'/></DataListCell>
|
|
||||||
]}/>
|
|
||||||
{!this.state.isTotpConfigured &&
|
|
||||||
<DataListAction aria-labelledby='foo' aria-label='foo action' id='setMobileAuthAction'>
|
|
||||||
<Button isDisabled={!this.state.twoFactorEnabled} variant='primary' onClick={()=> this.setUpTOTP.execute()}><Msg msgKey='setUp'/></Button>
|
|
||||||
</DataListAction>
|
|
||||||
}
|
|
||||||
{this.state.isTotpConfigured &&
|
|
||||||
<DataListAction aria-labelledby='foo' aria-label='foo action' id='setMobileAuthAction'>
|
|
||||||
<ContinueCancelModal buttonTitle='remove'
|
|
||||||
isDisabled={!this.state.twoFactorEnabled}
|
|
||||||
modalTitle={Msg.localize('removeMobileAuth')}
|
|
||||||
modalMessage={Msg.localize('stopMobileAuth')}
|
|
||||||
onContinue={this.handleRemoveTOTP}
|
|
||||||
/>
|
|
||||||
</DataListAction>
|
|
||||||
}
|
|
||||||
</DataListItemRow>
|
|
||||||
</DataList>
|
|
||||||
</StackItem>
|
|
||||||
<StackItem isFilled>
|
|
||||||
<Title headingLevel={TitleLevel.h2} size='2xl'>
|
|
||||||
<strong><Msg msgKey='passwordless'/></strong>
|
|
||||||
</Title>
|
|
||||||
</StackItem>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderCategories(): React.ReactNode {
|
||||||
|
return (<> {
|
||||||
|
Array.from(this.state.credentialContainers.keys()).map(category => (
|
||||||
|
<StackItem key={category} isFilled>
|
||||||
|
<Title headingLevel={TitleLevel.h2} size='2xl'>
|
||||||
|
<strong><Msg msgKey={category}/></strong>
|
||||||
|
</Title>
|
||||||
|
<DataList aria-label='foo'>
|
||||||
|
{this.renderTypes(this.state.credentialContainers.get(category)!)}
|
||||||
|
</DataList>
|
||||||
|
</StackItem>
|
||||||
|
))
|
||||||
|
|
||||||
|
}</>)
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderTypes(credTypeMap: CredTypeMap): React.ReactNode {
|
||||||
|
return (<> {
|
||||||
|
Array.from(credTypeMap.keys()).map((credType: CredType, index: number, typeArray: string[]) => ([
|
||||||
|
this.renderCredTypeTitle(credTypeMap.get(credType)!),
|
||||||
|
this.renderUserCredentials(credTypeMap, credType),
|
||||||
|
this.renderEmptyRow(credTypeMap.get(credType)!.type, index === typeArray.length - 1)
|
||||||
|
]))
|
||||||
|
}</>)
|
||||||
|
}
|
||||||
|
|
||||||
|
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): React.ReactNode {
|
||||||
|
const userCredentials: UserCredential[] = credTypeMap.get(credType)!.userCredentials;
|
||||||
|
const removeable: boolean = credTypeMap.get(credType)!.removeable;
|
||||||
|
const updateAction: string = credTypeMap.get(credType)!.updateAction;
|
||||||
|
const type: string = credTypeMap.get(credType)!.type;
|
||||||
|
|
||||||
|
if (userCredentials.length === 0) {
|
||||||
|
const localizedType = Msg.localize(type);
|
||||||
|
return (
|
||||||
|
<DataListItem aria-labelledby='no-credentials-list-item'>
|
||||||
|
<DataListItemRow key='no-credentials-list-item-row'>
|
||||||
|
<DataListItemCells
|
||||||
|
dataListCells={[
|
||||||
|
<DataListCell/>,
|
||||||
|
<strong><Msg msgKey='notSetUp' params={[localizedType]}/></strong>,
|
||||||
|
<DataListCell/>
|
||||||
|
]}/>
|
||||||
|
</DataListItemRow>
|
||||||
|
</DataListItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
userCredentials.forEach(credential => {
|
||||||
|
if (!credential.userLabel) credential.userLabel = Msg.localize(credential.type);
|
||||||
|
credential.strCreatedDate = moment(credential.createdDate).format('LLL');
|
||||||
|
});
|
||||||
|
|
||||||
|
let updateAIA: AIACommand;
|
||||||
|
if (updateAction) updateAIA = new AIACommand(updateAction, this.props.location.pathname);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key='userCredentials'> {
|
||||||
|
userCredentials.map(credential => (
|
||||||
|
<DataListItem aria-labelledby={'credential-list-item-' + credential.userLabel}>
|
||||||
|
<DataListItemRow key={'userCredentialRow-' + credential.id}>
|
||||||
|
<DataListItemCells
|
||||||
|
dataListCells={[
|
||||||
|
<DataListCell key={'userLabel-' + credential.id}>{credential.userLabel}</DataListCell>,
|
||||||
|
<DataListCell key={'created-' + credential.id}><strong><Msg msgKey='credentialCreatedAt'/>: </strong>{credential.strCreatedDate}</DataListCell>,
|
||||||
|
<DataListCell key={'spacer-' + credential.id}/>
|
||||||
|
]}/>
|
||||||
|
|
||||||
|
<CredentialAction credential={credential}
|
||||||
|
removeable={removeable}
|
||||||
|
updateAction={updateAIA}
|
||||||
|
credRemover={this.handleRemove}/>
|
||||||
|
</DataListItemRow>
|
||||||
|
</DataListItem>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</React.Fragment>)
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderCredTypeTitle(credContainer: CredentialContainer): React.ReactNode {
|
||||||
|
if (!credContainer.createAction) return;
|
||||||
|
|
||||||
|
const setupAction: AIACommand = new AIACommand(credContainer.createAction, this.props.location.pathname);
|
||||||
|
const credContainerType: string = Msg.localize(credContainer.type);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataListItem aria-labelledby={'type-datalistitem-' + credContainer.type}>
|
||||||
|
<DataListItemRow key={'credTitleRow-' + credContainer.type}>
|
||||||
|
<DataListItemCells
|
||||||
|
dataListCells={[
|
||||||
|
<DataListCell width={5} key={'credTypeTitle-' + credContainer.type}>
|
||||||
|
<Title headingLevel={TitleLevel.h3} size='2xl'>
|
||||||
|
<strong><Msg msgKey={credContainer.type}/></strong>
|
||||||
|
</Title>
|
||||||
|
<Msg msgKey={credContainer.helptext}/>
|
||||||
|
</DataListCell>,
|
||||||
|
|
||||||
|
]}/>
|
||||||
|
<DataListAction aria-labelledby='foo' aria-label='foo action' id={'setUpAction-' + credContainer.type}>
|
||||||
|
<button className="pf-c-button pf-m-link" type="button" onClick={()=> setupAction.execute()}>
|
||||||
|
<span className="pf-c-button__icon">
|
||||||
|
<i className="fas fa-plus-circle" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
<Msg msgKey='setUpNew' params={[credContainerType]}/>
|
||||||
|
</button>
|
||||||
|
</DataListAction>
|
||||||
|
</DataListItemRow>
|
||||||
|
</DataListItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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='foo' aria-label='foo action' id={'updateAction-' + this.props.credential.id}>
|
||||||
|
<Button variant='primary'onClick={()=> this.props.updateAction.execute()}><Msg msgKey='update'/></Button>
|
||||||
|
</DataListAction>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.removeable) {
|
||||||
|
const userLabel: string = this.props.credential.userLabel;
|
||||||
|
return (
|
||||||
|
<DataListAction aria-labelledby='foo' aria-label='foo action' id={'removeAction-' + this.props.credential.id }>
|
||||||
|
<ContinueCancelModal buttonTitle='remove'
|
||||||
|
modalTitle={Msg.localize('removeCred', [userLabel])}
|
||||||
|
modalMessage={Msg.localize('stopUsingCred', [userLabel])}
|
||||||
|
onContinue={() => this.props.credRemover(this.props.credential.id, userLabel)}
|
||||||
|
/>
|
||||||
|
</DataListAction>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const SigningInPageWithRouter = withRouter(SigningInPage);
|
const SigningInPageWithRouter = withRouter(SigningInPage);
|
||||||
export { SigningInPageWithRouter as SigningInPage};
|
export { SigningInPageWithRouter as SigningInPage};
|
|
@ -19,7 +19,7 @@ import * as React from 'react';
|
||||||
declare const l18nMsg: {[key: string]: string};
|
declare const l18nMsg: {[key: string]: string};
|
||||||
|
|
||||||
export interface MsgProps {
|
export interface MsgProps {
|
||||||
readonly msgKey: string;
|
readonly msgKey: string | undefined;
|
||||||
readonly params?: string[];
|
readonly params?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,9 @@ export class Msg extends React.Component<MsgProps> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static localize(msgKey: string, params?: string[]): string {
|
public static localize(msgKey: string | undefined, params?: string[]): string {
|
||||||
|
if (msgKey === undefined) return '';
|
||||||
|
|
||||||
let message: string = l18nMsg[this.processKey(msgKey)];
|
let message: string = l18nMsg[this.processKey(msgKey)];
|
||||||
if (message === undefined) message = msgKey;
|
if (message === undefined) message = msgKey;
|
||||||
|
|
||||||
|
|
|
@ -1333,7 +1333,7 @@
|
||||||
'./icons/playstation-icon.js': '@empty',
|
'./icons/playstation-icon.js': '@empty',
|
||||||
'./icons/plug-icon.js': '@empty',
|
'./icons/plug-icon.js': '@empty',
|
||||||
'./icons/plugged-icon.js': '@empty',
|
'./icons/plugged-icon.js': '@empty',
|
||||||
'./icons/plus-circle-icon.js': '@empty',
|
//'./icons/plus-circle-icon.js': '@empty',
|
||||||
'./icons/plus-icon.js': '@empty',
|
'./icons/plus-icon.js': '@empty',
|
||||||
'./icons/plus-square-icon.js': '@empty',
|
'./icons/plus-square-icon.js': '@empty',
|
||||||
'./icons/podcast-icon.js': '@empty',
|
'./icons/podcast-icon.js': '@empty',
|
||||||
|
|
Loading…
Reference in a new issue