KEYCLOAK-6503: Linked Accounts Page
This commit is contained in:
parent
2d3f771b70
commit
d439f4181a
6 changed files with 296 additions and 25 deletions
|
@ -24,6 +24,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
|
|||
*/
|
||||
public class LinkedAccountRepresentation implements Comparable<LinkedAccountRepresentation> {
|
||||
private boolean connected;
|
||||
private boolean isSocial;
|
||||
private String providerAlias;
|
||||
private String providerName;
|
||||
private String displayName;
|
||||
|
@ -47,6 +48,14 @@ public class LinkedAccountRepresentation implements Comparable<LinkedAccountRepr
|
|||
public void setConnected(boolean connected) {
|
||||
this.connected = connected;
|
||||
}
|
||||
|
||||
public boolean isSocial() {
|
||||
return this.isSocial;
|
||||
}
|
||||
|
||||
public void setSocial(boolean isSocial) {
|
||||
this.isSocial = isSocial;
|
||||
}
|
||||
|
||||
public String getProviderAlias() {
|
||||
return providerAlias;
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.services.resources.account;
|
|||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.SortedSet;
|
||||
|
@ -35,6 +36,7 @@ import javax.ws.rs.core.Response;
|
|||
import javax.ws.rs.core.UriBuilder;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.broker.social.SocialIdentityProvider;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.credential.CredentialModel;
|
||||
import org.keycloak.events.Details;
|
||||
|
@ -48,6 +50,7 @@ import org.keycloak.models.KeycloakSession;
|
|||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.representations.account.AccountLinkUriRepresentation;
|
||||
import org.keycloak.representations.account.LinkedAccountRepresentation;
|
||||
import org.keycloak.services.ErrorResponse;
|
||||
|
@ -96,6 +99,16 @@ public class LinkedAccountsResource {
|
|||
SortedSet<LinkedAccountRepresentation> linkedAccounts = getLinkedAccounts(this.session, this.realm, this.user);
|
||||
return Cors.add(request, Response.ok(linkedAccounts)).auth().allowedOrigins(auth.getToken()).build();
|
||||
}
|
||||
|
||||
private Set<String> findSocialIds() {
|
||||
Set<String> socialIds = new HashSet();
|
||||
List<ProviderFactory> providerFactories = session.getKeycloakSessionFactory().getProviderFactories(SocialIdentityProvider.class);
|
||||
for (ProviderFactory factory: providerFactories) {
|
||||
socialIds.add(factory.getId());
|
||||
}
|
||||
|
||||
return socialIds;
|
||||
}
|
||||
|
||||
public SortedSet<LinkedAccountRepresentation> getLinkedAccounts(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
|
||||
|
@ -103,6 +116,7 @@ public class LinkedAccountsResource {
|
|||
|
||||
if (identityProviders == null || identityProviders.isEmpty()) return linkedAccounts;
|
||||
|
||||
Set<String> socialIds = findSocialIds();
|
||||
Set<FederatedIdentityModel> identities = session.users().getFederatedIdentities(user, realm);
|
||||
for (IdentityProviderModel provider : identityProviders) {
|
||||
if (!provider.isEnabled()) {
|
||||
|
@ -117,6 +131,7 @@ public class LinkedAccountsResource {
|
|||
|
||||
LinkedAccountRepresentation rep = new LinkedAccountRepresentation();
|
||||
rep.setConnected(identity != null);
|
||||
rep.setSocial(socialIds.contains(provider.getProviderId()));
|
||||
rep.setProviderAlias(providerId);
|
||||
rep.setDisplayName(displayName);
|
||||
rep.setGuiOrder(guiOrder);
|
||||
|
|
|
@ -39,4 +39,17 @@ add=Add
|
|||
shareSuccess=Resource successfully shared.
|
||||
resourceAlreadyShared=Resource is already shared with this user.
|
||||
resourceNotShared=This resource is not shared.
|
||||
permissionRequests=Permission Requests
|
||||
permissionRequests=Permission Requests
|
||||
|
||||
# Linked Accounts Page
|
||||
linkedAccountsTitle=Linked Accounts
|
||||
linkedAccountsIntroMessage=Manage logins through third-party accounts.
|
||||
linkedLoginProviders=Linked Login Providers
|
||||
unlinkedLoginProviders=Unlinked Login Providers
|
||||
linkedEmpty=No Linked Providers
|
||||
unlinkedEmpty=No Unlinked Providers
|
||||
socialLogin=Social Login
|
||||
systemDefined=System Defined
|
||||
link=Link Account
|
||||
unLink=Unlink Account
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2018 Red Hat, Inc. and/or its affiliates.
|
||||
* Copyright 2019 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.
|
||||
|
@ -15,21 +15,213 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
export interface LinkedAccountsPageProps {
|
||||
import {withRouter, RouteComponentProps} from 'react-router-dom';
|
||||
import {AxiosResponse} from 'axios';
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DataList,
|
||||
DataListAction,
|
||||
DataListItemCells,
|
||||
DataListCell,
|
||||
DataListItemRow,
|
||||
Stack,
|
||||
StackItem,
|
||||
Title,
|
||||
TitleLevel,
|
||||
DataListItem,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import {
|
||||
BitbucketIcon,
|
||||
CubeIcon,
|
||||
FacebookIcon,
|
||||
GithubIcon,
|
||||
GitlabIcon,
|
||||
GoogleIcon,
|
||||
InstagramIcon,
|
||||
LinkIcon,
|
||||
LinkedinIcon,
|
||||
MicrosoftIcon,
|
||||
OpenshiftIcon,
|
||||
PaypalIcon,
|
||||
StackOverflowIcon,
|
||||
TwitterIcon,
|
||||
UnlinkIcon
|
||||
} from '@patternfly/react-icons';
|
||||
|
||||
import {AccountServiceClient} from '../../account-service/account.service';
|
||||
import {Msg} from '../../widgets/Msg';
|
||||
import {ContentPage} from '../ContentPage';
|
||||
import {createRedirect} from '../../util/RedirectUri';
|
||||
|
||||
interface LinkedAccount {
|
||||
connected: boolean;
|
||||
social: boolean;
|
||||
providerAlias: string;
|
||||
providerName: string;
|
||||
displayName: string;
|
||||
linkedUsername: string;
|
||||
}
|
||||
|
||||
export class LinkedAccountsPage extends React.Component<LinkedAccountsPageProps> {
|
||||
|
||||
interface LinkedAccountsPageProps extends RouteComponentProps {
|
||||
}
|
||||
|
||||
interface LinkedAccountsPageState {
|
||||
linkedAccounts: LinkedAccount[];
|
||||
unLinkedAccounts: LinkedAccount[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Stan Silvert
|
||||
*/
|
||||
class LinkedAccountsPage extends React.Component<LinkedAccountsPageProps, LinkedAccountsPageState> {
|
||||
|
||||
public constructor(props: LinkedAccountsPageProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
linkedAccounts: [],
|
||||
unLinkedAccounts: []
|
||||
}
|
||||
|
||||
this.getLinkedAccounts();
|
||||
}
|
||||
|
||||
private getLinkedAccounts(): void {
|
||||
AccountServiceClient.Instance.doGet("/linked-accounts")
|
||||
.then((response: AxiosResponse<LinkedAccount[]>) => {
|
||||
console.log({response});
|
||||
const linkedAccounts = response.data.filter((account) => account.connected);
|
||||
const unLinkedAccounts = response.data.filter((account) => !account.connected);
|
||||
this.setState({linkedAccounts: linkedAccounts, unLinkedAccounts: unLinkedAccounts});
|
||||
});
|
||||
}
|
||||
|
||||
private unLinkAccount(account: LinkedAccount): void {
|
||||
const url = '/linked-accounts/' + account.providerName;
|
||||
|
||||
AccountServiceClient.Instance.doDelete(url)
|
||||
.then((response: AxiosResponse) => {
|
||||
console.log({response});
|
||||
this.getLinkedAccounts();
|
||||
});
|
||||
}
|
||||
|
||||
private linkAccount(account: LinkedAccount): void {
|
||||
const url = '/linked-accounts/' + account.providerName;
|
||||
|
||||
const redirectUri: string = createRedirect(this.props.location.pathname);
|
||||
|
||||
AccountServiceClient.Instance.doGet(url, { params: {providerId: account.providerName, redirectUri}})
|
||||
.then((response: AxiosResponse<{accountLinkUri: string}>) => {
|
||||
console.log({response});
|
||||
window.location.href = response.data.accountLinkUri;
|
||||
});
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Hello Linked Accounts Page</h2>
|
||||
</div>
|
||||
<ContentPage title={Msg.localize('linkedAccountsTitle')} introMessage={Msg.localize('linkedAccountsIntroMessage')}>
|
||||
<Stack gutter='md'>
|
||||
<StackItem isFilled>
|
||||
<Title headingLevel={TitleLevel.h2} size='2xl'>
|
||||
<Msg msgKey='linkedLoginProviders'/>
|
||||
</Title>
|
||||
<DataList aria-label='foo'>
|
||||
{this.makeRows(this.state.linkedAccounts, true)}
|
||||
</DataList>
|
||||
</StackItem>
|
||||
<StackItem isFilled/>
|
||||
<StackItem isFilled>
|
||||
<Title headingLevel={TitleLevel.h2} size='2xl'>
|
||||
<Msg msgKey='unlinkedLoginProviders'/>
|
||||
</Title>
|
||||
<DataList aria-label='foo'>
|
||||
{this.makeRows(this.state.unLinkedAccounts, false)}
|
||||
</DataList>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</ContentPage>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private emptyRow(isLinked: boolean): React.ReactNode {
|
||||
let isEmptyMessage = '';
|
||||
if (isLinked) {
|
||||
isEmptyMessage = Msg.localize('linkedEmpty');
|
||||
} else {
|
||||
isEmptyMessage = Msg.localize('unlinkedEmpty');
|
||||
}
|
||||
|
||||
return (
|
||||
<DataListItem key='emptyItem' aria-labelledby="empty-item">
|
||||
<DataListItemRow key='emptyRow'>
|
||||
<DataListItemCells dataListCells={[
|
||||
<DataListCell key='empty'><strong>{isEmptyMessage}</strong></DataListCell>
|
||||
]}/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
)
|
||||
}
|
||||
|
||||
private makeRows(accounts: LinkedAccount[], isLinked: boolean): React.ReactNode {
|
||||
if (accounts.length === 0) {
|
||||
return this.emptyRow(isLinked);
|
||||
}
|
||||
|
||||
return (
|
||||
<> {
|
||||
|
||||
accounts.map( (account: LinkedAccount) => (
|
||||
<DataListItem key={account.providerName} aria-labelledby="simple-item1">
|
||||
<DataListItemRow key={account.providerName}>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key='idp'><Stack><StackItem isFilled>{this.findIcon(account)}</StackItem><StackItem isFilled><h2><strong>{account.displayName}</strong></h2></StackItem></Stack></DataListCell>,
|
||||
<DataListCell key='badge'><Stack><StackItem isFilled/><StackItem isFilled>{this.badge(account)}</StackItem></Stack></DataListCell>,
|
||||
<DataListCell key='username'><Stack><StackItem isFilled/><StackItem isFilled>{account.linkedUsername}</StackItem></Stack></DataListCell>,
|
||||
]}/>
|
||||
<DataListAction aria-labelledby='foo' aria-label='foo action' id='setPasswordAction'>
|
||||
{isLinked && <Button variant='link' onClick={() => this.unLinkAccount(account)}><UnlinkIcon size='sm'/> <Msg msgKey='unLink'/></Button>}
|
||||
{!isLinked && <Button variant='link' onClick={() => this.linkAccount(account)}><LinkIcon size='sm'/> <Msg msgKey='link'/></Button>}
|
||||
</DataListAction>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
))
|
||||
|
||||
} </>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
private badge(account: LinkedAccount): React.ReactNode {
|
||||
if (account.social) {
|
||||
return (<Badge><Msg msgKey='socialLogin'/></Badge>);
|
||||
}
|
||||
|
||||
return (<Badge style={{backgroundColor: "green"}} ><Msg msgKey='systemDefined'/></Badge>);
|
||||
}
|
||||
|
||||
private findIcon(account: LinkedAccount): React.ReactNode {
|
||||
if (account.providerName.toLowerCase().includes('github')) return (<GithubIcon size='xl'/>);
|
||||
if (account.providerName.toLowerCase().includes('linkedin')) return (<LinkedinIcon size='xl'/>);
|
||||
if (account.providerName.toLowerCase().includes('facebook')) return (<FacebookIcon size='xl'/>);
|
||||
if (account.providerName.toLowerCase().includes('google')) return (<GoogleIcon size='xl'/>);
|
||||
if (account.providerName.toLowerCase().includes('instagram')) return (<InstagramIcon size='xl'/>);
|
||||
if (account.providerName.toLowerCase().includes('microsoft')) return (<MicrosoftIcon size='xl'/>);
|
||||
if (account.providerName.toLowerCase().includes('bitbucket')) return (<BitbucketIcon size='xl'/>);
|
||||
if (account.providerName.toLowerCase().includes('twitter')) return (<TwitterIcon size='xl'/>);
|
||||
if (account.providerName.toLowerCase().includes('openshift')) return (<OpenshiftIcon size='xl'/>);
|
||||
if (account.providerName.toLowerCase().includes('gitlab')) return (<GitlabIcon size='xl'/>);
|
||||
if (account.providerName.toLowerCase().includes('paypal')) return (<PaypalIcon size='xl'/>);
|
||||
if (account.providerName.toLowerCase().includes('stackoverflow')) return (<StackOverflowIcon size='xl'/>);
|
||||
|
||||
return (<CubeIcon size='xl'/>);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const LinkedAccountsPagewithRouter = withRouter(LinkedAccountsPage);
|
||||
export {LinkedAccountsPagewithRouter as LinkedAccountsPage};
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
declare const baseUrl: string;
|
||||
declare const referrer: string;
|
||||
declare const referrerUri: string;
|
||||
|
||||
/**
|
||||
* Create a redirect uri that can return to this application with referrer and referrer_uri intact.
|
||||
*
|
||||
* @param currentLocation The ReactRouter location to return to.
|
||||
*
|
||||
* @author Stan Silvert
|
||||
*/
|
||||
export const createRedirect = (currentLocation: string): string => {
|
||||
let redirectUri: string = baseUrl;
|
||||
|
||||
if (typeof referrer !== 'undefined') {
|
||||
// '_hash_' is a workaround for when uri encoding is not
|
||||
// sufficient to escape the # character properly.
|
||||
// The problem is that both the redirect and the application URL contain a hash.
|
||||
// The browser will consider anything after the first hash to be client-side. So
|
||||
// it sees the hash in the redirect param and stops.
|
||||
redirectUri += "?referrer=" + referrer + "&referrer_uri=" + referrerUri.replace('#', '_hash_');
|
||||
}
|
||||
|
||||
return encodeURIComponent(redirectUri) + encodeURIComponent("/#" + currentLocation);
|
||||
}
|
|
@ -318,7 +318,7 @@
|
|||
'./icons/binoculars-icon.js': '@empty',
|
||||
'./icons/biohazard-icon.js': '@empty',
|
||||
'./icons/birthday-cake-icon.js': '@empty',
|
||||
'./icons/bitbucket-icon.js': '@empty',
|
||||
//'./icons/bitbucket-icon.js': '@empty',
|
||||
'./icons/bitcoin-icon.js': '@empty',
|
||||
'./icons/bity-icon.js': '@empty',
|
||||
'./icons/black-tie-icon.js': '@empty',
|
||||
|
@ -543,7 +543,7 @@
|
|||
'./icons/crutch-icon.js': '@empty',
|
||||
'./icons/css3-alt-icon.js': '@empty',
|
||||
'./icons/css3-icon.js': '@empty',
|
||||
'./icons/cube-icon.js': '@empty',
|
||||
//'./icons/cube-icon.js': '@empty',
|
||||
'./icons/cubes-icon.js': '@empty',
|
||||
'./icons/cut-icon.js': '@empty',
|
||||
'./icons/cuttlefish-icon.js': '@empty',
|
||||
|
@ -656,7 +656,7 @@
|
|||
'./icons/eye-icon.js': '@empty',
|
||||
'./icons/eye-slash-icon.js': '@empty',
|
||||
'./icons/facebook-f-icon.js': '@empty',
|
||||
'./icons/facebook-icon.js': '@empty',
|
||||
//'./icons/facebook-icon.js': '@empty',
|
||||
'./icons/facebook-messenger-icon.js': '@empty',
|
||||
'./icons/facebook-square-icon.js': '@empty',
|
||||
'./icons/fantasy-flight-games-icon.js': '@empty',
|
||||
|
@ -763,10 +763,10 @@
|
|||
'./icons/git-icon.js': '@empty',
|
||||
'./icons/git-square-icon.js': '@empty',
|
||||
'./icons/github-alt-icon.js': '@empty',
|
||||
'./icons/github-icon.js': '@empty',
|
||||
//'./icons/github-icon.js': '@empty',
|
||||
'./icons/github-square-icon.js': '@empty',
|
||||
'./icons/gitkraken-icon.js': '@empty',
|
||||
'./icons/gitlab-icon.js': '@empty',
|
||||
//'./icons/gitlab-icon.js': '@empty',
|
||||
'./icons/gitter-icon.js': '@empty',
|
||||
'./icons/glass-cheers-icon.js': '@empty',
|
||||
'./icons/glass-martini-alt-icon.js': '@empty',
|
||||
|
@ -786,7 +786,7 @@
|
|||
'./icons/goodreads-g-icon.js': '@empty',
|
||||
'./icons/goodreads-icon.js': '@empty',
|
||||
'./icons/google-drive-icon.js': '@empty',
|
||||
'./icons/google-icon.js': '@empty',
|
||||
//'./icons/google-icon.js': '@empty',
|
||||
'./icons/google-play-icon.js': '@empty',
|
||||
'./icons/google-plus-g-icon.js': '@empty',
|
||||
'./icons/google-plus-icon.js': '@empty',
|
||||
|
@ -910,7 +910,7 @@
|
|||
//'./icons/info-circle-icon.js': '@empty',
|
||||
'./icons/info-icon.js': '@empty',
|
||||
'./icons/infrastructure-icon.js': '@empty',
|
||||
'./icons/instagram-icon.js': '@empty',
|
||||
//'./icons/instagram-icon.js': '@empty',
|
||||
'./icons/integration-icon.js': '@empty',
|
||||
'./icons/intercom-icon.js': '@empty',
|
||||
//'./icons/internet-explorer-icon.js': '@empty',
|
||||
|
@ -970,8 +970,8 @@
|
|||
'./icons/life-ring-icon.js': '@empty',
|
||||
'./icons/lightbulb-icon.js': '@empty',
|
||||
'./icons/line-icon.js': '@empty',
|
||||
'./icons/link-icon.js': '@empty',
|
||||
'./icons/linkedin-icon.js': '@empty',
|
||||
//'./icons/link-icon.js': '@empty',
|
||||
//'./icons/linkedin-icon.js': '@empty',
|
||||
'./icons/linkedin-in-icon.js': '@empty',
|
||||
'./icons/linode-icon.js': '@empty',
|
||||
'./icons/linux-icon.js': '@empty',
|
||||
|
@ -1040,7 +1040,7 @@
|
|||
'./icons/microphone-icon.js': '@empty',
|
||||
'./icons/microphone-slash-icon.js': '@empty',
|
||||
'./icons/microscope-icon.js': '@empty',
|
||||
'./icons/microsoft-icon.js': '@empty',
|
||||
//'./icons/microsoft-icon.js': '@empty',
|
||||
'./icons/middleware-icon.js': '@empty',
|
||||
'./icons/migration-icon.js': '@empty',
|
||||
'./icons/minus-circle-icon.js': '@empty',
|
||||
|
@ -1098,7 +1098,7 @@
|
|||
'./icons/on-running-icon.js': '@empty',
|
||||
'./icons/opencart-icon.js': '@empty',
|
||||
'./icons/openid-icon.js': '@empty',
|
||||
'./icons/openshift-icon.js': '@empty',
|
||||
//'./icons/openshift-icon.js': '@empty',
|
||||
//'./icons/opera-icon.js': '@empty',
|
||||
'./icons/optimize-icon.js': '@empty',
|
||||
'./icons/optin-monster-icon.js': '@empty',
|
||||
|
@ -1281,7 +1281,7 @@
|
|||
'./icons/pause-icon.js': '@empty',
|
||||
'./icons/paused-icon.js': '@empty',
|
||||
'./icons/paw-icon.js': '@empty',
|
||||
'./icons/paypal-icon.js': '@empty',
|
||||
//'./icons/paypal-icon.js': '@empty',
|
||||
'./icons/peace-icon.js': '@empty',
|
||||
'./icons/pen-alt-icon.js': '@empty',
|
||||
'./icons/pen-fancy-icon.js': '@empty',
|
||||
|
@ -1549,7 +1549,7 @@
|
|||
'./icons/square-root-alt-icon.js': '@empty',
|
||||
'./icons/squarespace-icon.js': '@empty',
|
||||
'./icons/stack-exchange-icon.js': '@empty',
|
||||
'./icons/stack-overflow-icon.js': '@empty',
|
||||
//'./icons/stack-overflow-icon.js': '@empty',
|
||||
'./icons/stackpath-icon.js': '@empty',
|
||||
'./icons/stamp-icon.js': '@empty',
|
||||
'./icons/star-and-crescent-icon.js': '@empty',
|
||||
|
@ -1688,7 +1688,7 @@
|
|||
'./icons/tumblr-square-icon.js': '@empty',
|
||||
'./icons/tv-icon.js': '@empty',
|
||||
'./icons/twitch-icon.js': '@empty',
|
||||
'./icons/twitter-icon.js': '@empty',
|
||||
//'./icons/twitter-icon.js': '@empty',
|
||||
'./icons/twitter-square-icon.js': '@empty',
|
||||
'./icons/typo3-icon.js': '@empty',
|
||||
'./icons/uber-icon.js': '@empty',
|
||||
|
@ -1703,7 +1703,7 @@
|
|||
'./icons/universal-access-icon.js': '@empty',
|
||||
'./icons/university-icon.js': '@empty',
|
||||
'./icons/unknown-icon.js': '@empty',
|
||||
'./icons/unlink-icon.js': '@empty',
|
||||
//'./icons/unlink-icon.js': '@empty',
|
||||
'./icons/unlock-alt-icon.js': '@empty',
|
||||
'./icons/unlock-icon.js': '@empty',
|
||||
'./icons/unlocked-icon.js': '@empty',
|
||||
|
|
Loading…
Reference in a new issue