Skip to content

Commit 0bafb85

Browse files
committed
fix(front): use cookies for activa account storage and fix backend not using actor
1 parent 358939a commit 0bafb85

17 files changed

Lines changed: 177 additions & 64 deletions

File tree

app/(dashboard)/dashboard/community/[id]/layout.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { communityInfo, communityUserRoles } from "@/lib/queries/community";
99
import { userInfoOptions } from "@/lib/queries/user";
1010
import { ScreenContainerCentered } from "@/components/layout/screen-container";
1111
import DashboardCommunityContextProvider from "@/components/providers/dashboard-community-context-provider";
12+
import { getActiveAccountServer } from "@/lib/active-account/server";
1213

1314
interface DashboardCommunityManagementPageProps {
1415
params: Promise<{ id: string }>;
@@ -38,6 +39,9 @@ async function DashboardCommunityManagementPage({
3839
);
3940
}
4041

42+
const activeAccount = await getActiveAccountServer();
43+
const entityId = activeAccount?.id ?? userProfileId;
44+
4145
let communityData;
4246

4347
try {
@@ -47,7 +51,7 @@ async function DashboardCommunityManagementPage({
4751
}
4852

4953
const roles = await queryClient.fetchQuery(
50-
communityUserRoles(communityId, userProfileId),
54+
communityUserRoles(communityId, entityId),
5155
);
5256

5357
const renderLayout = () => (

app/(dashboard)/dashboard/community/communities-table.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,18 @@ import {
1010
} from "@/lib/queries/community";
1111
import useManualPagination from "@/hooks/use-manual-pagination";
1212
import { userInfoOptions } from "@/lib/queries/user";
13+
import { useActiveAccount } from "@/components/providers/active-account-provider";
1314

1415
export default function CommunitiesTable() {
1516
const { getToken, userId: authId } = useAuth();
17+
const { activeAccount } = useActiveAccount();
1618
const { data: userInfo } = useSuspenseQuery(
1719
userInfoOptions(getToken, authId),
1820
);
1921

22+
const entityId = activeAccount?.id ?? userInfo?.userId;
23+
const teamId = activeAccount?.type === "team" ? activeAccount.id : undefined;
24+
2025
const [tablePage, setTablePage] = useQueryState("page", {
2126
defaultValue: 1,
2227
parse: (value) => {
@@ -28,11 +33,12 @@ export default function CommunitiesTable() {
2833
const { data: communities, isFetching: isFetchingCommunities } =
2934
useSuspenseQuery(
3035
communitiesByUserRolesListSuspense(
31-
userInfo?.userId,
36+
entityId,
3237
tablePage - 1,
3338
DEFAULT_COMMUNITIES_LIMIT,
3439
["administrator"],
3540
getToken,
41+
teamId,
3642
),
3743
);
3844

app/(dashboard)/dashboard/event/[id]/(default)/layout.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import DashboardEventInfo from "@/components/features/dashboard/event/dashboard-
1616
import DashboardEventEditionContextProvider from "@/components/providers/dashboard-event-edition-context-provider";
1717
import DashboardEventContextProvider from "@/components/providers/dashboard-event-context-provider";
1818
import { withEventRoleRestrictions } from "@/lib/permissions/with-roles-required";
19+
import { getActiveAccountServer } from "@/lib/active-account/server";
1920

2021
interface DashboardEventInfoLayoutProps {
2122
params: Promise<{ id: string }>;
@@ -45,6 +46,10 @@ async function DashboardEventInfoLayoutProps({
4546
);
4647
}
4748

49+
const activeAccount = await getActiveAccountServer();
50+
const entityId = activeAccount?.id ?? userProfileId;
51+
const teamId = activeAccount?.type === "team" ? activeAccount.id : undefined;
52+
4853
let eventInfo;
4954

5055
try {
@@ -56,9 +61,7 @@ async function DashboardEventInfoLayoutProps({
5661
notFound();
5762
}
5863

59-
const roles = await queryClient.fetchQuery(
60-
eventUserRoles(eventId, userProfileId),
61-
);
64+
const roles = await queryClient.fetchQuery(eventUserRoles(eventId, entityId));
6265

6366
queryClient.prefetchInfiniteQuery(
6467
communitiesListByEvent(eventId, DEFAULT_COMMUNITIES_LIMIT),
@@ -85,7 +88,7 @@ async function DashboardEventInfoLayoutProps({
8588
);
8689
}
8790

88-
queryClient.prefetchQuery(eventGatekeepersEmails(eventId, getToken));
91+
queryClient.prefetchQuery(eventGatekeepersEmails(eventId, getToken, teamId));
8992

9093
return (
9194
<HydrationBoundary state={dehydrate(queryClient)}>

app/(general)/tickets/tickets-events-list.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import Text from "@/components/widgets/texts/text";
1616
import EventCardListLayout from "@/components/features/event/event-card-list-layout";
1717
import { LoaderMoreButton } from "@/components/widgets/buttons/load-more-button";
1818
import { SafeEventInfo } from "@/types/schemas";
19+
import { useActiveAccount } from "@/components/providers/active-account-provider";
1920

2021
export function TicketsEventsList({
2122
now,
@@ -27,6 +28,11 @@ export function TicketsEventsList({
2728
userId: string;
2829
}) {
2930
const { getToken } = useAuth();
31+
const { activeAccount } = useActiveAccount();
32+
33+
const entityId = activeAccount?.id ?? userId;
34+
const teamId = activeAccount?.type === "team" ? activeAccount.id : undefined;
35+
3036
const {
3137
data: eventsPages,
3238
isFetchingNextPage,
@@ -36,20 +42,22 @@ export function TicketsEventsList({
3642
} = useSuspenseInfiniteQuery(
3743
from === "upcoming"
3844
? eventsByParticipantList(
39-
userId,
45+
entityId,
4046
DiscoverableFilter.UNSPECIFIED,
4147
now,
4248
Number.MAX_SAFE_INTEGER,
4349
DEFAULT_EVENTS_LIMIT,
4450
getToken,
51+
teamId,
4552
)
4653
: eventsByParticipantList(
47-
userId,
54+
entityId,
4855
DiscoverableFilter.UNSPECIFIED,
4956
now - 1,
5057
0,
5158
DEFAULT_EVENTS_LIMIT,
5259
getToken,
60+
teamId,
5361
),
5462
);
5563

backend/list_events_by_user_roles.go

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,14 @@ func (s *ZenaoServer) ListEventsByUserRoles(ctx context.Context, req *connect.Re
2222
}
2323
}
2424

25-
// Viewing undiscoverable events is restricted to the user themselves
25+
// Viewing undiscoverable events is restricted to the user themselves (or their team)
2626
if req.Msg.DiscoverableFilter != zenaov1.DiscoverableFilter_DISCOVERABLE_FILTER_DISCOVERABLE {
27-
user := s.Auth.GetUser(ctx)
28-
if user == nil {
29-
return nil, errors.New("unauthorized: authentication required to view undiscoverable events")
30-
}
31-
zUser, err := s.EnsureUserExists(ctx, user)
27+
actor, err := s.GetActor(ctx, req.Header())
3228
if err != nil {
33-
return nil, err
29+
return nil, errors.New("unauthorized: authentication required to view undiscoverable events")
3430
}
35-
if zUser.ID != req.Msg.UserId {
36-
return nil, errors.New("unauthorized: user_id must match authenticated user")
31+
if actor.ID() != req.Msg.UserId {
32+
return nil, errors.New("unauthorized: user_id must match authenticated user or team")
3733
}
3834
}
3935

components/features/dashboard/home/events-table/index.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from "@/lib/queries/events-list";
1212
import { DiscoverableFilter } from "@/app/gen/zenao/v1/zenao_pb";
1313
import useManualPagination from "@/hooks/use-manual-pagination";
14+
import { useActiveAccount } from "@/components/providers/active-account-provider";
1415

1516
interface EventsTableProps {
1617
now: number;
@@ -19,10 +20,14 @@ interface EventsTableProps {
1920

2021
export default function EventsTable({ now, tab }: EventsTableProps) {
2122
const { getToken, userId } = useAuth();
23+
const { activeAccount } = useActiveAccount();
2224
const { data: userInfo } = useSuspenseQuery(
2325
userInfoOptions(getToken, userId),
2426
);
2527

28+
const entityId = activeAccount?.id ?? userInfo?.userId;
29+
const teamId = activeAccount?.type === "team" ? activeAccount.id : undefined;
30+
2631
const [tablePage, setTablePage] = useQueryState("page", {
2732
defaultValue: 1,
2833
parse: (value) => {
@@ -34,28 +39,30 @@ export default function EventsTable({ now, tab }: EventsTableProps) {
3439
const { data: pastEvents, isFetching: isFetchingPastEvents } =
3540
useSuspenseQuery(
3641
eventsByRolesListSuspense(
37-
userInfo?.userId,
42+
entityId,
3843
DiscoverableFilter.UNSPECIFIED,
3944
now - 1,
4045
0,
4146
DEFAULT_EVENTS_LIMIT,
4247
tablePage - 1,
4348
["organizer", "gatekeeper"],
4449
getToken,
50+
teamId,
4551
),
4652
);
4753

4854
const { data: upcomingEvents, isFetching: isFetchingUpcomingEvents } =
4955
useSuspenseQuery(
5056
eventsByRolesListSuspense(
51-
userInfo?.userId,
57+
entityId,
5258
DiscoverableFilter.UNSPECIFIED,
5359
now,
5460
Number.MAX_SAFE_INTEGER,
5561
DEFAULT_EVENTS_LIMIT,
5662
tablePage - 1,
5763
["organizer", "gatekeeper"],
5864
getToken,
65+
teamId,
5966
),
6067
);
6168

components/providers/active-account-provider.tsx

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,20 @@
11
"use client";
22

33
import { createContext, useContext, useState, useEffect } from "react";
4-
import { z } from "zod";
54
import { getQueryClient } from "@/lib/get-query-client";
6-
7-
const activeAccountSchema = z.object({
8-
type: z.enum(["personal", "team"]),
9-
id: z.string(),
10-
});
11-
12-
type ActiveAccount = z.infer<typeof activeAccountSchema>;
5+
import {
6+
type ActiveAccount,
7+
getActiveAccountCookie,
8+
setActiveAccountCookie,
9+
clearActiveAccountCookie,
10+
} from "@/lib/active-account/cookie";
1311

1412
type ActiveAccountContextProps = {
1513
activeAccount: ActiveAccount | null;
1614
switchAccount: (account: ActiveAccount) => void;
1715
isTeamContext: boolean;
1816
};
1917

20-
const STORAGE_KEY = "zenao-active-account";
21-
22-
function parseStoredAccount(stored: string): ActiveAccount | null {
23-
try {
24-
const parsed: unknown = JSON.parse(stored); // eslint-disable-line no-restricted-syntax
25-
const result = activeAccountSchema.safeParse(parsed);
26-
return result.success ? result.data : null;
27-
} catch {
28-
return null;
29-
}
30-
}
31-
3218
const defaultValue: ActiveAccountContextProps = {
3319
activeAccount: null,
3420
switchAccount: () => {},
@@ -46,20 +32,17 @@ function ActiveAccountProvider({ children }: { children: React.ReactNode }) {
4632
);
4733

4834
useEffect(() => {
49-
const stored = localStorage.getItem(STORAGE_KEY);
50-
if (stored) {
51-
const account = parseStoredAccount(stored);
52-
if (account) {
53-
setActiveAccount(account);
54-
} else {
55-
localStorage.removeItem(STORAGE_KEY);
56-
}
35+
const account = getActiveAccountCookie();
36+
if (account) {
37+
setActiveAccount(account);
38+
} else {
39+
clearActiveAccountCookie();
5740
}
5841
}, []);
5942

6043
const switchAccount = (account: ActiveAccount) => {
6144
setActiveAccount(account);
62-
localStorage.setItem(STORAGE_KEY, JSON.stringify(account));
45+
setActiveAccountCookie(account);
6346
getQueryClient().invalidateQueries();
6447
};
6548

hooks/use-account-switcher.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,43 @@
33
import { useState, useEffect } from "react";
44
import { useAuth } from "@clerk/nextjs";
55
import { useQuery } from "@tanstack/react-query";
6+
import { useRouter, usePathname } from "next/navigation";
67
import { useActiveAccount } from "@/components/providers/active-account-provider";
78
import { userTeamsOptions } from "@/lib/queries/team";
89

10+
const RESTRICTED_DASHBOARD_PATTERNS = [
11+
/^\/dashboard\/event\/[^/]+/,
12+
/^\/dashboard\/community\/[^/]+/,
13+
];
14+
15+
function isRestrictedPage(pathname: string): boolean {
16+
return RESTRICTED_DASHBOARD_PATTERNS.some((pattern) =>
17+
pattern.test(pathname),
18+
);
19+
}
20+
921
export function useAccountSwitcher(userId: string) {
1022
const { getToken } = useAuth();
1123
const { activeAccount, switchAccount } = useActiveAccount();
1224
const [isCreateTeamOpen, setIsCreateTeamOpen] = useState(false);
25+
const router = useRouter();
26+
const pathname = usePathname();
1327

1428
const { data: teams = [], isFetched } = useQuery(
1529
userTeamsOptions(getToken, userId),
1630
);
1731
const isPersonalActive = !activeAccount || activeAccount.type === "personal";
1832

1933
useEffect(() => {
20-
if (!isFetched || activeAccount?.type !== "team") return;
34+
if (!userId) return;
35+
36+
if (!activeAccount) {
37+
switchAccount({ type: "personal", id: userId });
38+
return;
39+
}
40+
41+
if (!isFetched || activeAccount.type !== "team") return;
42+
2143
const teamExists = teams.some((t) => t.teamId === activeAccount.id);
2244
if (!teamExists) {
2345
switchAccount({ type: "personal", id: userId });
@@ -29,13 +51,19 @@ export function useAccountSwitcher(userId: string) {
2951
type: "personal",
3052
id: userId,
3153
});
54+
if (isRestrictedPage(pathname)) {
55+
router.push("/dashboard");
56+
}
3257
};
3358

3459
const handleSwitchToTeam = (teamId: string) => {
3560
switchAccount({
3661
type: "team",
3762
id: teamId,
3863
});
64+
if (isRestrictedPage(pathname)) {
65+
router.push("/dashboard");
66+
}
3967
};
4068

4169
return {

lib/active-account/cookie.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { z } from "zod";
2+
3+
export const ACTIVE_ACCOUNT_COOKIE_KEY = "zenao-active-account";
4+
5+
export const activeAccountSchema = z.object({
6+
type: z.enum(["personal", "team"]),
7+
id: z.string(),
8+
});
9+
10+
export type ActiveAccount = z.infer<typeof activeAccountSchema>;
11+
12+
export function setActiveAccountCookie(account: ActiveAccount): void {
13+
const value = JSON.stringify(account);
14+
document.cookie = `${ACTIVE_ACCOUNT_COOKIE_KEY}=${encodeURIComponent(value)}; path=/; max-age=${60 * 60 * 24 * 365}; SameSite=Lax`;
15+
}
16+
17+
export function getActiveAccountCookie(): ActiveAccount | null {
18+
if (typeof document === "undefined") {
19+
return null;
20+
}
21+
22+
const cookies = document.cookie.split(";");
23+
for (const cookie of cookies) {
24+
const [name, ...valueParts] = cookie.trim().split("=");
25+
if (name === ACTIVE_ACCOUNT_COOKIE_KEY) {
26+
try {
27+
const value = decodeURIComponent(valueParts.join("="));
28+
const parsed: unknown = JSON.parse(value); // eslint-disable-line no-restricted-syntax
29+
const result = activeAccountSchema.safeParse(parsed);
30+
return result.success ? result.data : null;
31+
} catch {
32+
return null;
33+
}
34+
}
35+
}
36+
return null;
37+
}
38+
39+
export function clearActiveAccountCookie(): void {
40+
document.cookie = `${ACTIVE_ACCOUNT_COOKIE_KEY}=; path=/; max-age=0`;
41+
}

0 commit comments

Comments
 (0)