fix: rework sessions

This commit is contained in:
David Nguyen
2025-02-17 22:46:36 +11:00
parent 1ed1cb0773
commit 5fc724b247
57 changed files with 1512 additions and 1446 deletions

View File

@ -1,56 +1,57 @@
import { SubscriptionStatus } from '@prisma/client';
import { Outlet } from 'react-router';
import { getLoaderSession } from 'server/utils/get-loader-session';
import { Outlet, redirect } from 'react-router';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { getLimits } from '@documenso/ee/server-only/limits/client';
import { LimitsProvider } from '@documenso/ee/server-only/limits/provider/client';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { getSiteSettings } from '@documenso/lib/server-only/site-settings/get-site-settings';
import { SITE_SETTINGS_BANNER_ID } from '@documenso/lib/server-only/site-settings/schemas/banner';
import { AppBanner } from '~/components/general/app-banner';
import { Header } from '~/components/general/app-header';
import { TeamLayoutBillingBanner } from '~/components/general/teams/team-layout-billing-banner';
import { VerifyEmailBanner } from '~/components/general/verify-email-banner';
import type { Route } from './+types/_layout';
export const loader = async ({ request }: Route.LoaderArgs) => {
const { user, teams, currentTeam } = getLoaderSession();
/**
* Don't revalidate (run the loader on sequential navigations)
*
* Update values via providers.
*/
export const shouldRevalidate = () => false;
export const loader = async ({ request }: Route.LoaderArgs) => {
const requestHeaders = Object.fromEntries(request.headers.entries());
// Todo: Should only load this on first render.
const session = await getOptionalSession(request);
if (!session.isAuthenticated) {
return redirect('/signin');
}
const [limits, banner] = await Promise.all([
getLimits({ headers: requestHeaders, teamId: currentTeam?.id }),
getLimits({ headers: requestHeaders }),
getSiteSettings().then((settings) =>
settings.find((setting) => setting.id === SITE_SETTINGS_BANNER_ID),
),
]);
return {
user,
teams,
banner,
limits,
currentTeam,
};
};
export default function Layout({ loaderData }: Route.ComponentProps) {
const { user, teams, banner, limits, currentTeam } = loaderData;
const { user, teams } = useSession();
const { banner, limits } = loaderData;
return (
<LimitsProvider initialValue={limits} teamId={currentTeam?.id}>
{!user.emailVerified && <VerifyEmailBanner email={user.email} />}
<LimitsProvider initialValue={limits}>
<div id="portal-header"></div>
{currentTeam?.subscription &&
currentTeam.subscription.status !== SubscriptionStatus.ACTIVE && (
<TeamLayoutBillingBanner
subscription={currentTeam.subscription}
teamId={currentTeam.id}
userRole={currentTeam.currentTeamMember.role}
/>
)}
{!user.emailVerified && <VerifyEmailBanner email={user.email} />}
{banner && <AppBanner banner={banner} />}

View File

@ -1,14 +1,16 @@
import { Trans } from '@lingui/react/macro';
import { BarChart3, FileStack, Settings, Trophy, Users, Wallet2 } from 'lucide-react';
import { Link, Outlet, redirect, useLocation } from 'react-router';
import { getLoaderSession } from 'server/utils/get-loader-session';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { isAdmin } from '@documenso/lib/utils/is-admin';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
export function loader() {
const { user } = getLoaderSession();
import type { Route } from './+types/_layout';
export async function loader({ request }: Route.LoaderArgs) {
const { user } = await getSession(request);
if (!user || !isAdmin(user)) {
throw redirect('/documents');

View File

@ -3,13 +3,14 @@ import { Plural, Trans } from '@lingui/react/macro';
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
import { ChevronLeft, Clock9, Users2 } from 'lucide-react';
import { Link, redirect } from 'react-router';
import { getLoaderSession } from 'server/utils/get-loader-session';
import { match } from 'ts-pattern';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { Badge } from '@documenso/ui/primitives/badge';
@ -34,8 +35,14 @@ import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
import type { Route } from './+types/$id._index';
export async function loader({ params }: Route.LoaderArgs) {
const { user, currentTeam: team } = getLoaderSession();
export async function loader({ params, request }: Route.LoaderArgs) {
const { user } = await getSession(request);
let team: TGetTeamByUrlResponse | null = null;
if (params.teamUrl) {
team = await getTeamByUrl({ userId: user.id, teamUrl: params.teamUrl });
}
const { id } = params;

View File

@ -2,11 +2,12 @@ import { Plural, Trans } from '@lingui/react/macro';
import { DocumentStatus as InternalDocumentStatus, TeamMemberRole } from '@prisma/client';
import { ChevronLeft, Users2 } from 'lucide-react';
import { Link, redirect } from 'react-router';
import { getLoaderSession } from 'server/utils/get-loader-session';
import { match } from 'ts-pattern';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
@ -17,8 +18,14 @@ import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
import type { Route } from './+types/$id.edit';
export async function loader({ params }: Route.LoaderArgs) {
const { user, currentTeam: team } = getLoaderSession();
export async function loader({ params, request }: Route.LoaderArgs) {
const { user } = await getSession(request);
let team: TGetTeamByUrlResponse | null = null;
if (params.teamUrl) {
team = await getTeamByUrl({ userId: user.id, teamUrl: params.teamUrl });
}
const { id } = params;

View File

@ -6,10 +6,11 @@ import type { Recipient } from '@prisma/client';
import { ChevronLeft } from 'lucide-react';
import { DateTime } from 'luxon';
import { Link, redirect } from 'react-router';
import { getLoaderSession } from 'server/utils/get-loader-session';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { Card } from '@documenso/ui/primitives/card';
@ -23,10 +24,16 @@ import { DocumentLogsTable } from '~/components/tables/document-logs-table';
import type { Route } from './+types/$id.logs';
export async function loader({ params }: Route.LoaderArgs) {
const { id } = params;
export async function loader({ params, request }: Route.LoaderArgs) {
const { user } = await getSession(request);
const { user, currentTeam: team } = getLoaderSession();
let team: TGetTeamByUrlResponse | null = null;
if (params.teamUrl) {
team = await getTeamByUrl({ userId: user.id, teamUrl: params.teamUrl });
}
const { id } = params;
const documentId = Number(id);

View File

@ -59,15 +59,26 @@ export default function DocumentsPage() {
[searchParams],
);
const { data, isLoading, isLoadingError } = trpc.document.findDocumentsInternal.useQuery({
...findDocumentSearchParams,
});
const { data, isLoading, isLoadingError, refetch } = trpc.document.findDocumentsInternal.useQuery(
{
...findDocumentSearchParams,
},
);
// Refetch the documents when the team URL changes.
useEffect(() => {
void refetch();
}, [team?.url]);
const getTabHref = (value: keyof typeof ExtendedDocumentStatus) => {
const params = new URLSearchParams(searchParams);
params.set('status', value);
if (value === ExtendedDocumentStatus.ALL) {
params.delete('status');
}
if (params.has('page')) {
params.delete('page');
}

View File

@ -1,38 +0,0 @@
import { Trans } from '@lingui/react/macro';
import { ChevronLeft, Loader } from 'lucide-react';
import { Link } from 'react-router';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
export default function Loading() {
return (
<div className="mx-auto -mt-4 flex w-full max-w-screen-xl flex-col px-4 md:px-8">
<Link to="/documents" className="flex grow-0 items-center text-[#7AC455] hover:opacity-80">
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
<Trans>Documents</Trans>
</Link>
<h1 className="mt-4 grow-0 truncate text-2xl font-semibold md:text-3xl">
<Trans>Loading Document...</Trans>
</h1>
<div className="flex h-10 items-center">
<Skeleton className="my-6 h-4 w-24 rounded-2xl" />
</div>
<div className="mt-4 grid h-[80vh] max-h-[60rem] w-full grid-cols-12 gap-x-8">
<div className="dark:bg-background border-border col-span-12 rounded-xl border-2 bg-white/50 p-2 before:rounded-xl lg:col-span-6 xl:col-span-7">
<div className="flex h-[80vh] max-h-[60rem] flex-col items-center justify-center">
<Loader className="text-documenso h-12 w-12 animate-spin" />
<p className="text-muted-foreground mt-4">
<Trans>Loading document...</Trans>
</p>
</div>
</div>
<div className="bg-background border-border col-span-12 rounded-xl border-2 before:rounded-xl lg:col-span-6 xl:col-span-5" />
</div>
</div>
);
}

View File

@ -5,8 +5,8 @@ import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { TemplateDirectLink } from '@prisma/client';
import { TemplateType } from '@prisma/client';
import { getLoaderSession } from 'server/utils/get-loader-session';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { getUserPublicProfile } from '@documenso/lib/server-only/user/get-user-public-profile';
import { trpc } from '@documenso/trpc/react';
@ -44,8 +44,8 @@ const teamProfileText = {
templatesSubtitle: msg`Show templates in your team public profile for your audience to sign and get started quickly`,
};
export async function loader() {
const { user } = getLoaderSession();
export async function loader({ request }: Route.LoaderArgs) {
const { user } = await getSession(request);
const { profile } = await getUserPublicProfile({
userId: user.id,

View File

@ -2,8 +2,8 @@ import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { Link } from 'react-router';
import { getLoaderSession } from 'server/utils/get-loader-session';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { prisma } from '@documenso/prisma';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
@ -22,8 +22,8 @@ export function meta() {
return appMetaTags('Security');
}
export async function loader() {
const { user } = getLoaderSession();
export async function loader({ request }: Route.LoaderArgs) {
const { user } = await getSession(request);
// Todo: Use providers instead after RR7 migration.
// const accounts = await prisma.account.findMany({

View File

@ -1,14 +1,9 @@
import { redirect } from 'react-router';
import { getLoaderSession } from 'server/utils/get-loader-session';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
export function loader() {
const { currentTeam } = getLoaderSession();
import type { Route } from './+types/_index';
if (!currentTeam) {
throw redirect('/settings/teams');
}
throw redirect(formatDocumentsPath(currentTeam.url));
export function loader({ params }: Route.LoaderArgs) {
throw redirect(formatDocumentsPath(params.teamUrl));
}

View File

@ -1,147 +1,104 @@
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { ChevronLeft } from 'lucide-react';
import { Link, Outlet, isRouteErrorResponse, redirect, useNavigate } from 'react-router';
import { getLoaderSession } from 'server/utils/get-loader-session';
import { match } from 'ts-pattern';
import { useMemo } from 'react';
import { AppErrorCode } from '@documenso/lib/errors/app-error';
import { msg } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { SubscriptionStatus } from '@prisma/client';
import { Link, Outlet } from 'react-router';
import { TEAM_PLAN_LIMITS } from '@documenso/ee/server-only/limits/constants';
import { LimitsProvider } from '@documenso/ee/server-only/limits/provider/client';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { TrpcProvider } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
import { PortalComponent } from '~/components/general/portal';
import { TeamLayoutBillingBanner } from '~/components/general/teams/team-layout-billing-banner';
import { TeamProvider } from '~/providers/team';
import type { Route } from './+types/_layout';
export const loader = () => {
const { currentTeam } = getLoaderSession();
export default function Layout({ params }: Route.ComponentProps) {
const { teams } = useSession();
const currentTeam = teams.find((team) => team.url === params.teamUrl);
const limits = useMemo(() => {
if (!currentTeam) {
return undefined;
}
if (
currentTeam?.subscription &&
currentTeam.subscription.status === SubscriptionStatus.INACTIVE
) {
return {
quota: {
documents: 0,
recipients: 0,
directTemplates: 0,
},
remaining: {
documents: 0,
recipients: 0,
directTemplates: 0,
},
};
}
return {
quota: TEAM_PLAN_LIMITS,
remaining: TEAM_PLAN_LIMITS,
};
}, [currentTeam?.subscription, currentTeam?.id]);
if (!currentTeam) {
throw redirect('/settings/teams');
return (
<GenericErrorLayout
errorCode={404}
errorCodeMap={{
404: {
heading: msg`Team not found`,
subHeading: msg`404 Team not found`,
message: msg`The team you are looking for may have been removed, renamed or may have never
existed.`,
},
}}
primaryButton={
<Button asChild>
<Link to="/settings/teams">
<Trans>View teams</Trans>
</Link>
</Button>
}
></GenericErrorLayout>
);
}
const trpcHeaders = {
'x-team-Id': currentTeam.id.toString(),
};
return {
currentTeam,
trpcHeaders,
};
};
export default function Layout({ loaderData }: Route.ComponentProps) {
const { currentTeam, trpcHeaders } = loaderData;
return (
<TeamProvider team={currentTeam}>
<TrpcProvider headers={trpcHeaders}>
<main className="mt-8 pb-8 md:mt-12 md:pb-12">
<Outlet />
</main>
</TrpcProvider>
<LimitsProvider initialValue={limits} teamId={currentTeam.id}>
<TrpcProvider headers={trpcHeaders}>
{currentTeam?.subscription &&
currentTeam.subscription.status !== SubscriptionStatus.ACTIVE && (
<PortalComponent target="portal-header">
<TeamLayoutBillingBanner
subscriptionStatus={currentTeam.subscription.status}
teamId={currentTeam.id}
userRole={currentTeam.currentTeamMember.role}
/>
</PortalComponent>
)}
<main className="mt-8 pb-8 md:mt-12 md:pb-12">
<Outlet />
</main>
</TrpcProvider>
</LimitsProvider>
</TeamProvider>
);
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
const { _ } = useLingui();
const navigate = useNavigate();
let errorMessage = msg`Unknown error`;
let errorDetails: MessageDescriptor | null = null;
if (error instanceof Error && error.message === AppErrorCode.UNAUTHORIZED) {
errorMessage = msg`Unauthorized`;
errorDetails = msg`You are not authorized to view this page.`;
}
if (isRouteErrorResponse(error)) {
return match(error.status)
.with(404, () => (
<div className="mx-auto flex min-h-[80vh] w-full items-center justify-center py-32">
<div>
<p className="text-muted-foreground font-semibold">
<Trans>404 Team not found</Trans>
</p>
<h1 className="mt-3 text-2xl font-bold md:text-3xl">
<Trans>Oops! Something went wrong.</Trans>
</h1>
<p className="text-muted-foreground mt-4 text-sm">
<Trans>
The team you are looking for may have been removed, renamed or may have never
existed.
</Trans>
</p>
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
<Button asChild className="w-32">
<Link to="/settings/teams">
<ChevronLeft className="mr-2 h-4 w-4" />
<Trans>Go Back</Trans>
</Link>
</Button>
</div>
</div>
</div>
))
.with(500, () => (
<div className="mx-auto flex min-h-[80vh] w-full items-center justify-center py-32">
<div>
<p className="text-muted-foreground font-semibold">{_(errorMessage)}</p>
<h1 className="mt-3 text-2xl font-bold md:text-3xl">
<Trans>Oops! Something went wrong.</Trans>
</h1>
<p className="text-muted-foreground mt-4 text-sm">
{errorDetails ? _(errorDetails) : ''}
</p>
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
<Button
variant="ghost"
className="w-32"
onClick={() => {
void navigate(-1);
}}
>
<ChevronLeft className="mr-2 h-4 w-4" />
<Trans>Go Back</Trans>
</Button>
<Button asChild>
<Link to="/settings/teams">
<Trans>View teams</Trans>
</Link>
</Button>
</div>
</div>
</div>
))
.otherwise(() => (
<>
<h1>
{error.status} {error.statusText}
</h1>
<p>{error.data}</p>
</>
));
} else if (error instanceof Error) {
return (
<div>
<h1>Error</h1>
<p>{error.message}</p>
<p>The stack trace is:</p>
<pre>{error.stack}</pre>
</div>
);
} else {
return <h1>Unknown Error</h1>;
}
}

View File

@ -2,7 +2,9 @@ import { Trans } from '@lingui/react/macro';
import { CheckCircle2, Clock } from 'lucide-react';
import { P, match } from 'ts-pattern';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import { isTokenExpired } from '@documenso/lib/utils/token-verification';
@ -17,13 +19,27 @@ import { TeamUpdateForm } from '~/components/forms/team-update-form';
import { SettingsHeader } from '~/components/general/settings-header';
import { TeamEmailDropdown } from '~/components/general/teams/team-email-dropdown';
import { TeamTransferStatus } from '~/components/general/teams/team-transfer-status';
import { useCurrentTeam } from '~/providers/team';
export default function TeamsSettingsPage() {
import type { Route } from './+types/_index';
export async function loader({ request, params }: Route.LoaderArgs) {
const { user } = await getSession(request);
const team = await getTeamByUrl({
userId: user.id,
teamUrl: params.teamUrl,
});
return {
team,
};
}
export default function TeamsSettingsPage({ loaderData }: Route.ComponentProps) {
const { team } = loaderData;
const { user } = useSession();
const team = useCurrentTeam();
const isTransferVerificationExpired =
!team.transferVerification || isTokenExpired(team.transferVerification.expiresAt);

View File

@ -1,25 +1,37 @@
import { Trans } from '@lingui/react/macro';
import { Outlet } from 'react-router';
import { getLoaderTeamSession } from 'server/utils/get-loader-session';
import { Outlet, redirect } from 'react-router';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
import { TeamSettingsNavDesktop } from '~/components/general/teams/team-settings-nav-desktop';
import { TeamSettingsNavMobile } from '~/components/general/teams/team-settings-nav-mobile';
import { appMetaTags } from '~/utils/meta';
import type { Route } from './+types/_layout';
export function meta() {
return appMetaTags('Team Settings');
}
export function loader() {
const { currentTeam: team } = getLoaderTeamSession();
export async function loader({ request, params }: Route.LoaderArgs) {
const session = await getSession(request);
const team = await getTeamByUrl({
userId: session.user.id,
teamUrl: params.teamUrl,
});
if (!team || !canExecuteTeamAction('MANAGE_TEAM', team.currentTeamMember.role)) {
throw new Response(null, { status: 401 }); // Unauthorized.
throw redirect(`/t/${params.teamUrl}`);
}
}
export async function clientLoader() {
// Do nothing, we only want the loader to run on SSR.
}
export default function TeamsSettingsLayout() {
return (
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">

View File

@ -2,11 +2,12 @@ import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Plural, Trans } from '@lingui/react/macro';
import { DateTime } from 'luxon';
import { getLoaderTeamSession } from 'server/utils/get-loader-session';
import type Stripe from 'stripe';
import { match } from 'ts-pattern';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { stripe } from '@documenso/lib/server-only/stripe';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -16,8 +17,13 @@ import { TeamSettingsBillingInvoicesTable } from '~/components/tables/team-setti
import type { Route } from './+types/billing';
export async function loader() {
const { currentTeam: team } = getLoaderTeamSession();
export async function loader({ request, params }: Route.LoaderArgs) {
const session = await getSession(request);
const team = await getTeamByUrl({
userId: session.user.id,
teamUrl: params.teamUrl,
});
let teamSubscription: Stripe.Subscription | null = null;

View File

@ -1,16 +1,30 @@
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { TeamBrandingPreferencesForm } from '~/components/forms/team-branding-preferences-form';
import { TeamDocumentPreferencesForm } from '~/components/forms/team-document-preferences-form';
import { SettingsHeader } from '~/components/general/settings-header';
import { useCurrentTeam } from '~/providers/team';
export default function TeamsSettingsPage() {
import type { Route } from './+types/preferences';
export async function loader({ request, params }: Route.LoaderArgs) {
const { user } = await getSession(request);
const team = await getTeamByUrl({ userId: user.id, teamUrl: params.teamUrl });
return {
team,
};
}
export default function TeamsSettingsPage({ loaderData }: Route.ComponentProps) {
const { team } = loaderData;
const { _ } = useLingui();
const team = useCurrentTeam();
return (
<div>
<SettingsHeader

View File

@ -1,14 +1,22 @@
import { getLoaderTeamSession } from 'server/utils/get-loader-session';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { getTeamPublicProfile } from '@documenso/lib/server-only/team/get-team-public-profile';
import PublicProfilePage from '~/routes/_authenticated+/settings+/public-profile+/index';
export async function loader() {
const { user, currentTeam: team } = getLoaderTeamSession();
import type { Route } from './+types/public-profile';
// Todo: This can be optimized.
export async function loader({ request, params }: Route.LoaderArgs) {
const session = await getSession(request);
const team = await getTeamByUrl({
userId: session.user.id,
teamUrl: params.teamUrl,
});
const { profile } = await getTeamPublicProfile({
userId: user.id,
userId: session.user.id,
teamId: team.id,
});

View File

@ -1,10 +1,11 @@
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { DateTime } from 'luxon';
import { getLoaderTeamSession } from 'server/utils/get-loader-session';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { getTeamTokens } from '@documenso/lib/server-only/public-api/get-all-team-tokens';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { Button } from '@documenso/ui/primitives/button';
import TokenDeleteDialog from '~/components/dialogs/token-delete-dialog';
@ -12,8 +13,14 @@ import { ApiTokenForm } from '~/components/forms/token';
import type { Route } from './+types/tokens';
export async function loader() {
const { user, currentTeam: team } = getLoaderTeamSession();
// Todo: This can be optimized.
export async function loader({ request, params }: Route.LoaderArgs) {
const { user } = await getSession(request);
const team = await getTeamByUrl({
userId: user.id,
teamUrl: params.teamUrl,
});
const tokens = await getTeamTokens({ userId: user.id, teamId: team.id }).catch(() => null);

View File

@ -2,8 +2,9 @@ import { Trans } from '@lingui/react/macro';
import { DocumentSigningOrder, SigningStatus } from '@prisma/client';
import { ChevronLeft, LucideEdit } from 'lucide-react';
import { Link, redirect, useNavigate } from 'react-router';
import { getLoaderSession } from 'server/utils/get-loader-session';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
import { Button } from '@documenso/ui/primitives/button';
@ -25,8 +26,14 @@ import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
import type { Route } from './+types/$id._index';
export async function loader({ params }: Route.LoaderArgs) {
const { user, currentTeam: team } = getLoaderSession();
export async function loader({ params, request }: Route.LoaderArgs) {
const { user } = await getSession(request);
let team: TGetTeamByUrlResponse | null = null;
if (params.teamUrl) {
team = await getTeamByUrl({ userId: user.id, teamUrl: params.teamUrl });
}
const { id } = params;

View File

@ -1,9 +1,10 @@
import { Trans } from '@lingui/react/macro';
import { ChevronLeft } from 'lucide-react';
import { Link, redirect } from 'react-router';
import { getLoaderSession } from 'server/utils/get-loader-session';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { formatTemplatesPath } from '@documenso/lib/utils/teams';
@ -15,8 +16,14 @@ import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
import { TemplateDirectLinkDialogWrapper } from '../../../components/dialogs/template-direct-link-dialog-wrapper';
import type { Route } from './+types/$id.edit';
export async function loader({ params }: Route.LoaderArgs) {
const { user, currentTeam: team } = getLoaderSession();
export async function loader({ params, request }: Route.LoaderArgs) {
const { user } = await getSession(request);
let team: TGetTeamByUrlResponse | null = null;
if (params.teamUrl) {
team = await getTeamByUrl({ userId: user.id, teamUrl: params.teamUrl });
}
const { id } = params;

View File

@ -1,3 +1,5 @@
import { useEffect } from 'react';
import { Trans } from '@lingui/react/macro';
import { Bird } from 'lucide-react';
import { useSearchParams } from 'react-router';
@ -27,11 +29,16 @@ export default function TemplatesPage() {
const documentRootPath = formatDocumentsPath(team?.url);
const templateRootPath = formatTemplatesPath(team?.url);
const { data, isLoading, isLoadingError } = trpc.template.findTemplates.useQuery({
const { data, isLoading, isLoadingError, refetch } = trpc.template.findTemplates.useQuery({
page: page,
perPage: perPage,
});
// Refetch the templates when the team URL changes.
useEffect(() => {
void refetch();
}, [team?.url]);
return (
<div className="mx-auto max-w-screen-xl px-4 md:px-8">
<div className="flex items-baseline justify-between">

View File

@ -1,10 +1,13 @@
import { redirect } from 'react-router';
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
export function loader() {
const session = getOptionalLoaderSession();
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
if (session) {
import type { Route } from './+types/_index';
export async function loader({ request }: Route.LoaderArgs) {
const { isAuthenticated } = await getOptionalSession(request);
if (isAuthenticated) {
throw redirect('/documents');
}

View File

@ -59,7 +59,8 @@ export default function PublicProfilePage({ loaderData }: Route.ComponentProps)
const { profile, templates } = publicProfile;
const { user } = useOptionalSession();
const { sessionData } = useOptionalSession();
const user = sessionData?.user;
return (
<div className="flex flex-col items-center justify-center py-4 sm:py-32">

View File

@ -1,35 +1,26 @@
import { Trans } from '@lingui/react/macro';
import { ChevronLeft } from 'lucide-react';
import { Link, Outlet } from 'react-router';
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { Button } from '@documenso/ui/primitives/button';
import { Header as AuthenticatedHeader } from '~/components/general/app-header';
import type { Route } from './+types/_layout';
export function loader() {
const session = getOptionalLoaderSession();
return {
user: session?.user,
teams: session?.teams || [],
};
}
/**
* A layout to handle scenarios where the user is a recipient of a given resource
* where we do not care whether they are authenticated or not.
*
* Such as direct template access, or signing.
*/
export default function RecipientLayout({ loaderData }: Route.ComponentProps) {
const { user, teams } = loaderData;
export default function RecipientLayout() {
const { sessionData } = useOptionalSession();
return (
<div className="min-h-screen">
{user && <AuthenticatedHeader user={user} teams={teams} />}
{sessionData?.user && (
<AuthenticatedHeader user={sessionData.user} teams={sessionData.teams} />
)}
<main className="mb-8 mt-8 px-4 md:mb-12 md:mt-12 md:px-8">
<Outlet />
@ -38,6 +29,7 @@ export default function RecipientLayout({ loaderData }: Route.ComponentProps) {
);
}
// Todo: Use generic error boundary.
export function ErrorBoundary() {
return (
<div className="mx-auto flex min-h-[80vh] w-full items-center justify-center py-32">

View File

@ -1,9 +1,9 @@
import { Plural } from '@lingui/react/macro';
import { UsersIcon } from 'lucide-react';
import { redirect } from 'react-router';
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
import { match } from 'ts-pattern';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { getTemplateByDirectLinkToken } from '@documenso/lib/server-only/template/get-template-by-direct-link-token';
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
@ -17,8 +17,8 @@ import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
import type { Route } from './+types/_index';
export async function loader({ params }: Route.LoaderArgs) {
const session = getOptionalLoaderSession();
export async function loader({ params, request }: Route.LoaderArgs) {
const session = await getOptionalSession(request);
const { token } = params;
@ -48,7 +48,7 @@ export async function loader({ params }: Route.LoaderArgs) {
// Ensure typesafety when we add more options.
const isAccessAuthValid = match(derivedRecipientAccessAuth)
.with(DocumentAccessAuth.ACCOUNT, () => Boolean(session?.user))
.with(DocumentAccessAuth.ACCOUNT, () => Boolean(session.user))
.with(null, () => true)
.exhaustive();
@ -66,7 +66,8 @@ export async function loader({ params }: Route.LoaderArgs) {
}
export default function DirectTemplatePage() {
const { user } = useOptionalSession();
const { sessionData } = useOptionalSession();
const user = sessionData?.user;
const data = useSuperLoaderData<typeof loader>();

View File

@ -5,6 +5,7 @@ import { Link, redirect } from 'react-router';
import { getOptionalLoaderContext } from 'server/utils/get-loader-session';
import signingCelebration from '@documenso/assets/images/signing-celebration.png';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized';
@ -27,8 +28,10 @@ import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
import type { Route } from './+types/_index';
export async function loader({ params }: Route.LoaderArgs) {
const { session, requestMetadata } = getOptionalLoaderContext();
export async function loader({ params, request }: Route.LoaderArgs) {
const { requestMetadata } = getOptionalLoaderContext();
const { user } = await getOptionalSession(request);
const { token } = params;
@ -36,8 +39,6 @@ export async function loader({ params }: Route.LoaderArgs) {
throw new Response('Not Found', { status: 404 });
}
const user = session?.user;
const [document, recipient, fields, completedFields] = await Promise.all([
getDocumentAndSenderByToken({
token,
@ -136,7 +137,8 @@ export async function loader({ params }: Route.LoaderArgs) {
export default function SigningPage() {
const data = useSuperLoaderData<typeof loader>();
const { user } = useOptionalSession();
const { sessionData } = useOptionalSession();
const user = sessionData?.user;
if (!data.isDocumentAccessValid) {
return (

View File

@ -6,10 +6,10 @@ import { Trans } from '@lingui/react/macro';
import { type Document, DocumentStatus, FieldType, RecipientRole } from '@prisma/client';
import { CheckCircle2, Clock8, FileSearch } from 'lucide-react';
import { Link, useRevalidator } from 'react-router';
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
import { match } from 'ts-pattern';
import signingCelebration from '@documenso/assets/images/signing-celebration.png';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized';
@ -31,8 +31,8 @@ import { DocumentSigningAuthPageView } from '~/components/general/document-signi
import type { Route } from './+types/complete';
export async function loader({ params }: Route.LoaderArgs) {
const session = getOptionalLoaderSession();
export async function loader({ params, request }: Route.LoaderArgs) {
const { user } = await getOptionalSession(request);
const { token } = params;
@ -40,8 +40,6 @@ export async function loader({ params }: Route.LoaderArgs) {
throw new Response('Not Found', { status: 404 });
}
const user = session?.user;
const document = await getDocumentAndSenderByToken({
token,
requireAccessAuth: false,
@ -100,7 +98,8 @@ export async function loader({ params }: Route.LoaderArgs) {
export default function CompletedSigningPage({ loaderData }: Route.ComponentProps) {
const { _ } = useLingui();
const { user } = useOptionalSession();
const { sessionData } = useOptionalSession();
const user = sessionData?.user;
const {
isDocumentAccessValid,

View File

@ -2,8 +2,8 @@ import { Trans } from '@lingui/react/macro';
import { FieldType } from '@prisma/client';
import { XCircle } from 'lucide-react';
import { Link } from 'react-router';
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized';
@ -17,8 +17,8 @@ import { truncateTitle } from '~/utils/truncate-title';
import type { Route } from './+types/rejected';
export async function loader({ params }: Route.LoaderArgs) {
const session = getOptionalLoaderSession();
export async function loader({ params, request }: Route.LoaderArgs) {
const { user } = await getOptionalSession(request);
const { token } = params;
@ -26,8 +26,6 @@ export async function loader({ params }: Route.LoaderArgs) {
throw new Response('Not Found', { status: 404 });
}
const user = session?.user;
const document = await getDocumentAndSenderByToken({
token,
requireAccessAuth: false,
@ -76,7 +74,8 @@ export async function loader({ params }: Route.LoaderArgs) {
}
export default function RejectedSigningPage({ loaderData }: Route.ComponentProps) {
const { user } = useOptionalSession();
const { sessionData } = useOptionalSession();
const user = sessionData?.user;
const { isDocumentAccessValid, recipientReference, truncatedTitle } = loaderData;

View File

@ -2,8 +2,8 @@ import { Trans } from '@lingui/react/macro';
import type { Team } from '@prisma/client';
import { DocumentStatus } from '@prisma/client';
import { Link, redirect } from 'react-router';
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
@ -13,8 +13,8 @@ import { Button } from '@documenso/ui/primitives/button';
import type { Route } from './+types/waiting';
export async function loader({ params }: Route.LoaderArgs) {
const session = getOptionalLoaderSession();
export async function loader({ params, request }: Route.LoaderArgs) {
const { user } = await getOptionalSession(request);
const { token } = params;
@ -37,7 +37,6 @@ export async function loader({ params }: Route.LoaderArgs) {
let isOwnerOrTeamMember = false;
const user = session?.user;
let team: Team | null = null;
if (user) {

View File

@ -1,7 +1,7 @@
import { Trans } from '@lingui/react/macro';
import { Link, redirect } from 'react-router';
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import {
IS_GOOGLE_SSO_ENABLED,
IS_OIDC_SSO_ENABLED,
@ -18,15 +18,15 @@ export function meta() {
return appMetaTags('Sign In');
}
export function loader() {
const session = getOptionalLoaderSession();
export async function loader({ request }: Route.LoaderArgs) {
const { isAuthenticated } = await getOptionalSession(request);
// SSR env variables.
const isGoogleSSOEnabled = IS_GOOGLE_SSO_ENABLED;
const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED;
const oidcProviderLabel = OIDC_PROVIDER_LABEL;
if (session) {
if (isAuthenticated) {
throw redirect('/documents');
}

View File

@ -2,8 +2,8 @@ import { Trans } from '@lingui/react/macro';
import { TeamMemberInviteStatus } from '@prisma/client';
import { DateTime } from 'luxon';
import { Link } from 'react-router';
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
import { declineTeamInvitation } from '@documenso/lib/server-only/team/decline-team-invitation';
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
@ -12,8 +12,8 @@ import { Button } from '@documenso/ui/primitives/button';
import type { Route } from './+types/team.decline.$token';
export async function loader({ params }: Route.LoaderArgs) {
const session = getOptionalLoaderSession();
export async function loader({ params, request }: Route.LoaderArgs) {
const session = await getOptionalSession(request);
const { token } = params;
@ -74,7 +74,7 @@ export async function loader({ params }: Route.LoaderArgs) {
} as const;
}
const isSessionUserTheInvitedUser = user.id === session?.user.id;
const isSessionUserTheInvitedUser = user.id === session?.user?.id;
return {
state: 'Success',

View File

@ -2,8 +2,8 @@ import { Trans } from '@lingui/react/macro';
import { TeamMemberInviteStatus } from '@prisma/client';
import { DateTime } from 'luxon';
import { Link } from 'react-router';
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
import { acceptTeamInvitation } from '@documenso/lib/server-only/team/accept-team-invitation';
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
@ -12,8 +12,8 @@ import { Button } from '@documenso/ui/primitives/button';
import type { Route } from './+types/team.invite.$token';
export async function loader({ params }: Route.LoaderArgs) {
const session = getOptionalLoaderSession();
export async function loader({ params, request }: Route.LoaderArgs) {
const session = await getOptionalSession(request);
const { token } = params;
@ -77,7 +77,7 @@ export async function loader({ params }: Route.LoaderArgs) {
} as const;
}
const isSessionUserTheInvitedUser = user.id === session?.user.id;
const isSessionUserTheInvitedUser = user.id === session.user?.id;
return {
state: 'Success',

View File

@ -1,7 +1,7 @@
import { data } from 'react-router';
import { getLoaderSession } from 'server/utils/get-loader-session';
import { match } from 'ts-pattern';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
@ -18,7 +18,7 @@ import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
import type { Route } from './+types/direct.$url';
export async function loader({ params }: Route.LoaderArgs) {
export async function loader({ params, request }: Route.LoaderArgs) {
if (!params.url) {
throw new Response('Not found', { status: 404 });
}
@ -49,7 +49,7 @@ export async function loader({ params }: Route.LoaderArgs) {
);
}
const { user } = getLoaderSession();
const { user } = await getSession(request);
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
documentAuth: template.authOptions,

View File

@ -1,8 +1,8 @@
import { DocumentStatus, RecipientRole } from '@prisma/client';
import { data } from 'react-router';
import { getLoaderSession } from 'server/utils/get-loader-session';
import { match } from 'ts-pattern';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
@ -22,14 +22,14 @@ import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
import type { Route } from './+types/sign.$url';
export async function loader({ params }: Route.LoaderArgs) {
export async function loader({ params, request }: Route.LoaderArgs) {
if (!params.url) {
throw new Response('Not found', { status: 404 });
}
const token = params.url;
const { user } = getLoaderSession();
const { user } = await getSession(request);
const [document, fields, recipient] = await Promise.all([
getDocumentAndSenderByToken({