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:
73
src/store/middlewares/authenticationMiddleware.ts
Normal file
73
src/store/middlewares/authenticationMiddleware.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
authenticateCredentialsThunk,
|
||||
setError,
|
||||
selectIsAuthenticated,
|
||||
selectIsLoading,
|
||||
selectApplicationId,
|
||||
selectSecret,
|
||||
setUnauthorized
|
||||
} from 'store/slices/Authentication.slice';
|
||||
import {Middleware} from '@reduxjs/toolkit';
|
||||
|
||||
export const authenticateRequestMiddleware: Middleware = store => next => async action => {
|
||||
const state = store.getState();
|
||||
const isAuthenticated = selectIsAuthenticated(state);
|
||||
const isLoading = selectIsLoading(state);
|
||||
const applicationId = selectApplicationId(state);
|
||||
const secret = selectSecret(state);
|
||||
|
||||
console.log(
|
||||
'[authenticateRequest] action [%o] isAuthenticated [%o] isLoading [%o] applicationId [%o] secret [%o]',
|
||||
action,
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
applicationId,
|
||||
secret
|
||||
);
|
||||
|
||||
// Skip authentication middleware for authentication-related actions
|
||||
|
||||
if (
|
||||
typeof action === 'object' &&
|
||||
action !== null &&
|
||||
'type' in action &&
|
||||
typeof (action as any).type === 'string' &&
|
||||
(action as any).type.startsWith('authentication/')
|
||||
) {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
// If already authenticated, proceed normally
|
||||
if (isAuthenticated) {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
// If currently loading, wait for it to complete
|
||||
if (isLoading) {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
// If no credentials, set unauthorized
|
||||
if (!applicationId || !secret) {
|
||||
console.log('[authenticateRequest] No credentials available, proceeding with action');
|
||||
return next(setUnauthorized());
|
||||
}
|
||||
|
||||
// We have credentials but are not authenticated, try to authenticate
|
||||
try {
|
||||
console.log('[authenticateRequest] Attempting auto-authentication');
|
||||
// Use the Redux thunk to properly update the state
|
||||
const authResult = await store.dispatch(authenticateCredentialsThunk({applicationId, secret}) as any);
|
||||
|
||||
if (authResult.type.endsWith('/rejected') || authResult.payload === 'Authentication failed') {
|
||||
console.log('[authenticateRequest] Authentication failed');
|
||||
return next(setUnauthorized());
|
||||
}
|
||||
|
||||
console.log('[authenticateRequest] Auto-authentication successful, proceeding with action');
|
||||
return next(action);
|
||||
} catch (error) {
|
||||
console.error('[authenticateRequest] Auto-authentication failed:', error);
|
||||
return next(setUnauthorized());
|
||||
}
|
||||
};
|
||||
3
src/store/middlewares/index.ts
Normal file
3
src/store/middlewares/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './authenticationMiddleware';
|
||||
export * from './promiseMiddleware';
|
||||
export * from './loggerMiddleware';
|
||||
13
src/store/middlewares/loggerMiddleware.ts
Normal file
13
src/store/middlewares/loggerMiddleware.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {Middleware} from '@reduxjs/toolkit';
|
||||
|
||||
/**
|
||||
* Logs all actions and states after they are dispatched.
|
||||
*/
|
||||
export const loggerMiddleware: Middleware = store => next => action => {
|
||||
console.group((action as any).type);
|
||||
console.info('dispatching', action);
|
||||
const result = next(action);
|
||||
console.log('next state', store.getState());
|
||||
console.groupEnd();
|
||||
return result;
|
||||
};
|
||||
9
src/store/middlewares/promiseMiddleware.ts
Normal file
9
src/store/middlewares/promiseMiddleware.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import {Middleware} from '@reduxjs/toolkit';
|
||||
|
||||
export const vanillaPromiseMiddleware: Middleware = store => next => (action: any) => {
|
||||
if (typeof action.then !== 'function') {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
return Promise.resolve(action).then((resolvedAction: any) => store.dispatch(resolvedAction));
|
||||
};
|
||||
@@ -46,6 +46,18 @@ export const selectSessionInfo = createSelector([selectAuthentication], authenti
|
||||
roles: authentication.roles
|
||||
}));
|
||||
|
||||
export const selectApplicationId = createSelector([selectAuthentication], authentication => authentication.applicationId);
|
||||
|
||||
export const selectSecret = createSelector([selectAuthentication], authentication => authentication.secret);
|
||||
|
||||
export const selectSessionId = createSelector([selectAuthentication], authentication => authentication.sessionId);
|
||||
|
||||
export const selectRoles = createSelector([selectAuthentication], authentication => authentication.roles);
|
||||
|
||||
export const selectHasRole = createSelector([selectAuthentication, (_, role: string) => role], (authentication, role) => authentication.roles.includes(role));
|
||||
|
||||
export const selectIsOnline = createSelector([selectAuthentication], authentication => authentication.status === 'Online');
|
||||
|
||||
const authenticateCredentialsThunk = createAsyncThunk<IPhenixWebSocketResponse, {applicationId: string; secret: string}>(
|
||||
'authentication/authenticate',
|
||||
async (credentials, {rejectWithValue}) => {
|
||||
@@ -56,6 +68,7 @@ const authenticateCredentialsThunk = createAsyncThunk<IPhenixWebSocketResponse,
|
||||
} catch (error) {
|
||||
// Convert error to serializable format
|
||||
const errorMessage = error instanceof Error ? error.message : 'Authentication failed';
|
||||
|
||||
return rejectWithValue(errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -67,6 +80,7 @@ const signoutThunk = createAsyncThunk('authentication/signout', async (_, {rejec
|
||||
} catch (error) {
|
||||
// Convert error to serializable format
|
||||
const errorMessage = error instanceof Error ? error.message : 'Signout failed';
|
||||
|
||||
return rejectWithValue(errorMessage);
|
||||
}
|
||||
});
|
||||
@@ -95,14 +109,16 @@ const authenticationSlice = createSlice({
|
||||
setSessionId: (state, action: PayloadAction<string>) => {
|
||||
state.sessionId = action.payload;
|
||||
},
|
||||
setIsAuthenticated: (state, action: PayloadAction<boolean>) => {
|
||||
state.isAuthenticated = action.payload;
|
||||
setError: (state, action: PayloadAction<string | null>) => {
|
||||
state.error = action.payload;
|
||||
},
|
||||
setRoles: (state, action: PayloadAction<string[]>) => {
|
||||
state.roles = action.payload;
|
||||
},
|
||||
setApplicationId: (state, action: PayloadAction<string>) => {
|
||||
state.applicationId = action.payload;
|
||||
setUnauthorized: state => {
|
||||
state.isAuthenticated = false;
|
||||
state.isLoading = false;
|
||||
state.error = 'Unauthorized';
|
||||
state.secret = null;
|
||||
state.status = 'Offline';
|
||||
state.roles = [];
|
||||
}
|
||||
},
|
||||
extraReducers: builder => {
|
||||
@@ -119,17 +135,18 @@ const authenticationSlice = createSlice({
|
||||
state.sessionId = authenticationResponse.sessionId ?? null;
|
||||
state.isAuthenticated = true;
|
||||
state.roles = authenticationResponse.roles ?? [];
|
||||
state.status = 'Online';
|
||||
state.isLoading = false;
|
||||
} else {
|
||||
state.applicationId = null;
|
||||
state.sessionId = null;
|
||||
state.isAuthenticated = false;
|
||||
state.secret = null;
|
||||
state.roles = [];
|
||||
state.status = 'Offline';
|
||||
state.error = 'Invalid credentials. Please check your Application ID and Secret.';
|
||||
state.isLoading = false;
|
||||
}
|
||||
|
||||
state.status = 'Online';
|
||||
state.isLoading = false;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(authenticateCredentialsThunk.rejected, (state, action) => {
|
||||
state.applicationId = null;
|
||||
@@ -164,6 +181,6 @@ const authenticationSlice = createSlice({
|
||||
}
|
||||
});
|
||||
|
||||
export const {setIsLoading, setCredentials, clearState, setSessionId, setIsAuthenticated, setRoles, setApplicationId} = authenticationSlice.actions;
|
||||
export const {setUnauthorized, setIsLoading, setCredentials, clearState, setSessionId, setError} = authenticationSlice.actions;
|
||||
export {authenticateCredentialsThunk};
|
||||
export default authenticationSlice.reducer;
|
||||
export default authenticationSlice.reducer;
|
||||
|
||||
103
src/store/slices/Channels.slice.ts
Normal file
103
src/store/slices/Channels.slice.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import {createAsyncThunk, createSelector, createSlice, PayloadAction, WritableDraft} from '@reduxjs/toolkit';
|
||||
import {ApplicationCredentials, Channel} from '@techniker-me/pcast-api';
|
||||
import PCastApiService from 'services/PCastApi.service';
|
||||
|
||||
export interface IChannelsState {
|
||||
isLoading: boolean;
|
||||
channels: Channel[];
|
||||
selectedChannel: Channel | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export const initialChannelsState: IChannelsState = {
|
||||
isLoading: false,
|
||||
channels: [],
|
||||
selectedChannel: null,
|
||||
error: null
|
||||
};
|
||||
|
||||
export const selectChannels = (state: {channels: IChannelsState}) => state.channels;
|
||||
|
||||
export const selectChannelList = createSelector([selectChannels], channels => channels.channels);
|
||||
|
||||
export const fetchChannelList = createAsyncThunk('channels/fetchChannelList', async (_, {rejectWithValue}) => {
|
||||
try {
|
||||
return PCastApiService.channels.list();
|
||||
} catch (error) {
|
||||
return rejectWithValue(error);
|
||||
}
|
||||
});
|
||||
|
||||
export const fetchChannelsListPublisherStatus = createAsyncThunk(
|
||||
'channels/fetchChannelsListPublisherStatus',
|
||||
async (channels: Channel[], {rejectWithValue}) => {
|
||||
try {
|
||||
const channelResponses = await Promise.all(
|
||||
channels.map(async channel => {
|
||||
const publisherCount = await PCastApiService.channels.getPublisherCount(channel.channelId);
|
||||
return {
|
||||
...channel,
|
||||
isActivePublisher: publisherCount > 0
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return channelResponses as Channel[];
|
||||
} catch (error) {
|
||||
return rejectWithValue(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const channelsSlice = createSlice({
|
||||
name: 'channels',
|
||||
initialState: {...initialChannelsState},
|
||||
reducers: {
|
||||
initializeChannels: (state, action: PayloadAction<{pcastUri: string; applicationCredentials: ApplicationCredentials}>) => {
|
||||
PCastApiService.initialize(action.payload.pcastUri, action.payload.applicationCredentials);
|
||||
state.isLoading = false;
|
||||
state.error = null;
|
||||
},
|
||||
setChannels: (state, action: PayloadAction<Channel[]>) => {
|
||||
state.channels = action.payload as WritableDraft<Channel>[];
|
||||
},
|
||||
setIsLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.isLoading = action.payload;
|
||||
},
|
||||
setSelectedChannel: (state, action: PayloadAction<Channel | null>) => {
|
||||
state.selectedChannel = action.payload as WritableDraft<Channel> | null;
|
||||
},
|
||||
setError: (state, action: PayloadAction<string | null>) => {
|
||||
state.error = action.payload;
|
||||
}
|
||||
},
|
||||
extraReducers: builder => {
|
||||
builder.addCase(fetchChannelList.pending, state => {
|
||||
state.isLoading = true;
|
||||
});
|
||||
builder.addCase(fetchChannelList.fulfilled, (state, action) => {
|
||||
state.channels = action.payload as WritableDraft<Channel[]>;
|
||||
state.isLoading = false;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchChannelList.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
builder.addCase(fetchChannelsListPublisherStatus.pending, state => {
|
||||
state.isLoading = true;
|
||||
});
|
||||
builder.addCase(fetchChannelsListPublisherStatus.fulfilled, (state, action) => {
|
||||
state.channels = action.payload as WritableDraft<Channel[]>;
|
||||
state.isLoading = false;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchChannelsListPublisherStatus.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const {initializeChannels, setChannels, setIsLoading, setSelectedChannel, setError} = channelsSlice.actions;
|
||||
export default channelsSlice.reducer;
|
||||
@@ -1,10 +1,18 @@
|
||||
import {configureStore} from '@reduxjs/toolkit';
|
||||
import AuthenticationState from './slices/Authentication.slice';
|
||||
import AuthenticationReducer from './slices/Authentication.slice';
|
||||
import ChannelsReducer from './slices/Channels.slice';
|
||||
import {authenticateRequestMiddleware, loggerMiddleware, vanillaPromiseMiddleware} from './middlewares';
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
authentication: AuthenticationState
|
||||
}
|
||||
authentication: AuthenticationReducer,
|
||||
channels: ChannelsReducer
|
||||
},
|
||||
middleware: getDefaultMiddleware =>
|
||||
getDefaultMiddleware({
|
||||
thunk: true,
|
||||
serializableCheck: false
|
||||
}).concat(authenticateRequestMiddleware, vanillaPromiseMiddleware, loggerMiddleware)
|
||||
});
|
||||
|
||||
export default store;
|
||||
|
||||
Reference in New Issue
Block a user