mirror of
https://github.com/documenso/documenso.git
synced 2025-11-22 20:51:33 +10:00
fix: rework sessions
This commit is contained in:
@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user