Skip to content
This repository has been archived by the owner on Jun 6, 2024. It is now read-only.

Fix #131. Prepare for production #132

Merged
merged 1 commit into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ NODE_ENV=production npm run build
Then, start the production server with:

```shell
NODE_ENV=production npm run start
NODE_ENV=production npm run start # WARNING: THIS WILL USE THE PRODUCTION FIREBASE RESOURCES!
```

After a finished development cycle, exit the `server` container and run:
Expand Down
3 changes: 2 additions & 1 deletion app/account/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { FC, ReactElement } from "react";
import AccountPage from "@/components/client/account-page";
import { verifySession } from "@/utils/server-actions";
import { redirect } from "next/navigation";

const Account: FC = async (): Promise<ReactElement | null> => {
const session = await verifySession();

if (!session) {
return null;
redirect("/");
}

return <AccountPage />;
Expand Down
2 changes: 2 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { theme } from "@/app/theme";
import { AppRouterCacheProvider } from "@mui/material-nextjs/v14-appRouter";
import CssBaseline from "@mui/material/CssBaseline";
import InitColorSchemeScript from "@/components/client/init-color-scheme-script";
import { SpeedInsights } from "@vercel/speed-insights/next";

export const metadata: Metadata = {
title: "ChessTeacher",
Expand All @@ -27,6 +28,7 @@ const RootLayout: FC<PropsWithChildren> = ({ children }: PropsWithChildren) => {
</Providers>
</CssVarsProvider>
</AppRouterCacheProvider>
<SpeedInsights />
</body>
</html>
);
Expand Down
3 changes: 2 additions & 1 deletion app/match/[mid]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import MatchPage from "@/components/server/match-page";
import type { MatchId } from "@/types/database";
import { verifySession } from "@/utils/server-actions";
import MatchProvider from "@/components/client/providers/match-provider";
import { redirect } from "next/navigation";

interface MatchProps {
readonly params: {
Expand All @@ -16,7 +17,7 @@ const Match: FC<MatchProps> = async ({
const session = await verifySession();

if (!session) {
return null;
redirect("/");
}

return (
Expand Down
3 changes: 1 addition & 2 deletions components/client/lobby-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
} from "firebase/database";
import { getMatchesRef, getMatchPlayerRef } from "@/firebase/firebase";
import type { User } from "@firebase/auth";
import type { SessionUser } from "@/types/session";
import { findPlayerUidByPlayerNumber } from "@/utils/utils";
import type { Color } from "chess.js";
import { toast } from "react-toastify";
Expand All @@ -39,7 +38,7 @@ interface MatchRowModel {
const getMatchRowPlayerData = (
matchRecord: MatchRecord,
key: string,
uid: SessionUser["uid"] | undefined,
uid: User["uid"] | undefined,
): MatchRowPlayerData | undefined => {
if (undefined === uid) {
return undefined;
Expand Down
83 changes: 6 additions & 77 deletions firebase/auth.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,11 @@
import type { UserCredential } from "firebase/auth";
import { GoogleAuthProvider, signInWithPopup } from "firebase/auth";
import { auth, getActiveUserRef } from "@/firebase/firebase";
import type { DataSnapshot } from "firebase/database";
import { get, remove, set, update } from "firebase/database";
import type { ActiveUser, ActiveUserRecord } from "@/types/database";
import { auth } from "@/firebase/firebase";
import {
createSessionCookie,
deleteSessionCookie,
} from "@/utils/server-actions";

const sessionCountBeforeRemoval = 1;
const sessionCountIncrement = 1;

const findActiveUser = async (
uid: keyof ActiveUserRecord,
): Promise<DataSnapshot> => {
return get(getActiveUserRef(uid));
};

const isUserActive = async (uid: keyof ActiveUserRecord): Promise<boolean> => {
const snapshot = await findActiveUser(uid);

return snapshot.exists();
};

const setUserAsActive = async (user: ActiveUserRecord): Promise<void> => {
const [uid] = Object.keys(user);

await set(getActiveUserRef(uid), {
...user[uid],
});
};

const setUserAsInactive = async (
uid: keyof ActiveUserRecord,
): Promise<void> => {
const snapshot = await findActiveUser(uid);

if (!snapshot.exists()) {
return;
}

const { sessionCount: currentSessionCount } = snapshot.val() as ActiveUser;

if (sessionCountBeforeRemoval === currentSessionCount) {
await remove(getActiveUserRef(uid));

return;
}

const sessionCount = currentSessionCount - sessionCountIncrement;

await update(getActiveUserRef(uid), { sessionCount });
};

export const signInWithGoogle = async (): Promise<
UserCredential | undefined
> => {
Expand All @@ -69,42 +21,19 @@ export const signInWithGoogle = async (): Promise<
return undefined;
}

const { uid, displayName } = user.user;

const isOnline = await isUserActive(uid);

let sessionCount = 1;

if (isOnline) {
const snapshot = await findActiveUser(uid);
const { sessionCount: currentSessionCount } = snapshot.val() as ActiveUser;
sessionCount = currentSessionCount + sessionCountIncrement;

await update(getActiveUserRef(uid), { sessionCount });
} else {
await setUserAsActive({
[uid]: {
displayName,
sessionCount,
},
});
}

await createSessionCookie(uid);
await createSessionCookie(user.user.uid);

return user;
};

export const signOut = async (): Promise<void> => {
if (auth.currentUser) {
await setUserAsInactive(auth.currentUser.uid);
}

try {
await auth.signOut();

await deleteSessionCookie();
} catch (error) {
console.error("Error signing out with Google", error);

return;
}

await deleteSessionCookie();
};
11 changes: 1 addition & 10 deletions firebase/firebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ export const app = initializeApp(firebaseConfig);
export const database = getDatabase(app);
export const auth = getAuth(app);

export const getActiveUserRef = (uid: User["uid"]): DatabaseReference =>
ref(database, `activeUsers/${uid}`);

export const getMatchesRef = (): DatabaseReference => ref(database, "matches");

export const getMatchPlayersRef = (mid: MatchId): DatabaseReference =>
Expand All @@ -38,13 +35,7 @@ export const getMatchStateRef = (mid: MatchId): DatabaseReference =>
export const getMessagesRef = (mid: MatchId): DatabaseReference =>
ref(database, `messages/${mid}`);

/**
* TODO Currently the app is hardcoded to use the emulators by default. This
* should be changed later on so that it's convenient to switch between hosted
* emulated Firebase.
*/
// eslint-disable-next-line
if (true) {
if ("production" !== process.env.NODE_ENV) {
connectDatabaseEmulator(
database,
process.env.NEXT_PUBLIC_FIREBASE_DATABASE_EMULATOR_HOST ?? "localhost",
Expand Down
43 changes: 39 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "NODE_OPTIONS='--inspect' next dev",
"dev": "next dev --turbo",
"build": "next build",
"start": "next start",
"lint": "next lint",
Expand All @@ -24,6 +24,7 @@
"@mui/material-nextjs": "^5.15.11",
"@mui/x-data-grid": "^7.3.0",
"@tanstack/react-query": "^5.29.2",
"@vercel/speed-insights": "^1.0.10",
"chess.js": "^0.13.4",
"firebase": "^10.11.0",
"jose": "^5.2.4",
Expand Down
10 changes: 1 addition & 9 deletions types/database.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
import type { User } from "@firebase/auth";
import type { Color } from "chess.js";

export type BaseUser = Pick<User, "displayName">;

export interface ActiveUser extends BaseUser {
readonly sessionCount: number;
}

export type ActiveUserRecord = Readonly<Record<User["uid"], ActiveUser>>;

export enum PlayerNumber {
playerOne = 1,
playerTwo = 2,
}

export interface MatchPlayerInfo extends BaseUser {
export interface MatchPlayerInfo extends Pick<User, "displayName"> {
readonly color: Color;
readonly playerNumber: PlayerNumber;
}
Expand Down
4 changes: 1 addition & 3 deletions types/session.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import type { User } from "@firebase/auth";
import type { JWTPayload } from "jose";

export type SessionUser = Pick<User, "uid">;

export interface SessionPayload extends JWTPayload, SessionUser {
export interface SessionPayload extends JWTPayload, Pick<User, "uid"> {
readonly maxAge: number;
}
2 changes: 1 addition & 1 deletion utils/server-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const deleteSessionCookie = async (): Promise<void> => {
export const verifySession = async (): Promise<SessionPayload | null> => {
const sessionCookieValue = cookies().get("session")?.value;

if (undefined === sessionCookieValue) {
if (undefined === sessionCookieValue || "" === sessionCookieValue) {
return null;
}

Expand Down
5 changes: 3 additions & 2 deletions utils/session.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import "server-only";

import { jwtVerify, SignJWT } from "jose";
import type { SessionPayload, SessionUser } from "@/types/session";
import type { SessionPayload } from "@/types/session";
import { cookies } from "next/headers";
import type { User } from "@firebase/auth";

const encodedKey = new TextEncoder().encode(process.env.SECRET_KEY);

Expand Down Expand Up @@ -43,7 +44,7 @@ export const decrypt = async (
return undefined;
};

export const createSession = async (uid: SessionUser["uid"]): Promise<void> => {
export const createSession = async (uid: User["uid"]): Promise<void> => {
const sessionCookieValue = await encrypt({
uid,
maxAge: oneWeek,
Expand Down
Loading