UNPKG

@accounts/client

Version:

Fullstack authentication and accounts-management

212 lines (191 loc) 5.92 kB
import { type LoginResult, type Tokens, type ImpersonationResult, type User, } from '@accounts/types'; import { type TransportInterface } from './transport-interface'; import { type TokenStorage, type AccountsClientOptions } from './types'; import { tokenStorageLocal } from './token-storage-local'; import { isTokenExpired } from './utils'; enum TokenKey { AccessToken = 'accessToken', RefreshToken = 'refreshToken', OriginalAccessToken = 'originalAccessToken', OriginalRefreshToken = 'originalRefreshToken', } const defaultOptions = { tokenStorage: tokenStorageLocal, tokenStoragePrefix: 'accounts', tokenStorageSeparator: ':', }; export class AccountsClient { public transport: TransportInterface; private options: AccountsClientOptions & typeof defaultOptions; private storage: TokenStorage; constructor(options: AccountsClientOptions, transport: TransportInterface) { this.options = { ...defaultOptions, ...options }; this.storage = this.options.tokenStorage; if (!transport) { throw new Error('A valid transport is required'); } this.transport = transport; this.transport.client = this; } /** * Get the tokens from the storage */ public async getTokens(original?: boolean): Promise<Tokens | null> { const accessToken = await this.storage.getItem( original ? this.getTokenKey(TokenKey.OriginalAccessToken) : this.getTokenKey(TokenKey.AccessToken) ); const refreshToken = await this.storage.getItem( original ? this.getTokenKey(TokenKey.OriginalRefreshToken) : this.getTokenKey(TokenKey.RefreshToken) ); if (!accessToken || !refreshToken) { return null; } return { accessToken, refreshToken }; } /** * Store the tokens in the storage */ public async setTokens(tokens: Tokens, original?: boolean): Promise<void> { await this.storage.setItem( original ? this.getTokenKey(TokenKey.OriginalAccessToken) : this.getTokenKey(TokenKey.AccessToken), tokens.accessToken ); await this.storage.setItem( original ? this.getTokenKey(TokenKey.OriginalRefreshToken) : this.getTokenKey(TokenKey.RefreshToken), tokens.refreshToken ); } /** * Remove the tokens from the storage */ public async clearTokens(original?: boolean): Promise<void> { await this.storage.removeItem( original ? this.getTokenKey(TokenKey.OriginalAccessToken) : this.getTokenKey(TokenKey.AccessToken) ); await this.storage.removeItem( original ? this.getTokenKey(TokenKey.OriginalRefreshToken) : this.getTokenKey(TokenKey.RefreshToken) ); } /** * Authenticate the user with a specific service (not creating a session) */ public async authenticateWithService( service: string, credentials: { [key: string]: any } ): Promise<boolean> { return this.transport.authenticateWithService(service, credentials); } /** * Login the user with a specific service */ public async loginWithService( service: string, credentials: { [key: string]: any } ): Promise<LoginResult> { const response = await this.transport.loginWithService(service, credentials); await this.setTokens(response.tokens); return response; } /** * Refresh the user session * If the tokens have expired try to refresh them */ public async refreshSession(force = false): Promise<Tokens | null> { const tokens = await this.getTokens(); if (tokens) { try { const isAccessTokenExpired = isTokenExpired(tokens.accessToken); const isRefreshTokenExpired = isTokenExpired(tokens.refreshToken); // See if accessToken is expired and refreshToken is not if ((force || isAccessTokenExpired) && !isRefreshTokenExpired) { // Request a new token pair const refreshedSession = await this.transport.refreshTokens( tokens.accessToken, tokens.refreshToken ); await this.setTokens(refreshedSession.tokens); return refreshedSession.tokens; } else if (isRefreshTokenExpired) { // Refresh token is expired, user must sign back in await this.clearTokens(); return null; } return tokens; } catch (err) { await this.clearTokens(); throw err; } } return null; } /** * Impersonate to another user. */ public async impersonate(impersonated: { userId?: string; username?: string; email?: string; }): Promise<ImpersonationResult> { const tokens = await this.getTokens(); if (!tokens) { throw new Error('An access token is required'); } const res = await this.transport.impersonate(tokens.accessToken, impersonated); if (!res.authorized || !res.tokens) { throw new Error(`User unauthorized to impersonate`); } else { await this.setTokens(tokens, true); await this.setTokens(res.tokens); return res; } } /** * Stop the user impersonation. */ public async stopImpersonation(): Promise<void> { const tokens = await this.getTokens(true); if (tokens) { await this.logout(); await this.setTokens(tokens); await this.clearTokens(true); } } /** * Get the user infos */ public async getUser(): Promise<User> { return this.transport.getUser(); } /** * Logout the user * Call the server to invalidate the tokens * Clean user local storage */ public async logout(): Promise<void> { const tokens = await this.getTokens(); if (tokens) { await this.transport.logout(); } await this.clearTokens(); } private getTokenKey(tokenName: TokenKey): string { return `${this.options.tokenStoragePrefix}${this.options.tokenStorageSeparator}${tokenName}`; } }