@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* AuthService — device-based player authentication.
|
||||
*
|
||||
* Handles login via device ID (the primary auth flow for game clients),
|
||||
* logout (token clearing), and auth state observation.
|
||||
*/
|
||||
|
||||
import type { RudderContext } from '../core/context.js';
|
||||
import type { RemoteConfigShape } from '../state/RemoteConfigState.js';
|
||||
import { getOrCreateDeviceId } from '../device/DeviceId.js';
|
||||
import { api } from '../generated/api.js';
|
||||
import type { LoginViaDeviceResponse } from '../generated/auth.js';
|
||||
|
||||
export type AuthState = 'signed-in' | 'signed-out';
|
||||
|
||||
export type AuthStateListener = (state: AuthState) => void;
|
||||
|
||||
export interface LoginWithDeviceOptions {
|
||||
/** Player's region code (default: "global"). */
|
||||
region?: string;
|
||||
/** Player's language code (default: "en"). */
|
||||
language?: string;
|
||||
/** Optional player nickname; omitted from the request when not set. */
|
||||
nickname?: string;
|
||||
}
|
||||
|
||||
export class AuthService<TConfig extends RemoteConfigShape = RemoteConfigShape> {
|
||||
private readonly listeners = new Set<AuthStateListener>();
|
||||
private state: AuthState;
|
||||
|
||||
constructor(
|
||||
private readonly ctx: RudderContext,
|
||||
private readonly startRuntime: () => Promise<void>,
|
||||
private readonly stopRuntime: () => void,
|
||||
) {
|
||||
this.state = this.isAuthenticated ? 'signed-in' : 'signed-out';
|
||||
}
|
||||
|
||||
/** True while an access token is present in the token store. */
|
||||
get isAuthenticated(): boolean {
|
||||
return this.ctx.options.tokenStore.getAccessToken() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to auth state changes. The listener fires immediately with the
|
||||
* current state (like `SyncedState.onChange`). Returns an unsubscribe function.
|
||||
*/
|
||||
onAuthStateChange(listener: AuthStateListener): () => void {
|
||||
this.listeners.add(listener);
|
||||
// Resync with the token store — tokens may have been written externally
|
||||
// since construction.
|
||||
this.state = this.isAuthenticated ? 'signed-in' : 'signed-out';
|
||||
listener(this.state);
|
||||
return () => {
|
||||
this.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates the current device and returns access + refresh tokens.
|
||||
*
|
||||
* The device ID is auto-generated on first call and persisted in localStorage.
|
||||
* On success, tokens are saved to the client's TokenStore.
|
||||
*/
|
||||
async loginWithDevice(options: LoginWithDeviceOptions = {}): Promise<LoginViaDeviceResponse> {
|
||||
const { region = 'global', language = 'en', nickname } = options;
|
||||
const response = await api.loginViaDevice(this.ctx, {
|
||||
key: this.ctx.options.projectKey,
|
||||
deviceId: getOrCreateDeviceId(),
|
||||
region,
|
||||
language,
|
||||
nickname,
|
||||
});
|
||||
|
||||
this.ctx.options.tokenStore.saveTokens(
|
||||
response.accessToken ?? '',
|
||||
response.refreshToken ?? '',
|
||||
);
|
||||
await this.startRuntime();
|
||||
this.setState('signed-in');
|
||||
return response;
|
||||
}
|
||||
|
||||
/** Clears all stored tokens (logout). */
|
||||
logout(): void {
|
||||
this.ctx.options.tokenStore.clear();
|
||||
this.stopRuntime();
|
||||
this.setState('signed-out');
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the transport when the session is unrecoverable (refresh failed).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
notifySessionExpired(): void {
|
||||
this.setState('signed-out');
|
||||
}
|
||||
|
||||
private setState(state: AuthState): void {
|
||||
if (state === this.state) return;
|
||||
this.state = state;
|
||||
for (const listener of this.listeners) {
|
||||
listener(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user