From 265e14c8e5f02ffb1a280068eced012f72f799a1 Mon Sep 17 00:00:00 2001
From: Rodrigo Muniz <32344098+rcmuniz1994@users.noreply.github.com>
Date: Sun, 29 Oct 2023 14:23:37 -0300
Subject: [PATCH] Avoid same user in multi devices (#47)
---
src/App.tsx | 4 +
.../templates/HomeTemplate.test.tsx | 21 +++
src/components/templates/HomeTemplate.tsx | 25 ++--
src/features/auth/BlockedSession.test.tsx | 10 ++
src/features/auth/BlockedSession.tsx | 21 +++
src/hooks/useRedirectionRule.test.ts | 129 +++++++++++++++---
src/hooks/useRedirectionRule.ts | 42 +++++-
src/hooks/useValidateSession.ts | 35 +++++
src/services/firestore-auth.test.ts | 52 +++++++
src/services/firestore-auth.ts | 49 ++++++-
src/state/user.ts | 23 +++-
src/testing-helpers/call-fixtures.ts | 1 +
src/webrtc/index.ts | 1 +
13 files changed, 379 insertions(+), 34 deletions(-)
create mode 100644 src/features/auth/BlockedSession.test.tsx
create mode 100644 src/features/auth/BlockedSession.tsx
create mode 100644 src/hooks/useValidateSession.ts
diff --git a/src/App.tsx b/src/App.tsx
index 3219ca1..1422637 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -10,8 +10,11 @@ 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";
+import BlockedSession from "./features/auth/BlockedSession";
export default function App() {
+ useValidateSession();
const dispatch = useAppDispatch();
useEffect(() => {
@@ -29,6 +32,7 @@ export default function App() {
Left.
} />
+
diff --git a/src/components/templates/HomeTemplate.test.tsx b/src/components/templates/HomeTemplate.test.tsx
index 42d05e7..23b363a 100644
--- a/src/components/templates/HomeTemplate.test.tsx
+++ b/src/components/templates/HomeTemplate.test.tsx
@@ -161,4 +161,25 @@ describe("HomeTemplate", () => {
)
).toBe(null);
});
+
+ it("does not show logout and settings buttons when user session is blocked", () => {
+ fullRender(
+
+ Hello World!
+ ,
+ {
+ preloadedState: {
+ user: {
+ ...userInitialState,
+ uid: "123",
+ status: "authenticated",
+ isSessionBlocked: true,
+ },
+ },
+ }
+ );
+
+ expect(screen.queryByLabelText("Logout")).toBe(null);
+ expect(screen.queryByLabelText("Settings")).toBe(null);
+ });
});
diff --git a/src/components/templates/HomeTemplate.tsx b/src/components/templates/HomeTemplate.tsx
index ce49ab5..0305b7e 100644
--- a/src/components/templates/HomeTemplate.tsx
+++ b/src/components/templates/HomeTemplate.tsx
@@ -12,7 +12,11 @@ import mainCharWhiteOutlineImg from "../../assets/main-char-white-outline.png";
import mainCharPinkOutlineImg from "../../assets/main-char-pink-outline.png";
import { useAppDispatch, useAppSelector } from "../../state";
import useAgentHelper from "../../hooks/useAgentHelper";
-import { logout, selectIsUserAuthenticated } from "../../state/user";
+import {
+ logout,
+ selectIsSessionBlocked,
+ selectIsUserAuthenticated,
+} from "../../state/user";
import SettingsModal from "../settings/SettingsModal";
import ErrorAlert from "../basic/ErrorAlert";
import WarningAlert from "../basic/WarningAlert";
@@ -32,6 +36,7 @@ export default function HomeTemplate({
const handleLogoutClick = () => dispatch(logout());
const isAuthenticated = useAppSelector(selectIsUserAuthenticated);
+ const isSessionBlocked = useAppSelector(selectIsSessionBlocked);
const { canRunWebRTC, isChromeBased, isFirefoxBased } = useAgentHelper();
@@ -92,7 +97,7 @@ export default function HomeTemplate({
pb: 1,
}}
>
- {isAuthenticated && (
+ {isAuthenticated && !isSessionBlocked && (
)}
-
-
-
+ {!isSessionBlocked && (
+
+
+
+ )}
{!canRunWebRTC() && (
diff --git a/src/features/auth/BlockedSession.test.tsx b/src/features/auth/BlockedSession.test.tsx
new file mode 100644
index 0000000..6637f7e
--- /dev/null
+++ b/src/features/auth/BlockedSession.test.tsx
@@ -0,0 +1,10 @@
+import "../../testing-helpers/mock-firestore-auth";
+import { act } from "react-dom/test-utils";
+import fullRender from "../../testing-helpers/fullRender";
+import BlockedSession from "./BlockedSession";
+
+describe("BlockedSession", () => {
+ it("renders", async () => {
+ await act(() => fullRender());
+ });
+});
diff --git a/src/features/auth/BlockedSession.tsx b/src/features/auth/BlockedSession.tsx
new file mode 100644
index 0000000..e07c05f
--- /dev/null
+++ b/src/features/auth/BlockedSession.tsx
@@ -0,0 +1,21 @@
+import Box from "@mui/material/Box";
+import HomeTemplate from "../../components/templates/HomeTemplate";
+import useRedirectionRule from "../../hooks/useRedirectionRule";
+import { Redirect } from "wouter";
+import ErrorAlert from "../../components/basic/ErrorAlert";
+
+export default function BlockedSession() {
+ const goTo = useRedirectionRule();
+
+ if (goTo) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/hooks/useRedirectionRule.test.ts b/src/hooks/useRedirectionRule.test.ts
index 27a56db..968052a 100644
--- a/src/hooks/useRedirectionRule.test.ts
+++ b/src/hooks/useRedirectionRule.test.ts
@@ -2,18 +2,24 @@ import { getRedirectionRule } from "./useRedirectionRule";
describe("getRedirectionRule: / (auth)", () => {
it("when authenticated, go to creation", () => {
- const result = getRedirectionRule({ path: "/", hasAuth: true }, {});
+ const result = getRedirectionRule(
+ { path: "/", hasAuth: true, isSessionBlocked: false },
+ {}
+ );
expect(result).toBe("/create");
});
it("when not authenticated, stay here for auth", () => {
- const result = getRedirectionRule({ path: "/", hasAuth: false }, {});
+ const result = getRedirectionRule(
+ { path: "/", hasAuth: false, isSessionBlocked: false },
+ {}
+ );
expect(result).toBe("");
});
it("when autenticated and provided joining uid, then go to join page proceed to device testing", () => {
const result = getRedirectionRule(
- { path: "/", hasAuth: true },
+ { path: "/", hasAuth: true, isSessionBlocked: false },
{ joining: "123-321" }
);
expect(result).toBe("/join?callUid=123-321");
@@ -21,7 +27,7 @@ describe("getRedirectionRule: / (auth)", () => {
it("when autenticated and provided creating display name, then go to create page proceed to device testing", () => {
const result = getRedirectionRule(
- { path: "/", hasAuth: true },
+ { path: "/", hasAuth: true, isSessionBlocked: false },
{ creating: "Daily" }
);
expect(result).toBe("/create?callDisplayName=Daily"); // TODO: names with special characters?
@@ -30,28 +36,52 @@ describe("getRedirectionRule: / (auth)", () => {
describe("getRedirectionRule: /create", () => {
it("when not authenticated, then go back to root for login", () => {
- const result = getRedirectionRule({ path: "/create", hasAuth: false }, {});
+ const result = getRedirectionRule(
+ { path: "/create", hasAuth: false, isSessionBlocked: false },
+ {}
+ );
expect(result).toBe("/");
});
it("when authenticated, stay here for device testing and call creation", () => {
- const result = getRedirectionRule({ path: "/create", hasAuth: true }, {});
+ const result = getRedirectionRule(
+ { path: "/create", hasAuth: true, isSessionBlocked: false },
+ {}
+ );
expect(result).toBe("");
});
it("when authenticated and has ongoing call, go to its page", () => {
const result = getRedirectionRule(
- { path: "/create", hasAuth: true, ongoingCall: "123-321" },
+ {
+ path: "/create",
+ hasAuth: true,
+ ongoingCall: "123-321",
+ isSessionBlocked: false,
+ },
{}
);
expect(result).toBe("/p2p-call/123-321");
});
+
+ it("when session is blocked, got to blocked session page", () => {
+ const result = getRedirectionRule(
+ {
+ path: "/create",
+ hasAuth: true,
+ ongoingCall: "123-321",
+ isSessionBlocked: true,
+ },
+ {}
+ );
+ expect(result).toBe("/blocked-session");
+ });
});
describe("getRedirectionRule: /join", () => {
it("when authenticated and provided call uid, then stay here for device testing", () => {
const result = getRedirectionRule(
- { path: "/join", hasAuth: true },
+ { path: "/join", hasAuth: true, isSessionBlocked: false },
{ callUid: "123-321" }
);
expect(result).toBe("");
@@ -59,35 +89,64 @@ describe("getRedirectionRule: /join", () => {
it("when not authenticated but provided call uid, then go authenticate", () => {
const result = getRedirectionRule(
- { path: "/join", hasAuth: false },
+ { path: "/join", hasAuth: false, isSessionBlocked: false },
{ callUid: "123-321" }
);
expect(result).toBe("/?joining=123-321");
});
it("when not authenticated nor provided call uid, then start reset entire flow (came here by mistake?)", () => {
- const result = getRedirectionRule({ path: "/join", hasAuth: false }, {});
+ const result = getRedirectionRule(
+ { path: "/join", hasAuth: false, isSessionBlocked: false },
+ {}
+ );
expect(result).toBe("/");
});
it("when authenticated and didnt provide call uid, then stay to manually type it", () => {
- const result = getRedirectionRule({ path: "/join", hasAuth: true }, {});
+ const result = getRedirectionRule(
+ { path: "/join", hasAuth: true, isSessionBlocked: false },
+ {}
+ );
expect(result).toBe("");
});
it("when authenticated and has pending call, go to pending user page", () => {
const result = getRedirectionRule(
- { path: "/join", hasAuth: true, pendingCall: "123-call-321" },
+ {
+ path: "/join",
+ hasAuth: true,
+ pendingCall: "123-call-321",
+ isSessionBlocked: false,
+ },
{}
);
expect(result).toBe("/pending");
});
+
+ it("when session is blocked, got to blocked session page", () => {
+ const result = getRedirectionRule(
+ {
+ path: "/join",
+ hasAuth: true,
+ pendingCall: "123-call-321",
+ isSessionBlocked: true,
+ },
+ {}
+ );
+ expect(result).toBe("/blocked-session");
+ });
});
describe("getRedirectionRule: /join", () => {
it("if has pending call, stay waiting", () => {
const result = getRedirectionRule(
- { path: "/pending", hasAuth: true, pendingCall: "123-call-321" },
+ {
+ path: "/pending",
+ hasAuth: true,
+ pendingCall: "123-call-321",
+ isSessionBlocked: false,
+ },
{}
);
expect(result).toBe("");
@@ -95,27 +154,51 @@ describe("getRedirectionRule: /join", () => {
it("if has ongoing call, go for it", () => {
const result = getRedirectionRule(
- { path: "/pending", hasAuth: true, ongoingCall: "123-call-321" },
+ {
+ path: "/pending",
+ hasAuth: true,
+ ongoingCall: "123-call-321",
+ isSessionBlocked: false,
+ },
{}
);
expect(result).toBe("/p2p-call/123-call-321");
});
it("if has none, reset navigation flow", () => {
- const result = getRedirectionRule({ path: "/pending", hasAuth: true }, {});
+ const result = getRedirectionRule(
+ { path: "/pending", hasAuth: true, isSessionBlocked: false },
+ {}
+ );
expect(result).toBe("/");
});
it("when not authenticated, reset navigation flow", () => {
- const result = getRedirectionRule({ path: "/pending", hasAuth: false }, {});
+ const result = getRedirectionRule(
+ { path: "/pending", hasAuth: false, isSessionBlocked: false },
+ {}
+ );
expect(result).toBe("/");
});
+
+ it("when session is blocked, got to blocked session page", () => {
+ const result = getRedirectionRule(
+ { path: "/pending", hasAuth: true, isSessionBlocked: true },
+ {}
+ );
+ expect(result).toBe("/blocked-session");
+ });
});
describe("getRedirectionRule: /p2p-call", () => {
it("when authenticated with ongoing call, then stay here", () => {
const result = getRedirectionRule(
- { path: "/p2p-call/123-321", hasAuth: true, ongoingCall: "123-321" },
+ {
+ path: "/p2p-call/123-321",
+ hasAuth: true,
+ ongoingCall: "123-321",
+ isSessionBlocked: false,
+ },
{}
);
expect(result).toBe("");
@@ -123,7 +206,7 @@ describe("getRedirectionRule: /p2p-call", () => {
it("when authenticated without ongoing call, go to a page saying good bye", () => {
const result = getRedirectionRule(
- { path: "/p2p-call/123-321", hasAuth: true },
+ { path: "/p2p-call/123-321", hasAuth: true, isSessionBlocked: false },
{}
);
// expect(result).toBe("/left");
@@ -133,9 +216,17 @@ describe("getRedirectionRule: /p2p-call", () => {
it("when not authenticated, be forced to reset flow", () => {
// TODO: retrieve attempt call uid and convert into "/?joining=" flow
const result = getRedirectionRule(
- { path: "/p2p-call/123-321", hasAuth: false },
+ { path: "/p2p-call/123-321", hasAuth: false, isSessionBlocked: false },
{}
);
expect(result).toBe("/");
});
+
+ it("when session is blocked, got to blocked session page", () => {
+ const result = getRedirectionRule(
+ { path: "/p2p-call/123-321", hasAuth: false, isSessionBlocked: true },
+ {}
+ );
+ expect(result).toBe("/blocked-session");
+ });
});
diff --git a/src/hooks/useRedirectionRule.ts b/src/hooks/useRedirectionRule.ts
index 041e433..688231e 100644
--- a/src/hooks/useRedirectionRule.ts
+++ b/src/hooks/useRedirectionRule.ts
@@ -1,6 +1,9 @@
import { useLocation } from "wouter";
import { useAppSelector } from "../state";
-import { selectIsUserAuthenticated } from "../state/user";
+import {
+ selectIsSessionBlocked,
+ selectIsUserAuthenticated,
+} from "../state/user";
import { selectCallUid, selectCallUserStatus } from "../state/call";
export default function useRedirectionRule(): string {
@@ -8,6 +11,7 @@ export default function useRedirectionRule(): string {
const callUserStatus = useAppSelector(selectCallUserStatus);
const callUid = useAppSelector(selectCallUid);
+ const isSessionBlocked = useAppSelector(selectIsSessionBlocked);
const [location] = useLocation();
@@ -21,6 +25,7 @@ export default function useRedirectionRule(): string {
path: location,
pendingCall: callUserStatus === "pending-user" ? callUid : "",
ongoingCall: callUserStatus === "participant" ? callUid : "",
+ isSessionBlocked,
};
const goTo = getRedirectionRule(context, search);
@@ -30,15 +35,32 @@ export default function useRedirectionRule(): string {
export interface RedirectionContext {
path: string;
hasAuth: boolean;
+ isSessionBlocked: boolean;
pendingCall?: string;
ongoingCall?: string;
}
export function getRedirectionRule(
- { path, hasAuth, pendingCall, ongoingCall }: RedirectionContext,
+ {
+ path,
+ hasAuth,
+ pendingCall,
+ ongoingCall,
+ isSessionBlocked,
+ }: RedirectionContext,
search: Record
): string {
+ if (path === "/blocked-session") {
+ if (!isSessionBlocked) {
+ return "/";
+ }
+ }
+
if (path === "/") {
+ if (isSessionBlocked) {
+ return "/blocked-session";
+ }
+
if (hasAuth && search.joining) {
return `/join?callUid=${search.joining}`;
}
@@ -53,6 +75,10 @@ export function getRedirectionRule(
}
if (path === "/create") {
+ if (isSessionBlocked) {
+ return "/blocked-session";
+ }
+
if (hasAuth && ongoingCall) {
return `/p2p-call/${ongoingCall}`;
}
@@ -63,6 +89,10 @@ export function getRedirectionRule(
}
if (path === "/join") {
+ if (isSessionBlocked) {
+ return "/blocked-session";
+ }
+
if (pendingCall && hasAuth) {
return "/pending";
}
@@ -81,6 +111,10 @@ export function getRedirectionRule(
}
if (path === "/pending") {
+ if (isSessionBlocked) {
+ return "/blocked-session";
+ }
+
if (pendingCall) {
return "";
}
@@ -93,6 +127,10 @@ export function getRedirectionRule(
}
if (path.startsWith("/p2p-call")) {
+ if (isSessionBlocked) {
+ return "/blocked-session";
+ }
+
if (hasAuth && !ongoingCall) {
return "/create";
// return "/left";
diff --git a/src/hooks/useValidateSession.ts b/src/hooks/useValidateSession.ts
new file mode 100644
index 0000000..8d6b1d1
--- /dev/null
+++ b/src/hooks/useValidateSession.ts
@@ -0,0 +1,35 @@
+import { useEffect } from "react";
+import { useAppDispatch, useAppSelector } from "../state";
+import {
+ selectUserDeviceUuid,
+ selectUserUid,
+ setSessionActive,
+ setSessionBlocked,
+} 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;
+ }
+
+ if (session.deviceUuid === userDeviceUuid) {
+ dispatch(setSessionActive());
+ return;
+ }
+
+ dispatch(setSessionBlocked());
+ });
+ }, [dispatch, userDeviceUuid, userUid]);
+}
diff --git a/src/services/firestore-auth.test.ts b/src/services/firestore-auth.test.ts
index ca3999b..605e973 100644
--- a/src/services/firestore-auth.test.ts
+++ b/src/services/firestore-auth.test.ts
@@ -1,17 +1,41 @@
import "../testing-helpers/mock-firebase-auth";
import firestoreAuth from "./firestore-auth";
import * as firebaseAuth from "firebase/auth";
+import { doc, setDoc, onSnapshot } from "firebase/firestore";
+import { v4 as uuidv4 } from "uuid";
+
+jest.mock("firebase/firestore", () => ({
+ doc: jest.fn(),
+ setDoc: jest.fn(),
+ onSnapshot: jest.fn(),
+ collection: jest.fn(),
+ query: jest.fn(),
+}));
+
+jest.mock("uuid", () => ({
+ v4: jest.fn(),
+}));
+
+jest.mock("./firestore-connection", () => ({
+ db: jest.fn(),
+}));
describe("firestoreAuth", () => {
+ beforeEach(() => {
+ (uuidv4 as jest.Mock).mockReturnValueOnce("7390-4d50-91f3-abcdef1e0d50");
+ });
it("should successfully log in and return user information", async () => {
jest.spyOn(firebaseAuth, "getAuth");
jest.spyOn(firebaseAuth, "getRedirectResult");
+ (doc as jest.Mock).mockImplementationOnce((_, path) => path);
+ (setDoc as jest.Mock).mockResolvedValueOnce(Promise.resolve(null));
await firestoreAuth.login();
const user = await firestoreAuth.loadUser();
expect(user).toEqual({
uid: "abc123def456",
displayName: "Jane Doe",
email: "jane@example.com",
+ deviceUuid: "7390-4d50-91f3-abcdef1e0d50",
});
});
@@ -51,3 +75,31 @@ describe("firestoreAuth", () => {
await expect(firestoreAuth.logout()).resolves.not.toThrow();
});
});
+
+describe("listenUserSession", () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+ it("listenUserSession will notify the callback on each snapshot invocation", async () => {
+ const mockUserUid = "zJTvoYDGr9PuN1z69vheO0b4iWF2";
+ (onSnapshot as jest.Mock).mockImplementation((_, onNext) =>
+ onNext({
+ forEach: Array.prototype.forEach.bind([
+ {
+ id: mockUserUid,
+ data: () => ({
+ deviceUuid: "6db9ad2e-19f9-4a85-b383-7731e347b7d0",
+ }),
+ },
+ ]),
+ })
+ );
+
+ const mockCallback = jest.fn();
+ firestoreAuth.listenUserSession(mockUserUid, mockCallback);
+
+ expect(mockCallback).toHaveBeenCalledWith({
+ deviceUuid: "6db9ad2e-19f9-4a85-b383-7731e347b7d0",
+ });
+ });
+});
diff --git a/src/services/firestore-auth.ts b/src/services/firestore-auth.ts
index 81added..bb4a332 100644
--- a/src/services/firestore-auth.ts
+++ b/src/services/firestore-auth.ts
@@ -8,13 +8,24 @@ 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 {
+ DocumentData,
+ Unsubscribe,
+ collection,
+ doc,
+ onSnapshot,
+ query,
+ setDoc,
+} from "firebase/firestore";
const firestoreAuth = {
loadUser,
login,
logout,
+ listenUserSession,
};
export default firestoreAuth;
@@ -24,7 +35,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 +50,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,
});
};
@@ -67,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 c734310..5e92bf4 100644
--- a/src/state/user.ts
+++ b/src/state/user.ts
@@ -10,6 +10,7 @@ type UserStatus = "idle" | "pending" | "authenticated" | "error";
export interface UserState extends User {
status: UserStatus;
errorMessage: string;
+ isSessionBlocked: boolean;
}
export const userInitialState: UserState = {
@@ -17,19 +18,29 @@ export const userInitialState: UserState = {
displayName: "",
email: "",
status: "idle",
+ deviceUuid: "",
errorMessage: "",
+ isSessionBlocked: false,
};
export const userSlice = createSlice({
name: "user",
initialState: userInitialState,
- reducers: {},
+ reducers: {
+ setSessionBlocked: (state) => {
+ state.isSessionBlocked = true;
+ },
+ setSessionActive: (state) => {
+ state.isSessionBlocked = false;
+ },
+ },
extraReducers: (builder) => {
builder.addCase(login.pending, (state) => {
state.uid = "";
state.displayName = "";
state.email = "";
state.status = "pending";
+ state.deviceUuid = "";
state.errorMessage = "";
});
@@ -38,7 +49,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 +58,7 @@ export const userSlice = createSlice({
state.displayName = "";
state.email = "";
state.status = "error";
+ state.deviceUuid = "";
state.errorMessage = action.error.message || "Unknown error.";
});
@@ -87,10 +100,16 @@ 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";
export const selectIsUserAuthenticated = (state: RootState) =>
Boolean(state.user.status === "authenticated" && state.user.uid);
+export const selectIsSessionBlocked = (state: RootState) =>
+ state.user.isSessionBlocked;
+
+export const { setSessionBlocked, setSessionActive } = userSlice.actions;
+
export default userSlice.reducer;
diff --git a/src/testing-helpers/call-fixtures.ts b/src/testing-helpers/call-fixtures.ts
index d61d8eb..b9e6c84 100644
--- a/src/testing-helpers/call-fixtures.ts
+++ b/src/testing-helpers/call-fixtures.ts
@@ -6,6 +6,7 @@ export function createUser(overrideValues: Partial = {}): User {
uid: uuidv4(),
displayName: getRandomString(),
email: getRandomEmail(),
+ deviceUuid: uuidv4(),
};
return { ...defaultValues, ...overrideValues };
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 {