KEYCLOAK-13978 onTokenExpired + onAuthRefreshError

implemented handlers and use context for "services"
This commit is contained in:
Erik Jan de Wit 2020-06-11 10:51:36 +02:00 committed by Bruno Oliveira da Silva
parent c0744daa5b
commit f37fa31639
19 changed files with 211 additions and 151 deletions

View file

@ -30,22 +30,23 @@ import {
PageSidebar,
} from '@patternfly/react-core';
import { KeycloakContext } from './keycloak-service/KeycloakContext';
declare function toggleReact(): void;
declare function isWelcomePage(): boolean;
declare function loggedInUserName(): string;
declare const locale: string;
declare const resourceUrl: string;
declare const brandImg: string;
declare const brandUrl: string;
export interface AppProps {};
export class App extends React.Component<AppProps> {
private kcSvc: KeycloakService = KeycloakService.Instance;
static contextType = KeycloakContext;
context: React.ContextType<typeof KeycloakContext>;
public constructor(props: AppProps) {
public constructor(props: AppProps, context: React.ContextType<typeof KeycloakContext>) {
super(props);
this.context = context;
toggleReact();
}
@ -53,8 +54,8 @@ export class App extends React.Component<AppProps> {
toggleReact();
// check login
if (!this.kcSvc.authenticated() && !isWelcomePage()) {
this.kcSvc.login();
if (!this.context!.authenticated() && !isWelcomePage()) {
this.context!.login();
}
const username = (

View file

@ -22,6 +22,13 @@ import {HashRouter} from 'react-router-dom';
import {App} from './App';
import {ContentItem, ModulePageDef, flattenContent, initGroupAndItemIds, isExpansion, isModulePageDef} from './ContentPages';
import { KeycloakClient, KeycloakService } from './keycloak-service/keycloak.service';
import { KeycloakContext } from './keycloak-service/KeycloakContext';
import { AccountServiceClient } from './account-service/account.service';
import { AccountServiceContext } from './account-service/AccountServiceContext';
declare const keycloak: KeycloakClient;
declare let isReactLoading: boolean;
declare function toggleReact(): void;
@ -38,9 +45,14 @@ export class Main extends React.Component<MainProps> {
}
public render(): React.ReactNode {
const keycloakService = new KeycloakService(keycloak);
return (
<HashRouter>
<App/>
<KeycloakContext.Provider value={keycloakService}>
<AccountServiceContext.Provider value={new AccountServiceClient(keycloakService)}>
<App/>
</AccountServiceContext.Provider>
</KeycloakContext.Provider>
</HashRouter>
);
}

View file

@ -0,0 +1,4 @@
import * as React from 'react';
import { AccountServiceClient } from './account.service';
export const AccountServiceContext = React.createContext<AccountServiceClient | undefined>(undefined);

View file

@ -38,11 +38,14 @@ export class AccountServiceError extends Error {
*
* @author Stan Silvert ssilvert@redhat.com (C) 2018 Red Hat Inc.
*/
class AccountServiceClient {
private kcSvc: KeycloakService = KeycloakService.Instance;
private accountUrl: string = this.kcSvc.authServerUrl() + 'realms/' + this.kcSvc.realm() + '/account';
export class AccountServiceClient {
private kcSvc: KeycloakService;
private accountUrl: string;
constructor() {}
public constructor(keycloakService: KeycloakService) {
this.kcSvc = keycloakService;
this.accountUrl = this.kcSvc.authServerUrl() + 'realms/' + this.kcSvc.realm() + '/account';
}
public async doGet<T>(endpoint: string,
config?: RequestInitWithParams): Promise<HttpResponse<T>> {
@ -130,9 +133,6 @@ class AccountServiceClient {
}
const AccountService: AccountServiceClient = new AccountServiceClient();
export default AccountService;
window.addEventListener("unhandledrejection", (event: PromiseRejectionEvent) => {
event.promise.catch(error => {
if (error instanceof AccountServiceError) {

View file

@ -16,7 +16,8 @@
import * as React from 'react';
import { ActionGroup, Button, Form, FormGroup, TextInput } from '@patternfly/react-core';
import AccountService, {HttpResponse} from '../../account-service/account.service';
import { HttpResponse, AccountServiceClient } from '../../account-service/account.service';
import { AccountServiceContext } from '../../account-service/AccountServiceContext';
import { Features } from '../../widgets/features';
import { Msg } from '../../widgets/Msg';
import { ContentPage } from '../ContentPage';
@ -46,6 +47,8 @@ interface AccountPageState {
* @author Stan Silvert ssilvert@redhat.com (C) 2018 Red Hat Inc.
*/
export class AccountPage extends React.Component<AccountPageProps, AccountPageState> {
static contextType = AccountServiceContext;
context: React.ContextType<typeof AccountServiceContext>;
private isRegistrationEmailAsUsername: boolean = features.isRegistrationEmailAsUsername;
private isEditUserNameAllowed: boolean = features.isEditUserNameAllowed;
private readonly DEFAULT_STATE: AccountPageState = {
@ -66,13 +69,15 @@ export class AccountPage extends React.Component<AccountPageProps, AccountPageSt
public state: AccountPageState = this.DEFAULT_STATE;
public constructor(props: AccountPageProps) {
public constructor(props: AccountPageProps, context: React.ContextType<typeof AccountServiceContext>) {
super(props);
this.context = context;
this.fetchPersonalInfo();
}
private fetchPersonalInfo(): void {
AccountService.doGet<FormFields>("/")
this.context!.doGet<FormFields>("/")
.then((response: HttpResponse<FormFields>) => {
this.setState(this.DEFAULT_STATE);
const formFields = response.data;
@ -104,7 +109,7 @@ export class AccountPage extends React.Component<AccountPageProps, AccountPageSt
const isValid = form.checkValidity();
if (isValid) {
const reqData: FormFields = { ...this.state.formFields };
AccountService.doPost<void>("/", reqData)
this.context!.doPost<void>("/", reqData)
.then(() => {
ContentAlert.success('accountUpdatedMessage');
if (locale !== this.state.formFields.attributes!.locale![0]) {

View file

@ -31,6 +31,8 @@ import {
EmptyStateBody
} from '@patternfly/react-core';
import { PassportIcon } from '@patternfly/react-icons';
import { KeycloakService } from '../../keycloak-service/keycloak.service';
import { KeycloakContext } from '../../keycloak-service/KeycloakContext';
// Note: This class demonstrates two features of the ContentPages framework:
// 1) The PageDef is available as a React property.
@ -57,8 +59,8 @@ class ApplicationInitiatedActionPage extends React.Component<AppInitiatedActionP
super(props);
}
private handleClick = (): void => {
new AIACommand(this.props.pageDef.kcAction).execute();
private handleClick = (keycloak: KeycloakService): void => {
new AIACommand(keycloak, this.props.pageDef.kcAction).execute();
}
public render(): React.ReactNode {
@ -71,9 +73,14 @@ class ApplicationInitiatedActionPage extends React.Component<AppInitiatedActionP
<EmptyStateBody>
<Msg msgKey="actionRequiresIDP"/>
</EmptyStateBody>
<Button variant="primary"
onClick={this.handleClick}
target="_blank"><Msg msgKey="continue"/></Button>
<KeycloakContext.Consumer>
{ keycloak => (
<Button variant="primary"
onClick={() => this.handleClick(keycloak!)}
target="_blank"><Msg msgKey="continue"/></Button>
)}
</KeycloakContext.Consumer>
</EmptyState>
);
}

View file

@ -31,7 +31,8 @@ import {
import { InfoAltIcon, CheckIcon, BuilderImageIcon } from '@patternfly/react-icons';
import { ContentPage } from '../ContentPage';
import { ContinueCancelModal } from '../../widgets/ContinueCancelModal';
import AccountService, {HttpResponse} from '../../account-service/account.service';
import { HttpResponse } from '../../account-service/account.service';
import { AccountServiceContext } from '../../account-service/AccountServiceContext';
import { Msg } from '../../widgets/Msg';
declare const locale: string;
@ -69,9 +70,12 @@ interface Application {
}
export class ApplicationsPage extends React.Component<ApplicationsPageProps, ApplicationsPageState> {
static contextType = AccountServiceContext;
context: React.ContextType<typeof AccountServiceContext>;
public constructor(props: ApplicationsPageProps) {
public constructor(props: ApplicationsPageProps, context: React.ContextType<typeof AccountServiceContext>) {
super(props);
this.context = context;
this.state = {
isRowOpen: [],
applications: []
@ -81,7 +85,7 @@ export class ApplicationsPage extends React.Component<ApplicationsPageProps, App
}
private removeConsent = (clientId: string) => {
AccountService.doDelete("/applications/" + clientId + "/consent")
this.context!.doDelete("/applications/" + clientId + "/consent")
.then(() => {
this.fetchApplications();
});
@ -94,7 +98,7 @@ export class ApplicationsPage extends React.Component<ApplicationsPageProps, App
};
private fetchApplications(): void {
AccountService.doGet<Application[]>("/applications")
this.context!.doGet<Application[]>("/applications")
.then((response: HttpResponse<Application[]>) => {
const applications = response.data || [];
this.setState({

View file

@ -16,7 +16,8 @@
import * as React from 'react';
import AccountService, {HttpResponse} from '../../account-service/account.service';
import {HttpResponse} from '../../account-service/account.service';
import { AccountServiceContext } from '../../account-service/AccountServiceContext';
import TimeUtil from '../../util/TimeUtil';
import {
@ -46,12 +47,12 @@ import {
import {Msg} from '../../widgets/Msg';
import {ContinueCancelModal} from '../../widgets/ContinueCancelModal';
import {KeycloakService} from '../../keycloak-service/keycloak.service';
import { KeycloakService } from '../../keycloak-service/keycloak.service';
import { KeycloakContext } from '../../keycloak-service/KeycloakContext';
import {ContentPage} from '../ContentPage';
import { ContentAlert } from '../ContentAlert';
declare const baseUrl: string;
export interface DeviceActivityPageProps {
}
@ -91,9 +92,12 @@ interface Client {
* @author Stan Silvert ssilvert@redhat.com (C) 2019 Red Hat Inc.
*/
export class DeviceActivityPage extends React.Component<DeviceActivityPageProps, DeviceActivityPageState> {
static contextType = AccountServiceContext;
context: React.ContextType<typeof AccountServiceContext>;
public constructor(props: DeviceActivityPageProps) {
public constructor(props: DeviceActivityPageProps, context: React.ContextType<typeof AccountServiceContext>) {
super(props);
this.context = context;
this.state = {
devices: []
@ -102,15 +106,15 @@ export class DeviceActivityPage extends React.Component<DeviceActivityPageProps,
this.fetchDevices();
}
private signOutAll = () => {
AccountService.doDelete("/sessions")
private signOutAll = (keycloakService: KeycloakService) => {
this.context!.doDelete("/sessions")
.then( () => {
KeycloakService.Instance.logout(baseUrl);
keycloakService.logout();
});
}
private signOutSession = (device: Device, session: Session) => {
AccountService.doDelete("/sessions/" + session.id)
this.context!.doDelete("/sessions/" + session.id)
.then (() => {
this.fetchDevices();
ContentAlert.success('signedOutSession', [session.browser, device.os]);
@ -118,7 +122,7 @@ export class DeviceActivityPage extends React.Component<DeviceActivityPageProps,
}
private fetchDevices(): void {
AccountService.doGet<Device[]>("/sessions/devices")
this.context!.doGet<Device[]>("/sessions/devices")
.then((response: HttpResponse<Device[]>) => {
console.log({response});
@ -232,16 +236,20 @@ export class DeviceActivityPage extends React.Component<DeviceActivityPageProps,
</p>
</div>
</DataListCell>,
<DataListCell key='signOutAllButton' width={1}>
{this.isShowSignOutAll(this.state.devices) &&
<ContinueCancelModal buttonTitle='signOutAllDevices'
buttonId='sign-out-all'
modalTitle='signOutAllDevices'
modalMessage='signOutAllDevicesWarning'
onContinue={this.signOutAll}
/>
}
</DataListCell>
<KeycloakContext.Consumer>
{ (keycloak: KeycloakService) => (
<DataListCell key='signOutAllButton' width={1}>
{this.isShowSignOutAll(this.state.devices) &&
<ContinueCancelModal buttonTitle='signOutAllDevices'
buttonId='sign-out-all'
modalTitle='signOutAllDevices'
modalMessage='signOutAllDevicesWarning'
onContinue={() => this.signOutAll(keycloak)}
/>
}
</DataListCell>
)}
</KeycloakContext.Consumer>
]}
/>
</DataListItemRow>

View file

@ -50,7 +50,8 @@ import {
UnlinkIcon
} from '@patternfly/react-icons';
import AccountService, {HttpResponse} from '../../account-service/account.service';
import {HttpResponse} from '../../account-service/account.service';
import {AccountServiceContext} from '../../account-service/AccountServiceContext';
import {Msg} from '../../widgets/Msg';
import {ContentPage} from '../ContentPage';
import {createRedirect} from '../../util/RedirectUri';
@ -76,9 +77,13 @@ interface LinkedAccountsPageState {
* @author Stan Silvert
*/
class LinkedAccountsPage extends React.Component<LinkedAccountsPageProps, LinkedAccountsPageState> {
static contextType = AccountServiceContext;
context: React.ContextType<typeof AccountServiceContext>;
public constructor(props: LinkedAccountsPageProps) {
public constructor(props: LinkedAccountsPageProps, context: React.ContextType<typeof AccountServiceContext>) {
super(props);
this.context = context;
this.state = {
linkedAccounts: [],
unLinkedAccounts: []
@ -88,7 +93,7 @@ class LinkedAccountsPage extends React.Component<LinkedAccountsPageProps, Linked
}
private getLinkedAccounts(): void {
AccountService.doGet<LinkedAccount[]>("/linked-accounts")
this.context!.doGet<LinkedAccount[]>("/linked-accounts")
.then((response: HttpResponse<LinkedAccount[]>) => {
console.log({response});
const linkedAccounts = response.data!.filter((account) => account.connected);
@ -100,7 +105,7 @@ class LinkedAccountsPage extends React.Component<LinkedAccountsPageProps, Linked
private unLinkAccount(account: LinkedAccount): void {
const url = '/linked-accounts/' + account.providerName;
AccountService.doDelete<void>(url)
this.context!.doDelete<void>(url)
.then((response: HttpResponse<void>) => {
console.log({response});
this.getLinkedAccounts();
@ -112,7 +117,7 @@ class LinkedAccountsPage extends React.Component<LinkedAccountsPageProps, Linked
const redirectUri: string = createRedirect(this.props.location.pathname);
AccountService.doGet<{accountLinkUri: string}>(url, { params: {providerId: account.providerName, redirectUri}})
this.context!.doGet<{accountLinkUri: string}>(url, { params: {providerId: account.providerName, redirectUri}})
.then((response: HttpResponse<{accountLinkUri: string}>) => {
console.log({response});
window.location.href = response.data!.accountLinkUri;

View file

@ -28,7 +28,7 @@ import { OkIcon } from '@patternfly/react-icons';
import { Resource, Permission, Scope } from './MyResourcesPage';
import { Msg } from '../../widgets/Msg';
import AccountService from '../../account-service/account.service';
import { AccountServiceContext } from '../../account-service/AccountServiceContext';
import { ContentAlert } from '../ContentAlert';
import { PermissionSelect } from './PermissionSelect';
@ -46,9 +46,12 @@ interface EditTheResourceState {
export class EditTheResource extends React.Component<EditTheResourceProps, EditTheResourceState> {
protected static defaultProps = { permissions: [] };
static contextType = AccountServiceContext;
context: React.ContextType<typeof AccountServiceContext>;
public constructor(props: EditTheResourceProps) {
public constructor(props: EditTheResourceProps, context: React.ContextType<typeof AccountServiceContext>) {
super(props);
this.context = context;
this.state = {
changed: [],
@ -77,7 +80,7 @@ export class EditTheResource extends React.Component<EditTheResourceProps, EditT
}
async savePermission(permission: Permission): Promise<void> {
await AccountService.doPut(`/resources/${this.props.resource._id}/permissions`, [permission]);
await this.context!.doPut(`/resources/${this.props.resource._id}/permissions`, [permission]);
ContentAlert.success(Msg.localize('updateSuccess'));
}

View file

@ -20,7 +20,8 @@ import parse from '../../util/ParseLink';
import { Button, Level, LevelItem, Stack, StackItem, Tab, Tabs, TextInput } from '@patternfly/react-core';
import AccountService, {HttpResponse} from '../../account-service/account.service';
import {HttpResponse} from '../../account-service/account.service';
import {AccountServiceContext} from '../../account-service/AccountServiceContext';
import {ResourcesTable} from './ResourcesTable';
import {ContentPage} from '../ContentPage';
@ -83,11 +84,15 @@ const MY_RESOURCES_TAB = 0;
const SHARED_WITH_ME_TAB = 1;
export class MyResourcesPage extends React.Component<MyResourcesPageProps, MyResourcesPageState> {
static contextType = AccountServiceContext;
context: React.ContextType<typeof AccountServiceContext>;
private first = 0;
private max = 5;
public constructor(props: MyResourcesPageProps) {
public constructor(props: MyResourcesPageProps, context: React.ContextType<typeof AccountServiceContext>) {
super(props);
this.context = context;
this.state = {
activeTabKey: MY_RESOURCES_TAB,
nameFilter: '',
@ -136,7 +141,7 @@ export class MyResourcesPage extends React.Component<MyResourcesPageProps, MyRes
}
private fetchResources(url: string, extraParams?: Record<string, string|number>): void {
AccountService.doGet<Resource[]>(url, {params: extraParams})
this.context!.doGet<Resource[]>(url, {params: extraParams})
.then((response: HttpResponse<Resource[]>) => {
const resources: Resource[] = response.data || [];
resources.forEach((resource: Resource) => resource.shareRequests = []);
@ -163,7 +168,7 @@ export class MyResourcesPage extends React.Component<MyResourcesPageProps, MyRes
}
private fetchShareRequests(resource: Resource): void {
AccountService.doGet('/resources/' + resource._id + '/permissions/requests')
this.context!.doGet('/resources/' + resource._id + '/permissions/requests')
.then((response: HttpResponse<Permission[]>) => {
resource.shareRequests = response.data || [];
if (resource.shareRequests.length > 0) {
@ -173,7 +178,7 @@ export class MyResourcesPage extends React.Component<MyResourcesPageProps, MyRes
}
private fetchPending = async () => {
const response: HttpResponse<Resource[]> = await AccountService.doGet(`/resources/pending-requests`);
const response: HttpResponse<Resource[]> = await this.context!.doGet(`/resources/pending-requests`);
const resources: Resource[] = response.data || [];
resources.forEach((pendingRequest: Resource) => {
this.state.sharedWithMe.data.forEach(resource => {

View file

@ -31,7 +31,8 @@ import {
} from '@patternfly/react-core';
import { UserCheckIcon } from '@patternfly/react-icons';
import AccountService, {HttpResponse} from '../../account-service/account.service';
import { HttpResponse } from '../../account-service/account.service';
import { AccountServiceContext } from '../../account-service/AccountServiceContext';
import { Msg } from '../../widgets/Msg';
import { ContentAlert } from '../ContentAlert';
import { Resource, Scope, Permission } from './MyResourcesPage';
@ -48,10 +49,13 @@ interface PermissionRequestState {
export class PermissionRequest extends React.Component<PermissionRequestProps, PermissionRequestState> {
protected static defaultProps = { permissions: [], row: 0 };
static contextType = AccountServiceContext;
context: React.ContextType<typeof AccountServiceContext>;
public constructor(props: PermissionRequestProps) {
public constructor(props: PermissionRequestProps, context: React.ContextType<typeof AccountServiceContext>) {
super(props);
this.context = context;
this.state = {
isOpen: false,
};
@ -71,7 +75,7 @@ export class PermissionRequest extends React.Component<PermissionRequestProps, P
const id = this.props.resource._id
this.handleToggleDialog();
const permissionsRequest: HttpResponse<Permission[]> = await AccountService.doGet(`/resources/${id}/permissions`);
const permissionsRequest: HttpResponse<Permission[]> = await this.context!.doGet(`/resources/${id}/permissions`);
const permissions = permissionsRequest.data || [];
const foundPermission = permissions.find(p => p.username === username);
const userScopes = foundPermission ? (foundPermission.scopes as Scope[]): [];
@ -79,7 +83,7 @@ export class PermissionRequest extends React.Component<PermissionRequestProps, P
userScopes.push(...scopes);
}
try {
await AccountService.doPut(`/resources/${id}/permissions`, [{ username: username, scopes: userScopes }] )
await this.context!.doPut(`/resources/${id}/permissions`, [{ username: username, scopes: userScopes }] )
ContentAlert.success(Msg.localize('shareSuccess'));
this.props.onClose();
} catch (e) {

View file

@ -38,7 +38,8 @@ import { css } from '@patternfly/react-styles';
import { Remove2Icon, RepositoryIcon, ShareAltIcon, EditAltIcon } from '@patternfly/react-icons';
import AccountService, { HttpResponse } from '../../account-service/account.service';
import { HttpResponse } from '../../account-service/account.service';
import { AccountServiceContext } from '../../account-service/AccountServiceContext';
import { PermissionRequest } from "./PermissionRequest";
import { ShareTheResource } from "./ShareTheResource";
import { Permission, Resource } from "./MyResourcesPage";
@ -56,9 +57,13 @@ export interface CollapsibleResourcesTableState extends ResourcesTableState {
}
export class ResourcesTable extends AbstractResourcesTable<CollapsibleResourcesTableState> {
static contextType = AccountServiceContext;
context: React.ContextType<typeof AccountServiceContext>;
public constructor(props: ResourcesTableProps) {
public constructor(props: ResourcesTableProps, context: React.ContextType<typeof AccountServiceContext>) {
super(props);
this.context = context;
this.state = {
isRowOpen: [],
contextOpen: [],
@ -87,7 +92,7 @@ export class ResourcesTable extends AbstractResourcesTable<CollapsibleResourcesT
}
private fetchPermissions(resource: Resource, row: number): void {
AccountService.doGet(`/resources/${resource._id}/permissions`)
this.context!.doGet(`/resources/${resource._id}/permissions`)
.then((response: HttpResponse<Permission[]>) => {
const newPermissions: Map<number, Permission[]> = new Map(this.state.permissions);
newPermissions.set(row, response.data || []);
@ -97,7 +102,7 @@ export class ResourcesTable extends AbstractResourcesTable<CollapsibleResourcesT
private removeShare(resource: Resource, row: number): Promise<void> {
const permissions = this.state.permissions.get(row)!.map(a => ({ username: a.username, scopes: [] }));
return AccountService.doPut(`/resources/${resource._id}/permissions`, permissions)
return this.context!.doPut(`/resources/${resource._id}/permissions`, permissions)
.then(() => {
ContentAlert.success(Msg.localize('unShareSuccess'));
});

View file

@ -31,7 +31,7 @@ import {
TextInput
} from '@patternfly/react-core';
import AccountService from '../../account-service/account.service';
import { AccountServiceContext } from '../../account-service/AccountServiceContext';
import { Resource, Permission, Scope } from './MyResourcesPage';
import { Msg } from '../../widgets/Msg';
import {ContentAlert} from '../ContentAlert';
@ -58,10 +58,13 @@ interface ShareTheResourceState {
*/
export class ShareTheResource extends React.Component<ShareTheResourceProps, ShareTheResourceState> {
protected static defaultProps = {permissions: []};
static contextType = AccountServiceContext;
context: React.ContextType<typeof AccountServiceContext>;
public constructor(props: ShareTheResourceProps) {
public constructor(props: ShareTheResourceProps, context: React.ContextType<typeof AccountServiceContext>) {
super(props);
this.context = context;
this.state = {
isOpen: false,
permissionsSelected: [],
@ -96,7 +99,7 @@ export class ShareTheResource extends React.Component<ShareTheResourceProps, Sha
this.handleToggleDialog();
AccountService.doPut(`/resources/${rscId}/permissions`, permissions)
this.context!.doPut(`/resources/${rscId}/permissions`, permissions)
.then(() => {
ContentAlert.success('shareSuccess');
this.props.onClose();
@ -119,7 +122,7 @@ export class ShareTheResource extends React.Component<ShareTheResourceProps, Sha
private handleAddUsername = async () => {
if ((this.state.usernameInput !== '') && (!this.state.usernames.includes(this.state.usernameInput))) {
const response = await AccountService.doGet<{username: string}>(`/resources/${this.props.resource._id}/user`, { params: { value: this.state.usernameInput } });
const response = await this.context!.doGet<{username: string}>(`/resources/${this.props.resource._id}/user`, { params: { value: this.state.usernameInput } });
if (response.data && response.data.username) {
this.setState({ usernameInput: '', usernames: [...this.state.usernames, this.state.usernameInput] });
} else {

View file

@ -33,12 +33,15 @@ import {
import {AIACommand} from '../../util/AIACommand';
import TimeUtil from '../../util/TimeUtil';
import AccountService, {HttpResponse} from '../../account-service/account.service';
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';
declare const features: Features;
@ -84,9 +87,13 @@ interface SigningInPageState {
* @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) {
public constructor(props: SigningInPageProps, context: React.ContextType<typeof AccountServiceContext>) {
super(props);
this.context = context;
this.state = {
credentialContainers: new Map()
}
@ -95,7 +102,7 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
}
private getCredentialContainers(): void {
AccountService.doGet("/credentials")
this.context!.doGet("/credentials")
.then((response: HttpResponse<CredentialContainer[]>) => {
const allContainers: CredContainerMap = new Map();
@ -115,7 +122,7 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
}
private handleRemove = (credentialId: string, userLabel: string) => {
AccountService.doDelete("/credentials/" + credentialId)
this.context!.doDelete("/credentials/" + credentialId)
.then(() => {
this.getCredentialContainers();
ContentAlert.success('successRemovedMessage', [userLabel]);
@ -154,13 +161,19 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
}
private renderTypes(credTypeMap: CredTypeMap): React.ReactNode {
return (<> {
return (
<KeycloakContext.Consumer>
{ keycloak => (
<>{
Array.from(credTypeMap.keys()).map((credType: CredType, index: number, typeArray: string[]) => ([
this.renderCredTypeTitle(credTypeMap.get(credType)!),
this.renderUserCredentials(credTypeMap, credType),
this.renderCredTypeTitle(credTypeMap.get(credType)!, keycloak!),
this.renderUserCredentials(credTypeMap, credType, keycloak!),
this.renderEmptyRow(credTypeMap.get(credType)!.type, index === typeArray.length - 1)
]))
}</>)
}</>
)}
</KeycloakContext.Consumer>
);
}
private renderEmptyRow(type: string, isLast: boolean): React.ReactNode {
@ -175,7 +188,7 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
)
}
private renderUserCredentials(credTypeMap: CredTypeMap, credType: CredType): React.ReactNode {
private renderUserCredentials(credTypeMap: CredTypeMap, credType: CredType, keycloak: KeycloakService): React.ReactNode {
const credContainer: CredentialContainer = credTypeMap.get(credType)!;
const userCredentials: UserCredential[] = credContainer.userCredentials;
const removeable: boolean = credContainer.removeable;
@ -207,7 +220,7 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
let updateAIA: AIACommand;
if (credContainer.updateAction) {
updateAIA = new AIACommand(credContainer.updateAction);
updateAIA = new AIACommand(keycloak, credContainer.updateAction);
}
return (
@ -239,12 +252,12 @@ class SigningInPage extends React.Component<SigningInPageProps, SigningInPageSta
return credRowCells;
}
private renderCredTypeTitle(credContainer: CredentialContainer): React.ReactNode {
private renderCredTypeTitle(credContainer: CredentialContainer, keycloak: KeycloakService): React.ReactNode {
if (!credContainer.hasOwnProperty('helptext') && !credContainer.hasOwnProperty('createAction')) return;
let setupAction: AIACommand;
if (credContainer.createAction) {
setupAction = new AIACommand(credContainer.createAction);
setupAction = new AIACommand(keycloak, credContainer.createAction);
}
const credContainerDisplayName: string = Msg.localize(credContainer.displayName);

View file

@ -0,0 +1,4 @@
import * as React from 'react';
import { KeycloakService } from './keycloak.service';
export const KeycloakContext = React.createContext<KeycloakService | undefined>(undefined);

View file

@ -14,90 +14,59 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {KeycloakLoginOptions, KeycloakError} from "../../../../../../../../../../adapters/oidc/js/src/main/resources/keycloak";
// keycloak.js downloaded in index.ftl
declare function Keycloak(config?: string|{}): Keycloak.KeycloakInstance;
import {KeycloakLoginOptions} from "../../../../../../../../../../adapters/oidc/js/src/main/resources/keycloak";
declare const baseUrl: string;
export type KeycloakClient = Keycloak.KeycloakInstance;
type InitOptions = Keycloak.KeycloakInitOptions;
declare const keycloak: KeycloakClient;
export class KeycloakService {
private static keycloakAuth: KeycloakClient = keycloak;
private static instance: KeycloakService = new KeycloakService();
private keycloakAuth: KeycloakClient;
private constructor() {
}
public static get Instance(): KeycloakService {
return this.instance;
}
/**
* Configure and initialize the Keycloak adapter.
*
* @param configOptions Optionally, a path to keycloak.json, or an object containing
* url, realm, and clientId.
* @param adapterOptions Optional initiaization options. See javascript adapter docs
* for details.
* @returns {Promise<T>}
*/
public static init(configOptions?: string|{}, initOptions: InitOptions = {}): Promise<void> {
KeycloakService.keycloakAuth = Keycloak(configOptions);
return new Promise((resolve, reject) => {
KeycloakService.keycloakAuth.init(initOptions)
.success(() => {
resolve();
})
.error((errorData: KeycloakError) => {
reject(errorData);
});
});
public constructor(keycloak: KeycloakClient) {
this.keycloakAuth = keycloak;
this.keycloakAuth.onTokenExpired = () => this.getToken(true).catch(() => this.logout());
this.keycloakAuth.onAuthRefreshError = () => this.logout();
}
public authenticated(): boolean {
return KeycloakService.keycloakAuth.authenticated ? KeycloakService.keycloakAuth.authenticated : false;
return this.keycloakAuth.authenticated ? this.keycloakAuth.authenticated : false;
}
public login(options?: KeycloakLoginOptions): void {
KeycloakService.keycloakAuth.login(options);
this.keycloakAuth.login(options);
}
public logout(redirectUri?: string): void {
KeycloakService.keycloakAuth.logout({redirectUri: redirectUri});
public logout(redirectUri: string = baseUrl): void {
this.keycloakAuth.logout({redirectUri: redirectUri});
}
public account(): void {
KeycloakService.keycloakAuth.accountManagement();
this.keycloakAuth.accountManagement();
}
public authServerUrl(): string | undefined {
const authServerUrl = KeycloakService.keycloakAuth.authServerUrl;
return authServerUrl!.charAt(authServerUrl!.length - 1) === '/' ? authServerUrl : authServerUrl + '/';
const authServerUrl = this.keycloakAuth.authServerUrl;
return authServerUrl!.charAt(authServerUrl!.length - 1) === '/' ? authServerUrl : authServerUrl + '/';
}
public realm(): string | undefined {
return KeycloakService.keycloakAuth.realm;
return this.keycloakAuth.realm;
}
public getToken(): Promise<string> {
public getToken(force: boolean = false): Promise<string> {
return new Promise<string>((resolve, reject) => {
if (KeycloakService.keycloakAuth.token) {
KeycloakService.keycloakAuth
.updateToken(5)
if (this.keycloakAuth.token) {
this.keycloakAuth
.updateToken(force ? -1 : 5)
.success(() => {
resolve(KeycloakService.keycloakAuth.token as string);
resolve(this.keycloakAuth.token as string);
})
.error(() => {
reject('Failed to refresh token');
});
} else {
reject('Not loggen in');
reject('Not logged in');
}
});
}
}
}

View file

@ -21,10 +21,10 @@ import {KeycloakService} from '../keycloak-service/keycloak.service';
*/
export class AIACommand {
constructor(private action: string) {}
constructor(private keycloak: KeycloakService, private action: string) {}
public execute(): void {
KeycloakService.Instance.login({
public execute(): void {
this.keycloak.login({
action: this.action,
})

View file

@ -18,20 +18,24 @@ import * as React from 'react';
import {Msg} from './Msg';
import {KeycloakService} from '../keycloak-service/keycloak.service';
import { KeycloakContext } from '../keycloak-service/KeycloakContext';
import {Button, DropdownItem} from '@patternfly/react-core';
declare const baseUrl: string;
function handleLogout(): void {
KeycloakService.Instance.logout(baseUrl);
function handleLogout(keycloak: KeycloakService): void {
keycloak.logout();
}
interface LogoutProps {}
export class LogoutButton extends React.Component<LogoutProps> {
public render(): React.ReactNode {
return (
<Button id="signOutButton" onClick={handleLogout}><Msg msgKey="doSignOut"/></Button>
<KeycloakContext.Consumer>
{ keycloak => (
<Button id="signOutButton" onClick={() => handleLogout(keycloak!)}><Msg msgKey="doSignOut"/></Button>
)}
</KeycloakContext.Consumer>
);
}
}
@ -40,9 +44,13 @@ interface LogoutDropdownItemProps {}
export class LogoutDropdownItem extends React.Component<LogoutDropdownItemProps> {
public render(): React.ReactNode {
return (
<DropdownItem id="signOutLink" key="logout" onClick={handleLogout}>
{Msg.localize('doSignOut')}
</DropdownItem>
<KeycloakContext.Consumer>
{ keycloak => (
<DropdownItem id="signOutLink" key="logout" onClick={() => handleLogout(keycloak!)}>
{Msg.localize('doSignOut')}
</DropdownItem>
)}
</KeycloakContext.Consumer>
);
}
}