Skip to content

Commit

Permalink
Auth, Redux, and Summary fetching
Browse files Browse the repository at this point in the history
  • Loading branch information
NebraskaCoder committed Jun 28, 2024
1 parent 5a37563 commit a487322
Show file tree
Hide file tree
Showing 16 changed files with 203 additions and 48 deletions.
20 changes: 18 additions & 2 deletions app/[locale]/(loggedIn)/SummaryList.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
import { useTranslations } from "next-intl";
import { getAuthFetchHeaders } from "@/lib/server/auth";
import { cookies } from "next/headers";
import * as apiServerLib from "@/lib/server/apiServerLib";

import SummaryItem from "./components/SummaryItem";

import { CardGrid } from "@/components/ui/card-grid";

const SummaryList = () => {
const SummaryList = async () => {
const t = useTranslations("dashboard.summary");

const authHeaders = await getAuthFetchHeaders(cookies());

if (!authHeaders) {
return null;
}

const systems = await apiServerLib.getSystems(authHeaders);

if (!systems) {
return null;
}

const stats = {
totalSystems: 2,
totalSystems: systems.count,
totalScanners: 18,
totalScanLists: 9,
totalDepartments: 5,
Expand Down
6 changes: 5 additions & 1 deletion app/[locale]/(loggedIn)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Suspense } from "react";
import { useTranslations } from "next-intl";

import SummaryList from "./SummaryList";
Expand All @@ -17,7 +18,10 @@ export default function DashboardPage() {
return (
<div className="flex flex-col gap-y-5">
<H1>{t("summary.header")}</H1>
<SummaryList />
{/* TODO: Create SummaryList Loading Component */}
<Suspense fallback="Loading...">
<SummaryList />
</Suspense>
<RecentTransmissions />
</div>
);
Expand Down
27 changes: 23 additions & 4 deletions components/ReduxProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
"use client";

import { store } from "@/state/store/store";
import React, { ReactNode } from "react";
import { makeStore } from "@/state/store/store";
import React, { type ReactNode, useRef, useEffect } from "react";
import { Provider } from "react-redux";
import { setupListeners } from "@reduxjs/toolkit/query";

import type { AppStore } from "@/state/store";

type ReduxProviderType = {
children: ReactNode;
readonly children: ReactNode;
};

const ReduxProvider = ({ children }: ReduxProviderType) => {
return <Provider store={store}>{children}</Provider>;
const storeRef = useRef<AppStore | null>(null);

if (!storeRef.current) {
// Create the store instance the first time this renders
storeRef.current = makeStore();
}

useEffect(() => {
if (storeRef.current != null) {
// configure listeners using the provided defaults
// optional, but required for `refetchOnFocus`/`refetchOnReconnect` behaviors
const unsubscribe = setupListeners(storeRef.current.dispatch);
return unsubscribe;
}
}, []);

return <Provider store={storeRef.current}>{children}</Provider>;
};

export default ReduxProvider;
1 change: 1 addition & 0 deletions lib/client/trunkPlayerSocketClientLib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// TODO: Implement the client side of the trunk player Socket.IO client library
19 changes: 19 additions & 0 deletions lib/server/apiServerLib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { AuthFetchHeaders } from "./auth";
import type { PaginatedResponse } from "@/types/api/APIResponses";
import type { System } from "@/types/api/System";

export const getSystems = async (authHeaders: AuthFetchHeaders) => {
const systemsRes = await fetch(
`${process.env.SERVICE_API_BASE_URL}/radio/system/list`,
{
method: "GET",
headers: authHeaders,
}
);

if (!systemsRes.ok) {
return null;
}

return (await systemsRes.json()) as PaginatedResponse<System[]>;
};
37 changes: 32 additions & 5 deletions lib/server/auth.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { getServerSession as nextAuthGetServerSession } from "next-auth";
import { OPTIONS } from "@/config/nextAuthOptions";
import { getToken } from "next-auth/jwt";
import { type JWT, decode, getToken } from "next-auth/jwt";
import { parseRefreshToken } from "@/utils/fetchUtils";

import type { NextRequest } from "next/server";
import type { ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
import type {
GetServerSidePropsContext,
NextApiRequest,
NextApiResponse,
} from "next";
import type { NextRequest } from "next/server";

export const authSecret = process.env.NEXTAUTH_SECRET!;
export const sessionSecure =
process.env.NEXTAUTH_URL?.startsWith("https://") ?? false;
export const sessionCookieName = sessionSecure
? "__Secure-next-auth.session-token"
: "next-auth.session-token";

export async function getServerSession(
...args:
Expand All @@ -22,19 +30,38 @@ export async function getServerSession(
export async function getServerJWT(
req: GetServerSidePropsContext["req"] | NextRequest | NextApiRequest
) {
return await getToken({ req, secret: process.env.JWT_SECRET });
return await getToken({ req, secret: authSecret });
}

const getAuthJWTTokenFromCookies = async (
cookies: ReadonlyRequestCookies
): Promise<JWT | null> => {
const jwt = await decode({
secret: authSecret,
token: cookies.get(sessionCookieName)?.value,
});

return jwt;
};

export type AuthFetchHeaders =
| { Authorization: string }
| { Authorization: string; Cookie: string }
| undefined;

export async function getAuthFetchHeaders(
req: GetServerSidePropsContext["req"] | NextRequest | NextApiRequest
req:
| GetServerSidePropsContext["req"]
| NextRequest
| NextApiRequest
| ReadonlyRequestCookies
): Promise<AuthFetchHeaders> {
const session = await getServerSession();
const token = await getServerJWT(req);

const token =
"get" in req
? await getAuthJWTTokenFromCookies(req)
: await getServerJWT(req);

if (!session || !token) {
return undefined;
Expand Down
35 changes: 35 additions & 0 deletions state/slices/entitySlices/systemsSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { createEntityAdapter, createSlice } from "@reduxjs/toolkit";

import type { System } from "@/types/api/System";
import type { AppState } from "@/state/store";

const systemsAdapter = createEntityAdapter({
selectId: (system: System) => system.UUID,
sortComparer: (a, b) => a.name.localeCompare(b.name),
});

const initialState = systemsAdapter.getInitialState();

const systemsSlice = createSlice({
name: "systems",
initialState,
reducers: {
addSystem: systemsAdapter.addOne,
addSystems: systemsAdapter.addMany,
updateSystem: systemsAdapter.updateOne,
removeSystem: systemsAdapter.removeOne,
},
});

export const { addSystem, addSystems, updateSystem, removeSystem } =
systemsSlice.actions;

export default systemsSlice.reducer;

export const {
selectById: selectSystemById,
selectIds: selectSystemIds,
selectEntities: selectSystemEntities,
selectAll: selectAllSystems,
selectTotal: selectTotalSystems,
} = systemsAdapter.getSelectors<AppState>((state) => state.systems);
29 changes: 0 additions & 29 deletions state/slices/layoutSlice.ts

This file was deleted.

26 changes: 26 additions & 0 deletions state/slices/socketConnectionSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { SocketConnectionState } from "@/types/state/SocketConnectionState";

import type { AppState } from "../store";

const initialState: SocketConnectionState = {
isConnected: false,
};

export const socketConnectionSlice = createSlice({
name: "socketConnection",
initialState,
reducers: {
setIsConnected: (state, action: PayloadAction<boolean>) => {
state.isConnected = action.payload;
},
},
extraReducers: (_builder) => {},
});

export const { setIsConnected } = socketConnectionSlice.actions;

export const selectIsSocketConnected = (state: AppState) =>
state.socketConnection.isConnected;

export default socketConnectionSlice.reducer;
2 changes: 2 additions & 0 deletions state/store/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { store, makeStore } from "./store";
import { ThunkAction, Action } from "@reduxjs/toolkit";

export type AppStore = ReturnType<typeof makeStore>;

export type AppState = ReturnType<typeof store.getState>;

export type AppDispatch = typeof store.dispatch;
Expand Down
10 changes: 6 additions & 4 deletions state/store/store.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { configureStore } from "@reduxjs/toolkit";

import layoutReducer from "@/state/slices/layoutSlice";
import { socketConnectionSlice } from "../slices/socketConnectionSlice";
import systemsReducer from "../slices/entitySlices/systemsSlice";

export function makeStore() {
export const makeStore = () => {
return configureStore({
reducer: {
layout: layoutReducer,
socketConnection: socketConnectionSlice.reducer,
systems: systemsReducer,
},
devTools: process.env.NODE_ENV !== "production",
});
}
};

export const store = makeStore();
6 changes: 6 additions & 0 deletions types/api/APIResponses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface PaginatedResponse<T extends Array<any> = any[]> {
count: number;
next: string | null;
previous: string | null;
results: T;
}
10 changes: 10 additions & 0 deletions types/api/System.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface System {
UUID: string;
name: string;
systemACL: string;
rr_system_id?: string;
enable_talkgroup_acls: boolean;
prune_transmissions: boolean;
prune_transmissions_after_days: number;
notes: string;
}
3 changes: 0 additions & 3 deletions types/state/LayoutState.ts

This file was deleted.

3 changes: 3 additions & 0 deletions types/state/SocketConnectionState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface SocketConnectionState {
isConnected: boolean;
}
17 changes: 17 additions & 0 deletions utils/authUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { type AuthFetchHeaders, getAuthFetchHeaders } from "@/lib/server/auth";

import type { ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";

import type { NextRequest } from "next/server";

export const getAuthFetchHeadersOrThrow = async (
req: NextRequest | ReadonlyRequestCookies
): Promise<AuthFetchHeaders> => {
const fetchHeaders = await getAuthFetchHeaders(req);

if (!fetchHeaders) {
throw new Error("Unauthorized");
}

return fetchHeaders;
};

0 comments on commit a487322

Please sign in to comment.