From 3ebfac7241af80932fd2e9298e22a52398e405bf Mon Sep 17 00:00:00 2001 From: Muniz Date: Wed, 25 Oct 2023 22:23:23 -0300 Subject: [PATCH 01/11] feat: persist device uuid in colection session --- src/services/firestore-auth.ts | 18 ++++++++++++++++-- src/state/user.ts | 6 +++++- src/webrtc/index.ts | 1 + 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/services/firestore-auth.ts b/src/services/firestore-auth.ts index 81added..d038996 100644 --- a/src/services/firestore-auth.ts +++ b/src/services/firestore-auth.ts @@ -8,8 +8,10 @@ import { signInWithRedirect, signOut, } from "firebase/auth"; -import { googleAuthProvider } from "./firestore-connection"; +import { v4 as uuidv4 } from "uuid"; +import { db, googleAuthProvider } from "./firestore-connection"; import type { User } from "../webrtc"; +import { doc, setDoc } from "firebase/firestore"; const firestoreAuth = { loadUser, @@ -24,7 +26,12 @@ async function loadUser(): Promise { return new Promise((resolve, reject) => { const handleResult = (user: FirebaseUser | null) => { if (!user) { - const empty: User = { uid: "", displayName: "", email: "" }; + const empty: User = { + uid: "", + displayName: "", + email: "", + deviceUuid: "", + }; resolve(empty); return; } @@ -34,10 +41,17 @@ async function loadUser(): Promise { return; } + const deviceUuid = uuidv4(); + // Create or update the user session in collection "session" + setDoc(doc(db, `session/${user.uid}`), { + deviceUuid, + }); + resolve({ uid: user.uid, displayName: user.displayName ?? user.email, email: user.email, + deviceUuid, }); }; diff --git a/src/state/user.ts b/src/state/user.ts index c734310..15d2bf0 100644 --- a/src/state/user.ts +++ b/src/state/user.ts @@ -17,6 +17,7 @@ export const userInitialState: UserState = { displayName: "", email: "", status: "idle", + deviceUuid: "", errorMessage: "", }; @@ -30,6 +31,7 @@ export const userSlice = createSlice({ state.displayName = ""; state.email = ""; state.status = "pending"; + state.deviceUuid = ""; state.errorMessage = ""; }); @@ -38,7 +40,8 @@ export const userSlice = createSlice({ state.displayName = action.payload.displayName || ""; state.email = action.payload.email || ""; state.status = action.payload.uid ? "authenticated" : "idle"; - state.errorMessage = ""; + state.deviceUuid = action.payload.deviceUuid; + state.errorMessage = state.errorMessage = ""; }); builder.addCase(login.rejected, (state, action) => { @@ -46,6 +49,7 @@ export const userSlice = createSlice({ state.displayName = ""; state.email = ""; state.status = "error"; + state.deviceUuid = ""; state.errorMessage = action.error.message || "Unknown error."; }); diff --git a/src/webrtc/index.ts b/src/webrtc/index.ts index 3052232..6eeafa2 100644 --- a/src/webrtc/index.ts +++ b/src/webrtc/index.ts @@ -21,6 +21,7 @@ export default { export interface User extends UniqueEntity { displayName: string; email: string; + deviceUuid: string; } export interface Call extends UniqueEntity { From 77b346c505cc4fe45916ec78e96a578296162486 Mon Sep 17 00:00:00 2001 From: Muniz Date: Thu, 26 Oct 2023 00:02:51 -0300 Subject: [PATCH 02/11] feat: unlog old user session --- src/App.tsx | 2 ++ src/hooks/useValidateSession.ts | 34 +++++++++++++++++++++++++++++++++ src/services/firestore-auth.ts | 33 +++++++++++++++++++++++++++++++- src/state/user.ts | 1 + 4 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 src/hooks/useValidateSession.ts diff --git a/src/App.tsx b/src/App.tsx index 3219ca1..9da76c8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,8 +10,10 @@ import P2PCallMain from "./features/p2p-call/P2PCallMain"; import CallCreationMain from "./features/call-selection/CallCreationMain"; import CallJoinMain from "./features/call-selection/CallJoinMain"; import PendingUserMain from "./features/call-selection/PendingUserMain"; +import useValidateSession from "./hooks/useValidateSession"; export default function App() { + useValidateSession(); const dispatch = useAppDispatch(); useEffect(() => { diff --git a/src/hooks/useValidateSession.ts b/src/hooks/useValidateSession.ts new file mode 100644 index 0000000..4eb4c85 --- /dev/null +++ b/src/hooks/useValidateSession.ts @@ -0,0 +1,34 @@ +import { useEffect } from "react"; +import { useAppDispatch, useAppSelector } from "../state"; +import { logout, selectUserDeviceUuid, selectUserUid } from "../state/user"; +import firestoreAuth from "../services/firestore-auth"; + +export default function useValidateSession() { + const dispatch = useAppDispatch(); + + const userUid = useAppSelector(selectUserUid); + const userDeviceUuid = useAppSelector(selectUserDeviceUuid); + + useEffect(() => { + if (!userDeviceUuid) { + return () => null; + } + + return firestoreAuth.listenUserSession(userUid, (session) => { + if (!session || !session?.deviceUuid) { + return; + } + + console.log( + "session.deviceUuid === userDeviceUuid", + session.deviceUuid === userDeviceUuid + ); + + if (session.deviceUuid === userDeviceUuid) { + return; + } + + dispatch(logout()); + }); + }, [dispatch, userDeviceUuid, userUid]); +} diff --git a/src/services/firestore-auth.ts b/src/services/firestore-auth.ts index d038996..bb4a332 100644 --- a/src/services/firestore-auth.ts +++ b/src/services/firestore-auth.ts @@ -11,12 +11,21 @@ import { import { v4 as uuidv4 } from "uuid"; import { db, googleAuthProvider } from "./firestore-connection"; import type { User } from "../webrtc"; -import { doc, setDoc } from "firebase/firestore"; +import { + DocumentData, + Unsubscribe, + collection, + doc, + onSnapshot, + query, + setDoc, +} from "firebase/firestore"; const firestoreAuth = { loadUser, login, logout, + listenUserSession, }; export default firestoreAuth; @@ -81,3 +90,25 @@ async function login(): Promise { async function logout() { signOut(getAuth()); } + +interface SessionResult { + userUid: string; + deviceUuid: string; +} + +function listenUserSession( + userUid: string, + callback: (result?: Omit) => void +): Unsubscribe { + return onSnapshot(query(collection(db, `session`)), (querySnapshot) => { + const sessions: SessionResult[] = []; + + querySnapshot.forEach((doc: DocumentData) => { + sessions.push({ ...doc.data(), userUid: doc.id } as SessionResult); + }); + + const session = sessions.find((s) => s.userUid === userUid); + + callback({ deviceUuid: session?.deviceUuid || "" }); + }); +} diff --git a/src/state/user.ts b/src/state/user.ts index 15d2bf0..59ede61 100644 --- a/src/state/user.ts +++ b/src/state/user.ts @@ -91,6 +91,7 @@ export const selectUserDisplayName = (state: RootState) => state.user.displayName; export const selectUserUid = (state: RootState) => state.user.uid; +export const selectUserDeviceUuid = (state: RootState) => state.user.deviceUuid; export const selectIsUserPendingAuthentication = (state: RootState) => state.user.status === "pending"; From 9b5f605f8d1e9031cc32b22b5fc5428348b6faf3 Mon Sep 17 00:00:00 2001 From: Muniz Date: Thu, 26 Oct 2023 01:10:58 -0300 Subject: [PATCH 03/11] feat: show expired session message --- src/features/auth/AuthMain.tsx | 9 ++++++++- src/hooks/useValidateSession.ts | 13 ++++++------- src/state/user.ts | 27 +++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/features/auth/AuthMain.tsx b/src/features/auth/AuthMain.tsx index 74ab92a..a0703ef 100644 --- a/src/features/auth/AuthMain.tsx +++ b/src/features/auth/AuthMain.tsx @@ -4,7 +4,12 @@ import Typography from "@mui/material/Typography"; import GoogleIcon from "@mui/icons-material/Google"; import HomeTemplate from "../../components/templates/HomeTemplate"; import { useAppDispatch, useAppSelector } from "../../state"; -import { login, selectIsUserPendingAuthentication } from "../../state/user"; +import { + login, + selectExpiredSessionMessage, + selectIsUserPendingAuthentication, +} from "../../state/user"; +import ErrorAlert from "../../components/basic/ErrorAlert"; export default function AuthMain() { const dispatch = useAppDispatch(); @@ -12,6 +17,7 @@ export default function AuthMain() { const handleLoginClick = () => dispatch(login()); const isPending = useAppSelector(selectIsUserPendingAuthentication); + const expiredSessionMessage = useAppSelector(selectExpiredSessionMessage); return ( @@ -26,6 +32,7 @@ export default function AuthMain() { A proof of concept. +