Refactor account handling to improve performance

This commit dates the transition from a global user session to individual account handling based on user ID. The transition was made across several components, notably the account settings, icons, and selector. This change improves performance by reducing unnecessary requests and ensures more accurate data handling. The commit also includes some cleanups and minor fixes spread across different components.
This commit is contained in:
giancarlo
2024-05-10 20:33:05 +07:00
parent 6b3b3cb58b
commit 39e0a229b6
24 changed files with 106 additions and 106 deletions
@@ -18,6 +18,7 @@ export function HomeAccountSelector(props: {
image: string | null;
}>;
userId: string;
collapsed: boolean;
}) {
const router = useRouter();
@@ -27,6 +28,7 @@ export function HomeAccountSelector(props: {
collapsed={props.collapsed}
accounts={props.accounts}
features={features}
userId={props.userId}
onAccountChange={(value) => {
if (value) {
const path = pathsConfig.app.accountHome.replace('[account]', value);
@@ -12,7 +12,7 @@ import { personalAccountNavigationConfig } from '~/config/personal-account-navig
// home imports
import { HomeAccountSelector } from '../_components/home-account-selector';
import { UserNotifications } from '../_components/user-notifications';
import { type UserWorkspace } from '../_lib/server/load-user-workspace';
import { type UserWorkspace } from '../_lib/server/user-workspace.loader';
export function HomeMenuNavigation(props: { workspace: UserWorkspace }) {
const { workspace, user, accounts } = props.workspace;
@@ -50,7 +50,11 @@ export function HomeMenuNavigation(props: { workspace: UserWorkspace }) {
<div className={'flex justify-end space-x-2.5'}>
<If condition={featuresFlagConfig.enableTeamAccounts}>
<HomeAccountSelector accounts={accounts} collapsed={false} />
<HomeAccountSelector
userId={user.id}
accounts={accounts}
collapsed={false}
/>
</If>
<UserNotifications userId={user.id} />
@@ -22,7 +22,7 @@ import { personalAccountNavigationConfig } from '~/config/personal-account-navig
// home imports
import { HomeAccountSelector } from '../_components/home-account-selector';
import type { UserWorkspace } from '../_lib/server/load-user-workspace';
import type { UserWorkspace } from '../_lib/server/user-workspace.loader';
export function HomeMobileNavigation(props: { workspace: UserWorkspace }) {
const signOut = useSignOut();
@@ -69,6 +69,7 @@ export function HomeMobileNavigation(props: { workspace: UserWorkspace }) {
</DropdownMenuLabel>
<HomeAccountSelector
userId={props.workspace.user.id}
accounts={props.workspace.accounts}
collapsed={false}
/>
@@ -8,7 +8,7 @@ import { personalAccountNavigationConfig } from '~/config/personal-account-navig
import { UserNotifications } from '~/home/(user)/_components/user-notifications';
// home imports
import type { UserWorkspace } from '../_lib/server/load-user-workspace';
import type { UserWorkspace } from '../_lib/server/user-workspace.loader';
import { HomeAccountSelector } from './home-account-selector';
export function HomeSidebar(props: { workspace: UserWorkspace }) {
@@ -22,7 +22,11 @@ export function HomeSidebar(props: { workspace: UserWorkspace }) {
condition={featuresFlagConfig.enableTeamAccounts}
fallback={<AppLogo className={'py-2'} />}
>
<HomeAccountSelector collapsed={false} accounts={accounts} />
<HomeAccountSelector
userId={user.id}
collapsed={false}
accounts={accounts}
/>
</If>
<UserNotifications userId={user.id} />
@@ -1,6 +1,9 @@
import { cache } from 'react';
import { redirect } from 'next/navigation';
import { createAccountsApi } from '@kit/accounts/api';
import { requireUser } from '@kit/supabase/require-user';
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
import featureFlagsConfig from '~/config/feature-flags.config';
@@ -24,20 +27,19 @@ export const loadUserWorkspace = cache(async () => {
: () => Promise.resolve([]);
const workspacePromise = api.getAccountWorkspace();
const userPromise = client.auth.getUser();
const [accounts, workspace, userResult] = await Promise.all([
const [accounts, workspace, auth] = await Promise.all([
accountsPromise(),
workspacePromise,
userPromise,
requireUser(client),
]);
const user = userResult.data.user;
if (!user) {
throw new Error('User is not logged in');
if (!auth.data) {
return redirect(auth.redirectTo);
}
const user = auth.data;
return {
accounts,
workspace,
+1 -1
View File
@@ -18,7 +18,7 @@ import { withI18n } from '~/lib/i18n/with-i18n';
import { HomeMenuNavigation } from './_components/home-menu-navigation';
import { HomeMobileNavigation } from './_components/home-mobile-navigation';
import { HomeSidebar } from './_components/home-sidebar';
import { loadUserWorkspace } from './_lib/server/load-user-workspace';
import { loadUserWorkspace } from './_lib/server/user-workspace.loader';
function UserHomeLayout({ children }: React.PropsWithChildren) {
const workspace = use(loadUserWorkspace());
+10 -1
View File
@@ -1,8 +1,11 @@
import { use } from 'react';
import { PersonalAccountSettingsContainer } from '@kit/accounts/personal-account-settings';
import { PageBody } from '@kit/ui/page';
import featureFlagsConfig from '~/config/feature-flags.config';
import pathsConfig from '~/config/paths.config';
import { loadUserWorkspace } from '~/home/(user)/_lib/server/user-workspace.loader';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
@@ -24,10 +27,16 @@ export const generateMetadata = async () => {
};
function PersonalAccountSettingsPage() {
const { user } = use(loadUserWorkspace());
return (
<PageBody>
<div className={'flex w-full flex-1 flex-col lg:max-w-2xl'}>
<PersonalAccountSettingsContainer features={features} paths={paths} />
<PersonalAccountSettingsContainer
userId={user.id}
features={features}
paths={paths}
/>
</div>
</PageBody>
);
@@ -13,6 +13,8 @@ const features = {
export function TeamAccountAccountsSelector(params: {
selectedAccount: string;
userId: string;
accounts: Array<{
label: string | null;
value: string | null;
@@ -25,6 +27,7 @@ export function TeamAccountAccountsSelector(params: {
<AccountSelector
selectedAccount={params.selectedAccount}
accounts={params.accounts}
userId={params.userId}
collapsed={false}
features={features}
onAccountChange={(value) => {
@@ -41,6 +41,7 @@ const features = {
export const TeamAccountLayoutMobileNavigation = (
props: React.PropsWithChildren<{
account: string;
userId: string;
accounts: Accounts;
}>,
) => {
@@ -83,7 +84,7 @@ export const TeamAccountLayoutMobileNavigation = (
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={10} className={'w-screen rounded-none'}>
<TeamAccountsModal accounts={props.accounts} />
<TeamAccountsModal userId={props.userId} accounts={props.accounts} />
{Links}
@@ -137,7 +138,7 @@ function SignOutDropdownItem(
);
}
function TeamAccountsModal(props: { accounts: Accounts }) {
function TeamAccountsModal(props: { accounts: Accounts; userId: string }) {
const router = useRouter();
return (
@@ -165,6 +166,7 @@ function TeamAccountsModal(props: { accounts: Accounts }) {
<div className={'py-16'}>
<AccountSelector
className={'w-full max-w-full'}
userId={props.userId}
onAccountChange={(value) => {
const path = value
? pathsConfig.app.accountHome.replace('[account]', value)
@@ -59,7 +59,8 @@ function SidebarContainer(props: {
collapsible?: boolean;
user: User;
}) {
const { account, accounts } = props;
const { account, accounts, user } = props;
const userId = user.id;
return (
<>
@@ -68,12 +69,13 @@ function SidebarContainer(props: {
className={'flex max-w-full items-center justify-between space-x-4'}
>
<TeamAccountAccountsSelector
userId={userId}
selectedAccount={account}
accounts={accounts}
/>
<TeamAccountNotifications
userId={props.user.id}
userId={userId}
accountId={props.accountId}
/>
</div>
@@ -50,6 +50,7 @@ export function TeamAccountNavigationMenu(props: {
<div className={'flex justify-end space-x-2.5'}>
<TeamAccountAccountsSelector
userId={user.id}
selectedAccount={account.slug}
accounts={accounts.map((account) => ({
label: account.name,
@@ -4,6 +4,7 @@ import { cache } from 'react';
import { redirect } from 'next/navigation';
import { requireUser } from '@kit/supabase/require-user';
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
import { createTeamAccountsApi } from '@kit/team-accounts/api';
@@ -26,19 +27,25 @@ export const loadTeamWorkspace = cache(async (accountSlug: string) => {
const client = getSupabaseServerComponentClient();
const api = createTeamAccountsApi(client);
const workspace = await api.getAccountWorkspace(accountSlug);
if (workspace.error) {
throw workspace.error;
}
const account = workspace.data.account;
const [workspace, auth] = await Promise.all([
api.getAccountWorkspace(accountSlug),
requireUser(client),
]);
// we cannot find any record for the selected account
// so we redirect the user to the home page
if (!account) {
if (!workspace.data?.account) {
return redirect(pathsConfig.app.home);
}
return workspace.data;
if (!auth.data) {
return redirect(auth.redirectTo);
}
const user = auth.data;
return {
...workspace.data,
user,
};
});
+1
View File
@@ -62,6 +62,7 @@ function TeamWorkspaceLayout({
<div className={'flex space-x-4'}>
<TeamAccountLayoutMobileNavigation
userId={data.user.id}
accounts={accounts}
account={params.account}
/>
@@ -19,7 +19,6 @@ export async function loadMembersPageData(
loadTeamWorkspace(slug),
loadAccountMembers(client, slug),
loadInvitations(client, slug),
loadUser(client),
canAddMember,
]);
}
@@ -38,16 +37,6 @@ async function canAddMember() {
return Promise.resolve(true);
}
async function loadUser(client: SupabaseClient<Database>) {
const { data, error } = await client.auth.getUser();
if (error) {
throw error;
}
return data.user;
}
/**
* Load account members
* @param client
+4 -1
View File
@@ -18,6 +18,7 @@ import { If } from '@kit/ui/if';
import { PageBody } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { loadTeamWorkspace } from '~/home/[account]/_lib/server/team-account-workspace.loader';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
@@ -43,7 +44,9 @@ export const generateMetadata = async () => {
async function TeamAccountMembersPage({ params }: Params) {
const client = getSupabaseServerComponentClient();
const [{ account }, members, invitations, user, canAddMember] =
const { user } = await loadTeamWorkspace(params.account);
const [{ account }, members, invitations, canAddMember] =
await loadMembersPageData(client, params.account);
const canManageRoles = account.permissions.includes('roles.manage');
@@ -19,7 +19,7 @@ const features = {
export function ProfileAccountDropdownContainer(props: {
collapsed: boolean;
user: User | null;
user: User;
account?: {
id: string | null;
@@ -29,7 +29,7 @@ export function ProfileAccountDropdownContainer(props: {
}) {
const signOut = useSignOut();
const user = useUser(props.user);
const userData = user.data ?? props.user ?? null;
const userData = user.data as User;
return (
<div className={props.collapsed ? '' : 'w-full'}>
-5
View File
@@ -22,11 +22,6 @@ export const rootMetadata: Metadata = {
},
icons: {
icon: '/images/favicon/favicon.ico',
shortcut: '/shortcut-icon.png',
apple: '/images/favicon/apple-touch-icon.png',
other: {
rel: 'apple-touch-icon-precomposed',
url: '/apple-touch-icon-precomposed.png',
},
},
};
+14 -34
View File
@@ -1,8 +1,6 @@
import type { NextRequest } from 'next/server';
import { NextResponse, URLPattern } from 'next/server';
import type { UserResponse } from '@supabase/supabase-js';
import { CsrfError, createCsrfProtect } from '@edge-csrf/nextjs';
import { checkRequiresMultiFactorAuthentication } from '@kit/supabase/check-requires-mfa';
@@ -15,17 +13,17 @@ const CSRF_SECRET_COOKIE = 'csrfSecret';
const NEXT_ACTION_HEADER = 'next-action';
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|locales|assets|api/*).*)',
],
matcher: ['/((?!_next/static|_next/image|images|locales|assets|api/*).*)'],
};
const getUser = (request: NextRequest, response: NextResponse) => {
const supabase = createMiddlewareClient(request, response);
return supabase.auth.getUser();
};
export async function middleware(request: NextRequest) {
const response = NextResponse.next();
const supabase = createMiddlewareClient(request, response);
// get the user from the session (no matter if it's logged in or not)
const userResponse = await supabase.auth.getUser();
// set a unique request ID for each request
// this helps us log and trace requests
@@ -39,11 +37,7 @@ export async function middleware(request: NextRequest) {
// if a pattern handler exists, call it
if (handlePattern) {
const patternHandlerResponse = await handlePattern(
request,
csrfResponse,
userResponse,
);
const patternHandlerResponse = await handlePattern(request, csrfResponse);
// if a pattern handler returns a response, return it
if (patternHandlerResponse) {
@@ -95,18 +89,14 @@ function isServerAction(request: NextRequest) {
return headers.has(NEXT_ACTION_HEADER);
}
function adminMiddleware(
request: NextRequest,
response: NextResponse,
userResponse: UserResponse,
) {
async function adminMiddleware(request: NextRequest, response: NextResponse) {
const isAdminPath = request.nextUrl.pathname.startsWith('/admin');
if (!isAdminPath) {
return response;
}
const { data, error } = userResponse;
const { data, error } = await getUser(request, response);
// If user is not logged in, redirect to sign in page.
// This should never happen, but just in case.
@@ -138,12 +128,8 @@ function getPatterns() {
},
{
pattern: new URLPattern({ pathname: '/auth*' }),
handler: (
req: NextRequest,
_: NextResponse,
userResponse: UserResponse,
) => {
const user = userResponse.data.user;
handler: async (req: NextRequest, res: NextResponse) => {
const { data: user } = await getUser(req, res);
// the user is logged out, so we don't need to do anything
if (!user) {
@@ -164,14 +150,8 @@ function getPatterns() {
},
{
pattern: new URLPattern({ pathname: '/home*' }),
handler: async (
req: NextRequest,
res: NextResponse,
userResponse: UserResponse,
) => {
const {
data: { user },
} = userResponse;
handler: async (req: NextRequest, res: NextResponse) => {
const { data: user } = await getUser(req, res);
const origin = req.nextUrl.origin;
const next = req.nextUrl.pathname;
@@ -36,6 +36,7 @@ interface AccountSelectorProps {
enableTeamCreation: boolean;
};
userId: string;
selectedAccount?: string;
collapsed?: boolean;
className?: string;
@@ -49,6 +50,7 @@ export function AccountSelector({
accounts,
selectedAccount,
onAccountChange,
userId,
className,
features = {
enableTeamCreation: true,
@@ -63,7 +65,7 @@ export function AccountSelector({
);
const { t } = useTranslation('teams');
const personalData = usePersonalAccountData();
const personalData = usePersonalAccountData(userId);
useEffect(() => {
setValue(selectedAccount ?? PERSONAL_ACCOUNT_SLUG);
@@ -38,8 +38,7 @@ export function PersonalAccountDropdown({
features,
account,
}: {
className?: string;
user: User | null;
user: User;
account?: {
id: string | null;
@@ -48,7 +47,6 @@ export function PersonalAccountDropdown({
};
signOutRequested: () => unknown;
showProfileName?: boolean;
paths: {
home: string;
@@ -57,8 +55,14 @@ export function PersonalAccountDropdown({
features: {
enableThemeToggle: boolean;
};
className?: string;
showProfileName?: boolean;
}) {
const { data: personalAccountData } = usePersonalAccountData(account);
const { data: personalAccountData } = usePersonalAccountData(
user.id,
account,
);
const signedInAsLabel = useMemo(() => {
const email = user?.email ?? undefined;
@@ -24,6 +24,8 @@ import { UpdateAccountImageContainer } from './update-account-image-container';
export function PersonalAccountSettingsContainer(
props: React.PropsWithChildren<{
userId: string;
features: {
enableAccountDeletion: boolean;
};
@@ -34,7 +36,7 @@ export function PersonalAccountSettingsContainer(
}>,
) {
const supportsLanguageSelection = useSupportMultiLanguage();
const user = usePersonalAccountData();
const user = usePersonalAccountData(props.userId);
if (!user.data || user.isPending) {
return <LoadingOverlay fullPage />;
@@ -3,9 +3,9 @@ import { useCallback } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
import { useUser } from '@kit/supabase/hooks/use-user';
export function usePersonalAccountData(
userId: string,
partialAccount?:
| {
id: string | null;
@@ -15,9 +15,6 @@ export function usePersonalAccountData(
| undefined,
) {
const client = useSupabase();
const user = useUser();
const userId = user.data?.id;
const queryKey = ['account:data', userId];
const queryFn = async () => {
@@ -78,16 +78,9 @@ export class TeamAccountsApi {
const accountsPromise = this.client.from('user_accounts').select('*');
const [
accountResult,
accountsResult,
{
data: { user },
},
] = await Promise.all([
const [accountResult, accountsResult] = await Promise.all([
accountPromise,
accountsPromise,
this.client.auth.getUser(),
]);
if (accountResult.error) {
@@ -104,13 +97,6 @@ export class TeamAccountsApi {
};
}
if (!user) {
return {
error: new Error('User is not logged in'),
data: null,
};
}
const accountData = accountResult.data[0];
if (!accountData) {
@@ -124,7 +110,6 @@ export class TeamAccountsApi {
data: {
account: accountData,
accounts: accountsResult.data,
user,
},
error: null,
};
+5
View File
@@ -24,10 +24,15 @@ export function useUser(initialData?: User | null) {
return Promise.reject('Unexpected result format');
};
// update staleTime to 5 minutes
const staleTime = 1000 * 60 * 5;
return useQuery({
queryFn,
queryKey,
initialData,
staleTime,
refetchInterval: false,
refetchOnMount: false,
refetchOnWindowFocus: false,
});