fix: rework sessions
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
18
apps/remix/app/components/general/portal.tsx
Normal file
18
apps/remix/app/components/general/portal.tsx
Normal 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;
|
||||||
|
};
|
||||||
@@ -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">
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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} />}
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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));
|
||||||
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
@@ -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 (
|
||||||
|
|||||||
Reference in New Issue
Block a user