KEYCLOAK-11550: Signing In page

This commit is contained in:
Stan Silvert 2020-01-22 16:41:41 -05:00
parent 812b69af13
commit 210fd92d23
9 changed files with 401 additions and 139 deletions

View file

@ -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

View file

@ -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

View file

@ -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( {

View file

@ -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') {

View file

@ -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>,
]} ]}
/> />

View file

@ -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();
} }
private setLastPwdUpdate(): void { this.getCredentialContainers();
AccountServiceClient.Instance.doGet("/credentials/password")
.then((response: AxiosResponse<PasswordDetails>) => {
if (response.data.lastUpdate) {
const lastUpdate: number = response.data.lastUpdate;
this.setState({lastPasswordUpdate: lastUpdate});
} }
private getCredentialContainers(): void {
AccountServiceClient.Instance.doGet("/credentials")
.then((response: AxiosResponse<CredentialContainer[]>) => {
const allContainers: CredContainerMap = new Map();
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};

View file

@ -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;

View file

@ -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',