fix: rework sessions
This commit is contained in:
@@ -56,7 +56,8 @@ export const DirectTemplateConfigureForm = ({
|
||||
}: DirectTemplateConfigureFormProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const { user } = useOptionalSession();
|
||||
const { sessionData } = useOptionalSession();
|
||||
const user = sessionData?.user;
|
||||
|
||||
const { recipients } = template;
|
||||
const { derivedRecipientAccessAuth } = useRequiredDocumentSigningAuthContext();
|
||||
|
||||
@@ -48,7 +48,8 @@ export const DocumentSigningForm = ({
|
||||
allRecipients = [],
|
||||
setSelectedSignerId,
|
||||
}: DocumentSigningFormProps) => {
|
||||
const { user } = useOptionalSession();
|
||||
const { sessionData } = useOptionalSession();
|
||||
const user = sessionData?.user;
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
@@ -139,10 +139,7 @@ export const DocumentPageViewDropdown = ({ document }: DocumentPageViewDropdownP
|
||||
<Trans>Duplicate</Trans>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
disabled={Boolean(!canManageDocument && team?.teamEmail) || isDeleted}
|
||||
>
|
||||
<DropdownMenuItem onClick={() => setDeleteDialogOpen(true)} disabled={isDeleted}>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
<Trans>Delete</Trans>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -8,7 +8,6 @@ import { Link, useNavigate } from 'react-router';
|
||||
|
||||
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
@@ -66,7 +65,7 @@ export const GenericErrorLayout = ({
|
||||
errorCodeMap[errorCode || 404] ?? defaultErrorCodeMap[500];
|
||||
|
||||
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">
|
||||
<motion.div
|
||||
className="flex h-full w-full items-center justify-center"
|
||||
@@ -85,7 +84,7 @@ export const GenericErrorLayout = ({
|
||||
</motion.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>
|
||||
<p className="text-muted-foreground font-semibold">{_(subHeading)}</p>
|
||||
|
||||
|
||||
@@ -262,7 +262,7 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
|
||||
{selectedTeam &&
|
||||
canExecuteTeamAction('MANAGE_TEAM', selectedTeam.currentTeamMember.role) && (
|
||||
<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>
|
||||
</Link>
|
||||
</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';
|
||||
|
||||
export default function Loading() {
|
||||
export default function DocumentEditSkeleton() {
|
||||
return (
|
||||
<div className="mx-auto -mt-4 flex w-full max-w-screen-xl flex-col px-4 md:px-8">
|
||||
<Link to="/documents" className="flex grow-0 items-center text-[#7AC455] hover:opacity-80">
|
||||
@@ -4,7 +4,7 @@ import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { TeamMemberRole } from '@prisma/client';
|
||||
import { type Subscription, SubscriptionStatus } from '@prisma/client';
|
||||
import { SubscriptionStatus } from '@prisma/client';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
@@ -22,13 +22,13 @@ import {
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type TeamLayoutBillingBannerProps = {
|
||||
subscription: Subscription;
|
||||
subscriptionStatus: SubscriptionStatus;
|
||||
teamId: number;
|
||||
userRole: TeamMemberRole;
|
||||
};
|
||||
|
||||
export const TeamLayoutBillingBanner = ({
|
||||
subscription,
|
||||
subscriptionStatus,
|
||||
teamId,
|
||||
userRole,
|
||||
}: TeamLayoutBillingBannerProps) => {
|
||||
@@ -59,7 +59,7 @@ export const TeamLayoutBillingBanner = ({
|
||||
}
|
||||
};
|
||||
|
||||
if (subscription.status === SubscriptionStatus.ACTIVE) {
|
||||
if (subscriptionStatus === SubscriptionStatus.ACTIVE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -68,16 +68,16 @@ export const TeamLayoutBillingBanner = ({
|
||||
<div
|
||||
className={cn({
|
||||
'bg-yellow-200 text-yellow-900 dark:bg-yellow-400':
|
||||
subscription.status === SubscriptionStatus.PAST_DUE,
|
||||
subscriptionStatus === SubscriptionStatus.PAST_DUE,
|
||||
'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="flex items-center">
|
||||
<AlertTriangle className="mr-2.5 h-5 w-5" />
|
||||
|
||||
{match(subscription.status)
|
||||
{match(subscriptionStatus)
|
||||
.with(SubscriptionStatus.PAST_DUE, () => <Trans>Payment overdue</Trans>)
|
||||
.with(SubscriptionStatus.INACTIVE, () => <Trans>Teams restricted</Trans>)
|
||||
.exhaustive()}
|
||||
@@ -87,9 +87,9 @@ export const TeamLayoutBillingBanner = ({
|
||||
variant="ghost"
|
||||
className={cn({
|
||||
'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':
|
||||
subscription.status === SubscriptionStatus.INACTIVE,
|
||||
subscriptionStatus === SubscriptionStatus.INACTIVE,
|
||||
})}
|
||||
disabled={isPending}
|
||||
onClick={() => setIsOpen(true)}
|
||||
@@ -106,7 +106,7 @@ export const TeamLayoutBillingBanner = ({
|
||||
<Trans>Payment overdue</Trans>
|
||||
</DialogTitle>
|
||||
|
||||
{match(subscription.status)
|
||||
{match(subscriptionStatus)
|
||||
.with(SubscriptionStatus.PAST_DUE, () => (
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
|
||||
@@ -170,10 +170,7 @@ export const DocumentsTableActionDropdown = ({ row }: DocumentsTableActionDropdo
|
||||
Void
|
||||
</DropdownMenuItem> */}
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
disabled={Boolean(!canManageDocument && team?.teamEmail)}
|
||||
>
|
||||
<DropdownMenuItem onClick={() => setDeleteDialogOpen(true)}>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
{canManageDocument ? _(msg`Delete`) : _(msg`Hide`)}
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useEffect } from 'react';
|
||||
import posthog from 'posthog-js';
|
||||
import { useLocation, useSearchParams } from 'react-router';
|
||||
|
||||
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { extractPostHogConfig } from '@documenso/lib/constants/feature-flags';
|
||||
|
||||
export function PostHogPageview() {
|
||||
@@ -12,19 +11,20 @@ export function PostHogPageview() {
|
||||
const { pathname } = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const { user } = useOptionalSession();
|
||||
// const { sessionData } = useOptionalSession();
|
||||
// const user = sessionData?.user;
|
||||
|
||||
if (typeof window !== 'undefined' && postHogConfig) {
|
||||
posthog.init(postHogConfig.key, {
|
||||
api_host: postHogConfig.host,
|
||||
disable_session_recording: true,
|
||||
loaded: () => {
|
||||
if (user) {
|
||||
posthog.identify(user.email ?? user.id.toString());
|
||||
} else {
|
||||
posthog.reset();
|
||||
}
|
||||
},
|
||||
// loaded: () => {
|
||||
// if (user) {
|
||||
// posthog.identify(user.email ?? user.id.toString());
|
||||
// } else {
|
||||
// posthog.reset();
|
||||
// }
|
||||
// },
|
||||
custom_campaign_params: ['src'],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { createContext, useContext } 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 {
|
||||
children: React.ReactNode;
|
||||
team: TGetTeamByUrlResponse;
|
||||
team: TeamProviderValue;
|
||||
}
|
||||
|
||||
const TeamContext = createContext<TGetTeamByUrlResponse | null>(null);
|
||||
const TeamContext = createContext<TeamProviderValue | null>(null);
|
||||
|
||||
export const useCurrentTeam = () => {
|
||||
const context = useContext(TeamContext);
|
||||
|
||||
@@ -13,10 +13,11 @@ import {
|
||||
useLocation,
|
||||
} from 'react-router';
|
||||
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 { 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 { extractLocaleData } from '@documenso/lib/utils/i18n';
|
||||
import { TrpcProvider } from '@documenso/trpc/react';
|
||||
@@ -59,8 +60,21 @@ export function meta() {
|
||||
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) {
|
||||
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);
|
||||
|
||||
@@ -74,7 +88,13 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
{
|
||||
lang,
|
||||
theme: getTheme(),
|
||||
session,
|
||||
session: session.isAuthenticated
|
||||
? {
|
||||
user: session.user,
|
||||
session: session.session,
|
||||
teams,
|
||||
}
|
||||
: null,
|
||||
publicEnv: createPublicEnv(),
|
||||
},
|
||||
{
|
||||
@@ -113,7 +133,7 @@ export function App() {
|
||||
<script>0</script>
|
||||
</head>
|
||||
<body>
|
||||
<SessionProvider session={session}>
|
||||
<SessionProvider initialSession={session}>
|
||||
<TooltipProvider>
|
||||
<TrpcProvider>
|
||||
<Outlet />
|
||||
|
||||
@@ -1,56 +1,57 @@
|
||||
import { SubscriptionStatus } from '@prisma/client';
|
||||
import { Outlet } from 'react-router';
|
||||
import { getLoaderSession } from 'server/utils/get-loader-session';
|
||||
import { Outlet, redirect } from 'react-router';
|
||||
|
||||
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { getLimits } from '@documenso/ee/server-only/limits/client';
|
||||
import { LimitsProvider } from '@documenso/ee/server-only/limits/provider/client';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { getSiteSettings } from '@documenso/lib/server-only/site-settings/get-site-settings';
|
||||
import { SITE_SETTINGS_BANNER_ID } from '@documenso/lib/server-only/site-settings/schemas/banner';
|
||||
|
||||
import { AppBanner } from '~/components/general/app-banner';
|
||||
import { Header } from '~/components/general/app-header';
|
||||
import { TeamLayoutBillingBanner } from '~/components/general/teams/team-layout-billing-banner';
|
||||
import { VerifyEmailBanner } from '~/components/general/verify-email-banner';
|
||||
|
||||
import type { Route } from './+types/_layout';
|
||||
|
||||
export const loader = async ({ request }: Route.LoaderArgs) => {
|
||||
const { user, teams, currentTeam } = getLoaderSession();
|
||||
/**
|
||||
* Don't revalidate (run the loader on sequential navigations)
|
||||
*
|
||||
* Update values via providers.
|
||||
*/
|
||||
export const shouldRevalidate = () => false;
|
||||
|
||||
export const loader = async ({ request }: Route.LoaderArgs) => {
|
||||
const requestHeaders = Object.fromEntries(request.headers.entries());
|
||||
|
||||
// Todo: Should only load this on first render.
|
||||
const session = await getOptionalSession(request);
|
||||
|
||||
if (!session.isAuthenticated) {
|
||||
return redirect('/signin');
|
||||
}
|
||||
|
||||
const [limits, banner] = await Promise.all([
|
||||
getLimits({ headers: requestHeaders, teamId: currentTeam?.id }),
|
||||
getLimits({ headers: requestHeaders }),
|
||||
getSiteSettings().then((settings) =>
|
||||
settings.find((setting) => setting.id === SITE_SETTINGS_BANNER_ID),
|
||||
),
|
||||
]);
|
||||
|
||||
return {
|
||||
user,
|
||||
teams,
|
||||
banner,
|
||||
limits,
|
||||
currentTeam,
|
||||
};
|
||||
};
|
||||
|
||||
export default function Layout({ loaderData }: Route.ComponentProps) {
|
||||
const { user, teams, banner, limits, currentTeam } = loaderData;
|
||||
const { user, teams } = useSession();
|
||||
|
||||
const { banner, limits } = loaderData;
|
||||
|
||||
return (
|
||||
<LimitsProvider initialValue={limits} teamId={currentTeam?.id}>
|
||||
{!user.emailVerified && <VerifyEmailBanner email={user.email} />}
|
||||
<LimitsProvider initialValue={limits}>
|
||||
<div id="portal-header"></div>
|
||||
|
||||
{currentTeam?.subscription &&
|
||||
currentTeam.subscription.status !== SubscriptionStatus.ACTIVE && (
|
||||
<TeamLayoutBillingBanner
|
||||
subscription={currentTeam.subscription}
|
||||
teamId={currentTeam.id}
|
||||
userRole={currentTeam.currentTeamMember.role}
|
||||
/>
|
||||
)}
|
||||
{!user.emailVerified && <VerifyEmailBanner email={user.email} />}
|
||||
|
||||
{banner && <AppBanner banner={banner} />}
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { BarChart3, FileStack, Settings, Trophy, Users, Wallet2 } from 'lucide-react';
|
||||
import { Link, Outlet, redirect, useLocation } from 'react-router';
|
||||
import { getLoaderSession } from 'server/utils/get-loader-session';
|
||||
|
||||
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { isAdmin } from '@documenso/lib/utils/is-admin';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
export function loader() {
|
||||
const { user } = getLoaderSession();
|
||||
import type { Route } from './+types/_layout';
|
||||
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
const { user } = await getSession(request);
|
||||
|
||||
if (!user || !isAdmin(user)) {
|
||||
throw redirect('/documents');
|
||||
|
||||
@@ -3,13 +3,14 @@ import { Plural, Trans } from '@lingui/react/macro';
|
||||
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
|
||||
import { ChevronLeft, Clock9, Users2 } from 'lucide-react';
|
||||
import { Link, redirect } from 'react-router';
|
||||
import { getLoaderSession } from 'server/utils/get-loader-session';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
|
||||
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
||||
import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { Badge } from '@documenso/ui/primitives/badge';
|
||||
@@ -34,8 +35,14 @@ import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
|
||||
|
||||
import type { Route } from './+types/$id._index';
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
const { user, currentTeam: team } = getLoaderSession();
|
||||
export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
const { user } = await getSession(request);
|
||||
|
||||
let team: TGetTeamByUrlResponse | null = null;
|
||||
|
||||
if (params.teamUrl) {
|
||||
team = await getTeamByUrl({ userId: user.id, teamUrl: params.teamUrl });
|
||||
}
|
||||
|
||||
const { id } = params;
|
||||
|
||||
|
||||
@@ -2,11 +2,12 @@ import { Plural, Trans } from '@lingui/react/macro';
|
||||
import { DocumentStatus as InternalDocumentStatus, TeamMemberRole } from '@prisma/client';
|
||||
import { ChevronLeft, Users2 } from 'lucide-react';
|
||||
import { Link, redirect } from 'react-router';
|
||||
import { getLoaderSession } from 'server/utils/get-loader-session';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
|
||||
import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
|
||||
@@ -17,8 +18,14 @@ import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
|
||||
|
||||
import type { Route } from './+types/$id.edit';
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
const { user, currentTeam: team } = getLoaderSession();
|
||||
export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
const { user } = await getSession(request);
|
||||
|
||||
let team: TGetTeamByUrlResponse | null = null;
|
||||
|
||||
if (params.teamUrl) {
|
||||
team = await getTeamByUrl({ userId: user.id, teamUrl: params.teamUrl });
|
||||
}
|
||||
|
||||
const { id } = params;
|
||||
|
||||
|
||||
@@ -6,10 +6,11 @@ import type { Recipient } from '@prisma/client';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Link, redirect } from 'react-router';
|
||||
import { getLoaderSession } from 'server/utils/get-loader-session';
|
||||
|
||||
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
||||
import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { Card } from '@documenso/ui/primitives/card';
|
||||
|
||||
@@ -23,10 +24,16 @@ import { DocumentLogsTable } from '~/components/tables/document-logs-table';
|
||||
|
||||
import type { Route } from './+types/$id.logs';
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
const { id } = params;
|
||||
export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
const { user } = await getSession(request);
|
||||
|
||||
const { user, currentTeam: team } = getLoaderSession();
|
||||
let team: TGetTeamByUrlResponse | null = null;
|
||||
|
||||
if (params.teamUrl) {
|
||||
team = await getTeamByUrl({ userId: user.id, teamUrl: params.teamUrl });
|
||||
}
|
||||
|
||||
const { id } = params;
|
||||
|
||||
const documentId = Number(id);
|
||||
|
||||
|
||||
@@ -59,15 +59,26 @@ export default function DocumentsPage() {
|
||||
[searchParams],
|
||||
);
|
||||
|
||||
const { data, isLoading, isLoadingError } = trpc.document.findDocumentsInternal.useQuery({
|
||||
...findDocumentSearchParams,
|
||||
});
|
||||
const { data, isLoading, isLoadingError, refetch } = trpc.document.findDocumentsInternal.useQuery(
|
||||
{
|
||||
...findDocumentSearchParams,
|
||||
},
|
||||
);
|
||||
|
||||
// Refetch the documents when the team URL changes.
|
||||
useEffect(() => {
|
||||
void refetch();
|
||||
}, [team?.url]);
|
||||
|
||||
const getTabHref = (value: keyof typeof ExtendedDocumentStatus) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
|
||||
params.set('status', value);
|
||||
|
||||
if (value === ExtendedDocumentStatus.ALL) {
|
||||
params.delete('status');
|
||||
}
|
||||
|
||||
if (params.has('page')) {
|
||||
params.delete('page');
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { TemplateDirectLink } from '@prisma/client';
|
||||
import { TemplateType } from '@prisma/client';
|
||||
import { getLoaderSession } from 'server/utils/get-loader-session';
|
||||
|
||||
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { getUserPublicProfile } from '@documenso/lib/server-only/user/get-user-public-profile';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
@@ -44,8 +44,8 @@ const teamProfileText = {
|
||||
templatesSubtitle: msg`Show templates in your team public profile for your audience to sign and get started quickly`,
|
||||
};
|
||||
|
||||
export async function loader() {
|
||||
const { user } = getLoaderSession();
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
const { user } = await getSession(request);
|
||||
|
||||
const { profile } = await getUserPublicProfile({
|
||||
userId: user.id,
|
||||
|
||||
@@ -2,8 +2,8 @@ import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Link } from 'react-router';
|
||||
import { getLoaderSession } from 'server/utils/get-loader-session';
|
||||
|
||||
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
@@ -22,8 +22,8 @@ export function meta() {
|
||||
return appMetaTags('Security');
|
||||
}
|
||||
|
||||
export async function loader() {
|
||||
const { user } = getLoaderSession();
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
const { user } = await getSession(request);
|
||||
|
||||
// Todo: Use providers instead after RR7 migration.
|
||||
// const accounts = await prisma.account.findMany({
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { redirect } from 'react-router';
|
||||
import { getLoaderSession } from 'server/utils/get-loader-session';
|
||||
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
|
||||
export function loader() {
|
||||
const { currentTeam } = getLoaderSession();
|
||||
import type { Route } from './+types/_index';
|
||||
|
||||
if (!currentTeam) {
|
||||
throw redirect('/settings/teams');
|
||||
}
|
||||
|
||||
throw redirect(formatDocumentsPath(currentTeam.url));
|
||||
export function loader({ params }: Route.LoaderArgs) {
|
||||
throw redirect(formatDocumentsPath(params.teamUrl));
|
||||
}
|
||||
|
||||
@@ -1,147 +1,104 @@
|
||||
import type { MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import { Link, Outlet, isRouteErrorResponse, redirect, useNavigate } from 'react-router';
|
||||
import { getLoaderSession } from 'server/utils/get-loader-session';
|
||||
import { match } from 'ts-pattern';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { SubscriptionStatus } from '@prisma/client';
|
||||
import { Link, Outlet } from 'react-router';
|
||||
|
||||
import { TEAM_PLAN_LIMITS } from '@documenso/ee/server-only/limits/constants';
|
||||
import { LimitsProvider } from '@documenso/ee/server-only/limits/provider/client';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { TrpcProvider } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
|
||||
import { PortalComponent } from '~/components/general/portal';
|
||||
import { TeamLayoutBillingBanner } from '~/components/general/teams/team-layout-billing-banner';
|
||||
import { TeamProvider } from '~/providers/team';
|
||||
|
||||
import type { Route } from './+types/_layout';
|
||||
|
||||
export const loader = () => {
|
||||
const { currentTeam } = getLoaderSession();
|
||||
export default function Layout({ params }: Route.ComponentProps) {
|
||||
const { teams } = useSession();
|
||||
|
||||
const currentTeam = teams.find((team) => team.url === params.teamUrl);
|
||||
|
||||
const limits = useMemo(() => {
|
||||
if (!currentTeam) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
currentTeam?.subscription &&
|
||||
currentTeam.subscription.status === SubscriptionStatus.INACTIVE
|
||||
) {
|
||||
return {
|
||||
quota: {
|
||||
documents: 0,
|
||||
recipients: 0,
|
||||
directTemplates: 0,
|
||||
},
|
||||
remaining: {
|
||||
documents: 0,
|
||||
recipients: 0,
|
||||
directTemplates: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
quota: TEAM_PLAN_LIMITS,
|
||||
remaining: TEAM_PLAN_LIMITS,
|
||||
};
|
||||
}, [currentTeam?.subscription, currentTeam?.id]);
|
||||
|
||||
if (!currentTeam) {
|
||||
throw redirect('/settings/teams');
|
||||
return (
|
||||
<GenericErrorLayout
|
||||
errorCode={404}
|
||||
errorCodeMap={{
|
||||
404: {
|
||||
heading: msg`Team not found`,
|
||||
subHeading: msg`404 Team not found`,
|
||||
message: msg`The team you are looking for may have been removed, renamed or may have never
|
||||
existed.`,
|
||||
},
|
||||
}}
|
||||
primaryButton={
|
||||
<Button asChild>
|
||||
<Link to="/settings/teams">
|
||||
<Trans>View teams</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
></GenericErrorLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const trpcHeaders = {
|
||||
'x-team-Id': currentTeam.id.toString(),
|
||||
};
|
||||
|
||||
return {
|
||||
currentTeam,
|
||||
trpcHeaders,
|
||||
};
|
||||
};
|
||||
|
||||
export default function Layout({ loaderData }: Route.ComponentProps) {
|
||||
const { currentTeam, trpcHeaders } = loaderData;
|
||||
|
||||
return (
|
||||
<TeamProvider team={currentTeam}>
|
||||
<TrpcProvider headers={trpcHeaders}>
|
||||
<main className="mt-8 pb-8 md:mt-12 md:pb-12">
|
||||
<Outlet />
|
||||
</main>
|
||||
</TrpcProvider>
|
||||
<LimitsProvider initialValue={limits} teamId={currentTeam.id}>
|
||||
<TrpcProvider headers={trpcHeaders}>
|
||||
{currentTeam?.subscription &&
|
||||
currentTeam.subscription.status !== SubscriptionStatus.ACTIVE && (
|
||||
<PortalComponent target="portal-header">
|
||||
<TeamLayoutBillingBanner
|
||||
subscriptionStatus={currentTeam.subscription.status}
|
||||
teamId={currentTeam.id}
|
||||
userRole={currentTeam.currentTeamMember.role}
|
||||
/>
|
||||
</PortalComponent>
|
||||
)}
|
||||
|
||||
<main className="mt-8 pb-8 md:mt-12 md:pb-12">
|
||||
<Outlet />
|
||||
</main>
|
||||
</TrpcProvider>
|
||||
</LimitsProvider>
|
||||
</TeamProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
let errorMessage = msg`Unknown error`;
|
||||
let errorDetails: MessageDescriptor | null = null;
|
||||
|
||||
if (error instanceof Error && error.message === AppErrorCode.UNAUTHORIZED) {
|
||||
errorMessage = msg`Unauthorized`;
|
||||
errorDetails = msg`You are not authorized to view this page.`;
|
||||
}
|
||||
|
||||
if (isRouteErrorResponse(error)) {
|
||||
return match(error.status)
|
||||
.with(404, () => (
|
||||
<div className="mx-auto flex min-h-[80vh] w-full items-center justify-center py-32">
|
||||
<div>
|
||||
<p className="text-muted-foreground font-semibold">
|
||||
<Trans>404 Team not found</Trans>
|
||||
</p>
|
||||
|
||||
<h1 className="mt-3 text-2xl font-bold md:text-3xl">
|
||||
<Trans>Oops! Something went wrong.</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground mt-4 text-sm">
|
||||
<Trans>
|
||||
The team you are looking for may have been removed, renamed or may have never
|
||||
existed.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
|
||||
<Button asChild className="w-32">
|
||||
<Link to="/settings/teams">
|
||||
<ChevronLeft className="mr-2 h-4 w-4" />
|
||||
<Trans>Go Back</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
.with(500, () => (
|
||||
<div className="mx-auto flex min-h-[80vh] w-full items-center justify-center py-32">
|
||||
<div>
|
||||
<p className="text-muted-foreground font-semibold">{_(errorMessage)}</p>
|
||||
|
||||
<h1 className="mt-3 text-2xl font-bold md:text-3xl">
|
||||
<Trans>Oops! Something went wrong.</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground mt-4 text-sm">
|
||||
{errorDetails ? _(errorDetails) : ''}
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-32"
|
||||
onClick={() => {
|
||||
void navigate(-1);
|
||||
}}
|
||||
>
|
||||
<ChevronLeft className="mr-2 h-4 w-4" />
|
||||
<Trans>Go Back</Trans>
|
||||
</Button>
|
||||
|
||||
<Button asChild>
|
||||
<Link to="/settings/teams">
|
||||
<Trans>View teams</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
.otherwise(() => (
|
||||
<>
|
||||
<h1>
|
||||
{error.status} {error.statusText}
|
||||
</h1>
|
||||
<p>{error.data}</p>
|
||||
</>
|
||||
));
|
||||
} else if (error instanceof Error) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Error</h1>
|
||||
<p>{error.message}</p>
|
||||
<p>The stack trace is:</p>
|
||||
<pre>{error.stack}</pre>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <h1>Unknown Error</h1>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ import { Trans } from '@lingui/react/macro';
|
||||
import { CheckCircle2, Clock } from 'lucide-react';
|
||||
import { P, match } from 'ts-pattern';
|
||||
|
||||
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||
import { isTokenExpired } from '@documenso/lib/utils/token-verification';
|
||||
@@ -17,13 +19,27 @@ import { TeamUpdateForm } from '~/components/forms/team-update-form';
|
||||
import { SettingsHeader } from '~/components/general/settings-header';
|
||||
import { TeamEmailDropdown } from '~/components/general/teams/team-email-dropdown';
|
||||
import { TeamTransferStatus } from '~/components/general/teams/team-transfer-status';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
export default function TeamsSettingsPage() {
|
||||
import type { Route } from './+types/_index';
|
||||
|
||||
export async function loader({ request, params }: Route.LoaderArgs) {
|
||||
const { user } = await getSession(request);
|
||||
|
||||
const team = await getTeamByUrl({
|
||||
userId: user.id,
|
||||
teamUrl: params.teamUrl,
|
||||
});
|
||||
|
||||
return {
|
||||
team,
|
||||
};
|
||||
}
|
||||
|
||||
export default function TeamsSettingsPage({ loaderData }: Route.ComponentProps) {
|
||||
const { team } = loaderData;
|
||||
|
||||
const { user } = useSession();
|
||||
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const isTransferVerificationExpired =
|
||||
!team.transferVerification || isTokenExpired(team.transferVerification.expiresAt);
|
||||
|
||||
|
||||
@@ -1,25 +1,37 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Outlet } from 'react-router';
|
||||
import { getLoaderTeamSession } from 'server/utils/get-loader-session';
|
||||
import { Outlet, redirect } from 'react-router';
|
||||
|
||||
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
|
||||
|
||||
import { TeamSettingsNavDesktop } from '~/components/general/teams/team-settings-nav-desktop';
|
||||
import { TeamSettingsNavMobile } from '~/components/general/teams/team-settings-nav-mobile';
|
||||
import { appMetaTags } from '~/utils/meta';
|
||||
|
||||
import type { Route } from './+types/_layout';
|
||||
|
||||
export function meta() {
|
||||
return appMetaTags('Team Settings');
|
||||
}
|
||||
|
||||
export function loader() {
|
||||
const { currentTeam: team } = getLoaderTeamSession();
|
||||
export async function loader({ request, params }: Route.LoaderArgs) {
|
||||
const session = await getSession(request);
|
||||
|
||||
const team = await getTeamByUrl({
|
||||
userId: session.user.id,
|
||||
teamUrl: params.teamUrl,
|
||||
});
|
||||
|
||||
if (!team || !canExecuteTeamAction('MANAGE_TEAM', team.currentTeamMember.role)) {
|
||||
throw new Response(null, { status: 401 }); // Unauthorized.
|
||||
throw redirect(`/t/${params.teamUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function clientLoader() {
|
||||
// Do nothing, we only want the loader to run on SSR.
|
||||
}
|
||||
|
||||
export default function TeamsSettingsLayout() {
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
|
||||
|
||||
@@ -2,11 +2,12 @@ import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Plural, Trans } from '@lingui/react/macro';
|
||||
import { DateTime } from 'luxon';
|
||||
import { getLoaderTeamSession } from 'server/utils/get-loader-session';
|
||||
import type Stripe from 'stripe';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { stripe } from '@documenso/lib/server-only/stripe';
|
||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
|
||||
@@ -16,8 +17,13 @@ import { TeamSettingsBillingInvoicesTable } from '~/components/tables/team-setti
|
||||
|
||||
import type { Route } from './+types/billing';
|
||||
|
||||
export async function loader() {
|
||||
const { currentTeam: team } = getLoaderTeamSession();
|
||||
export async function loader({ request, params }: Route.LoaderArgs) {
|
||||
const session = await getSession(request);
|
||||
|
||||
const team = await getTeamByUrl({
|
||||
userId: session.user.id,
|
||||
teamUrl: params.teamUrl,
|
||||
});
|
||||
|
||||
let teamSubscription: Stripe.Subscription | null = null;
|
||||
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
|
||||
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
|
||||
import { TeamBrandingPreferencesForm } from '~/components/forms/team-branding-preferences-form';
|
||||
import { TeamDocumentPreferencesForm } from '~/components/forms/team-document-preferences-form';
|
||||
import { SettingsHeader } from '~/components/general/settings-header';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
export default function TeamsSettingsPage() {
|
||||
import type { Route } from './+types/preferences';
|
||||
|
||||
export async function loader({ request, params }: Route.LoaderArgs) {
|
||||
const { user } = await getSession(request);
|
||||
|
||||
const team = await getTeamByUrl({ userId: user.id, teamUrl: params.teamUrl });
|
||||
|
||||
return {
|
||||
team,
|
||||
};
|
||||
}
|
||||
|
||||
export default function TeamsSettingsPage({ loaderData }: Route.ComponentProps) {
|
||||
const { team } = loaderData;
|
||||
|
||||
const { _ } = useLingui();
|
||||
|
||||
const team = useCurrentTeam();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SettingsHeader
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import { getLoaderTeamSession } from 'server/utils/get-loader-session';
|
||||
|
||||
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
import { getTeamPublicProfile } from '@documenso/lib/server-only/team/get-team-public-profile';
|
||||
|
||||
import PublicProfilePage from '~/routes/_authenticated+/settings+/public-profile+/index';
|
||||
|
||||
export async function loader() {
|
||||
const { user, currentTeam: team } = getLoaderTeamSession();
|
||||
import type { Route } from './+types/public-profile';
|
||||
|
||||
// Todo: This can be optimized.
|
||||
export async function loader({ request, params }: Route.LoaderArgs) {
|
||||
const session = await getSession(request);
|
||||
|
||||
const team = await getTeamByUrl({
|
||||
userId: session.user.id,
|
||||
teamUrl: params.teamUrl,
|
||||
});
|
||||
|
||||
const { profile } = await getTeamPublicProfile({
|
||||
userId: user.id,
|
||||
userId: session.user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { DateTime } from 'luxon';
|
||||
import { getLoaderTeamSession } from 'server/utils/get-loader-session';
|
||||
|
||||
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { getTeamTokens } from '@documenso/lib/server-only/public-api/get-all-team-tokens';
|
||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
import TokenDeleteDialog from '~/components/dialogs/token-delete-dialog';
|
||||
@@ -12,8 +13,14 @@ import { ApiTokenForm } from '~/components/forms/token';
|
||||
|
||||
import type { Route } from './+types/tokens';
|
||||
|
||||
export async function loader() {
|
||||
const { user, currentTeam: team } = getLoaderTeamSession();
|
||||
// Todo: This can be optimized.
|
||||
export async function loader({ request, params }: Route.LoaderArgs) {
|
||||
const { user } = await getSession(request);
|
||||
|
||||
const team = await getTeamByUrl({
|
||||
userId: user.id,
|
||||
teamUrl: params.teamUrl,
|
||||
});
|
||||
|
||||
const tokens = await getTeamTokens({ userId: user.id, teamId: team.id }).catch(() => null);
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@ import { Trans } from '@lingui/react/macro';
|
||||
import { DocumentSigningOrder, SigningStatus } from '@prisma/client';
|
||||
import { ChevronLeft, LucideEdit } from 'lucide-react';
|
||||
import { Link, redirect, useNavigate } from 'react-router';
|
||||
import { getLoaderSession } from 'server/utils/get-loader-session';
|
||||
|
||||
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
||||
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@@ -25,8 +26,14 @@ import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
|
||||
|
||||
import type { Route } from './+types/$id._index';
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
const { user, currentTeam: team } = getLoaderSession();
|
||||
export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
const { user } = await getSession(request);
|
||||
|
||||
let team: TGetTeamByUrlResponse | null = null;
|
||||
|
||||
if (params.teamUrl) {
|
||||
team = await getTeamByUrl({ userId: user.id, teamUrl: params.teamUrl });
|
||||
}
|
||||
|
||||
const { id } = params;
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import { Link, redirect } from 'react-router';
|
||||
import { getLoaderSession } from 'server/utils/get-loader-session';
|
||||
|
||||
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
||||
import { formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||
|
||||
@@ -15,8 +16,14 @@ import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
|
||||
import { TemplateDirectLinkDialogWrapper } from '../../../components/dialogs/template-direct-link-dialog-wrapper';
|
||||
import type { Route } from './+types/$id.edit';
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
const { user, currentTeam: team } = getLoaderSession();
|
||||
export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
const { user } = await getSession(request);
|
||||
|
||||
let team: TGetTeamByUrlResponse | null = null;
|
||||
|
||||
if (params.teamUrl) {
|
||||
team = await getTeamByUrl({ userId: user.id, teamUrl: params.teamUrl });
|
||||
}
|
||||
|
||||
const { id } = params;
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Bird } from 'lucide-react';
|
||||
import { useSearchParams } from 'react-router';
|
||||
@@ -27,11 +29,16 @@ export default function TemplatesPage() {
|
||||
const documentRootPath = formatDocumentsPath(team?.url);
|
||||
const templateRootPath = formatTemplatesPath(team?.url);
|
||||
|
||||
const { data, isLoading, isLoadingError } = trpc.template.findTemplates.useQuery({
|
||||
const { data, isLoading, isLoadingError, refetch } = trpc.template.findTemplates.useQuery({
|
||||
page: page,
|
||||
perPage: perPage,
|
||||
});
|
||||
|
||||
// Refetch the templates when the team URL changes.
|
||||
useEffect(() => {
|
||||
void refetch();
|
||||
}, [team?.url]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-screen-xl px-4 md:px-8">
|
||||
<div className="flex items-baseline justify-between">
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { redirect } from 'react-router';
|
||||
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
|
||||
|
||||
export function loader() {
|
||||
const session = getOptionalLoaderSession();
|
||||
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
|
||||
if (session) {
|
||||
import type { Route } from './+types/_index';
|
||||
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
const { isAuthenticated } = await getOptionalSession(request);
|
||||
|
||||
if (isAuthenticated) {
|
||||
throw redirect('/documents');
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,8 @@ export default function PublicProfilePage({ loaderData }: Route.ComponentProps)
|
||||
|
||||
const { profile, templates } = publicProfile;
|
||||
|
||||
const { user } = useOptionalSession();
|
||||
const { sessionData } = useOptionalSession();
|
||||
const user = sessionData?.user;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-4 sm:py-32">
|
||||
|
||||
@@ -1,35 +1,26 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import { Link, Outlet } from 'react-router';
|
||||
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
|
||||
|
||||
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
import { Header as AuthenticatedHeader } from '~/components/general/app-header';
|
||||
|
||||
import type { Route } from './+types/_layout';
|
||||
|
||||
export function loader() {
|
||||
const session = getOptionalLoaderSession();
|
||||
|
||||
return {
|
||||
user: session?.user,
|
||||
teams: session?.teams || [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A layout to handle scenarios where the user is a recipient of a given resource
|
||||
* where we do not care whether they are authenticated or not.
|
||||
*
|
||||
* Such as direct template access, or signing.
|
||||
*/
|
||||
export default function RecipientLayout({ loaderData }: Route.ComponentProps) {
|
||||
const { user, teams } = loaderData;
|
||||
export default function RecipientLayout() {
|
||||
const { sessionData } = useOptionalSession();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
{user && <AuthenticatedHeader user={user} teams={teams} />}
|
||||
{sessionData?.user && (
|
||||
<AuthenticatedHeader user={sessionData.user} teams={sessionData.teams} />
|
||||
)}
|
||||
|
||||
<main className="mb-8 mt-8 px-4 md:mb-12 md:mt-12 md:px-8">
|
||||
<Outlet />
|
||||
@@ -38,6 +29,7 @@ export default function RecipientLayout({ loaderData }: Route.ComponentProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// Todo: Use generic error boundary.
|
||||
export function ErrorBoundary() {
|
||||
return (
|
||||
<div className="mx-auto flex min-h-[80vh] w-full items-center justify-center py-32">
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Plural } from '@lingui/react/macro';
|
||||
import { UsersIcon } from 'lucide-react';
|
||||
import { redirect } from 'react-router';
|
||||
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { getTemplateByDirectLinkToken } from '@documenso/lib/server-only/template/get-template-by-direct-link-token';
|
||||
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
||||
@@ -17,8 +17,8 @@ import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
|
||||
|
||||
import type { Route } from './+types/_index';
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
const session = getOptionalLoaderSession();
|
||||
export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
const session = await getOptionalSession(request);
|
||||
|
||||
const { token } = params;
|
||||
|
||||
@@ -48,7 +48,7 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||
|
||||
// Ensure typesafety when we add more options.
|
||||
const isAccessAuthValid = match(derivedRecipientAccessAuth)
|
||||
.with(DocumentAccessAuth.ACCOUNT, () => Boolean(session?.user))
|
||||
.with(DocumentAccessAuth.ACCOUNT, () => Boolean(session.user))
|
||||
.with(null, () => true)
|
||||
.exhaustive();
|
||||
|
||||
@@ -66,7 +66,8 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||
}
|
||||
|
||||
export default function DirectTemplatePage() {
|
||||
const { user } = useOptionalSession();
|
||||
const { sessionData } = useOptionalSession();
|
||||
const user = sessionData?.user;
|
||||
|
||||
const data = useSuperLoaderData<typeof loader>();
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Link, redirect } from 'react-router';
|
||||
import { getOptionalLoaderContext } from 'server/utils/get-loader-session';
|
||||
|
||||
import signingCelebration from '@documenso/assets/images/signing-celebration.png';
|
||||
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||
import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized';
|
||||
@@ -27,8 +28,10 @@ import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
|
||||
|
||||
import type { Route } from './+types/_index';
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
const { session, requestMetadata } = getOptionalLoaderContext();
|
||||
export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
const { requestMetadata } = getOptionalLoaderContext();
|
||||
|
||||
const { user } = await getOptionalSession(request);
|
||||
|
||||
const { token } = params;
|
||||
|
||||
@@ -36,8 +39,6 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||
throw new Response('Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
const user = session?.user;
|
||||
|
||||
const [document, recipient, fields, completedFields] = await Promise.all([
|
||||
getDocumentAndSenderByToken({
|
||||
token,
|
||||
@@ -136,7 +137,8 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||
export default function SigningPage() {
|
||||
const data = useSuperLoaderData<typeof loader>();
|
||||
|
||||
const { user } = useOptionalSession();
|
||||
const { sessionData } = useOptionalSession();
|
||||
const user = sessionData?.user;
|
||||
|
||||
if (!data.isDocumentAccessValid) {
|
||||
return (
|
||||
|
||||
@@ -6,10 +6,10 @@ import { Trans } from '@lingui/react/macro';
|
||||
import { type Document, DocumentStatus, FieldType, RecipientRole } from '@prisma/client';
|
||||
import { CheckCircle2, Clock8, FileSearch } from 'lucide-react';
|
||||
import { Link, useRevalidator } from 'react-router';
|
||||
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import signingCelebration from '@documenso/assets/images/signing-celebration.png';
|
||||
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||
import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized';
|
||||
@@ -31,8 +31,8 @@ import { DocumentSigningAuthPageView } from '~/components/general/document-signi
|
||||
|
||||
import type { Route } from './+types/complete';
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
const session = getOptionalLoaderSession();
|
||||
export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
const { user } = await getOptionalSession(request);
|
||||
|
||||
const { token } = params;
|
||||
|
||||
@@ -40,8 +40,6 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||
throw new Response('Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
const user = session?.user;
|
||||
|
||||
const document = await getDocumentAndSenderByToken({
|
||||
token,
|
||||
requireAccessAuth: false,
|
||||
@@ -100,7 +98,8 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||
export default function CompletedSigningPage({ loaderData }: Route.ComponentProps) {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const { user } = useOptionalSession();
|
||||
const { sessionData } = useOptionalSession();
|
||||
const user = sessionData?.user;
|
||||
|
||||
const {
|
||||
isDocumentAccessValid,
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Trans } from '@lingui/react/macro';
|
||||
import { FieldType } from '@prisma/client';
|
||||
import { XCircle } from 'lucide-react';
|
||||
import { Link } from 'react-router';
|
||||
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
|
||||
|
||||
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||
import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized';
|
||||
@@ -17,8 +17,8 @@ import { truncateTitle } from '~/utils/truncate-title';
|
||||
|
||||
import type { Route } from './+types/rejected';
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
const session = getOptionalLoaderSession();
|
||||
export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
const { user } = await getOptionalSession(request);
|
||||
|
||||
const { token } = params;
|
||||
|
||||
@@ -26,8 +26,6 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||
throw new Response('Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
const user = session?.user;
|
||||
|
||||
const document = await getDocumentAndSenderByToken({
|
||||
token,
|
||||
requireAccessAuth: false,
|
||||
@@ -76,7 +74,8 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||
}
|
||||
|
||||
export default function RejectedSigningPage({ loaderData }: Route.ComponentProps) {
|
||||
const { user } = useOptionalSession();
|
||||
const { sessionData } = useOptionalSession();
|
||||
const user = sessionData?.user;
|
||||
|
||||
const { isDocumentAccessValid, recipientReference, truncatedTitle } = loaderData;
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Trans } from '@lingui/react/macro';
|
||||
import type { Team } from '@prisma/client';
|
||||
import { DocumentStatus } from '@prisma/client';
|
||||
import { Link, redirect } from 'react-router';
|
||||
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
|
||||
|
||||
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||
@@ -13,8 +13,8 @@ import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
import type { Route } from './+types/waiting';
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
const session = getOptionalLoaderSession();
|
||||
export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
const { user } = await getOptionalSession(request);
|
||||
|
||||
const { token } = params;
|
||||
|
||||
@@ -37,7 +37,6 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||
|
||||
let isOwnerOrTeamMember = false;
|
||||
|
||||
const user = session?.user;
|
||||
let team: Team | null = null;
|
||||
|
||||
if (user) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Link, redirect } from 'react-router';
|
||||
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
|
||||
|
||||
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import {
|
||||
IS_GOOGLE_SSO_ENABLED,
|
||||
IS_OIDC_SSO_ENABLED,
|
||||
@@ -18,15 +18,15 @@ export function meta() {
|
||||
return appMetaTags('Sign In');
|
||||
}
|
||||
|
||||
export function loader() {
|
||||
const session = getOptionalLoaderSession();
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
const { isAuthenticated } = await getOptionalSession(request);
|
||||
|
||||
// SSR env variables.
|
||||
const isGoogleSSOEnabled = IS_GOOGLE_SSO_ENABLED;
|
||||
const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED;
|
||||
const oidcProviderLabel = OIDC_PROVIDER_LABEL;
|
||||
|
||||
if (session) {
|
||||
if (isAuthenticated) {
|
||||
throw redirect('/documents');
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Trans } from '@lingui/react/macro';
|
||||
import { TeamMemberInviteStatus } from '@prisma/client';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Link } from 'react-router';
|
||||
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
|
||||
|
||||
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
|
||||
import { declineTeamInvitation } from '@documenso/lib/server-only/team/decline-team-invitation';
|
||||
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
||||
@@ -12,8 +12,8 @@ import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
import type { Route } from './+types/team.decline.$token';
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
const session = getOptionalLoaderSession();
|
||||
export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
const session = await getOptionalSession(request);
|
||||
|
||||
const { token } = params;
|
||||
|
||||
@@ -74,7 +74,7 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||
} as const;
|
||||
}
|
||||
|
||||
const isSessionUserTheInvitedUser = user.id === session?.user.id;
|
||||
const isSessionUserTheInvitedUser = user.id === session?.user?.id;
|
||||
|
||||
return {
|
||||
state: 'Success',
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Trans } from '@lingui/react/macro';
|
||||
import { TeamMemberInviteStatus } from '@prisma/client';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Link } from 'react-router';
|
||||
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
|
||||
|
||||
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
|
||||
import { acceptTeamInvitation } from '@documenso/lib/server-only/team/accept-team-invitation';
|
||||
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
||||
@@ -12,8 +12,8 @@ import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
import type { Route } from './+types/team.invite.$token';
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
const session = getOptionalLoaderSession();
|
||||
export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
const session = await getOptionalSession(request);
|
||||
|
||||
const { token } = params;
|
||||
|
||||
@@ -77,7 +77,7 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||
} as const;
|
||||
}
|
||||
|
||||
const isSessionUserTheInvitedUser = user.id === session?.user.id;
|
||||
const isSessionUserTheInvitedUser = user.id === session.user?.id;
|
||||
|
||||
return {
|
||||
state: 'Success',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { data } from 'react-router';
|
||||
import { getLoaderSession } from 'server/utils/get-loader-session';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
@@ -18,7 +18,7 @@ import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
|
||||
|
||||
import type { Route } from './+types/direct.$url';
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
if (!params.url) {
|
||||
throw new Response('Not found', { status: 404 });
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||
);
|
||||
}
|
||||
|
||||
const { user } = getLoaderSession();
|
||||
const { user } = await getSession(request);
|
||||
|
||||
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
||||
documentAuth: template.authOptions,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { DocumentStatus, RecipientRole } from '@prisma/client';
|
||||
import { data } from 'react-router';
|
||||
import { getLoaderSession } from 'server/utils/get-loader-session';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
@@ -22,14 +22,14 @@ import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
|
||||
|
||||
import type { Route } from './+types/sign.$url';
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
if (!params.url) {
|
||||
throw new Response('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
const token = params.url;
|
||||
|
||||
const { user } = getLoaderSession();
|
||||
const { user } = await getSession(request);
|
||||
|
||||
const [document, fields, recipient] = await Promise.all([
|
||||
getDocumentAndSenderByToken({
|
||||
|
||||
@@ -1,79 +1,37 @@
|
||||
import type { Context, Next } from 'hono';
|
||||
|
||||
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 {
|
||||
type RequestMetadata,
|
||||
extractRequestMetadata,
|
||||
} from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { AppDebugger } from '@documenso/lib/utils/debugger';
|
||||
|
||||
const debug = new AppDebugger('Middleware');
|
||||
|
||||
export type AppContext = {
|
||||
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) => {
|
||||
const initTime = Date.now();
|
||||
|
||||
const request = c.req.raw;
|
||||
const url = new URL(request.url);
|
||||
|
||||
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)) {
|
||||
// debug.log('Pathname ignored', url.pathname);
|
||||
|
||||
setAppContext(c, {
|
||||
requestMetadata: extractRequestMetadata(request),
|
||||
session: null,
|
||||
});
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
const splitUrl = url.pathname.replace('.data', '').split('/');
|
||||
|
||||
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,
|
||||
});
|
||||
// Add context to any pages you want here.
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { getContext } from 'hono/context-storage';
|
||||
import { redirect } from 'react-router';
|
||||
import type { AppContext } from 'server/context';
|
||||
import type { HonoEnv } from 'server/router';
|
||||
|
||||
import type { AppSession } from '@documenso/lib/client-only/providers/session';
|
||||
|
||||
/**
|
||||
* Get the full context passed to the loader.
|
||||
*
|
||||
@@ -14,46 +11,3 @@ export const getOptionalLoaderContext = (): AppContext => {
|
||||
const { context } = getContext<HonoEnv>().var;
|
||||
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 { hc } from 'hono/client';
|
||||
import superjson from 'superjson';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
|
||||
import type { AuthAppType } from '../server';
|
||||
import type { SessionValidationResult } from '../server/lib/session/session';
|
||||
import { handleSignInRedirect } from '../server/lib/utils/redirect';
|
||||
import type {
|
||||
TDisableTwoFactorRequestSchema,
|
||||
@@ -45,8 +47,14 @@ export class AuthClient {
|
||||
window.location.href = redirectPath ?? this.signOutredirectPath;
|
||||
}
|
||||
|
||||
public async session() {
|
||||
return this.client.session.$get();
|
||||
public async getSession() {
|
||||
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> {
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { Hono } from 'hono';
|
||||
import superjson from 'superjson';
|
||||
|
||||
import type { SessionValidationResult } from '../lib/session/session';
|
||||
import { getOptionalSession } from '../lib/utils/get-session';
|
||||
|
||||
export const sessionRoute = new Hono().get('/session', async (c) => {
|
||||
const session: SessionValidationResult = await getOptionalSession(c);
|
||||
export const sessionRoute = new Hono()
|
||||
.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 type { SessionUser } from '@documenso/auth/server/lib/session/session';
|
||||
import type { Session } from '@documenso/prisma/client';
|
||||
import { useLocation } from 'react-router';
|
||||
|
||||
import type { TGetTeamByUrlResponse } from '../../server-only/team/get-team';
|
||||
import type { TGetTeamsResponse } from '../../server-only/team/get-teams';
|
||||
import { authClient } from '@documenso/auth/client';
|
||||
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 = {
|
||||
session: Session;
|
||||
user: SessionUser;
|
||||
currentTeam: TGetTeamByUrlResponse | null;
|
||||
teams: TGetTeamsResponse;
|
||||
};
|
||||
|
||||
interface SessionProviderProps {
|
||||
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 = () => {
|
||||
const context = useContext(SessionContext);
|
||||
@@ -28,18 +34,78 @@ export const useSession = () => {
|
||||
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 = () => {
|
||||
return (
|
||||
useContext(SessionContext) || {
|
||||
user: null,
|
||||
session: null,
|
||||
}
|
||||
);
|
||||
const context = useContext(SessionContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useOptionalSession must be used within a SessionProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export const SessionProvider = ({ children, session }: SessionProviderProps) => {
|
||||
return <SessionContext.Provider value={session}>{children}</SessionContext.Provider>;
|
||||
export const SessionProvider = ({ children, initialSession }: SessionProviderProps) => {
|
||||
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 { prisma } from '@documenso/prisma';
|
||||
import { SubscriptionSchema } from '@documenso/prisma/generated/zod/modelSchema/SubscriptionSchema';
|
||||
import { TeamMemberSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamMemberSchema';
|
||||
import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
|
||||
|
||||
@@ -12,6 +13,9 @@ export const ZGetTeamsResponseSchema = TeamSchema.extend({
|
||||
currentTeamMember: TeamMemberSchema.pick({
|
||||
role: true,
|
||||
}),
|
||||
subscription: SubscriptionSchema.pick({
|
||||
status: true,
|
||||
}).nullable(),
|
||||
}).array();
|
||||
|
||||
export type TGetTeamsResponse = z.infer<typeof ZGetTeamsResponseSchema>;
|
||||
@@ -26,6 +30,11 @@ export const getTeams = async ({ userId }: GetTeamsOptions): Promise<TGetTeamsRe
|
||||
},
|
||||
},
|
||||
include: {
|
||||
subscription: {
|
||||
select: {
|
||||
status: true,
|
||||
},
|
||||
},
|
||||
members: {
|
||||
where: {
|
||||
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 { 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 type { AppRouter } from '../server/router';
|
||||
|
||||
export { getQueryKey } from '@trpc/react-query';
|
||||
@@ -39,24 +38,27 @@ export interface TrpcProviderProps {
|
||||
export function TrpcProvider({ children, headers }: TrpcProviderProps) {
|
||||
const [queryClient] = useState(() => new QueryClient());
|
||||
|
||||
const [trpcClient] = useState(() =>
|
||||
trpc.createClient({
|
||||
links: [
|
||||
splitLink({
|
||||
condition: (op) => op.context.skipBatch === true,
|
||||
true: httpLink({
|
||||
url: `${getBaseUrl()}/api/trpc`,
|
||||
headers,
|
||||
transformer: SuperJSON,
|
||||
// May cause remounting issues.
|
||||
const trpcClient = useMemo(
|
||||
() =>
|
||||
trpc.createClient({
|
||||
links: [
|
||||
splitLink({
|
||||
condition: (op) => op.context.skipBatch === true,
|
||||
true: httpLink({
|
||||
url: `${getBaseUrl()}/api/trpc`,
|
||||
headers,
|
||||
transformer: SuperJSON,
|
||||
}),
|
||||
false: httpBatchLink({
|
||||
url: `${getBaseUrl()}/api/trpc`,
|
||||
headers,
|
||||
transformer: SuperJSON,
|
||||
}),
|
||||
}),
|
||||
false: httpBatchLink({
|
||||
url: `${getBaseUrl()}/api/trpc`,
|
||||
headers,
|
||||
transformer: SuperJSON,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
[headers],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user