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

@@ -56,7 +56,8 @@ export const DirectTemplateConfigureForm = ({
}: DirectTemplateConfigureFormProps) => { }: DirectTemplateConfigureFormProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { user } = useOptionalSession(); const { sessionData } = useOptionalSession();
const user = sessionData?.user;
const { recipients } = template; const { recipients } = template;
const { derivedRecipientAccessAuth } = useRequiredDocumentSigningAuthContext(); const { derivedRecipientAccessAuth } = useRequiredDocumentSigningAuthContext();

View File

@@ -48,7 +48,8 @@ export const DocumentSigningForm = ({
allRecipients = [], allRecipients = [],
setSelectedSignerId, setSelectedSignerId,
}: DocumentSigningFormProps) => { }: DocumentSigningFormProps) => {
const { user } = useOptionalSession(); const { sessionData } = useOptionalSession();
const user = sessionData?.user;
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();

View File

@@ -139,10 +139,7 @@ export const DocumentPageViewDropdown = ({ document }: DocumentPageViewDropdownP
<Trans>Duplicate</Trans> <Trans>Duplicate</Trans>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem onClick={() => setDeleteDialogOpen(true)} disabled={isDeleted}>
onClick={() => setDeleteDialogOpen(true)}
disabled={Boolean(!canManageDocument && team?.teamEmail) || isDeleted}
>
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" />
<Trans>Delete</Trans> <Trans>Delete</Trans>
</DropdownMenuItem> </DropdownMenuItem>

View File

@@ -8,7 +8,6 @@ import { Link, useNavigate } from 'react-router';
import backgroundPattern from '@documenso/assets/images/background-pattern.png'; import backgroundPattern from '@documenso/assets/images/background-pattern.png';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { useOptionalCurrentTeam } from '~/providers/team'; import { useOptionalCurrentTeam } from '~/providers/team';
@@ -66,7 +65,7 @@ export const GenericErrorLayout = ({
errorCodeMap[errorCode || 404] ?? defaultErrorCodeMap[500]; errorCodeMap[errorCode || 404] ?? defaultErrorCodeMap[500];
return ( return (
<div className={cn('relative max-w-[100vw] overflow-hidden')}> <div className="fixed inset-0 z-0 flex h-screen w-screen items-center justify-center">
<div className="absolute -inset-24 -z-10"> <div className="absolute -inset-24 -z-10">
<motion.div <motion.div
className="flex h-full w-full items-center justify-center" className="flex h-full w-full items-center justify-center"
@@ -85,7 +84,7 @@ export const GenericErrorLayout = ({
</motion.div> </motion.div>
</div> </div>
<div className="container mx-auto flex h-full min-h-screen items-center justify-center px-6 py-32"> <div className="inset-0 mx-auto flex h-full flex-grow items-center justify-center px-6 py-32">
<div> <div>
<p className="text-muted-foreground font-semibold">{_(subHeading)}</p> <p className="text-muted-foreground font-semibold">{_(subHeading)}</p>

View File

@@ -262,7 +262,7 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
{selectedTeam && {selectedTeam &&
canExecuteTeamAction('MANAGE_TEAM', selectedTeam.currentTeamMember.role) && ( canExecuteTeamAction('MANAGE_TEAM', selectedTeam.currentTeamMember.role) && (
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild> <DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
<Link to={`/t/${selectedTeam.url}/settings/`}> <Link to={`/t/${selectedTeam.url}/settings`}>
<Trans>Team settings</Trans> <Trans>Team settings</Trans>
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>

View File

@@ -0,0 +1,18 @@
import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
type PortalComponentProps = {
children: React.ReactNode;
target: string;
};
export const PortalComponent = ({ children, target }: PortalComponentProps) => {
const [portalRoot, setPortalRoot] = useState<HTMLElement | null>(null);
useEffect(() => {
setPortalRoot(document.getElementById(target));
}, [target]);
return portalRoot ? createPortal(children, portalRoot) : null;
};

View File

@@ -4,7 +4,7 @@ import { Link } from 'react-router';
import { Skeleton } from '@documenso/ui/primitives/skeleton'; import { Skeleton } from '@documenso/ui/primitives/skeleton';
export default function Loading() { export default function DocumentEditSkeleton() {
return ( return (
<div className="mx-auto -mt-4 flex w-full max-w-screen-xl flex-col px-4 md:px-8"> <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"> <Link to="/documents" className="flex grow-0 items-center text-[#7AC455] hover:opacity-80">

View File

@@ -4,7 +4,7 @@ import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import type { TeamMemberRole } from '@prisma/client'; import type { TeamMemberRole } from '@prisma/client';
import { type Subscription, SubscriptionStatus } from '@prisma/client'; import { SubscriptionStatus } from '@prisma/client';
import { AlertTriangle } from 'lucide-react'; import { AlertTriangle } from 'lucide-react';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
@@ -22,13 +22,13 @@ import {
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type TeamLayoutBillingBannerProps = { export type TeamLayoutBillingBannerProps = {
subscription: Subscription; subscriptionStatus: SubscriptionStatus;
teamId: number; teamId: number;
userRole: TeamMemberRole; userRole: TeamMemberRole;
}; };
export const TeamLayoutBillingBanner = ({ export const TeamLayoutBillingBanner = ({
subscription, subscriptionStatus,
teamId, teamId,
userRole, userRole,
}: TeamLayoutBillingBannerProps) => { }: TeamLayoutBillingBannerProps) => {
@@ -59,7 +59,7 @@ export const TeamLayoutBillingBanner = ({
} }
}; };
if (subscription.status === SubscriptionStatus.ACTIVE) { if (subscriptionStatus === SubscriptionStatus.ACTIVE) {
return null; return null;
} }
@@ -68,16 +68,16 @@ export const TeamLayoutBillingBanner = ({
<div <div
className={cn({ className={cn({
'bg-yellow-200 text-yellow-900 dark:bg-yellow-400': 'bg-yellow-200 text-yellow-900 dark:bg-yellow-400':
subscription.status === SubscriptionStatus.PAST_DUE, subscriptionStatus === SubscriptionStatus.PAST_DUE,
'bg-destructive text-destructive-foreground': 'bg-destructive text-destructive-foreground':
subscription.status === SubscriptionStatus.INACTIVE, subscriptionStatus === SubscriptionStatus.INACTIVE,
})} })}
> >
<div className="mx-auto flex max-w-screen-xl items-center justify-center gap-x-4 px-4 py-2 text-sm font-medium"> <div className="mx-auto flex max-w-screen-xl items-center justify-center gap-x-4 px-4 py-2 text-sm font-medium">
<div className="flex items-center"> <div className="flex items-center">
<AlertTriangle className="mr-2.5 h-5 w-5" /> <AlertTriangle className="mr-2.5 h-5 w-5" />
{match(subscription.status) {match(subscriptionStatus)
.with(SubscriptionStatus.PAST_DUE, () => <Trans>Payment overdue</Trans>) .with(SubscriptionStatus.PAST_DUE, () => <Trans>Payment overdue</Trans>)
.with(SubscriptionStatus.INACTIVE, () => <Trans>Teams restricted</Trans>) .with(SubscriptionStatus.INACTIVE, () => <Trans>Teams restricted</Trans>)
.exhaustive()} .exhaustive()}
@@ -87,9 +87,9 @@ export const TeamLayoutBillingBanner = ({
variant="ghost" variant="ghost"
className={cn({ className={cn({
'text-yellow-900 hover:bg-yellow-100 hover:text-yellow-900 dark:hover:bg-yellow-500': 'text-yellow-900 hover:bg-yellow-100 hover:text-yellow-900 dark:hover:bg-yellow-500':
subscription.status === SubscriptionStatus.PAST_DUE, subscriptionStatus === SubscriptionStatus.PAST_DUE,
'text-destructive-foreground hover:bg-destructive-foreground hover:text-white': 'text-destructive-foreground hover:bg-destructive-foreground hover:text-white':
subscription.status === SubscriptionStatus.INACTIVE, subscriptionStatus === SubscriptionStatus.INACTIVE,
})} })}
disabled={isPending} disabled={isPending}
onClick={() => setIsOpen(true)} onClick={() => setIsOpen(true)}
@@ -106,7 +106,7 @@ export const TeamLayoutBillingBanner = ({
<Trans>Payment overdue</Trans> <Trans>Payment overdue</Trans>
</DialogTitle> </DialogTitle>
{match(subscription.status) {match(subscriptionStatus)
.with(SubscriptionStatus.PAST_DUE, () => ( .with(SubscriptionStatus.PAST_DUE, () => (
<DialogDescription> <DialogDescription>
<Trans> <Trans>

View File

@@ -170,10 +170,7 @@ export const DocumentsTableActionDropdown = ({ row }: DocumentsTableActionDropdo
Void Void
</DropdownMenuItem> */} </DropdownMenuItem> */}
<DropdownMenuItem <DropdownMenuItem onClick={() => setDeleteDialogOpen(true)}>
onClick={() => setDeleteDialogOpen(true)}
disabled={Boolean(!canManageDocument && team?.teamEmail)}
>
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" />
{canManageDocument ? _(msg`Delete`) : _(msg`Hide`)} {canManageDocument ? _(msg`Delete`) : _(msg`Hide`)}
</DropdownMenuItem> </DropdownMenuItem>

View File

@@ -3,7 +3,6 @@ import { useEffect } from 'react';
import posthog from 'posthog-js'; import posthog from 'posthog-js';
import { useLocation, useSearchParams } from 'react-router'; import { useLocation, useSearchParams } from 'react-router';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { extractPostHogConfig } from '@documenso/lib/constants/feature-flags'; import { extractPostHogConfig } from '@documenso/lib/constants/feature-flags';
export function PostHogPageview() { export function PostHogPageview() {
@@ -12,19 +11,20 @@ export function PostHogPageview() {
const { pathname } = useLocation(); const { pathname } = useLocation();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const { user } = useOptionalSession(); // const { sessionData } = useOptionalSession();
// const user = sessionData?.user;
if (typeof window !== 'undefined' && postHogConfig) { if (typeof window !== 'undefined' && postHogConfig) {
posthog.init(postHogConfig.key, { posthog.init(postHogConfig.key, {
api_host: postHogConfig.host, api_host: postHogConfig.host,
disable_session_recording: true, disable_session_recording: true,
loaded: () => { // loaded: () => {
if (user) { // if (user) {
posthog.identify(user.email ?? user.id.toString()); // posthog.identify(user.email ?? user.id.toString());
} else { // } else {
posthog.reset(); // posthog.reset();
} // }
}, // },
custom_campaign_params: ['src'], custom_campaign_params: ['src'],
}); });
} }

View File

@@ -1,14 +1,16 @@
import { createContext, useContext } from 'react'; import { createContext, useContext } from 'react';
import React from 'react'; import React from 'react';
import type { TGetTeamByUrlResponse } from '@documenso/lib/server-only/team/get-team'; import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
type TeamProviderValue = TGetTeamsResponse[0];
interface TeamProviderProps { interface TeamProviderProps {
children: React.ReactNode; children: React.ReactNode;
team: TGetTeamByUrlResponse; team: TeamProviderValue;
} }
const TeamContext = createContext<TGetTeamByUrlResponse | null>(null); const TeamContext = createContext<TeamProviderValue | null>(null);
export const useCurrentTeam = () => { export const useCurrentTeam = () => {
const context = useContext(TeamContext); const context = useContext(TeamContext);

View File

@@ -13,10 +13,11 @@ import {
useLocation, useLocation,
} from 'react-router'; } from 'react-router';
import { PreventFlashOnWrongTheme, ThemeProvider, useTheme } from 'remix-themes'; import { PreventFlashOnWrongTheme, ThemeProvider, useTheme } from 'remix-themes';
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { SessionProvider } from '@documenso/lib/client-only/providers/session'; import { SessionProvider } from '@documenso/lib/client-only/providers/session';
import { APP_I18N_OPTIONS, type SupportedLanguageCodes } from '@documenso/lib/constants/i18n'; import { APP_I18N_OPTIONS, type SupportedLanguageCodes } from '@documenso/lib/constants/i18n';
import { type TGetTeamsResponse, getTeams } from '@documenso/lib/server-only/team/get-teams';
import { createPublicEnv, env } from '@documenso/lib/utils/env'; import { createPublicEnv, env } from '@documenso/lib/utils/env';
import { extractLocaleData } from '@documenso/lib/utils/i18n'; import { extractLocaleData } from '@documenso/lib/utils/i18n';
import { TrpcProvider } from '@documenso/trpc/react'; import { TrpcProvider } from '@documenso/trpc/react';
@@ -59,8 +60,21 @@ export function meta() {
return appMetaTags(); return appMetaTags();
} }
/**
* Don't revalidate (run the loader on sequential navigations) on the root layout
*
* Update values via providers.
*/
export const shouldRevalidate = () => false;
export async function loader({ request }: Route.LoaderArgs) { export async function loader({ request }: Route.LoaderArgs) {
const session = getOptionalLoaderSession(); const session = await getOptionalSession(request);
let teams: TGetTeamsResponse = [];
if (session.isAuthenticated) {
teams = await getTeams({ userId: session.user.id });
}
const { getTheme } = await themeSessionResolver(request); const { getTheme } = await themeSessionResolver(request);
@@ -74,7 +88,13 @@ export async function loader({ request }: Route.LoaderArgs) {
{ {
lang, lang,
theme: getTheme(), theme: getTheme(),
session, session: session.isAuthenticated
? {
user: session.user,
session: session.session,
teams,
}
: null,
publicEnv: createPublicEnv(), publicEnv: createPublicEnv(),
}, },
{ {
@@ -113,7 +133,7 @@ export function App() {
<script>0</script> <script>0</script>
</head> </head>
<body> <body>
<SessionProvider session={session}> <SessionProvider initialSession={session}>
<TooltipProvider> <TooltipProvider>
<TrpcProvider> <TrpcProvider>
<Outlet /> <Outlet />

View File

@@ -1,56 +1,57 @@
import { SubscriptionStatus } from '@prisma/client'; import { Outlet, redirect } from 'react-router';
import { Outlet } from 'react-router';
import { getLoaderSession } from 'server/utils/get-loader-session';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { getLimits } from '@documenso/ee/server-only/limits/client'; import { getLimits } from '@documenso/ee/server-only/limits/client';
import { LimitsProvider } from '@documenso/ee/server-only/limits/provider/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 { 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 { SITE_SETTINGS_BANNER_ID } from '@documenso/lib/server-only/site-settings/schemas/banner';
import { AppBanner } from '~/components/general/app-banner'; import { AppBanner } from '~/components/general/app-banner';
import { Header } from '~/components/general/app-header'; 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 { VerifyEmailBanner } from '~/components/general/verify-email-banner';
import type { Route } from './+types/_layout'; 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()); 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([ const [limits, banner] = await Promise.all([
getLimits({ headers: requestHeaders, teamId: currentTeam?.id }), getLimits({ headers: requestHeaders }),
getSiteSettings().then((settings) => getSiteSettings().then((settings) =>
settings.find((setting) => setting.id === SITE_SETTINGS_BANNER_ID), settings.find((setting) => setting.id === SITE_SETTINGS_BANNER_ID),
), ),
]); ]);
return { return {
user,
teams,
banner, banner,
limits, limits,
currentTeam,
}; };
}; };
export default function Layout({ loaderData }: Route.ComponentProps) { export default function Layout({ loaderData }: Route.ComponentProps) {
const { user, teams, banner, limits, currentTeam } = loaderData; const { user, teams } = useSession();
const { banner, limits } = loaderData;
return ( return (
<LimitsProvider initialValue={limits} teamId={currentTeam?.id}> <LimitsProvider initialValue={limits}>
{!user.emailVerified && <VerifyEmailBanner email={user.email} />} <div id="portal-header"></div>
{currentTeam?.subscription && {!user.emailVerified && <VerifyEmailBanner email={user.email} />}
currentTeam.subscription.status !== SubscriptionStatus.ACTIVE && (
<TeamLayoutBillingBanner
subscription={currentTeam.subscription}
teamId={currentTeam.id}
userRole={currentTeam.currentTeamMember.role}
/>
)}
{banner && <AppBanner banner={banner} />} {banner && <AppBanner banner={banner} />}

View File

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

View File

@@ -3,13 +3,14 @@ import { Plural, Trans } from '@lingui/react/macro';
import { DocumentStatus, TeamMemberRole } from '@prisma/client'; import { DocumentStatus, TeamMemberRole } from '@prisma/client';
import { ChevronLeft, Clock9, Users2 } from 'lucide-react'; import { ChevronLeft, Clock9, Users2 } from 'lucide-react';
import { Link, redirect } from 'react-router'; import { Link, redirect } from 'react-router';
import { getLoaderSession } from 'server/utils/get-loader-session';
import { match } from 'ts-pattern'; 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 { useSession } from '@documenso/lib/client-only/providers/session';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; 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 { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-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 { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { Badge } from '@documenso/ui/primitives/badge'; 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'; import type { Route } from './+types/$id._index';
export async function loader({ params }: Route.LoaderArgs) { export async function loader({ params, request }: Route.LoaderArgs) {
const { user, currentTeam: team } = getLoaderSession(); 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; 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 { DocumentStatus as InternalDocumentStatus, TeamMemberRole } from '@prisma/client';
import { ChevronLeft, Users2 } from 'lucide-react'; import { ChevronLeft, Users2 } from 'lucide-react';
import { Link, redirect } from 'react-router'; import { Link, redirect } from 'react-router';
import { getLoaderSession } from 'server/utils/get-loader-session';
import { match } from 'ts-pattern'; 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 { 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 { 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 { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; 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'; import type { Route } from './+types/$id.edit';
export async function loader({ params }: Route.LoaderArgs) { export async function loader({ params, request }: Route.LoaderArgs) {
const { user, currentTeam: team } = getLoaderSession(); 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; const { id } = params;

View File

@@ -6,10 +6,11 @@ import type { Recipient } from '@prisma/client';
import { ChevronLeft } from 'lucide-react'; import { ChevronLeft } from 'lucide-react';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { Link, redirect } from 'react-router'; 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 { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-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 { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { Card } from '@documenso/ui/primitives/card'; 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'; import type { Route } from './+types/$id.logs';
export async function loader({ params }: Route.LoaderArgs) { export async function loader({ params, request }: Route.LoaderArgs) {
const { id } = params; 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); const documentId = Number(id);

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,147 +1,104 @@
import type { MessageDescriptor } from '@lingui/core'; import { useMemo } from 'react';
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 { 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 { TrpcProvider } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; 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 { TeamProvider } from '~/providers/team';
import type { Route } from './+types/_layout'; import type { Route } from './+types/_layout';
export const loader = () => { export default function Layout({ params }: Route.ComponentProps) {
const { currentTeam } = getLoaderSession(); 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) { 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 = { const trpcHeaders = {
'x-team-Id': currentTeam.id.toString(), 'x-team-Id': currentTeam.id.toString(),
}; };
return {
currentTeam,
trpcHeaders,
};
};
export default function Layout({ loaderData }: Route.ComponentProps) {
const { currentTeam, trpcHeaders } = loaderData;
return ( return (
<TeamProvider team={currentTeam}> <TeamProvider team={currentTeam}>
<TrpcProvider headers={trpcHeaders}> <LimitsProvider initialValue={limits} teamId={currentTeam.id}>
<main className="mt-8 pb-8 md:mt-12 md:pb-12"> <TrpcProvider headers={trpcHeaders}>
<Outlet /> {currentTeam?.subscription &&
</main> currentTeam.subscription.status !== SubscriptionStatus.ACTIVE && (
</TrpcProvider> <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> </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 { CheckCircle2, Clock } from 'lucide-react';
import { P, match } from 'ts-pattern'; 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 { 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 { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter'; import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import { isTokenExpired } from '@documenso/lib/utils/token-verification'; 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 { SettingsHeader } from '~/components/general/settings-header';
import { TeamEmailDropdown } from '~/components/general/teams/team-email-dropdown'; import { TeamEmailDropdown } from '~/components/general/teams/team-email-dropdown';
import { TeamTransferStatus } from '~/components/general/teams/team-transfer-status'; 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 { user } = useSession();
const team = useCurrentTeam();
const isTransferVerificationExpired = const isTransferVerificationExpired =
!team.transferVerification || isTokenExpired(team.transferVerification.expiresAt); !team.transferVerification || isTokenExpired(team.transferVerification.expiresAt);

View File

@@ -1,25 +1,37 @@
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { Outlet } from 'react-router'; import { Outlet, redirect } from 'react-router';
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 { canExecuteTeamAction } from '@documenso/lib/utils/teams'; import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
import { TeamSettingsNavDesktop } from '~/components/general/teams/team-settings-nav-desktop'; import { TeamSettingsNavDesktop } from '~/components/general/teams/team-settings-nav-desktop';
import { TeamSettingsNavMobile } from '~/components/general/teams/team-settings-nav-mobile'; import { TeamSettingsNavMobile } from '~/components/general/teams/team-settings-nav-mobile';
import { appMetaTags } from '~/utils/meta'; import { appMetaTags } from '~/utils/meta';
import type { Route } from './+types/_layout';
export function meta() { export function meta() {
return appMetaTags('Team Settings'); return appMetaTags('Team Settings');
} }
export function loader() { export async function loader({ request, params }: Route.LoaderArgs) {
const { currentTeam: team } = getLoaderTeamSession(); const session = await getSession(request);
const team = await getTeamByUrl({
userId: session.user.id,
teamUrl: params.teamUrl,
});
if (!team || !canExecuteTeamAction('MANAGE_TEAM', team.currentTeamMember.role)) { 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() { export default function TeamsSettingsLayout() {
return ( return (
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8"> <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 { useLingui } from '@lingui/react';
import { Plural, Trans } from '@lingui/react/macro'; import { Plural, Trans } from '@lingui/react/macro';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { getLoaderTeamSession } from 'server/utils/get-loader-session';
import type Stripe from 'stripe'; import type Stripe from 'stripe';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { stripe } from '@documenso/lib/server-only/stripe'; 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 { canExecuteTeamAction } from '@documenso/lib/utils/teams';
import { Card, CardContent } from '@documenso/ui/primitives/card'; 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'; import type { Route } from './+types/billing';
export async function loader() { export async function loader({ request, params }: Route.LoaderArgs) {
const { currentTeam: team } = getLoaderTeamSession(); const session = await getSession(request);
const team = await getTeamByUrl({
userId: session.user.id,
teamUrl: params.teamUrl,
});
let teamSubscription: Stripe.Subscription | null = null; let teamSubscription: Stripe.Subscription | null = null;

View File

@@ -1,16 +1,30 @@
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; 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 { TeamBrandingPreferencesForm } from '~/components/forms/team-branding-preferences-form';
import { TeamDocumentPreferencesForm } from '~/components/forms/team-document-preferences-form'; import { TeamDocumentPreferencesForm } from '~/components/forms/team-document-preferences-form';
import { SettingsHeader } from '~/components/general/settings-header'; 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 { _ } = useLingui();
const team = useCurrentTeam();
return ( return (
<div> <div>
<SettingsHeader <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 { getTeamPublicProfile } from '@documenso/lib/server-only/team/get-team-public-profile';
import PublicProfilePage from '~/routes/_authenticated+/settings+/public-profile+/index'; import PublicProfilePage from '~/routes/_authenticated+/settings+/public-profile+/index';
export async function loader() { import type { Route } from './+types/public-profile';
const { user, currentTeam: team } = getLoaderTeamSession();
// 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({ const { profile } = await getTeamPublicProfile({
userId: user.id, userId: session.user.id,
teamId: team.id, teamId: team.id,
}); });

View File

@@ -1,10 +1,11 @@
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { DateTime } from 'luxon'; 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 { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { getTeamTokens } from '@documenso/lib/server-only/public-api/get-all-team-tokens'; 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 { Button } from '@documenso/ui/primitives/button';
import TokenDeleteDialog from '~/components/dialogs/token-delete-dialog'; import TokenDeleteDialog from '~/components/dialogs/token-delete-dialog';
@@ -12,8 +13,14 @@ import { ApiTokenForm } from '~/components/forms/token';
import type { Route } from './+types/tokens'; import type { Route } from './+types/tokens';
export async function loader() { // Todo: This can be optimized.
const { user, currentTeam: team } = getLoaderTeamSession(); 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); 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 { DocumentSigningOrder, SigningStatus } from '@prisma/client';
import { ChevronLeft, LucideEdit } from 'lucide-react'; import { ChevronLeft, LucideEdit } from 'lucide-react';
import { Link, redirect, useNavigate } from 'react-router'; 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 { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
import { Button } from '@documenso/ui/primitives/button'; 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'; import type { Route } from './+types/$id._index';
export async function loader({ params }: Route.LoaderArgs) { export async function loader({ params, request }: Route.LoaderArgs) {
const { user, currentTeam: team } = getLoaderSession(); 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; const { id } = params;

View File

@@ -1,9 +1,10 @@
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { ChevronLeft } from 'lucide-react'; import { ChevronLeft } from 'lucide-react';
import { Link, redirect } from 'react-router'; 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 { 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 { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { formatTemplatesPath } from '@documenso/lib/utils/teams'; 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 { TemplateDirectLinkDialogWrapper } from '../../../components/dialogs/template-direct-link-dialog-wrapper';
import type { Route } from './+types/$id.edit'; import type { Route } from './+types/$id.edit';
export async function loader({ params }: Route.LoaderArgs) { export async function loader({ params, request }: Route.LoaderArgs) {
const { user, currentTeam: team } = getLoaderSession(); 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; const { id } = params;

View File

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

View File

@@ -1,10 +1,13 @@
import { redirect } from 'react-router'; import { redirect } from 'react-router';
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
export function loader() { import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
const session = getOptionalLoaderSession();
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'); throw redirect('/documents');
} }

View File

@@ -59,7 +59,8 @@ export default function PublicProfilePage({ loaderData }: Route.ComponentProps)
const { profile, templates } = publicProfile; const { profile, templates } = publicProfile;
const { user } = useOptionalSession(); const { sessionData } = useOptionalSession();
const user = sessionData?.user;
return ( return (
<div className="flex flex-col items-center justify-center py-4 sm:py-32"> <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 { Trans } from '@lingui/react/macro';
import { ChevronLeft } from 'lucide-react'; import { ChevronLeft } from 'lucide-react';
import { Link, Outlet } from 'react-router'; 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 { Button } from '@documenso/ui/primitives/button';
import { Header as AuthenticatedHeader } from '~/components/general/app-header'; 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 * 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. * where we do not care whether they are authenticated or not.
* *
* Such as direct template access, or signing. * Such as direct template access, or signing.
*/ */
export default function RecipientLayout({ loaderData }: Route.ComponentProps) { export default function RecipientLayout() {
const { user, teams } = loaderData; const { sessionData } = useOptionalSession();
return ( return (
<div className="min-h-screen"> <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"> <main className="mb-8 mt-8 px-4 md:mb-12 md:mt-12 md:px-8">
<Outlet /> <Outlet />
@@ -38,6 +29,7 @@ export default function RecipientLayout({ loaderData }: Route.ComponentProps) {
); );
} }
// Todo: Use generic error boundary.
export function ErrorBoundary() { export function ErrorBoundary() {
return ( return (
<div className="mx-auto flex min-h-[80vh] w-full items-center justify-center py-32"> <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 { Plural } from '@lingui/react/macro';
import { UsersIcon } from 'lucide-react'; import { UsersIcon } from 'lucide-react';
import { redirect } from 'react-router'; import { redirect } from 'react-router';
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
import { match } from 'ts-pattern'; 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 { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { getTemplateByDirectLinkToken } from '@documenso/lib/server-only/template/get-template-by-direct-link-token'; import { getTemplateByDirectLinkToken } from '@documenso/lib/server-only/template/get-template-by-direct-link-token';
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth'; 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'; import type { Route } from './+types/_index';
export async function loader({ params }: Route.LoaderArgs) { export async function loader({ params, request }: Route.LoaderArgs) {
const session = getOptionalLoaderSession(); const session = await getOptionalSession(request);
const { token } = params; const { token } = params;
@@ -48,7 +48,7 @@ export async function loader({ params }: Route.LoaderArgs) {
// Ensure typesafety when we add more options. // Ensure typesafety when we add more options.
const isAccessAuthValid = match(derivedRecipientAccessAuth) const isAccessAuthValid = match(derivedRecipientAccessAuth)
.with(DocumentAccessAuth.ACCOUNT, () => Boolean(session?.user)) .with(DocumentAccessAuth.ACCOUNT, () => Boolean(session.user))
.with(null, () => true) .with(null, () => true)
.exhaustive(); .exhaustive();
@@ -66,7 +66,8 @@ export async function loader({ params }: Route.LoaderArgs) {
} }
export default function DirectTemplatePage() { export default function DirectTemplatePage() {
const { user } = useOptionalSession(); const { sessionData } = useOptionalSession();
const user = sessionData?.user;
const data = useSuperLoaderData<typeof loader>(); 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 { getOptionalLoaderContext } from 'server/utils/get-loader-session';
import signingCelebration from '@documenso/assets/images/signing-celebration.png'; 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 { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token'; import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized'; 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'; import type { Route } from './+types/_index';
export async function loader({ params }: Route.LoaderArgs) { export async function loader({ params, request }: Route.LoaderArgs) {
const { session, requestMetadata } = getOptionalLoaderContext(); const { requestMetadata } = getOptionalLoaderContext();
const { user } = await getOptionalSession(request);
const { token } = params; const { token } = params;
@@ -36,8 +39,6 @@ export async function loader({ params }: Route.LoaderArgs) {
throw new Response('Not Found', { status: 404 }); throw new Response('Not Found', { status: 404 });
} }
const user = session?.user;
const [document, recipient, fields, completedFields] = await Promise.all([ const [document, recipient, fields, completedFields] = await Promise.all([
getDocumentAndSenderByToken({ getDocumentAndSenderByToken({
token, token,
@@ -136,7 +137,8 @@ export async function loader({ params }: Route.LoaderArgs) {
export default function SigningPage() { export default function SigningPage() {
const data = useSuperLoaderData<typeof loader>(); const data = useSuperLoaderData<typeof loader>();
const { user } = useOptionalSession(); const { sessionData } = useOptionalSession();
const user = sessionData?.user;
if (!data.isDocumentAccessValid) { if (!data.isDocumentAccessValid) {
return ( return (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { data } from 'react-router'; import { data } from 'react-router';
import { getLoaderSession } from 'server/utils/get-loader-session';
import { match } from 'ts-pattern'; 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 { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform'; import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app'; 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'; 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) { if (!params.url) {
throw new Response('Not found', { status: 404 }); 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({ const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
documentAuth: template.authOptions, documentAuth: template.authOptions,

View File

@@ -1,8 +1,8 @@
import { DocumentStatus, RecipientRole } from '@prisma/client'; import { DocumentStatus, RecipientRole } from '@prisma/client';
import { data } from 'react-router'; import { data } from 'react-router';
import { getLoaderSession } from 'server/utils/get-loader-session';
import { match } from 'ts-pattern'; 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 { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform'; import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app'; 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'; 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) { if (!params.url) {
throw new Response('Not found', { status: 404 }); throw new Response('Not found', { status: 404 });
} }
const token = params.url; const token = params.url;
const { user } = getLoaderSession(); const { user } = await getSession(request);
const [document, fields, recipient] = await Promise.all([ const [document, fields, recipient] = await Promise.all([
getDocumentAndSenderByToken({ getDocumentAndSenderByToken({

View File

@@ -1,79 +1,37 @@
import type { Context, Next } from 'hono'; import type { Context, Next } from 'hono';
import { extractSessionCookieFromHeaders } from '@documenso/auth/server/lib/session/session-cookies'; import { extractSessionCookieFromHeaders } from '@documenso/auth/server/lib/session/session-cookies';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import type { AppSession } from '@documenso/lib/client-only/providers/session';
import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { type TGetTeamsResponse, getTeams } from '@documenso/lib/server-only/team/get-teams';
import { import {
type RequestMetadata, type RequestMetadata,
extractRequestMetadata, extractRequestMetadata,
} from '@documenso/lib/universal/extract-request-metadata'; } from '@documenso/lib/universal/extract-request-metadata';
import { AppDebugger } from '@documenso/lib/utils/debugger';
const debug = new AppDebugger('Middleware');
export type AppContext = { export type AppContext = {
requestMetadata: RequestMetadata; requestMetadata: RequestMetadata;
session: AppSession | null;
}; };
/**
* Apply a context which can be accessed throughout the app.
*
* Keep this as lean as possible in terms of awaiting, because anything
* here will increase each page load time.
*/
export const appContext = async (c: Context, next: Next) => { export const appContext = async (c: Context, next: Next) => {
const initTime = Date.now();
const request = c.req.raw; const request = c.req.raw;
const url = new URL(request.url); const url = new URL(request.url);
const noSessionCookie = extractSessionCookieFromHeaders(request.headers) === null; const noSessionCookie = extractSessionCookieFromHeaders(request.headers) === null;
setAppContext(c, {
requestMetadata: extractRequestMetadata(request),
});
// These are non page paths like API.
if (!isPageRequest(request) || noSessionCookie || blacklistedPathsRegex.test(url.pathname)) { if (!isPageRequest(request) || noSessionCookie || blacklistedPathsRegex.test(url.pathname)) {
// debug.log('Pathname ignored', url.pathname);
setAppContext(c, {
requestMetadata: extractRequestMetadata(request),
session: null,
});
return next(); return next();
} }
const splitUrl = url.pathname.replace('.data', '').split('/'); // Add context to any pages you want here.
let team: TGetTeamByUrlResponse | null = null;
let teams: TGetTeamsResponse = [];
const session = await getOptionalSession(c);
if (session.isAuthenticated) {
let teamUrl = null;
if (splitUrl[1] === 't' && splitUrl[2]) {
teamUrl = splitUrl[2];
}
const result = await Promise.all([
getTeams({ userId: session.user.id }),
teamUrl ? getTeamByUrl({ userId: session.user.id, teamUrl }).catch(() => null) : null,
]);
teams = result[0];
team = result[1];
}
const endTime = Date.now();
debug.log(`Pathname accepted in ${endTime - initTime}ms`, url.pathname);
setAppContext(c, {
requestMetadata: extractRequestMetadata(request),
session: session.isAuthenticated
? {
session: session.session,
user: session.user,
currentTeam: team,
teams,
}
: null,
});
return next(); return next();
}; };

View File

@@ -1,10 +1,7 @@
import { getContext } from 'hono/context-storage'; import { getContext } from 'hono/context-storage';
import { redirect } from 'react-router';
import type { AppContext } from 'server/context'; import type { AppContext } from 'server/context';
import type { HonoEnv } from 'server/router'; import type { HonoEnv } from 'server/router';
import type { AppSession } from '@documenso/lib/client-only/providers/session';
/** /**
* Get the full context passed to the loader. * Get the full context passed to the loader.
* *
@@ -14,46 +11,3 @@ export const getOptionalLoaderContext = (): AppContext => {
const { context } = getContext<HonoEnv>().var; const { context } = getContext<HonoEnv>().var;
return context; return context;
}; };
/**
* Returns the session extracted from the app context.
*
* @returns The session, or null if not authenticated.
*/
export const getOptionalLoaderSession = (): AppSession | null => {
const { context } = getContext<HonoEnv>().var;
return context.session;
};
/**
* Returns the session context or throws a redirect to signin if it is not present.
*/
export const getLoaderSession = (): AppSession => {
const session = getOptionalLoaderSession();
if (!session) {
throw redirect('/signin'); // Todo: Maybe add a redirect cookie to come back?
}
return session;
};
/**
* Returns the team session context or throws a redirect to signin if it is not present.
*/
export const getLoaderTeamSession = () => {
const session = getOptionalLoaderSession();
if (!session) {
throw redirect('/signin'); // Todo: Maybe add a redirect cookie to come back?
}
if (!session.currentTeam) {
throw new Response(null, { status: 404 }); // Todo: Test that 404 page shows up.
}
return {
...session,
currentTeam: session.currentTeam,
};
};

View File

@@ -1,10 +1,12 @@
import type { ClientResponse, InferRequestType } from 'hono/client'; import type { ClientResponse, InferRequestType } from 'hono/client';
import { hc } from 'hono/client'; import { hc } from 'hono/client';
import superjson from 'superjson';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError } from '@documenso/lib/errors/app-error'; import { AppError } from '@documenso/lib/errors/app-error';
import type { AuthAppType } from '../server'; import type { AuthAppType } from '../server';
import type { SessionValidationResult } from '../server/lib/session/session';
import { handleSignInRedirect } from '../server/lib/utils/redirect'; import { handleSignInRedirect } from '../server/lib/utils/redirect';
import type { import type {
TDisableTwoFactorRequestSchema, TDisableTwoFactorRequestSchema,
@@ -45,8 +47,14 @@ export class AuthClient {
window.location.href = redirectPath ?? this.signOutredirectPath; window.location.href = redirectPath ?? this.signOutredirectPath;
} }
public async session() { public async getSession() {
return this.client.session.$get(); const response = await this.client['session-json'].$get();
await this.handleError(response);
const result = await response.json();
return superjson.deserialize<SessionValidationResult>(result);
} }
private async handleError<T>(response: ClientResponse<T>): Promise<void> { private async handleError<T>(response: ClientResponse<T>): Promise<void> {

View File

@@ -1,10 +1,17 @@
import { Hono } from 'hono'; import { Hono } from 'hono';
import superjson from 'superjson';
import type { SessionValidationResult } from '../lib/session/session'; import type { SessionValidationResult } from '../lib/session/session';
import { getOptionalSession } from '../lib/utils/get-session'; import { getOptionalSession } from '../lib/utils/get-session';
export const sessionRoute = new Hono().get('/session', async (c) => { export const sessionRoute = new Hono()
const session: SessionValidationResult = await getOptionalSession(c); .get('/session', async (c) => {
const session: SessionValidationResult = await getOptionalSession(c);
return c.json(session); return c.json(session);
}); })
.get('/session-json', async (c) => {
const session: SessionValidationResult = await getOptionalSession(c);
return c.json(superjson.serialize(session));
});

View File

@@ -1,25 +1,31 @@
import { createContext, useContext } from 'react'; import { createContext, useCallback, useContext, useEffect, useState } from 'react';
import React from 'react'; import React from 'react';
import type { SessionUser } from '@documenso/auth/server/lib/session/session'; import { useLocation } from 'react-router';
import type { Session } from '@documenso/prisma/client';
import type { TGetTeamByUrlResponse } from '../../server-only/team/get-team'; import { authClient } from '@documenso/auth/client';
import type { TGetTeamsResponse } from '../../server-only/team/get-teams'; import type { SessionUser } from '@documenso/auth/server/lib/session/session';
import { type TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
import type { Session } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/client';
export type AppSession = { export type AppSession = {
session: Session; session: Session;
user: SessionUser; user: SessionUser;
currentTeam: TGetTeamByUrlResponse | null;
teams: TGetTeamsResponse; teams: TGetTeamsResponse;
}; };
interface SessionProviderProps { interface SessionProviderProps {
children: React.ReactNode; children: React.ReactNode;
session: AppSession | null; initialSession: AppSession | null;
} }
const SessionContext = createContext<AppSession | null>(null); interface SessionContextValue {
sessionData: AppSession | null;
refresh: () => Promise<void>;
}
const SessionContext = createContext<SessionContextValue | null>(null);
export const useSession = () => { export const useSession = () => {
const context = useContext(SessionContext); const context = useContext(SessionContext);
@@ -28,18 +34,78 @@ export const useSession = () => {
throw new Error('useSession must be used within a SessionProvider'); throw new Error('useSession must be used within a SessionProvider');
} }
return context; if (!context.sessionData) {
throw new Error('Session not found');
}
return {
...context.sessionData,
refresh: context.refresh,
};
}; };
export const useOptionalSession = () => { export const useOptionalSession = () => {
return ( const context = useContext(SessionContext);
useContext(SessionContext) || {
user: null, if (!context) {
session: null, throw new Error('useOptionalSession must be used within a SessionProvider');
} }
);
return context;
}; };
export const SessionProvider = ({ children, session }: SessionProviderProps) => { export const SessionProvider = ({ children, initialSession }: SessionProviderProps) => {
return <SessionContext.Provider value={session}>{children}</SessionContext.Provider>; const [session, setSession] = useState<AppSession | null>(initialSession);
const location = useLocation();
const refreshSession = useCallback(async () => {
const newSession = await authClient.getSession();
if (!newSession.isAuthenticated) {
setSession(null);
return;
}
const teams = await trpc.team.getTeams.query().catch(() => {
// Todo: Log
return [];
});
setSession({
session: newSession.session,
user: newSession.user,
teams,
});
}, []);
useEffect(() => {
const onFocus = () => {
void refreshSession();
};
window.addEventListener('focus', onFocus);
return () => {
window.removeEventListener('focus', onFocus);
};
}, [refreshSession]);
/**
* Refresh session in background on navigation.
*/
useEffect(() => {
void refreshSession();
}, [location.pathname]);
return (
<SessionContext.Provider
value={{
sessionData: session,
refresh: refreshSession,
}}
>
{children}
</SessionContext.Provider>
);
}; };

View File

@@ -1,6 +1,7 @@
import type { z } from 'zod'; import type { z } from 'zod';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { SubscriptionSchema } from '@documenso/prisma/generated/zod/modelSchema/SubscriptionSchema';
import { TeamMemberSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamMemberSchema'; import { TeamMemberSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamMemberSchema';
import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema'; import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
@@ -12,6 +13,9 @@ export const ZGetTeamsResponseSchema = TeamSchema.extend({
currentTeamMember: TeamMemberSchema.pick({ currentTeamMember: TeamMemberSchema.pick({
role: true, role: true,
}), }),
subscription: SubscriptionSchema.pick({
status: true,
}).nullable(),
}).array(); }).array();
export type TGetTeamsResponse = z.infer<typeof ZGetTeamsResponseSchema>; export type TGetTeamsResponse = z.infer<typeof ZGetTeamsResponseSchema>;
@@ -26,6 +30,11 @@ export const getTeams = async ({ userId }: GetTeamsOptions): Promise<TGetTeamsRe
}, },
}, },
include: { include: {
subscription: {
select: {
status: true,
},
},
members: { members: {
where: { where: {
userId, userId,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
import { useState } from 'react'; import { useMemo, useState } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink, httpLink, splitLink } from '@trpc/client'; import { httpBatchLink, httpLink, splitLink } from '@trpc/client';
@@ -7,7 +7,6 @@ import SuperJSON from 'superjson';
import { getBaseUrl } from '@documenso/lib/universal/get-base-url'; import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
// import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
import type { AppRouter } from '../server/router'; import type { AppRouter } from '../server/router';
export { getQueryKey } from '@trpc/react-query'; export { getQueryKey } from '@trpc/react-query';
@@ -39,24 +38,27 @@ export interface TrpcProviderProps {
export function TrpcProvider({ children, headers }: TrpcProviderProps) { export function TrpcProvider({ children, headers }: TrpcProviderProps) {
const [queryClient] = useState(() => new QueryClient()); const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() => // May cause remounting issues.
trpc.createClient({ const trpcClient = useMemo(
links: [ () =>
splitLink({ trpc.createClient({
condition: (op) => op.context.skipBatch === true, links: [
true: httpLink({ splitLink({
url: `${getBaseUrl()}/api/trpc`, condition: (op) => op.context.skipBatch === true,
headers, true: httpLink({
transformer: SuperJSON, url: `${getBaseUrl()}/api/trpc`,
headers,
transformer: SuperJSON,
}),
false: httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
headers,
transformer: SuperJSON,
}),
}), }),
false: httpBatchLink({ ],
url: `${getBaseUrl()}/api/trpc`, }),
headers, [headers],
transformer: SuperJSON,
}),
}),
],
}),
); );
return ( return (