diff --git a/core/app/(user)/login/page.tsx b/core/app/(user)/login/page.tsx index 4e9acc3b3..9e24d33b3 100644 --- a/core/app/(user)/login/page.tsx +++ b/core/app/(user)/login/page.tsx @@ -1,5 +1,7 @@ +import { cookies } from "next/headers"; import { redirect } from "next/navigation"; +import { LAST_VISITED_COOKIE } from "~/app/components/LastVisitedCommunity/constants"; import { getLoginData } from "~/lib/authentication/loginData"; import LoginForm from "./LoginForm"; @@ -14,9 +16,12 @@ export default async function Login({ if (user?.id) { const firstSlug = user.memberships[0]?.community?.slug; + const cookieStore = await cookies(); + const lastVisited = cookieStore.get(LAST_VISITED_COOKIE); + const communitySlug = lastVisited?.value ?? firstSlug; if (firstSlug) { - redirect(`/c/${firstSlug}/stages`); + redirect(`/c/${communitySlug}/stages`); } redirect("/settings"); diff --git a/core/app/c/[communitySlug]/CommunitySwitcher.tsx b/core/app/c/[communitySlug]/CommunitySwitcher.tsx index 39468bd6b..ef1117452 100644 --- a/core/app/c/[communitySlug]/CommunitySwitcher.tsx +++ b/core/app/c/[communitySlug]/CommunitySwitcher.tsx @@ -7,7 +7,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "ui/dropdown-menu"; -import { ChevronDown, ChevronsUpDown } from "ui/icon"; +import { ChevronsUpDown } from "ui/icon"; import { SidebarMenuButton } from "ui/sidebar"; import { cn } from "utils"; @@ -25,8 +25,10 @@ const CommunitySwitcher: React.FC = function ({ community, availableCommu return ( - {/*
*/} - + {community.name[0]} @@ -34,7 +36,6 @@ const CommunitySwitcher: React.FC = function ({ community, availableCommu {community.name} - {/*
*/}
{availableCommunities diff --git a/core/app/c/[communitySlug]/layout.tsx b/core/app/c/[communitySlug]/layout.tsx index 2e90ce16c..c824b3d64 100644 --- a/core/app/c/[communitySlug]/layout.tsx +++ b/core/app/c/[communitySlug]/layout.tsx @@ -3,9 +3,11 @@ import type { Metadata } from "next"; import { cookies } from "next/headers"; import { notFound, redirect } from "next/navigation"; -import { Sidebar, SidebarProvider, SidebarTrigger } from "ui/sidebar"; +import { SidebarProvider, SidebarTrigger } from "ui/sidebar"; import { cn } from "utils"; +import { LAST_VISITED_COOKIE } from "~/app/components/LastVisitedCommunity/constants"; +import SetLastVisited from "~/app/components/LastVisitedCommunity/SetLastVisited"; import { CommunityProvider } from "~/app/components/providers/CommunityProvider"; import { getPageLoginData } from "~/lib/authentication/loginData"; import { getCommunityRole } from "~/lib/authentication/roles"; @@ -55,8 +57,13 @@ export default async function MainLayout(props: Props) { const availableCommunities = user?.memberships.map((m) => m.community) ?? []; + const lastVisited = cookieStore.get(LAST_VISITED_COOKIE); + return ( + {params.communitySlug !== lastVisited?.value && ( + + )}
diff --git a/core/app/components/LastVisitedCommunity/SetLastVisited.tsx b/core/app/components/LastVisitedCommunity/SetLastVisited.tsx new file mode 100644 index 000000000..313ccac9c --- /dev/null +++ b/core/app/components/LastVisitedCommunity/SetLastVisited.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { useEffect } from "react"; + +import { LAST_VISITED_COOKIE, LAST_VISITED_COOKIE_MAX_AGE } from "./constants"; + +export default function SetLastVisited({ communitySlug }: { communitySlug: string }) { + useEffect(() => { + document.cookie = `${LAST_VISITED_COOKIE}=${communitySlug}; path=/; max-age=${LAST_VISITED_COOKIE_MAX_AGE}`; + }, [communitySlug]); + + return <>; +} diff --git a/core/app/components/LastVisitedCommunity/constants.ts b/core/app/components/LastVisitedCommunity/constants.ts new file mode 100644 index 000000000..c17a28738 --- /dev/null +++ b/core/app/components/LastVisitedCommunity/constants.ts @@ -0,0 +1,2 @@ +export const LAST_VISITED_COOKIE = "lastVisitedCommunitySlug"; +export const LAST_VISITED_COOKIE_MAX_AGE = 60 * 60 * 24 * 30; diff --git a/core/app/page.tsx b/core/app/page.tsx index cf724ea8d..17383fa89 100644 --- a/core/app/page.tsx +++ b/core/app/page.tsx @@ -1,20 +1,23 @@ +import { cookies } from "next/headers"; import { redirect } from "next/navigation"; +import { LAST_VISITED_COOKIE } from "~/app/components/LastVisitedCommunity/constants"; import { getPageLoginData } from "~/lib/authentication/loginData"; export default async function Page() { const { user } = await getPageLoginData(); - // if user and no commuhnmitiy, redirect to settings if (!user) { redirect("/login"); } - const memberSlug = user.memberships[0]?.community?.slug; + const cookieStore = await cookies(); + const lastVisited = cookieStore.get(LAST_VISITED_COOKIE); + const communitySlug = lastVisited?.value ?? user.memberships[0]?.community?.slug; - if (!memberSlug) { + if (!communitySlug) { redirect("/settings"); } - redirect(`/c/${memberSlug}/stages`); + redirect(`/c/${communitySlug}/stages`); } diff --git a/core/lib/authentication/actions.ts b/core/lib/authentication/actions.ts index a18196e9c..061092843 100644 --- a/core/lib/authentication/actions.ts +++ b/core/lib/authentication/actions.ts @@ -14,6 +14,7 @@ import { lucia, validateRequest } from "~/lib/authentication/lucia"; import { validatePassword } from "~/lib/authentication/password"; import { defineServerAction } from "~/lib/server/defineServerAction"; import { getUser, setUserPassword, updateUser } from "~/lib/server/user"; +import { LAST_VISITED_COOKIE } from "../../app/components/LastVisitedCommunity/constants"; import * as Email from "../server/email"; import { invalidateTokensForUser } from "../server/token"; import { getLoginData } from "./loginData"; @@ -32,16 +33,19 @@ type LoginUser = Prettify< const getUserWithPasswordHash = async (props: Parameters[0]) => getUser(props).select("users.passwordHash").executeTakeFirst(); -function redirectUser( +async function redirectUser( memberships?: (Omit & { community: Communities | null; })[] -): never { +): Promise { if (!memberships?.length) { redirect("/settings"); } + const cookieStore = await cookies(); + const lastVisited = cookieStore.get(LAST_VISITED_COOKIE); + const communitySlug = lastVisited?.value ?? memberships[0].community?.slug; - redirect(`/c/${memberships[0].community?.slug}/stages`); + redirect(`/c/${communitySlug}/stages`); } export const loginWithPassword = defineServerAction(async function loginWithPassword(props: { @@ -89,7 +93,7 @@ export const loginWithPassword = defineServerAction(async function loginWithPass redirect(props.redirectTo); } - redirectUser(user.memberships); + await redirectUser(user.memberships); }); export const logout = defineServerAction(async function logout() { @@ -283,5 +287,5 @@ export const signup = defineServerAction(async function signup(props: { if (props.redirect) { redirect(props.redirect); } - redirectUser(); + await redirectUser(); }); diff --git a/core/playwright/login.spec.ts b/core/playwright/login.spec.ts index c8ff2510d..94dc733c4 100644 --- a/core/playwright/login.spec.ts +++ b/core/playwright/login.spec.ts @@ -1,8 +1,7 @@ import { expect, test } from "@playwright/test"; import { LoginPage } from "./fixtures/login-page"; -import { inbucketClient, retryAction } from "./helpers"; -import * as loginFlows from "./login.flows"; +import { inbucketClient } from "./helpers"; test.describe("general auth", () => { test("Login with invalid credentials", async ({ page }) => { @@ -110,3 +109,26 @@ test.describe("Auth with lucia", () => { await page.waitForURL(/\/c\/\w+\/stages/); }); }); + +test("Last visited community is remembered", async ({ page }) => { + const unjournalRegex = new RegExp("/c/unjournal/"); + const croccrocRegex = new RegExp("/c/croccroc/"); + const loginPage = new LoginPage(page); + + // Login and visit default community + await loginPage.goto(); + await loginPage.loginAndWaitForNavigation("all@pubpub.org", "pubpub-all"); + await expect(page).toHaveURL(unjournalRegex); + + // Switch communities and logout + await page.getByRole("button", { name: "Select a community" }).click(); + await page.getByRole("menuitem", { name: "CrocCroc" }).click(); + await page.waitForURL(croccrocRegex); + await page.getByRole("button", { name: "User menu" }).click(); + await page.getByRole("button", { name: "Logout" }).click(); + + // Log back in and switched community should be remembered + await page.waitForURL("/login"); + await loginPage.loginAndWaitForNavigation("all@pubpub.org", "pubpub-all"); + await expect(page).toHaveURL(croccrocRegex); +});