Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions src/components/ui/theme-switcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { useTranslation } from 'react-i18next';
import { match } from 'ts-pattern';

import { cn } from '@/lib/tailwind/utils';
import { useHydrated } from '@/hooks/use-hydrated';

import { Button } from '@/components/ui/button';
import {
Expand All @@ -25,11 +24,6 @@ export const themes = ['system', 'light', 'dark'] as const;
export const ThemeSwitcher = (props: { iconOnly?: boolean }) => {
const { t } = useTranslation(['common']);
const { theme, setTheme } = useTheme();
const hydrated = useHydrated();

if (!hydrated) {
return <div className="size-9" />;
}

return (
<DropdownMenu>
Expand All @@ -41,13 +35,13 @@ export const ThemeSwitcher = (props: { iconOnly?: boolean }) => {
/>
}
>
{match(theme as (typeof themes)[number])
{match((theme as (typeof themes)[number]) ?? 'system')
.with('system', () => <SunMoonIcon className="opacity-50" />)
.with('light', () => <SunIcon className="opacity-50" />)
.with('dark', () => <MoonIcon className="opacity-50" />)
.exhaustive()}
<span className={cn(props.iconOnly && 'sr-only')}>
{match(theme as (typeof themes)[number])
{match((theme as (typeof themes)[number]) ?? 'system')
.with('system', () => t('common:themes.values.system'))
.with('light', () => t('common:themes.values.light'))
.with('dark', () => t('common:themes.values.dark'))
Expand Down
4 changes: 2 additions & 2 deletions src/features/account/change-name-drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ import {
FormFieldsAccountUpdateName,
zFormFieldsAccountUpdateName,
} from '@/features/account/schema';
import { authClient } from '@/features/auth/client';
import { useSession } from '@/features/auth/use-session';

export const ChangeNameDrawer = (props: { children: ReactElement }) => {
const { t } = useTranslation(['account']);
const [open, setOpen] = useState(false);
const session = authClient.useSession();
const session = useSession();
const form = useForm<FormFieldsAccountUpdateName>({
resolver: zodResolver(zFormFieldsAccountUpdateName()),
values: {
Expand Down
4 changes: 2 additions & 2 deletions src/features/account/user-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import { Card, CardAction, CardHeader, CardTitle } from '@/components/ui/card';

import { AccountCardRow } from '@/features/account/account-card-row';
import { ChangeNameDrawer } from '@/features/account/change-name-drawer';
import { authClient } from '@/features/auth/client';
import { ConfirmSignOut } from '@/features/auth/confirm-signout';
import { useSession } from '@/features/auth/use-session';

export const UserCard = () => {
const { t } = useTranslation(['auth', 'account']);
const session = authClient.useSession();
const session = useSession();
return (
<Card className="gap-0 p-0">
<CardHeader className="gap-y-0 py-4">
Expand Down
31 changes: 20 additions & 11 deletions src/features/auth/guard-authenticated.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { useRouter } from '@tanstack/react-router';
import { ReactNode } from 'react';
import { ReactNode, useEffect } from 'react';

import { PageError } from '@/components/errors/page-error';
import { Spinner } from '@/components/ui/spinner';

import { authClient } from '@/features/auth/client';
import { PageOnboarding } from '@/features/auth/page-onboarding';
import { Permission, Role } from '@/features/auth/permissions';
import { useSession } from '@/features/auth/use-session';

export const GuardAuthenticated = ({
children,
Expand All @@ -15,9 +16,26 @@ export const GuardAuthenticated = ({
children?: ReactNode;
permissionApps?: Permission['apps'];
}) => {
const session = authClient.useSession();
const session = useSession();
const router = useRouter();

useEffect(() => {
if (
session.isPending ||
(session.error && session.error.status > 0) ||
session.data?.user
)
return;

router.navigate({
to: '/login',
replace: true,
search: {
redirect: location.href,
},
});
}, [router, session.isPending, session.data?.user, session.error]);

if (session.isPending) {
return <Spinner full className="opacity-60" />;
}
Expand All @@ -27,22 +45,13 @@ export const GuardAuthenticated = ({
}

if (!session.data?.user) {
router.navigate({
to: '/login',
replace: true,
search: {
redirect: location.href,
},
});
return null;
}

// Check if onboarding is done
if (!session.data.user.onboardedAt) {
return <PageOnboarding />;
}

// Unauthorized if the user permission do not match
if (
permissionApps &&
!authClient.admin.checkRolePermission({
Expand Down
4 changes: 2 additions & 2 deletions src/features/auth/guard-public-only.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { Activity, ReactNode } from 'react';
import { PageError } from '@/components/errors/page-error';
import { Spinner } from '@/components/ui/spinner';

import { authClient } from '@/features/auth/client';
import { useSession } from '@/features/auth/use-session';
import { useRedirectAfterLogin } from '@/features/auth/utils';

export const GuardPublicOnly = ({ children }: { children?: ReactNode }) => {
const session = authClient.useSession();
const session = useSession();
useRedirectAfterLogin();

if (session.error && session.error.status > 0) {
Expand Down
3 changes: 2 additions & 1 deletion src/features/auth/page-login-verify.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
FormFieldsLoginVerify,
zFormFieldsLoginVerify,
} from '@/features/auth/schema';
import { useSession } from '@/features/auth/use-session';
import { LoginEmailOtpHint } from '@/features/devtools/login-hint';

const I18N_KEY_PAGE_PREFIX = AUTH_SIGNUP_ENABLED
Expand All @@ -36,7 +37,7 @@ export default function PageLoginVerify({
search: { redirect?: string; email: string };
}) {
const { t } = useTranslation(['auth', 'common']);
const session = authClient.useSession();
const session = useSession();

const form = useForm({
mode: 'onSubmit',
Expand Down
5 changes: 3 additions & 2 deletions src/features/auth/page-logout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import { PageError } from '@/components/errors/page-error';
import { Spinner } from '@/components/ui/spinner';

import { authClient } from '@/features/auth/client';
import { useSession } from '@/features/auth/use-session';

export const PageLogout = () => {
const navigate = useNavigate();
const session = authClient.useSession();
const session = useSession();
const { mutate, error } = useMutation({
mutationFn: async () => {
const response = await authClient.signOut();
Expand All @@ -20,7 +21,7 @@ export const PageLogout = () => {
},
onSuccess: () => {
navigate({
to: '/',
to: '/login',
});
},
});
Expand Down
4 changes: 2 additions & 2 deletions src/features/auth/page-onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ import {
} from '@/components/form';
import { Button } from '@/components/ui/button';

import { authClient } from '@/features/auth/client';
import { ConfirmSignOut } from '@/features/auth/confirm-signout';
import { LayoutLogin } from '@/features/auth/layout-login';
import { useMascot } from '@/features/auth/mascot';
import { zFormFieldsOnboarding } from '@/features/auth/schema';
import { useSession } from '@/features/auth/use-session';

export const PageOnboarding = () => {
const { t } = useTranslation(['auth']);
const session = authClient.useSession();
const session = useSession();

const submitOnboarding = useMutation(
orpc.account.submitOnboarding.mutationOptions({
Expand Down
20 changes: 20 additions & 0 deletions src/features/auth/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createServerFn } from '@tanstack/react-start';
import { getRequestHeaders } from '@tanstack/react-start/server';

import { auth } from '@/server/auth';

export const initAuthSsr = createServerFn({ method: 'GET' }).handler(
async () => {
const authSession = await auth.api.getSession({
headers: getRequestHeaders(),
});

return {
authSession,
};
}
);

export type AuthSession = Awaited<
ReturnType<typeof initAuthSsr>
>['authSession'];
26 changes: 26 additions & 0 deletions src/features/auth/use-session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { authClient } from '@/features/auth/client';
import { RootRouteApi } from '@/routes/__root';

/**
* Wraps `authClient.useSession()` and, while the client-side session hook is
* still pending on first mount, falls back to the session pre-fetched on the
* server in the root route `beforeLoad`. This prevents content shifts after a
* refresh for components that read session data directly (UserCard, NavUser,
* onboarding, etc.) — they get real data on the very first render.
*
* Drop-in replacement for `authClient.useSession()`.
*/
export const useSession = () => {
const session = authClient.useSession();
const { authSession } = RootRouteApi.useRouteContext();

if (session.isPending && authSession?.session) {
return {
...session,
data: authSession satisfies typeof session.data,
isPending: false,
};
}

return session;
};
3 changes: 2 additions & 1 deletion src/features/auth/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { useEffect } from 'react';

import { authClient } from '@/features/auth/client';
import { Role } from '@/features/auth/permissions';
import { useSession } from '@/features/auth/use-session';

export const useRedirectAfterLogin = () => {
const search = useSearch({ strict: false });
const router = useRouter();
const session = authClient.useSession();
const session = useSession();
const searchRedirect = search.redirect;

useEffect(() => {
Expand Down
3 changes: 2 additions & 1 deletion src/features/auth/with-permissions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { ReactNode } from 'react';

import { authClient } from '@/features/auth/client';
import { Permission, Role } from '@/features/auth/permissions';
import { useSession } from '@/features/auth/use-session';

export const WithPermissions = (props: {
permissions: Permission[];
children?: ReactNode;
loadingFallback?: ReactNode;
fallback?: ReactNode;
}) => {
const session = authClient.useSession();
const session = useSession();
const userRole = session.data?.user.role;

if (session.isPending) {
Expand Down
3 changes: 2 additions & 1 deletion src/features/demo/demo-app-switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ import {

import { authClient } from '@/features/auth/client';
import { Role } from '@/features/auth/permissions';
import { useSession } from '@/features/auth/use-session';

export const DemoAppSwitch = () => {
const { t } = useTranslation(['demo']);
const session = authClient.useSession();
const session = useSession();
const userRole = session.data?.user.role;
const matchRoute = useMatchRoute();
const currentApp = matchRoute({ to: '/manager', fuzzy: true })
Expand Down
4 changes: 2 additions & 2 deletions src/features/user/manager/form-user.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import {
FormFieldLabel,
} from '@/components/form';

import { authClient } from '@/features/auth/client';
import { rolesNames } from '@/features/auth/permissions';
import { useSession } from '@/features/auth/use-session';
import { FormFieldsUser } from '@/features/user/schema';

export const FormUser = (props: { userId?: string }) => {
const { t } = useTranslation(['user']);
const session = authClient.useSession();
const session = useSession();
const form = useFormContext<FormFieldsUser>();
const isCurrentUser = props.userId === session.data?.user.id;

Expand Down
10 changes: 6 additions & 4 deletions src/features/user/manager/page-user-new.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { ORPCError } from '@orpc/client';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useRouter } from '@tanstack/react-router';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';

import { orpc } from '@/lib/orpc/client';
import { useNavigateBack } from '@/hooks/use-navigate-back';

import { BackButton } from '@/components/back-button';
import { Form } from '@/components/form';
Expand All @@ -25,7 +25,7 @@ import {

export const PageUserNew = () => {
const { t } = useTranslation(['user']);
const { navigateBack } = useNavigateBack();
const router = useRouter();
const queryClient = useQueryClient();
const form = useForm({
resolver: zodResolver(zFormFieldsUser()),
Expand All @@ -45,8 +45,10 @@ export const PageUserNew = () => {
type: 'all',
});

// Redirect
navigateBack({ ignoreBlocker: true });
await router.navigate({
to: '/manager/users',
ignoreBlocker: true,
});
},
onError: (error) => {
if (
Expand Down
4 changes: 2 additions & 2 deletions src/features/user/manager/page-user-update.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';

import { authClient } from '@/features/auth/client';
import { useSession } from '@/features/auth/use-session';
import { FormUser } from '@/features/user/manager/form-user';
import { zFormFieldsUser } from '@/features/user/schema';
import {
Expand All @@ -30,7 +30,7 @@ import {
export const PageUserUpdate = (props: { params: { id: string } }) => {
const { t } = useTranslation(['user']);
const { navigateBack } = useNavigateBack();
const session = authClient.useSession();
const session = useSession();
const queryClient = useQueryClient();
const userQuery = useQuery(
orpc.user.getById.queryOptions({ input: { id: props.params.id } })
Expand Down
Loading
Loading