Skip to content

Commit

Permalink
refactor(auth): create onBeforeLogout and onAfterLogout callbacks
Browse files Browse the repository at this point in the history
This greatly simplifies the timing of the cleanups performed when the current user logs out of the application

Signed-off-by: Fernando Fernández <[email protected]>
  • Loading branch information
ferferga committed Dec 29, 2024
1 parent ad8cc2c commit 7a1d35c
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 71 deletions.
29 changes: 27 additions & 2 deletions frontend/src/plugins/remote/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import SDK, { useOneTimeAPI } from './sdk/sdk-utils';
import { isAxiosError, isNil, sealed } from '@/utils/validation';
import { i18n } from '@/plugins/i18n';
import { useSnackbar } from '@/composables/use-snackbar';
import { CommonStore } from '@/store/super/common-store';
import { BaseState } from '@/store/super/base-state';

export interface ServerInfo extends BetterOmit<PublicSystemInfo, 'LocalAddress'> {
PublicAddress: string;
Expand All @@ -34,7 +34,12 @@ interface AuthState {
}

@sealed
class RemotePluginAuth extends CommonStore<AuthState> {
class RemotePluginAuth extends BaseState<AuthState> {
private readonly _callbacks = {
beforeLogout: [] as MaybePromise<void>[],
afterLogout: [] as MaybePromise<void>[]
};

public readonly servers = computed(() => this._state.value.servers);
public readonly currentServer = computed(() => this._state.value.servers[this._state.value.currentServerIndex]);
public readonly currentUser = computed(() => this._state.value.users[this._state.value.currentUserIndex]);
Expand Down Expand Up @@ -104,6 +109,18 @@ class RemotePluginAuth extends CommonStore<AuthState> {
};
};

private readonly _runCallbacks = async (callbacks: MaybePromise<void>[]) =>
await Promise.allSettled(callbacks.map(fn => fn()));

/**
* Runs the passed function before logging out the user
*/
public readonly onBeforeLogout = (fn: MaybePromise<void>) =>
this._callbacks.beforeLogout.push(fn);

public readonly onAfterLogout = (fn: MaybePromise<void>) =>
this._callbacks.afterLogout.push(fn);

/**
* Connects to a server
*
Expand Down Expand Up @@ -240,9 +257,17 @@ class RemotePluginAuth extends CommonStore<AuthState> {
*/
public readonly logoutCurrentUser = async (skipRequest = false): Promise<void> => {
if (!isNil(this.currentUser.value) && !isNil(this.currentServer.value)) {
await this._runCallbacks(this._callbacks.beforeLogout);
await this.logoutUser(this.currentUser.value, this.currentServer.value, skipRequest);

this._state.value.currentUserIndex = -1;
/**
* We need this so the callbacks are run after all the dependencies are updated
* (i.e the page component is routed to index).
*/
globalThis.requestAnimationFrame(() =>
globalThis.setTimeout(() => void this._runCallbacks(this._callbacks.afterLogout))
);
}
};

Expand Down
3 changes: 3 additions & 0 deletions frontend/src/plugins/workers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import type { ICanvasDrawer } from './canvas-drawer.worker';
import CanvasDrawer from './canvas-drawer.worker?worker';
import type { IGenericWorker } from './generic.worker';
import GenericWorker from './generic.worker?worker';
import { remote } from '@/plugins/remote';

/**
* A worker for decoding blurhash strings into pixels
*/
export const blurhashDecoder = wrap<IBlurhashDecoder>(new BlurhashDecoder());

remote.auth.onAfterLogout(async () => await blurhashDecoder.clearCache());

/**
* A worker for drawing canvas offscreen. The canvas must be transferred like this:
* ```ts
Expand Down
12 changes: 1 addition & 11 deletions frontend/src/store/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,17 +210,7 @@ class ApiStore {
}
);

watch(remote.auth.currentUser,
() => {
globalThis.requestAnimationFrame(() => {
globalThis.setTimeout(() => {
if (!remote.auth.currentUser.value) {
this._clear();
}
});
});
}, { flush: 'post' }
);
remote.auth.onAfterLogout(this._clear);
}
}

Expand Down
15 changes: 7 additions & 8 deletions frontend/src/store/playback-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ class PlaybackManagerStore extends CommonStore<PlaybackManagerState> {
* Report playback stopped to the server. Used by the "Now playing" statistics in other clients.
*/
private readonly _reportPlaybackStopped = async (
itemId: string,
itemId = this.currentItemId.value,
sessionId = this._state.value.playSessionId,
currentTime = this.currentTime.value
): Promise<void> => {
Expand Down Expand Up @@ -989,14 +989,13 @@ class PlaybackManagerStore extends CommonStore<PlaybackManagerState> {
});

/**
* Dispose on logout
* Report playback stop before logging out
*/
watch(remote.auth.currentUser,
() => {
if (isNil(remote.auth.currentUser.value)) {
this.stop();
}
}, { flush: 'post' }
remote.auth.onBeforeLogout(
async () => {
await this._reportPlaybackStopped(this.currentItemId.value);
this._reset();
}
);
}
}
Expand Down
64 changes: 64 additions & 0 deletions frontend/src/store/super/base-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* This class provides the base class functionality for stores that contains
* reactive state. It provides a default state, a way to reset the state, and
* persistence options.
*
* This class is intended to be used by plugins and other abstract classes. It
* should not be used by stores (check CommonStore for that).
*/
import { useStorage } from '@vueuse/core';
import { ref, type Ref } from 'vue';
import type { UnknownRecord } from 'type-fest';
import { mergeExcludingUnknown } from '@/utils/data-manipulation';
import { isNil } from '@/utils/validation';

export interface BaseStateParams<T> {
defaultState: () => T;
/**
* Key to be used as an identifier
*/
storeKey: string;
persistenceType?: 'localStorage' | 'sessionStorage';
}

export abstract class BaseState<
T extends object = UnknownRecord,
/**
* State properties that should be exposed to consumers
* of this class. If not provided, all properties
* will be private.
* Exposed properties are also writable.
*/
K extends keyof T = never
> {
private readonly _defaultState;
protected readonly _storeKey;
protected readonly _state: Ref<T>;
/**
* Same as _state, but we use the type system to define which properties
* we want to have accessible to consumers of the extended class.
*/
public readonly state: Ref<Pick<T, K>>;

protected readonly _reset = () => {
this._state.value = this._defaultState();
};

protected constructor({
defaultState,
storeKey,
persistenceType
}: BaseStateParams<T>
) {
this._storeKey = storeKey;
this._defaultState = defaultState;

this._state = isNil(persistenceType) || isNil(storeKey)
? ref(this._defaultState()) as Ref<T>
: useStorage(storeKey, this._defaultState(), globalThis[persistenceType], {
mergeDefaults: (storageValue, defaults) =>
mergeExcludingUnknown(storageValue, defaults)
});
this.state = this._state;
}
}
66 changes: 16 additions & 50 deletions frontend/src/store/super/common-store.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { useStorage } from '@vueuse/core';
import { ref, watch, type Ref } from 'vue';
/**
* CommonStore is a base class for all stores. It extends from BaseState,
* providing also a way to reset the state on logout automatically.
*
* This class is intended to be used by stores. It
* should not be used by plugins (check BaseState for that)
* since it has a dependency on the auth plugin.
*/
import type { UnknownRecord } from 'type-fest';
import { mergeExcludingUnknown } from '@/utils/data-manipulation';
import { isFunc, isNil } from '@/utils/validation';
import { isBool } from '@/utils/validation';
import { remote } from '@/plugins/remote';
import { BaseState, type BaseStateParams } from '@/store/super/base-state';

export interface CommonStoreParams<T> {
defaultState: () => T;
/**
* Key to be used as an identifier
*/
storeKey: string;
persistenceType?: 'localStorage' | 'sessionStorage';
resetOnLogout?: boolean | (() => void);
export interface CommonStoreParams<T> extends BaseStateParams<T> {
resetOnLogout?: boolean | MaybePromise<T>;
}

export abstract class CommonStore<
Expand All @@ -23,53 +24,18 @@ export abstract class CommonStore<
* Exposed properties are also writable.
*/
K extends keyof T = never
> {
private readonly _defaultState;
protected readonly _storeKey;
protected readonly _state: Ref<T>;
/**
* Same as _state, but we use the type system to define which properties
* we want to have accessible to consumers of the extended class.
*/
public readonly state: Ref<Pick<T, K>>;

protected readonly _reset = (): void => {
Object.assign(this._state.value, this._defaultState());
};

> extends BaseState<T, K> {
protected constructor({
defaultState,
storeKey,
persistenceType,
resetOnLogout
}: CommonStoreParams<T>
) {
this._storeKey = storeKey;
this._defaultState = defaultState;

this._state = isNil(persistenceType) || isNil(storeKey)
? ref(this._defaultState()) as Ref<T>
: useStorage(storeKey, this._defaultState(), globalThis[persistenceType], {
mergeDefaults: (storageValue, defaults) =>
mergeExcludingUnknown(storageValue, defaults)
});
this.state = this._state;
super({ defaultState, storeKey, persistenceType });

if (resetOnLogout) {
// eslint-disable-next-line sonarjs/no-async-constructor
void (async () => {
const { remote } = await import('@/plugins/remote');

watch(remote.auth.currentUser,
() => {
if (!remote.auth.currentUser.value) {
const funcToRun = isFunc(resetOnLogout) ? resetOnLogout : this._reset;

funcToRun();
}
}, { flush: 'post' }
);
})();
remote.auth.onAfterLogout(isBool(resetOnLogout) ? this._reset : resetOnLogout);
}
}
}
2 changes: 2 additions & 0 deletions frontend/types/global/util.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ type BetterOmit<T, K extends keyof never> = T extends Record<never, never>
* Sets a type as nullish
*/
type Nullish<T> = T | null | undefined;

type MaybePromise<T> = (() => Promise<T>) | (() => T);

0 comments on commit 7a1d35c

Please sign in to comment.