From d385857bf5d15ff58326378f06c087e0e8278be0 Mon Sep 17 00:00:00 2001 From: Hugues Chocart Date: Wed, 2 Oct 2024 08:40:09 +0100 Subject: [PATCH] feat: session expired redirection (#582) --- e2e/login.spec.ts | 34 +++++++++---------- packages/backend/src/api/v1/auth/utils.ts | 5 ++- .../components/blocks/RunInputOutput.tsx | 10 +++--- packages/frontend/pages/login.tsx | 14 ++++++++ packages/frontend/utils/auth.tsx | 12 ++++++- packages/frontend/utils/fetcher.ts | 7 ++++ 6 files changed, 58 insertions(+), 24 deletions(-) diff --git a/e2e/login.spec.ts b/e2e/login.spec.ts index c55cb74f..d33a159a 100644 --- a/e2e/login.spec.ts +++ b/e2e/login.spec.ts @@ -1,32 +1,32 @@ -import { test } from "@playwright/test" +import { test } from "@playwright/test"; test("logout and back in login", async ({ page }) => { - await page.goto("/") + await page.goto("/"); - await page.waitForLoadState("networkidle") + await page.waitForLoadState("networkidle"); // logout - await page.getByTestId("account-sidebar-item").click() - await page.getByTestId("logout-button").click() + await page.getByTestId("account-sidebar-item").click(); + await page.getByTestId("logout-button").click(); - await page.waitForURL("**/login") + await page.waitForURL("**/login*"); // log back in - await page.getByPlaceholder("Your email").click() - await page.getByPlaceholder("Your email").fill("test@lunary.ai") + await page.getByPlaceholder("Your email").click(); + await page.getByPlaceholder("Your email").fill("test@lunary.ai"); const promise = page.waitForResponse((resp) => { - return resp.url().includes("/method") - }) + return resp.url().includes("/method"); + }); - await page.getByTestId("continue-button").click() + await page.getByTestId("continue-button").click(); - await promise + await promise; - await page.getByPlaceholder("Your password").click() - await page.getByPlaceholder("Your password").fill("testtest") + await page.getByPlaceholder("Your password").click(); + await page.getByPlaceholder("Your password").fill("testtest"); - await page.getByRole("button", { name: "Login" }).click() + await page.getByRole("button", { name: "Login" }).click(); - await page.waitForURL("**/analytics") -}) + await page.waitForURL("**/analytics*"); +}); diff --git a/packages/backend/src/api/v1/auth/utils.ts b/packages/backend/src/api/v1/auth/utils.ts index f084f79a..1dc69aaa 100644 --- a/packages/backend/src/api/v1/auth/utils.ts +++ b/packages/backend/src/api/v1/auth/utils.ts @@ -8,6 +8,7 @@ import * as argon2 from "argon2"; import bcrypt from "bcrypt"; import { validateUUID } from "@/src/utils/misc"; import { sendEmail, RESET_PASSWORD } from "@/src/emails"; +import { JWTExpired } from "jose/errors"; export function sanitizeEmail(email: string) { return email.toLowerCase().trim(); @@ -153,7 +154,6 @@ export async function authMiddleware(ctx: Context, next: Next) { throw new Error("No bearer token provided."); } const { payload } = await verifyJWT(key); - ctx.state.userId = payload.userId; ctx.state.orgId = payload.orgId; @@ -176,6 +176,9 @@ export async function authMiddleware(ctx: Context, next: Next) { } } catch (error) { console.error(error); + if (error instanceof JWTExpired) { + ctx.throw(401, "Session expired"); + } ctx.throw(401, "Invalid access token"); } } diff --git a/packages/frontend/components/blocks/RunInputOutput.tsx b/packages/frontend/components/blocks/RunInputOutput.tsx index 58168faf..6577de33 100644 --- a/packages/frontend/components/blocks/RunInputOutput.tsx +++ b/packages/frontend/components/blocks/RunInputOutput.tsx @@ -1,3 +1,4 @@ +import config from "@/utils/config"; import { useDatasets, useOrg, useRun, useUser } from "@/utils/dataHooks"; import { Badge, @@ -11,22 +12,21 @@ import { Switch, Text, } from "@mantine/core"; -import { notifications, showNotification } from "@mantine/notifications"; +import { notifications } from "@mantine/notifications"; import { IconBinaryTree2, IconCheck, IconPencilShare, } from "@tabler/icons-react"; import Link from "next/link"; +import { useState } from "react"; import { hasAccess } from "shared"; import SmartViewer from "../SmartViewer"; +import AppUserAvatar from "./AppUserAvatar"; import CopyText, { SuperCopyButton } from "./CopyText"; import ErrorBoundary from "./ErrorBoundary"; -import TokensBadge from "./TokensBadge"; import Feedbacks from "./Feedbacks"; -import config from "@/utils/config"; -import { useState } from "react"; -import AppUserAvatar from "./AppUserAvatar"; +import TokensBadge from "./TokensBadge"; const isChatMessages = (obj) => { return Array.isArray(obj) diff --git a/packages/frontend/pages/login.tsx b/packages/frontend/pages/login.tsx index b488d1f9..16ff8f79 100644 --- a/packages/frontend/pages/login.tsx +++ b/packages/frontend/pages/login.tsx @@ -104,6 +104,7 @@ function LoginPage() { } auth.setJwt(token); + router.push("/"); analytics.track("Login", { method: "password" }); } catch (error) { console.error(error); @@ -138,6 +139,19 @@ function LoginPage() { if (ott) exchangeToken(ott); }, [router.query.ott]); + useEffect(() => { + const email = router.query.email + ? decodeURIComponent(router.query.email as string) + : ""; + console.log(form.values.password); + if (email && !form.values.email) { + console.log("here"); + form.setFieldValue("email", email); + determineAuthMethod(email); + console.log(step); + } + }, [router.query.email]); + return ( diff --git a/packages/frontend/utils/auth.tsx b/packages/frontend/utils/auth.tsx index f3692538..f925047f 100644 --- a/packages/frontend/utils/auth.tsx +++ b/packages/frontend/utils/auth.tsx @@ -1,11 +1,21 @@ import { useLocalStorage } from "@mantine/hooks"; import { decodeJwt } from "jose"; +import Router from "next/router"; import { createContext, useContext, useEffect, useMemo } from "react"; const SIGN_OUT_EVENT = "sign-out"; + export async function signOut() { + const jwt = window.localStorage.getItem("auth-token"); + if (jwt) { + const payload = decodeJwt(jwt); + const email = payload.email as string; + const encodedEmail = encodeURIComponent(email); + Router.push(`/login?email=${encodedEmail}`); + } else { + Router.push("/login"); + } window.localStorage.clear(); - window.dispatchEvent(new Event(SIGN_OUT_EVENT)); } interface AuthContext { diff --git a/packages/frontend/utils/fetcher.ts b/packages/frontend/utils/fetcher.ts index a324ef95..5f9b1e75 100644 --- a/packages/frontend/utils/fetcher.ts +++ b/packages/frontend/utils/fetcher.ts @@ -1,6 +1,7 @@ import Router from "next/router"; import { signOut } from "./auth"; import { showErrorNotification } from "./errors"; +import { showNotification } from "@mantine/notifications"; const BASE_URL = process.env.NEXT_PUBLIC_API_URL as string; @@ -149,6 +150,12 @@ async function handleResponse(res: Response) { const { error, message } = await res.json(); + if (message === "Session expired") { + showErrorNotification(message, "Please log in again."); + return; + } else if (message === "Invalid access token") { + return; + } showErrorNotification(error, message); throw new Error(message); }