store last visited org in cookie

This commit is contained in:
miloschwartz 2025-06-24 14:54:07 -04:00
parent 34180ca454
commit 9bb4d8b2a3
No known key found for this signature in database
6 changed files with 14229 additions and 14065 deletions

105
package-lock.json generated
View file

@ -14054,6 +14054,111 @@
"peerDependencies": { "peerDependencies": {
"zod": "^3.24.4" "zod": "^3.24.4"
} }
},
"node_modules/@next/swc-darwin-x64": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.3.tgz",
"integrity": "sha512-XHdzH/yBc55lu78k/XwtuFR/ZXUTcflpRXcsu0nKmF45U96jt1tsOZhVrn5YH+paw66zOANpOnFQ9i6/j+UYvw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.3.tgz",
"integrity": "sha512-VZ3sYL2LXB8znNGcjhocikEkag/8xiLgnvQts41tq6i+wql63SMS1Q6N8RVXHw5pEUjiof+II3HkDd7GFcgkzw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.3.tgz",
"integrity": "sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.3.tgz",
"integrity": "sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.3.tgz",
"integrity": "sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.3.tgz",
"integrity": "sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.3.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.3.tgz",
"integrity": "sha512-4QZG6F8enl9/S2+yIiOiju0iCTFd93d8VC1q9LZS4p/Xuk81W2QDjCFeoogmrWWkAD59z8ZxepBQap2dKS5ruw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
} }
} }
} }

View file

@ -193,12 +193,14 @@ export async function createOrg(
); );
} }
if (allDomains.length) {
await trx.insert(orgDomains).values( await trx.insert(orgDomains).values(
allDomains.map((domain) => ({ allDomains.map((domain) => ({
orgId: newOrg[0].orgId, orgId: newOrg[0].orgId,
domainId: domain.domainId domainId: domain.domainId
})) }))
); );
}
if (req.user) { if (req.user) {
await trx.insert(userOrgs).values({ await trx.insert(userOrgs).values({

View file

@ -5,7 +5,7 @@ import { Org, orgs, userOrgs } from "@server/db";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import { sql, inArray, eq } from "drizzle-orm"; import { sql, inArray, eq, and } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromZodError } from "zod-validation-error"; import { fromZodError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
@ -40,8 +40,10 @@ const listOrgsSchema = z.object({
// responses: {} // responses: {}
// }); // });
type ResponseOrg = Org & { isOwner?: boolean };
export type ListUserOrgsResponse = { export type ListUserOrgsResponse = {
orgs: Org[]; orgs: ResponseOrg[];
pagination: { total: number; limit: number; offset: number }; pagination: { total: number; limit: number; offset: number };
}; };
@ -106,6 +108,10 @@ export async function listUserOrgs(
.select() .select()
.from(orgs) .from(orgs)
.where(inArray(orgs.orgId, userOrgIds)) .where(inArray(orgs.orgId, userOrgIds))
.leftJoin(
userOrgs,
and(eq(userOrgs.orgId, orgs.orgId), eq(userOrgs.userId, userId))
)
.limit(limit) .limit(limit)
.offset(offset); .offset(offset);
@ -115,9 +121,19 @@ export async function listUserOrgs(
.where(inArray(orgs.orgId, userOrgIds)); .where(inArray(orgs.orgId, userOrgIds));
const totalCount = totalCountResult[0].count; const totalCount = totalCountResult[0].count;
const responseOrgs = organizations.map((val) => {
const res = {
...val.orgs
} as ResponseOrg;
if (val.userOrgs && val.userOrgs.isOwner) {
res.isOwner = val.userOrgs.isOwner;
}
return res;
});
return response<ListUserOrgsResponse>(res, { return response<ListUserOrgsResponse>(res, {
data: { data: {
orgs: organizations, orgs: responseOrgs,
pagination: { pagination: {
total: totalCount, total: totalCount,
limit, limit,

View file

@ -8,6 +8,7 @@ import { GetOrgUserResponse } from "@server/routers/user";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { cache } from "react"; import { cache } from "react";
import SetLastOrgCookie from "@app/components/SetLastOrgCookie";
export default async function OrgLayout(props: { export default async function OrgLayout(props: {
children: React.ReactNode; children: React.ReactNode;
@ -52,6 +53,7 @@ export default async function OrgLayout(props: {
return ( return (
<> <>
{props.children} {props.children}
<SetLastOrgCookie orgId={orgId} />
</> </>
); );
} }

View file

@ -12,6 +12,7 @@ import { cleanRedirect } from "@app/lib/cleanRedirect";
import { Layout } from "@app/components/Layout"; import { Layout } from "@app/components/Layout";
import { rootNavItems } from "./navigation"; import { rootNavItems } from "./navigation";
import { InitialSetupCompleteResponse } from "@server/routers/auth"; import { InitialSetupCompleteResponse } from "@server/routers/auth";
import { cookies } from "next/headers";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@ -72,6 +73,25 @@ export default async function Page(props: {
} }
} }
// Check for pangolin-last-org cookie and redirect if valid
const allCookies = await cookies();
const lastOrgCookie = allCookies.get("pangolin-last-org")?.value;
if (lastOrgCookie && orgs.length > 0) {
// Check if the last org from cookie exists in user's organizations
const lastOrgExists = orgs.some(org => org.orgId === lastOrgCookie);
if (lastOrgExists) {
redirect(`/${lastOrgCookie}`);
} else {
const ownedOrg = orgs.find(org => org.isOwner);
if (ownedOrg) {
redirect(`/${ownedOrg.orgId}`);
} else {
redirect("/setup");
}
}
}
return ( return (
<UserProvider user={user}> <UserProvider user={user}>
<Layout orgs={orgs} navItems={rootNavItems} showBreadcrumbs={false}> <Layout orgs={orgs} navItems={rootNavItems} showBreadcrumbs={false}>

View file

@ -0,0 +1,19 @@
"use client";
import { useEffect } from "react";
interface SetLastOrgCookieProps {
orgId: string;
}
export default function SetLastOrgCookie({ orgId }: SetLastOrgCookieProps) {
useEffect(() => {
const isSecure =
typeof window !== "undefined" &&
window.location.protocol === "https:";
document.cookie = `pangolin-last-org=${orgId}; path=/; max-age=${60 * 60 * 24 * 30}; samesite=lax${isSecure ? "; secure" : ""}`;
}, [orgId]);
return null;
}