New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: support idle management in AuthClient #547
Merged
krpeacock
merged 25 commits into
main
from
SDK-307-support-invalidating-and-replacing-identities
Mar 30, 2022
Merged
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
2afc69a
feat: idleManager
krpeacock f91524e
feat: setup idleManager in authClient
krpeacock 409c32d
test: full lifecycle test of idle logout
krpeacock 5bf003d
some basic documentation
krpeacock f43a3d1
feat: registerActor and registerCallback
krpeacock 6088842
fixing test typing
krpeacock f941b32
reverting actorMap and using onIdle for creation options
krpeacock e826b43
friendlier JSDOC formatting
krpeacock 22a1db2
Merge branch 'main' into SDK-307-support-invalidating-and-replacing-i…
krpeacock 730eae6
changelog
krpeacock d2f0077
Merge branch 'SDK-307-support-invalidating-and-replacing-identities' …
krpeacock 70c5085
typescript checks for tests
krpeacock 2d23b7a
feat: idleManager
krpeacock e8f4378
feat: setup idleManager in authClient
krpeacock 88aba80
test: full lifecycle test of idle logout
krpeacock 6ad006e
some basic documentation
krpeacock fad7732
feat: registerActor and registerCallback
krpeacock 5faae7b
fixing test typing
krpeacock eb10643
reverting actorMap and using onIdle for creation options
krpeacock 3d1d737
friendlier JSDOC formatting
krpeacock 1f113bb
changelog
krpeacock 5a03a97
typescript checks for tests
krpeacock fb5908e
Merge branch 'main' into SDK-307-support-invalidating-and-replacing-i…
krpeacock eaebae3
chore: setting default idle timeout to 10 minutes
krpeacock 03c8782
Merge branch 'SDK-307-support-invalidating-and-replacing-identities' …
krpeacock File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import IdleManager from './idleManager'; | ||
|
||
jest.useFakeTimers(); | ||
|
||
describe('IdleManager tests', () => { | ||
it('should call its callback after time spent inactive', () => { | ||
const cb = jest.fn(); | ||
const manager = IdleManager.create({ onIdle: cb, captureScroll: true }); | ||
expect(cb).not.toHaveBeenCalled(); | ||
// simulate user being inactive for 10 minutes | ||
jest.advanceTimersByTime(10 * 60 * 1000); | ||
expect(cb).toHaveBeenCalled(); | ||
manager.exit(); | ||
}); | ||
it('should delay allow configuration of the timeout', () => { | ||
const cb = jest.fn(); | ||
const extraDelay = 100; | ||
IdleManager.create({ onIdle: cb, idleTimeout: 10 * 60 * 1000 + extraDelay }); | ||
expect(cb).not.toHaveBeenCalled(); | ||
// simulate user being inactive for 10 minutes | ||
jest.advanceTimersByTime(10 * 60 * 1000); | ||
expect(cb).not.toHaveBeenCalled(); | ||
jest.advanceTimersByTime(extraDelay); | ||
expect(cb).toHaveBeenCalled(); | ||
}); | ||
it('should delay its callback on keyboard events', () => { | ||
const cb = jest.fn(); | ||
const manager = IdleManager.create({ onIdle: cb }); | ||
expect(cb).not.toHaveBeenCalled(); | ||
// simulate user being inactive for 9 minutes | ||
jest.advanceTimersByTime(9 * 60 * 1000); | ||
expect(cb).not.toHaveBeenCalled(); | ||
document.dispatchEvent(new KeyboardEvent('keypress')); | ||
|
||
// wait 5 minutes | ||
jest.advanceTimersByTime(5 * 60 * 1000); | ||
expect(cb).not.toHaveBeenCalled(); | ||
// simulate user being inactive for 9 minutes | ||
jest.advanceTimersByTime(9 * 60 * 1000); | ||
expect(cb).toHaveBeenCalled(); | ||
}); | ||
it('should delay its callback on mouse events', () => { | ||
const cb = jest.fn(); | ||
const manager = IdleManager.create({ onIdle: cb }); | ||
expect(cb).not.toHaveBeenCalled(); | ||
// simulate user being inactive for 9 minutes | ||
jest.advanceTimersByTime(9 * 60 * 1000); | ||
expect(cb).not.toHaveBeenCalled(); | ||
// simulate user moving the mouse | ||
document.dispatchEvent(new MouseEvent('mousemove')); | ||
|
||
// wait 5 minutes | ||
jest.advanceTimersByTime(5 * 60 * 1000); | ||
expect(cb).not.toHaveBeenCalled(); | ||
// simulate user being inactive for 9 minutes | ||
jest.advanceTimersByTime(9 * 60 * 1000); | ||
expect(cb).toHaveBeenCalled(); | ||
}); | ||
|
||
it('should delay its callback on touch events', () => { | ||
const cb = jest.fn(); | ||
const manager = IdleManager.create({ onIdle: cb }); | ||
expect(cb).not.toHaveBeenCalled(); | ||
// simulate user being inactive for 9 minutes | ||
jest.advanceTimersByTime(9 * 60 * 1000); | ||
expect(cb).not.toHaveBeenCalled(); | ||
// simulate user touching the screen | ||
document.dispatchEvent(new TouchEvent('touchstart')); | ||
|
||
// wait 5 minutes | ||
jest.advanceTimersByTime(5 * 60 * 1000); | ||
expect(cb).not.toHaveBeenCalled(); | ||
// simulate user being inactive for 9 minutes | ||
jest.advanceTimersByTime(9 * 60 * 1000); | ||
expect(cb).toHaveBeenCalled(); | ||
}); | ||
it('should delay its callback on scroll events', () => { | ||
const cb = jest.fn(); | ||
|
||
const scrollDebounce = 100; | ||
|
||
const manager = IdleManager.create({ onIdle: cb, captureScroll: true, scrollDebounce }); | ||
expect(cb).not.toHaveBeenCalled(); | ||
// simulate user being inactive for 9 minutes | ||
jest.advanceTimersByTime(9 * 60 * 1000); | ||
expect(cb).not.toHaveBeenCalled(); | ||
// simulate user scrolling | ||
document.dispatchEvent(new WheelEvent('scroll')); | ||
|
||
// wait 5 minutes | ||
jest.advanceTimersByTime(5 * 60 * 1000); | ||
expect(cb).not.toHaveBeenCalled(); | ||
// simulate user being inactive for 9 minutes, plus the debounce | ||
jest.advanceTimersByTime(9 * 60 * 1000 + scrollDebounce); | ||
expect(cb).toHaveBeenCalled(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
type IdleCB = () => unknown; | ||
export type IdleManagerOptions = { | ||
/** | ||
* Callback after the user has gone idle | ||
*/ | ||
onIdle?: IdleCB; | ||
/** | ||
* timeout in ms | ||
* @default 30 minutes [600_000] | ||
*/ | ||
idleTimeout?: number; | ||
/** | ||
* capture scroll events | ||
* @default false | ||
*/ | ||
captureScroll?: boolean; | ||
/** | ||
* scroll debounce time in ms | ||
* @default 100 | ||
*/ | ||
scrollDebounce?: number; | ||
}; | ||
|
||
const events = ['mousedown', 'mousemove', 'keypress', 'touchstart', 'wheel']; | ||
|
||
/** | ||
* Detects if the user has been idle for a duration of `idleTimeout` ms, and calls `onIdle`. | ||
* | ||
* @param {IdleManagerOptions} options | ||
*/ | ||
class IdleManager { | ||
callbacks: IdleCB[] = []; | ||
idleTimeout: IdleManagerOptions['idleTimeout'] = 30 * 60 * 1000; | ||
timeoutID?: number = undefined; | ||
|
||
/** | ||
* Creates an idle manager | ||
* @param {IdleManagerOptions} options Optional configuration | ||
* @param options.onIdle Callback once user has been idle. Use to prompt for fresh login, and use Actor.agentOf(your_actor).invalidateIdentity() to protect the user | ||
* @param options.idleTimeout timeout in ms | ||
* @param options.captureScroll capture scroll events | ||
* @param options.scrollDebounce scroll debounce time in ms | ||
*/ | ||
public static create( | ||
options: { | ||
/** | ||
* Callback after the user has gone idle | ||
*/ | ||
onIdle?: IdleCB; | ||
/** | ||
* timeout in ms | ||
* @default 10 minutes [600_000] | ||
*/ | ||
idleTimeout?: number; | ||
/** | ||
* capture scroll events | ||
* @default false | ||
*/ | ||
captureScroll?: boolean; | ||
/** | ||
* scroll debounce time in ms | ||
* @default 100 | ||
*/ | ||
scrollDebounce?: number; | ||
} = {}, | ||
): IdleManager { | ||
return new this(options); | ||
} | ||
|
||
protected constructor(options: IdleManagerOptions = {}) { | ||
const { onIdle, idleTimeout = 10 * 60 * 1000 } = options || {}; | ||
this.callbacks = onIdle ? [onIdle] : []; | ||
this.idleTimeout = idleTimeout; | ||
|
||
const resetTimer = this.resetTimer.bind(this); | ||
|
||
window.addEventListener('load', resetTimer, true); | ||
|
||
events.forEach(function (name) { | ||
document.addEventListener(name, resetTimer, true); | ||
}); | ||
|
||
// eslint-disable-next-line @typescript-eslint/ban-types | ||
const debounce = (func: Function, wait: number) => { | ||
let timeout: number | undefined; | ||
return (...args: unknown[]) => { | ||
// eslint-disable-next-line @typescript-eslint/no-this-alias | ||
const context = this; | ||
const later = function () { | ||
timeout = undefined; | ||
func.apply(context, args); | ||
}; | ||
clearTimeout(timeout); | ||
timeout = window.setTimeout(later, wait); | ||
}; | ||
}; | ||
|
||
if (options?.captureScroll) { | ||
// debounce scroll events | ||
const scroll = debounce(resetTimer, options?.scrollDebounce ?? 100); | ||
window.addEventListener('scroll', scroll, true); | ||
} | ||
|
||
resetTimer(); | ||
} | ||
|
||
/** | ||
* adds a callback to the list of callbacks | ||
* @param {IdleCB} callback | ||
*/ | ||
public registerCallback(callback: IdleCB): void { | ||
this.callbacks.push(callback); | ||
} | ||
|
||
/** | ||
* Cleans up the idle manager and its listeners | ||
*/ | ||
public exit(): void { | ||
this.callbacks.forEach(cb => cb()); | ||
clearTimeout(this.timeoutID); | ||
window.removeEventListener('load', this.resetTimer, true); | ||
|
||
const resetTimer = this.resetTimer.bind(this); | ||
events.forEach(function (name) { | ||
document.removeEventListener(name, resetTimer, true); | ||
}); | ||
} | ||
|
||
resetTimer(): void { | ||
const exit = this.exit.bind(this); | ||
window.clearTimeout(this.timeoutID); | ||
this.timeoutID = window.setTimeout(exit, this.idleTimeout); | ||
} | ||
} | ||
|
||
export default IdleManager; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is auth-client ever meant to be used in a nodejs environment?