Add authentication and assets

This commit is contained in:
2025-09-03 01:38:47 -04:00
parent c8a9e9329a
commit 04488c43c5
48 changed files with 3710 additions and 0 deletions

13
src/store/index.ts Normal file
View File

@@ -0,0 +1,13 @@
import {useDispatch, useSelector} from 'react-redux';
import store from './store';
export default store;
// Use throughout the app instead of plain `useDispatch` and `useSelector`
// Infer the `RootState`, `AppDispatch`, and `AppStore` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
// Inferred type: {posts: PostsState, comments: C
export type AppDispatch = typeof store.dispatch;
export type AppStore = typeof store;
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();

View File

@@ -0,0 +1,169 @@
import {createSlice, PayloadAction, createAsyncThunk, createSelector} from '@reduxjs/toolkit';
import AuthenticationService from '../../services/Authentication.service';
import {PhenixWebSocketStatusType} from 'services/net/websockets/PhenixWebSocketStatus';
import {IPhenixWebSocketResponse} from 'services/net/websockets/PhenixWebSocket';
export interface IAuthenticationState {
applicationId: string | null;
secret: string | null;
isAuthenticated: boolean;
isLoading: boolean;
status: PhenixWebSocketStatusType;
error: string | null;
sessionId: string | null;
roles: string[];
}
const initialAuthenticationState: IAuthenticationState = {
applicationId: null,
sessionId: null,
isAuthenticated: false,
isLoading: false,
status: 'Offline',
roles: [],
error: null,
secret: null
};
// Memoized selectors
export const selectAuthentication = (state: {authentication: IAuthenticationState}) => state.authentication;
export const selectIsLoading = createSelector([selectAuthentication], authentication => authentication.isLoading);
export const selectIsAuthenticated = createSelector([selectAuthentication], authentication => authentication.isAuthenticated);
export const selectError = createSelector([selectAuthentication], authentication => authentication.error);
export const selectStatus = createSelector([selectAuthentication], authentication => authentication.status);
export const selectCredentials = createSelector([selectAuthentication], authentication => ({
id: authentication.applicationId,
secret: authentication.secret
}));
export const selectSessionInfo = createSelector([selectAuthentication], authentication => ({
sessionId: authentication.sessionId,
roles: authentication.roles
}));
const authenticateCredentialsThunk = createAsyncThunk<IPhenixWebSocketResponse, {applicationId: string; secret: string}>(
'authentication/authenticate',
async (credentials, {rejectWithValue}) => {
try {
const response = await AuthenticationService.authenticate(credentials.applicationId, credentials.secret);
return response as IPhenixWebSocketResponse;
} catch (error) {
// Convert error to serializable format
const errorMessage = error instanceof Error ? error.message : 'Authentication failed';
return rejectWithValue(errorMessage);
}
}
);
const signoutThunk = createAsyncThunk('authentication/signout', async (_, {rejectWithValue}) => {
try {
return await AuthenticationService.signout();
} catch (error) {
// Convert error to serializable format
const errorMessage = error instanceof Error ? error.message : 'Signout failed';
return rejectWithValue(errorMessage);
}
});
const authenticationSlice = createSlice({
name: 'authentication',
initialState: {...initialAuthenticationState},
reducers: {
setIsLoading: (state, action: PayloadAction<boolean>) => {
state.isLoading = action.payload;
},
setCredentials: (state, action: PayloadAction<{applicationId: string; secret: string}>) => {
state.applicationId = action.payload.applicationId;
state.secret = action.payload.secret;
},
clearState: state => {
state.applicationId = null;
state.sessionId = null;
state.isAuthenticated = false;
state.isLoading = false;
state.error = null;
state.secret = null;
state.status = 'Offline';
state.roles = [];
},
setSessionId: (state, action: PayloadAction<string>) => {
state.sessionId = action.payload;
},
setIsAuthenticated: (state, action: PayloadAction<boolean>) => {
state.isAuthenticated = action.payload;
},
setRoles: (state, action: PayloadAction<string[]>) => {
state.roles = action.payload;
},
setApplicationId: (state, action: PayloadAction<string>) => {
state.applicationId = action.payload;
}
},
extraReducers: builder => {
builder
.addCase(authenticateCredentialsThunk.pending, state => {
state.isLoading = true;
state.error = null;
})
.addCase(authenticateCredentialsThunk.fulfilled, (state, action) => {
const authenticationResponse = action.payload;
if (authenticationResponse.status === 'ok') {
state.applicationId = authenticationResponse.applicationId ?? null;
state.sessionId = authenticationResponse.sessionId ?? null;
state.isAuthenticated = true;
state.roles = authenticationResponse.roles ?? [];
} else {
state.applicationId = null;
state.sessionId = null;
state.isAuthenticated = false;
state.secret = null;
state.roles = [];
}
state.status = 'Online';
state.isLoading = false;
state.error = null;
})
.addCase(authenticateCredentialsThunk.rejected, (state, action) => {
state.applicationId = null;
state.sessionId = null;
state.isAuthenticated = false;
state.isLoading = false;
state.error = (action.payload as string) || 'Authentication failed';
state.secret = null;
state.status = 'Offline';
state.roles = [];
})
.addCase(signoutThunk.pending, state => {
state.isLoading = true;
state.error = null;
})
.addCase(signoutThunk.fulfilled, state => {
state.isAuthenticated = false;
state.isLoading = false;
state.error = null;
state.secret = null;
state.status = 'Offline';
state.roles = [];
})
.addCase(signoutThunk.rejected, (state, action) => {
state.isAuthenticated = false;
state.isLoading = false;
state.error = (action.payload as string) || 'Signout failed';
state.secret = null;
state.status = 'Offline';
state.roles = [];
});
}
});
export const {setIsLoading, setCredentials, clearState, setSessionId, setIsAuthenticated, setRoles, setApplicationId} = authenticationSlice.actions;
export {authenticateCredentialsThunk};
export default authenticationSlice.reducer;

10
src/store/store.ts Normal file
View File

@@ -0,0 +1,10 @@
import {configureStore} from '@reduxjs/toolkit';
import AuthenticationState from './slices/Authentication.slice';
const store = configureStore({
reducer: {
authentication: AuthenticationState
}
});
export default store;