Update dependencies, refactor authentication, and enhance UI components

- Upgraded @reduxjs/toolkit to version 2.9.0 and added new dependencies including @techniker-me/pcast-api and moment.
- Refactored authentication logic and added middleware for improved request handling.
- Introduced new UI components such as buttons, loaders, and forms, along with a theme system following SOLID principles.
- Updated routing to include protected routes and improved the login form with better error handling.
- Removed unused CSS and organized the project structure for better maintainability.
This commit is contained in:
2025-09-04 01:10:03 -04:00
parent 04488c43c5
commit 1469c7f52f
85 changed files with 3610 additions and 125 deletions

View File

@@ -0,0 +1,41 @@
import {ApplicationCredentials, Channels, PCastApi, Reporting, Streams} from '@techniker-me/pcast-api';
export default class PCastApiService {
private static _instance: PCastApi;
public static initialize(pcastUri: string, applciationCredentials: ApplicationCredentials) {
PCastApiService._instance = PCastApi.create(pcastUri, applciationCredentials);
}
public static getInstance(): PCastApiService {
if (!PCastApiService._instance) {
throw new Error('PCastApiService has not been initialized');
}
return PCastApiService._instance;
}
static get channels(): Channels {
if (!PCastApiService._instance) {
throw new Error('PCastApiService has not been initialized');
}
return PCastApiService._instance.channels;
}
static get streams(): Streams {
if (!PCastApiService._instance) {
throw new Error('PCastApiService has not been initialized');
}
return PCastApiService._instance.streams;
}
static get reporting(): Reporting {
if (!PCastApiService._instance) {
throw new Error('PCastApiService has not been initialized');
}
return PCastApiService._instance.reporting;
}
}

View File

@@ -0,0 +1,6 @@
export default interface IUserDataStore {
setItem(key: string, value: string): void;
getItem(key: string): string | null;
removeItem(key: string): void;
clear(): void;
}

View File

@@ -0,0 +1,23 @@
import IUserDataStore from './IUserDataStore';
export class IndexedDB implements IUserDataStore {
static isSupported(): boolean {
return 'indexedDB' in window;
}
public getItem(key: string): string | null {
throw new Error('Not Implemented');
}
public setItem(key: string, value: string): void {
throw new Error('Not Implemented');
}
public removeItem(key: string): void {
throw new Error('Not Implemented');
}
public clear(): void {
throw new Error('Not Implemented');
}
}

View File

@@ -0,0 +1,23 @@
import IUserDataStore from './IUserDataStore';
export class LocalStorage implements IUserDataStore {
static isSupported(): boolean {
return 'localStorage' in window;
}
public getItem(key: string): string | null {
throw new Error('Not Implemented');
}
public setItem(key: string, value: string): void {
throw new Error('Not Implemented');
}
public removeItem(key: string): void {
throw new Error('Not Implemented');
}
public clear(): void {
throw new Error('Not Implemented');
}
}

View File

@@ -0,0 +1,23 @@
import IUserDataStore from './IUserDataStore';
export class ObjectStrore implements IUserDataStore {
static isSupported(): boolean {
return true;
}
public getItem(key: string): string | null {
throw new Error('Not Implemented');
}
public setItem(key: string, value: string): void {
throw new Error('Not Implemented');
}
public removeItem(key: string): void {
throw new Error('Not Implemented');
}
public clear(): void {
throw new Error('Not Implemented');
}
}

View File

@@ -0,0 +1,24 @@
import IUserDataStore from './IUserDataStore';
import {IndexedDB} from './IndexedDB';
import {LocalStorage} from './LocalStorage';
import {ObjectStrore} from './ObjectStore';
class UserDataStoreService {
private static _instance: IUserDataStore;
static {
if (IndexedDB.isSupported()) {
this._instance = new IndexedDB();
} else if (LocalStorage.isSupported()) {
this._instance = new LocalStorage();
} else {
this._instance = new ObjectStrore();
}
}
public static getInstance(): IUserDataStore {
return this._instance;
}
}
export default UserDataStoreService.getInstance();

View File

@@ -20,6 +20,7 @@ export interface IPhenixWebSocketResponse {
sessionId: string;
redirect: string;
roles: string[];
[key: string]: unknown;
}
export class PhenixWebSocket extends MQWebSocket {
@@ -60,7 +61,9 @@ export class PhenixWebSocket extends MQWebSocket {
public async sendMessage<T>(kind: PhenixWebSocketMessage, message: T): Promise<IPhenixWebSocketResponse> {
if (this._status.value !== PhenixWebSocketStatus.Online) {
throw new Error(`Unable to send message, web socket is not [Online] WebSocket status [${PhenixWebSocketStatusMapping.convertPhenixWebSocketStatusToPhenixWebSocketStatusType(this._status.value)}]`);
throw new Error(
`Unable to send message, web socket is not [Online] WebSocket status [${PhenixWebSocketStatusMapping.convertPhenixWebSocketStatusToPhenixWebSocketStatusType(this._status.value)}]`
);
}
this._pendingRequests++;
@@ -89,27 +92,27 @@ export class PhenixWebSocket extends MQWebSocket {
private initialize(): void {
super.onEvent('connected', () => {
this.setStatus(PhenixWebSocketStatus.Online);
})
});
super.onEvent('disconnected', () => {
this.setStatus(PhenixWebSocketStatus.Offline);
})
});
super.onEvent('error', (error: unknown) => {
this._logger.error('Error [%s]', error);
this.setStatus(PhenixWebSocketStatus.Error);
})
});
super.onEvent('reconnecting', () => {
this.setStatus(PhenixWebSocketStatus.Reconnecting);
})
});
super.onEvent('reconnected', () => {
this.setStatus(PhenixWebSocketStatus.Online);
})
});
super.onEvent('timeout', () => {
this.setStatus(PhenixWebSocketStatus.Error);
})
});
}
}
}

View File

@@ -0,0 +1,191 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
type NavigatorUAData = {
brands?: { brand: string; version: string }[];
mobile?: boolean;
platform?: string;
getHighEntropyValues?: (hints: string[]) => Promise<Record<string, string>>;
toJSON?: () => object;
};
export default class PlatformDetectionService {
private static readonly _userAgent: string = globalThis.navigator?.userAgent ?? '';
// @ts-expect-error NavigatorUAData is experimental and not defined in the lib dom yet https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData
private static readonly _userAgentData: NavigatorUAData | undefined = globalThis.navigator?.userAgentData;
private static readonly _areClientHintsSupported: boolean = !!PlatformDetectionService._userAgentData;
private static _platform: string = 'Unknown';
private static _platformVersion: string = '';
private static _browserName: string = 'Unknown';
private static _browserVersion: string = '?';
private static _isWebview: boolean = false;
static {
if (PlatformDetectionService._areClientHintsSupported) {
PlatformDetectionService.initFromClientHints();
} else {
PlatformDetectionService.initFromUserAgent();
}
}
private constructor() {
throw new Error('PlatformDetectionService is a static class that may not be instantiated');
}
// ---- Public API ----
static get platform(): string {
return PlatformDetectionService._platform;
}
static get platformVersion(): string {
return PlatformDetectionService._platformVersion;
}
static get userAgent(): string {
return PlatformDetectionService._userAgent;
}
static get browserName(): string {
return PlatformDetectionService._browserName;
}
static get browserVersion(): string {
return PlatformDetectionService._browserVersion;
}
static get isWebview(): boolean {
return PlatformDetectionService._isWebview;
}
static get areClientHintsSupported(): boolean {
return PlatformDetectionService._areClientHintsSupported;
}
/**
* Optional async initialization for high-entropy values like platformVersion
*/
static async initAsync(): Promise<void> {
if (PlatformDetectionService._areClientHintsSupported && PlatformDetectionService._userAgentData?.getHighEntropyValues) {
const values = await PlatformDetectionService._userAgentData.getHighEntropyValues(['platformVersion']);
if (values.platformVersion) {
PlatformDetectionService._platformVersion = values.platformVersion;
}
}
}
// ---- Init strategies ----
private static initFromClientHints() {
const data = PlatformDetectionService._userAgentData as NavigatorUAData;
const nonChromiumBrand = data.brands?.find(b => b.brand !== 'Chromium');
PlatformDetectionService._browserName = nonChromiumBrand?.brand ?? 'Unknown';
PlatformDetectionService._browserVersion = nonChromiumBrand?.version ?? '?';
PlatformDetectionService._platform = data.platform ?? 'Unknown';
PlatformDetectionService._isWebview = PlatformDetectionService.extractIsWebviewFromUserAgent(); // Fallback check
}
private static initFromUserAgent() {
PlatformDetectionService._platform = PlatformDetectionService.extractPlatformFromUserAgent();
PlatformDetectionService._platformVersion = PlatformDetectionService.extractPlatformVersionFromUserAgent();
PlatformDetectionService._browserName = PlatformDetectionService.extractBrowserNameFromUserAgent();
PlatformDetectionService._browserVersion = PlatformDetectionService.extractBrowserVersionFromUserAgent();
PlatformDetectionService._isWebview = PlatformDetectionService.extractIsWebviewFromUserAgent();
}
// ---- Helpers ----
private static extractBrowserNameFromUserAgent(): string {
if (/Edg\//.test(PlatformDetectionService._userAgent)) {
return 'Edge';
}
if (/OPR\//.test(PlatformDetectionService._userAgent)) {
return 'Opera';
}
if (/Firefox\//.test(PlatformDetectionService._userAgent)) {
return 'Firefox';
}
if (/Trident\/.*rv:/.test(PlatformDetectionService._userAgent)) {
return 'IE';
}
if (/Chrome\//.test(PlatformDetectionService._userAgent)) {
return 'Chrome';
}
if (/Safari\//.test(PlatformDetectionService._userAgent)) {
return 'Safari';
}
if (/ReactNative\//.test(PlatformDetectionService._userAgent)) {
return 'ReactNative';
}
return 'Unknown';
}
private static extractBrowserVersionFromUserAgent(): string {
return (
PlatformDetectionService.matchVersion(/Edg\/([\d.]+)/) ??
PlatformDetectionService.matchVersion(/OPR\/([\d.]+)/) ??
PlatformDetectionService.matchVersion(/Firefox\/([\d.]+)/) ??
PlatformDetectionService.matchVersion(/rv:([\d.]+)/) ?? // IE
PlatformDetectionService.matchVersion(/Chrome\/([\d.]+)/) ??
PlatformDetectionService.matchVersion(/Version\/([\d.]+)/) ?? // Safari often uses "Version/"
PlatformDetectionService.matchVersion(/Safari\/([\d.]+)/) ??
PlatformDetectionService.matchVersion(/ReactNative\/([\d.]+)/) ??
'?'
);
}
private static extractPlatformFromUserAgent(): string {
if (/Windows/.test(PlatformDetectionService._userAgent)) {
return 'Windows';
}
if (/iPhone|iPad|iPod/.test(PlatformDetectionService._userAgent)) {
return 'iOS';
}
if (/Mac OS X/.test(PlatformDetectionService._userAgent)) {
return 'macOS';
}
if (/Android/.test(PlatformDetectionService._userAgent)) {
return 'Android';
}
if (/Linux/.test(PlatformDetectionService._userAgent)) {
return 'Linux';
}
return 'Unknown';
}
private static extractPlatformVersionFromUserAgent(): string {
switch (PlatformDetectionService._platform) {
case 'Windows':
return PlatformDetectionService.matchVersion(/Windows NT ([\d.]+)/) ?? '';
case 'iOS':
return PlatformDetectionService.matchVersion(/OS ([\d_]+)/)?.replace(/_/g, '.') ?? '';
case 'macOS':
return PlatformDetectionService.matchVersion(/Mac OS X ([\d_]+)/)?.replace(/_/g, '.') ?? '';
case 'Android':
return PlatformDetectionService.matchVersion(/Android ([\d.]+)/) ?? '';
default:
return '';
}
}
private static extractIsWebviewFromUserAgent(): boolean {
return (
/; wv/.test(PlatformDetectionService._userAgent) || // Android webview
(/Android/.test(PlatformDetectionService._userAgent) && /Version\/[\d.]+/.test(PlatformDetectionService._userAgent)) || // Some Android webviews
/(iPhone|iPod|iPad).*AppleWebKit(?!.*Safari)/.test(PlatformDetectionService._userAgent) // IOS webview
);
}
private static matchVersion(pattern: RegExp): string | null {
const match = PlatformDetectionService._userAgent.match(pattern);
return match ? match[1] : null;
}
}