From f37fa31639dc188270876c6027e3c1e0e1ac4d26 Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Thu, 11 Jun 2020 10:51:36 +0200 Subject: [PATCH] KEYCLOAK-13978 onTokenExpired + onAuthRefreshError implemented handlers and use context for "services" --- .../keycloak-preview/account/src/app/App.tsx | 15 ++-- .../keycloak-preview/account/src/app/Main.tsx | 14 +++- .../account-service/AccountServiceContext.tsx | 4 + .../app/account-service/account.service.ts | 14 ++-- .../app/content/account-page/AccountPage.tsx | 13 +++- .../aia-page/AppInitiatedActionPage.tsx | 17 +++-- .../applications-page/ApplicationsPage.tsx | 12 ++- .../DeviceActivityPage.tsx | 48 +++++++----- .../LinkedAccountsPage.tsx | 15 ++-- .../my-resources-page/EditTheResource.tsx | 9 ++- .../my-resources-page/MyResourcesPage.tsx | 15 ++-- .../my-resources-page/PermissionRequest.tsx | 14 ++-- .../my-resources-page/ResourcesTable.tsx | 13 +++- .../my-resources-page/ShareTheResource.tsx | 13 ++-- .../content/signingin-page/SigningInPage.tsx | 37 ++++++--- .../app/keycloak-service/KeycloakContext.tsx | 4 + .../app/keycloak-service/keycloak.service.ts | 75 ++++++------------- .../account/src/app/util/AIACommand.ts | 6 +- .../account/src/app/widgets/Logout.tsx | 24 ++++-- 19 files changed, 211 insertions(+), 151 deletions(-) create mode 100644 themes/src/main/resources/theme/keycloak-preview/account/src/app/account-service/AccountServiceContext.tsx create mode 100644 themes/src/main/resources/theme/keycloak-preview/account/src/app/keycloak-service/KeycloakContext.tsx diff --git a/themes/src/main/resources/theme/keycloak-preview/account/src/app/App.tsx b/themes/src/main/resources/theme/keycloak-preview/account/src/app/App.tsx index 2f1e5fd8ae..9a6f25951d 100644 --- a/themes/src/main/resources/theme/keycloak-preview/account/src/app/App.tsx +++ b/themes/src/main/resources/theme/keycloak-preview/account/src/app/App.tsx @@ -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 { - private kcSvc: KeycloakService = KeycloakService.Instance; + static contextType = KeycloakContext; + context: React.ContextType; - public constructor(props: AppProps) { + public constructor(props: AppProps, context: React.ContextType) { super(props); + this.context = context; toggleReact(); } @@ -53,8 +54,8 @@ export class App extends React.Component { toggleReact(); // check login - if (!this.kcSvc.authenticated() && !isWelcomePage()) { - this.kcSvc.login(); + if (!this.context!.authenticated() && !isWelcomePage()) { + this.context!.login(); } const username = ( diff --git a/themes/src/main/resources/theme/keycloak-preview/account/src/app/Main.tsx b/themes/src/main/resources/theme/keycloak-preview/account/src/app/Main.tsx index c45dedfbe8..cf9da334bb 100644 --- a/themes/src/main/resources/theme/keycloak-preview/account/src/app/Main.tsx +++ b/themes/src/main/resources/theme/keycloak-preview/account/src/app/Main.tsx @@ -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 { } public render(): React.ReactNode { + const keycloakService = new KeycloakService(keycloak); return ( - + + + + + ); } diff --git a/themes/src/main/resources/theme/keycloak-preview/account/src/app/account-service/AccountServiceContext.tsx b/themes/src/main/resources/theme/keycloak-preview/account/src/app/account-service/AccountServiceContext.tsx new file mode 100644 index 0000000000..f4c36679eb --- /dev/null +++ b/themes/src/main/resources/theme/keycloak-preview/account/src/app/account-service/AccountServiceContext.tsx @@ -0,0 +1,4 @@ +import * as React from 'react'; +import { AccountServiceClient } from './account.service'; + +export const AccountServiceContext = React.createContext(undefined); \ No newline at end of file diff --git a/themes/src/main/resources/theme/keycloak-preview/account/src/app/account-service/account.service.ts b/themes/src/main/resources/theme/keycloak-preview/account/src/app/account-service/account.service.ts index 9f74627bef..baab4c33a7 100644 --- a/themes/src/main/resources/theme/keycloak-preview/account/src/app/account-service/account.service.ts +++ b/themes/src/main/resources/theme/keycloak-preview/account/src/app/account-service/account.service.ts @@ -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(endpoint: string, config?: RequestInitWithParams): Promise> { @@ -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) { diff --git a/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/account-page/AccountPage.tsx b/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/account-page/AccountPage.tsx index a9df0f6532..da1f337656 100644 --- a/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/account-page/AccountPage.tsx +++ b/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/account-page/AccountPage.tsx @@ -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 { + static contextType = AccountServiceContext; + context: React.ContextType; 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) { super(props); + this.context = context; + this.fetchPersonalInfo(); } private fetchPersonalInfo(): void { - AccountService.doGet("/") + this.context!.doGet("/") .then((response: HttpResponse) => { this.setState(this.DEFAULT_STATE); const formFields = response.data; @@ -104,7 +109,7 @@ export class AccountPage extends React.Component("/", reqData) + this.context!.doPost("/", reqData) .then(() => { ContentAlert.success('accountUpdatedMessage'); if (locale !== this.state.formFields.attributes!.locale![0]) { diff --git a/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/aia-page/AppInitiatedActionPage.tsx b/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/aia-page/AppInitiatedActionPage.tsx index 6a989e631a..b5bb15d214 100644 --- a/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/aia-page/AppInitiatedActionPage.tsx +++ b/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/aia-page/AppInitiatedActionPage.tsx @@ -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 { - 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 - + + { keycloak => ( + + )} + + ); } diff --git a/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/applications-page/ApplicationsPage.tsx b/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/applications-page/ApplicationsPage.tsx index 24b5dfe578..4bd57a231c 100644 --- a/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/applications-page/ApplicationsPage.tsx +++ b/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/applications-page/ApplicationsPage.tsx @@ -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 { + static contextType = AccountServiceContext; + context: React.ContextType; - public constructor(props: ApplicationsPageProps) { + public constructor(props: ApplicationsPageProps, context: React.ContextType) { super(props); + this.context = context; this.state = { isRowOpen: [], applications: [] @@ -81,7 +85,7 @@ export class ApplicationsPage extends React.Component { - AccountService.doDelete("/applications/" + clientId + "/consent") + this.context!.doDelete("/applications/" + clientId + "/consent") .then(() => { this.fetchApplications(); }); @@ -94,7 +98,7 @@ export class ApplicationsPage extends React.Component("/applications") + this.context!.doGet("/applications") .then((response: HttpResponse) => { const applications = response.data || []; this.setState({ diff --git a/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/device-activity-page/DeviceActivityPage.tsx b/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/device-activity-page/DeviceActivityPage.tsx index 49bea9712a..59c80c5404 100644 --- a/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/device-activity-page/DeviceActivityPage.tsx +++ b/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/device-activity-page/DeviceActivityPage.tsx @@ -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 { + static contextType = AccountServiceContext; + context: React.ContextType; - public constructor(props: DeviceActivityPageProps) { + public constructor(props: DeviceActivityPageProps, context: React.ContextType) { super(props); + this.context = context; this.state = { devices: [] @@ -102,15 +106,15 @@ export class DeviceActivityPage extends React.Component { - 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("/sessions/devices") + this.context!.doGet("/sessions/devices") .then((response: HttpResponse) => { console.log({response}); @@ -232,16 +236,20 @@ export class DeviceActivityPage extends React.Component , - - {this.isShowSignOutAll(this.state.devices) && - - } - + + { (keycloak: KeycloakService) => ( + + {this.isShowSignOutAll(this.state.devices) && + this.signOutAll(keycloak)} + /> + } + + )} + ]} /> diff --git a/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/linked-accounts-page/LinkedAccountsPage.tsx b/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/linked-accounts-page/LinkedAccountsPage.tsx index d54f52961c..85a2f45733 100644 --- a/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/linked-accounts-page/LinkedAccountsPage.tsx +++ b/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/linked-accounts-page/LinkedAccountsPage.tsx @@ -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 { + static contextType = AccountServiceContext; + context: React.ContextType; - public constructor(props: LinkedAccountsPageProps) { + public constructor(props: LinkedAccountsPageProps, context: React.ContextType) { super(props); + this.context = context; + this.state = { linkedAccounts: [], unLinkedAccounts: [] @@ -88,7 +93,7 @@ class LinkedAccountsPage extends React.Component("/linked-accounts") + this.context!.doGet("/linked-accounts") .then((response: HttpResponse) => { console.log({response}); const linkedAccounts = response.data!.filter((account) => account.connected); @@ -100,7 +105,7 @@ class LinkedAccountsPage extends React.Component(url) + this.context!.doDelete(url) .then((response: HttpResponse) => { console.log({response}); this.getLinkedAccounts(); @@ -112,7 +117,7 @@ class LinkedAccountsPage extends React.Component(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; diff --git a/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/my-resources-page/EditTheResource.tsx b/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/my-resources-page/EditTheResource.tsx index 0e75ccff03..59d1f76c97 100644 --- a/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/my-resources-page/EditTheResource.tsx +++ b/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/my-resources-page/EditTheResource.tsx @@ -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 { protected static defaultProps = { permissions: [] }; + static contextType = AccountServiceContext; + context: React.ContextType; - public constructor(props: EditTheResourceProps) { + public constructor(props: EditTheResourceProps, context: React.ContextType) { super(props); + this.context = context; this.state = { changed: [], @@ -77,7 +80,7 @@ export class EditTheResource extends React.Component { - 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')); } diff --git a/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/my-resources-page/MyResourcesPage.tsx b/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/my-resources-page/MyResourcesPage.tsx index a32939086b..f1897c1970 100644 --- a/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/my-resources-page/MyResourcesPage.tsx +++ b/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/my-resources-page/MyResourcesPage.tsx @@ -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 { + static contextType = AccountServiceContext; + context: React.ContextType; private first = 0; private max = 5; - public constructor(props: MyResourcesPageProps) { + public constructor(props: MyResourcesPageProps, context: React.ContextType) { super(props); + this.context = context; + this.state = { activeTabKey: MY_RESOURCES_TAB, nameFilter: '', @@ -136,7 +141,7 @@ export class MyResourcesPage extends React.Component): void { - AccountService.doGet(url, {params: extraParams}) + this.context!.doGet(url, {params: extraParams}) .then((response: HttpResponse) => { const resources: Resource[] = response.data || []; resources.forEach((resource: Resource) => resource.shareRequests = []); @@ -163,7 +168,7 @@ export class MyResourcesPage extends React.Component) => { resource.shareRequests = response.data || []; if (resource.shareRequests.length > 0) { @@ -173,7 +178,7 @@ export class MyResourcesPage extends React.Component { - const response: HttpResponse = await AccountService.doGet(`/resources/pending-requests`); + const response: HttpResponse = await this.context!.doGet(`/resources/pending-requests`); const resources: Resource[] = response.data || []; resources.forEach((pendingRequest: Resource) => { this.state.sharedWithMe.data.forEach(resource => { diff --git a/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/my-resources-page/PermissionRequest.tsx b/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/my-resources-page/PermissionRequest.tsx index 50e5f026b5..d328035b8e 100644 --- a/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/my-resources-page/PermissionRequest.tsx +++ b/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/my-resources-page/PermissionRequest.tsx @@ -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 { protected static defaultProps = { permissions: [], row: 0 }; + static contextType = AccountServiceContext; + context: React.ContextType; - public constructor(props: PermissionRequestProps) { + public constructor(props: PermissionRequestProps, context: React.ContextType) { super(props); - + this.context = context; + this.state = { isOpen: false, }; @@ -71,7 +75,7 @@ export class PermissionRequest extends React.Component = await AccountService.doGet(`/resources/${id}/permissions`); + const permissionsRequest: HttpResponse = 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 { + static contextType = AccountServiceContext; + context: React.ContextType; - public constructor(props: ResourcesTableProps) { + public constructor(props: ResourcesTableProps, context: React.ContextType) { super(props); + this.context = context; + this.state = { isRowOpen: [], contextOpen: [], @@ -87,7 +92,7 @@ export class ResourcesTable extends AbstractResourcesTable) => { const newPermissions: Map = new Map(this.state.permissions); newPermissions.set(row, response.data || []); @@ -97,7 +102,7 @@ export class ResourcesTable extends AbstractResourcesTable { 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')); }); diff --git a/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/my-resources-page/ShareTheResource.tsx b/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/my-resources-page/ShareTheResource.tsx index 7adeb973ed..ed4728bd13 100644 --- a/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/my-resources-page/ShareTheResource.tsx +++ b/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/my-resources-page/ShareTheResource.tsx @@ -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 { protected static defaultProps = {permissions: []}; + static contextType = AccountServiceContext; + context: React.ContextType; - public constructor(props: ShareTheResourceProps) { + public constructor(props: ShareTheResourceProps, context: React.ContextType) { super(props); - + this.context = context; + this.state = { isOpen: false, permissionsSelected: [], @@ -96,7 +99,7 @@ export class ShareTheResource extends React.Component { ContentAlert.success('shareSuccess'); this.props.onClose(); @@ -119,7 +122,7 @@ export class ShareTheResource extends React.Component { 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 { diff --git a/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/signingin-page/SigningInPage.tsx b/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/signingin-page/SigningInPage.tsx index adfdb6b2a8..7049b53a65 100644 --- a/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/signingin-page/SigningInPage.tsx +++ b/themes/src/main/resources/theme/keycloak-preview/account/src/app/content/signingin-page/SigningInPage.tsx @@ -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 { + static contextType = AccountServiceContext; + context: React.ContextType; - public constructor(props: SigningInPageProps) { + public constructor(props: SigningInPageProps, context: React.ContextType) { super(props); + this.context = context; + this.state = { credentialContainers: new Map() } @@ -95,7 +102,7 @@ class SigningInPage extends React.Component) => { const allContainers: CredContainerMap = new Map(); @@ -115,7 +122,7 @@ class SigningInPage extends React.Component { - 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 { + return ( + + { 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) ])) - }) + } + )} + + ); } private renderEmptyRow(type: string, isLast: boolean): React.ReactNode { @@ -175,7 +188,7 @@ class SigningInPage extends React.Component(undefined); \ No newline at end of file diff --git a/themes/src/main/resources/theme/keycloak-preview/account/src/app/keycloak-service/keycloak.service.ts b/themes/src/main/resources/theme/keycloak-preview/account/src/app/keycloak-service/keycloak.service.ts index 4924f36420..bf29c12748 100644 --- a/themes/src/main/resources/theme/keycloak-preview/account/src/app/keycloak-service/keycloak.service.ts +++ b/themes/src/main/resources/theme/keycloak-preview/account/src/app/keycloak-service/keycloak.service.ts @@ -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} - */ - public static init(configOptions?: string|{}, initOptions: InitOptions = {}): Promise { - 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 { + public getToken(force: boolean = false): Promise { return new Promise((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'); } }); } -} +} \ No newline at end of file diff --git a/themes/src/main/resources/theme/keycloak-preview/account/src/app/util/AIACommand.ts b/themes/src/main/resources/theme/keycloak-preview/account/src/app/util/AIACommand.ts index a7793a1360..78536de2cc 100644 --- a/themes/src/main/resources/theme/keycloak-preview/account/src/app/util/AIACommand.ts +++ b/themes/src/main/resources/theme/keycloak-preview/account/src/app/util/AIACommand.ts @@ -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, }) diff --git a/themes/src/main/resources/theme/keycloak-preview/account/src/app/widgets/Logout.tsx b/themes/src/main/resources/theme/keycloak-preview/account/src/app/widgets/Logout.tsx index f5351d29bd..cb5910948b 100644 --- a/themes/src/main/resources/theme/keycloak-preview/account/src/app/widgets/Logout.tsx +++ b/themes/src/main/resources/theme/keycloak-preview/account/src/app/widgets/Logout.tsx @@ -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 { public render(): React.ReactNode { return ( - + + { keycloak => ( + + )} + + ); } } @@ -40,9 +44,13 @@ interface LogoutDropdownItemProps {} export class LogoutDropdownItem extends React.Component { public render(): React.ReactNode { return ( - - {Msg.localize('doSignOut')} - + + { keycloak => ( + handleLogout(keycloak!)}> + {Msg.localize('doSignOut')} + + )} + ); } } \ No newline at end of file