Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Change history for ui-developer

## IN PROGRESS

* Display IST, FLST, RTR values statically (no form). Refs UID-234.

## [11.0.0](https://github.com/folio-org/ui-developer/tree/v11.0.0) (2026-04-21)
[Full Changelog](https://github.com/folio-org/ui-developer/compare/v10.0.0...v11.0.0)

Expand Down
178 changes: 40 additions & 138 deletions src/settings/RefreshTokenRotation.js
Original file line number Diff line number Diff line change
@@ -1,158 +1,60 @@
import { merge } from 'lodash';
import PropTypes from 'prop-types';
import { useCallback, useEffect, useState } from 'react';
import { Field, Form } from 'react-final-form';
import { useEffect, useState } from 'react';
import { FormattedMessage } from 'react-intl';

import { getTokenExpiry } from '@folio/stripes/core';
import { Button, LoadingPane, Pane, PaneHeader, TextField } from '@folio/stripes/components';
import { getTokenExpiry, useStripes } from '@folio/stripes/core';
import { LoadingPane, MessageBanner, Pane, PaneHeader } from '@folio/stripes/components';

/**
* manipulate AT/RT expiration dates in storage in order to test RTR.
* @returns
* display AT/RT expiration data, and IST and RTR config values
*/
const RefreshTokenRotation = ({ stripes }) => {
// why WHY copy this string here instead of importing it from stripes-core?
//
// RTR_FORCE_REFRESH_EVENT will be present in stripes-core 10.2.0 (stripes
// 9.2.0). Importing it would force the stripes peer depedency to bump from
// ^9.1.0 to ^9.2.0.If we copy the string instead of importing it, we can
// remain compatible with 9.1.0.
//
// OK, compatibility is nice. But it's still gross, right? Yep, super gross.
// Aren't you nauseated? Yes, yes I am. 🤢🧼🛁
const RTR_FORCE_REFRESH_EVENT = '@folio/stripes/core::RTRForceRefresh';

const RefreshTokenRotation = () => {
const { config: { rtr } } = useStripes();
const [isLoading, setIsLoading] = useState(true);
const [tokenExpiration, setTokenExpiration] = useState({});
const [expiry, setExpiry] = useState({});

useEffect(() => {
setIsLoading(true);
getTokenExpiry().then((te) => {
setTokenExpiration(te ?? { atExpires: -1, rtExpires: -1 });
setExpiry(te ?? { atExpires: -1, rtExpires: -1 });
setIsLoading(false);
});
}, []);

/**
* forceRefresh
* dispatch an event to force a token rotation
*/
const forceRefresh = useCallback(
() => window.dispatchEvent(new Event(RTR_FORCE_REFRESH_EVENT)),
[],
);

/**
* saveRtrConfig
* update stripes.config.rtr from form
*/
const saveRtrConfig = useCallback(
(values) => {
merge(stripes.config.rtr, {
idleSessionTTL: values.idleSessionTTL,
idleModalTTL: values.idleModalTTL,
fixedLengthSessionWarningTTL: values.fixedLengthSessionWarningTTL,
rotationIntervalFraction: Number(values.rotationIntervalFraction),
activityEvents: values.activityEvents.split(',').map((e) => e.trim()),
});

forceRefresh();
},
[stripes, forceRefresh],
);

if (!isLoading) {
return (
<Pane
defaultWidth="fill"
renderHeader={(renderProps) => (
<PaneHeader {...renderProps} paneTitle={<FormattedMessage id="ui-developer.rtr" />} />
)}
>
<ul>
<li>
stripes logs RTR events in the category <code>rtr</code>
</li>
</ul>
{!isLoading && (
<dl>
<dt>AT Expiry</dt>
<dd>{new Date(tokenExpiration.atExpires).toISOString()}</dd>
<dt>RT Expiry</dt>
<dd>{new Date(tokenExpiration.rtExpires).toISOString()}</dd>
</dl>
)}
<div>
<Button onClick={forceRefresh}>
<FormattedMessage id="ui-developer.rtr.forceRefresh" />
</Button>

<Form
onSubmit={saveRtrConfig}
initialValues={{
...stripes.config.rtr,
activityEvents: stripes.config.rtr.activityEvents.join(','),
}}
>
{({ handleSubmit, pristine, submitting }) => (
<form onSubmit={handleSubmit}>
<Field
component={TextField}
name="idleSessionTTL"
label={<FormattedMessage id="ui-developer.rtr.idleSessionTTL" />}
/>
<Field
component={TextField}
name="idleModalTTL"
label={<FormattedMessage id="ui-developer.rtr.idleModalTTL" />}
/>
<Field
component={TextField}
name="fixedLengthSessionWarningTTL"
label={<FormattedMessage id="ui-developer.rtr.fixedLengthSessionWarningTTL" />}
/>
<Field
component={TextField}
name="rotationIntervalFraction"
label={<FormattedMessage id="ui-developer.rtr.rotationIntervalFraction" />}
type="number"
step={0.01}
min={0}
/>
<Field
component={TextField}
name="activityEvents"
label={<FormattedMessage id="ui-developer.rtr.activityEvents" />}
/>
<Button buttonStyle="primary" type="submit" disabled={pristine || submitting}>
<FormattedMessage id="stripes-core.button.save" />
</Button>
</form>
)}
</Form>
</div>
</Pane>
);
} else {
if (isLoading) {
return <LoadingPane />;
}
};

RefreshTokenRotation.propTypes = {
stripes: PropTypes.shape({
okapi: PropTypes.shape({
tenant: PropTypes.string,
}),
config: PropTypes.shape({
rtr: PropTypes.shape({
idleSessionTTL: PropTypes.string,
idleModalTTL: PropTypes.string,
rotationIntervalFraction: PropTypes.number,
activityEvents: PropTypes.arrayOf(PropTypes.string),
}),
}),
}).isRequired,
return (
<Pane
defaultWidth="fill"
renderHeader={(renderProps) => (
<PaneHeader {...renderProps} paneTitle={<FormattedMessage id="ui-developer.rtr" />} />
)}
>
<MessageBanner>
<FormattedMessage id="ui-developer.rtr.logCategories" />
</MessageBanner>
<dl>
<dt><FormattedMessage id="ui-developer.rtr.atExpiration" /></dt>
<dd>{new Date(expiry.atExpires).toISOString()}</dd>

<dt><FormattedMessage id="ui-developer.rtr.rtExpiration" /></dt>
<dd>{new Date(expiry.rtExpires).toISOString()}</dd>

<dt><FormattedMessage id="ui-developer.rtr.idleSessionTTL" /></dt>
<dd>{rtr.idleSessionTTL}</dd>

<dt><FormattedMessage id="ui-developer.rtr.idleModalTTL" /></dt>
<dd>{rtr.idleModalTTL}</dd>

<dt><FormattedMessage id="ui-developer.rtr.fixedLengthSessionWarningTTL" /></dt>
<dd>{rtr.fixedLengthSessionWarningTTL}</dd>

<dt><FormattedMessage id="ui-developer.rtr.activityEvents" /></dt>
{rtr.activityEvents.toSorted((a, b) => a.localeCompare(b)).map(i => <dd key={i}>{i}</dd>)}
</dl>
</Pane>
);
};

export default RefreshTokenRotation;
10 changes: 6 additions & 4 deletions translations/ui-developer/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,14 @@
"rtr": "Refresh token rotation",
"rtr.forceRefresh": "Force refresh",
"rtr.idleSessionTTL": "idleSessionTTL: duration an idle session lasts before being killed (e.g. 1h, 1m, 5s, 10ms)",
"rtr.idleModalTTL": "idleModalTTL: duration the idle modal should be shown before session is killed (e.g. 1h, 1m, 5s, 10ms)",
"rtr.fixedLengthSessionWarningTTL": "fixedLengthSessionWarningTTL: how long the “your session is going to die!” warning should be shown before the session is killed (e.g. 1h, 1m, 5s, 10ms)",
"rtr.rotationIntervalFraction": "rotationIntervalFraction: decimal fraction of how early to refresh the access token (e.g. 0.6 is 60% into the lifetime of the token)",
"rtr.activityEvents": "activityEvents: which DOM events constitute user activity (comma-separated, e.g. 'mousemove,keydown')",
"rtr.idleModalTTL": "idleModalTTL: duration the 'keep working?' modal is shown before an idle session is killed (e.g. 1h, 1m, 5s, 10ms)",
"rtr.fixedLengthSessionWarningTTL": "fixedLengthSessionWarningTTL: how long the “your session will end soon!” warning should be shown before the session is killed (e.g. 1h, 1m, 5s, 10ms)",
"rtr.activityEvents": "activityEvents: which DOM events constitute user activity",
"rtr.registerServiceWorker": "Register the service worker",
"rtr.unregisterServiceWorker": "Unregister the service worker",
"rtr.logCategories": "stripes logs RTR events in the categories <code>rtr, rtrv</code>",
"rtr.atExpiration": "AT Expiration date (next rotation on/after)",
"rtr.rtExpiration": "RT Expiration date (end of session)",

"sessionTimezone": "Session timezone",
"sessionTimezone.temporarySessionTimezone": "TEMPORARY Session timezone",
Expand Down
9 changes: 6 additions & 3 deletions translations/ui-developer/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,13 @@
"searchBy": "Search by",
"rtr.forceRefresh": "Force refresh",
"rtr.idleSessionTTL": "idleSessionTTL: duration an idle session lasts before being killed (e.g. 1h, 1m, 5s, 10ms)",
"rtr.idleModalTTL": "idleModalTTL: duration the idle modal should be shown before session is killed (e.g. 1h, 1m, 5s, 10ms)",
"rtr.fixedLengthSessionWarningTTL": "fixedLengthSessionWarningTTL: how long the “your session is going to die!” warning should be shown before the session is killed (e.g. 1h, 1m, 5s, 10ms)",
"rtr.idleModalTTL": "idleModalTTL: duration the 'keep working?' modal is shown before an idle session is killed (e.g. 1h, 1m, 5s, 10ms)",
"rtr.fixedLengthSessionWarningTTL": "fixedLengthSessionWarningTTL: how long the “your session will end soon!” warning should be shown before the session is killed (e.g. 1h, 1m, 5s, 10ms)",
"rtr.rotationIntervalFraction": "rotationIntervalFraction: decimal fraction of how early to refresh the access token (e.g. 0.6 is 60% into the lifetime of the token)",
"rtr.activityEvents": "activityEvents: which DOM events constitute user activity (comma-separated, e.g. 'mousemove,keydown')",
"rtr.activityEvents": "activityEvents: which DOM events constitute user activity",
"rtr.logCategories": "stripes logs RTR events in the categories <code>rtr, rtrv</code>",
"rtr.atExpiration": "AT Expiration date (next rotation on/after)",
"rtr.rtExpiration": "RT Expiration date (end of session)",
"sessionTimezone": "Session timezone",
"sessionTimezone.temporarySessionTimezone": "TEMPORARY Session timezone",
"sessionTimezone.timeZone": "Time zone (time zone used when showing date time information)",
Expand Down