This commit is contained in:
 Илья Шалонин 2025-10-29 13:43:43 +05:00
commit 8538ab969a
434 changed files with 44051 additions and 0 deletions

View file

@ -0,0 +1,73 @@
import { config } from "@repo/config";
import { getSession } from "@saas/auth/lib/server";
import { SettingsMenu } from "@saas/settings/components/SettingsMenu";
import { PageHeader } from "@saas/shared/components/PageHeader";
import { SidebarContentLayout } from "@saas/shared/components/SidebarContentLayout";
import { Logo } from "@shared/components/Logo";
import { Building2Icon, UsersIcon } from "lucide-react";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
import type { PropsWithChildren } from "react";
export default async function AdminLayout({ children }: PropsWithChildren) {
const t = await getTranslations();
const session = await getSession();
if (!session) {
return redirect("/auth/login");
}
if (session.user?.role !== "admin") {
redirect("/app");
}
return (
<>
<PageHeader
title={t("admin.title")}
subtitle={t("admin.description")}
/>
<SidebarContentLayout
sidebar={
<SettingsMenu
menuItems={[
{
avatar: (
<Logo
className="size-8"
withLabel={false}
/>
),
title: t("admin.title"),
items: [
{
title: t("admin.menu.users"),
href: "/app/admin/users",
icon: (
<UsersIcon className="size-4 opacity-50" />
),
},
...(config.organizations.enable
? [
{
title: t(
"admin.menu.organizations",
),
href: "/app/admin/organizations",
icon: (
<Building2Icon className="size-4 opacity-50" />
),
},
]
: []),
],
},
]}
/>
}
>
{children}
</SidebarContentLayout>
</>
);
}

View file

@ -0,0 +1,52 @@
import { auth } from "@repo/auth";
import { OrganizationForm } from "@saas/admin/component/organizations/OrganizationForm";
import { getAdminPath } from "@saas/admin/lib/links";
import { fullOrganizationQueryKey } from "@saas/organizations/lib/api";
import { getServerQueryClient } from "@shared/lib/server";
import { HydrationBoundary, dehydrate } from "@tanstack/react-query";
import { Button } from "@ui/components/button";
import { ArrowLeftIcon } from "lucide-react";
import { getTranslations } from "next-intl/server";
import { headers } from "next/headers";
import Link from "next/link";
export default async function OrganizationFormPage({
params,
searchParams,
}: {
params: Promise<{ id: string }>;
searchParams: Promise<{ backTo?: string }>;
}) {
const { id } = await params;
const { backTo } = await searchParams;
const t = await getTranslations();
const queryClient = getServerQueryClient();
await queryClient.prefetchQuery({
queryKey: fullOrganizationQueryKey(id),
queryFn: async () =>
await auth.api.getFullOrganization({
query: {
organizationId: id,
},
headers: await headers(),
}),
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<div>
<div className="mb-2 flex justify-start">
<Button variant="link" size="sm" asChild className="px-0">
<Link href={backTo ?? getAdminPath("/organizations")}>
<ArrowLeftIcon className="mr-1.5 size-4" />
{t("admin.organizations.backToList")}
</Link>
</Button>
</div>
<OrganizationForm organizationId={id} />
</div>
</HydrationBoundary>
);
}

View file

@ -0,0 +1,5 @@
import { OrganizationList } from "@saas/admin/component/organizations/OrganizationList";
export default function AdminOrganizationsPage() {
return <OrganizationList />;
}

View file

@ -0,0 +1,9 @@
import { UserList } from "@saas/admin/component/users/UserList";
export default function AdminUserPage() {
return (
<div>
<UserList />
</div>
);
}

View file

@ -0,0 +1,54 @@
import { AiChat } from "@saas/ai/components/AiChat";
import { aiChatListQueryKey, aiChatQueryKey } from "@saas/ai/lib/api";
import { PageHeader } from "@saas/shared/components/PageHeader";
import { getServerApiClient, getServerQueryClient } from "@shared/lib/server";
export default async function AiDemoPage() {
const queryClient = getServerQueryClient();
const apiClient = await getServerApiClient();
const chats = await (async () => {
const response = await apiClient.ai.chats.$get({});
if (!response.ok) {
throw new Error("Failed to fetch chats");
}
return response.json().then((data) => data.chats);
})();
await queryClient.prefetchQuery({
queryKey: aiChatListQueryKey(),
queryFn: async () => chats,
});
if (chats.length > 0) {
await queryClient.prefetchQuery({
queryKey: aiChatQueryKey(chats[0].id),
queryFn: async () => {
const response = await apiClient.ai.chats[":id"].$get({
param: {
id: chats[0].id,
},
});
if (!response.ok) {
throw new Error("Failed to fetch chat");
}
return response.json();
},
});
}
return (
<>
<PageHeader
title="AI Chatbot"
subtitle="This is an example chatbot built with the OpenAI API"
/>
<AiChat />
</>
);
}

View file

@ -0,0 +1,6 @@
import { AppWrapper } from "@saas/shared/components/AppWrapper";
import type { PropsWithChildren } from "react";
export default function UserLayout({ children }: PropsWithChildren) {
return <AppWrapper>{children}</AppWrapper>;
}

View file

@ -0,0 +1,26 @@
import { getSession } from "@saas/auth/lib/server";
import { PageHeader } from "@saas/shared/components/PageHeader";
import UserStart from "@saas/start/UserStart";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
export default async function AppStartPage() {
const session = await getSession();
if (!session) {
return redirect("/auth/login");
}
const t = await getTranslations();
return (
<div className="">
<PageHeader
title={t("start.welcome", { name: session?.user.name })}
subtitle={t("start.subtitle")}
/>
<UserStart />
</div>
);
}

View file

@ -0,0 +1,40 @@
import { createPurchasesHelper } from "@repo/payments/lib/helper";
import { getSession } from "@saas/auth/lib/server";
import { ActivePlan } from "@saas/payments/components/ActivePlan";
import { ChangePlan } from "@saas/payments/components/ChangePlan";
import { purchasesQueryKey } from "@saas/payments/lib/api";
import { getPurchases } from "@saas/payments/lib/server";
import { SettingsList } from "@saas/shared/components/SettingsList";
import { getServerQueryClient } from "@shared/lib/server";
import { getTranslations } from "next-intl/server";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: t("settings.billing.title"),
};
}
export default async function BillingSettingsPage() {
const session = await getSession();
const purchases = await getPurchases();
const queryClient = getServerQueryClient();
await queryClient.prefetchQuery({
queryKey: purchasesQueryKey(),
queryFn: () => purchases,
});
const { activePlan } = createPurchasesHelper(purchases);
return (
<SettingsList>
{activePlan && <ActivePlan />}
<ChangePlan
userId={session?.user.id}
activePlanId={activePlan?.id}
/>
</SettingsList>
);
}

View file

@ -0,0 +1,27 @@
import { getSession } from "@saas/auth/lib/server";
import { DeleteAccountForm } from "@saas/settings/components/DeleteAccountForm";
import { SettingsList } from "@saas/shared/components/SettingsList";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: t("settings.account.title"),
};
}
export default async function AccountSettingsPage() {
const session = await getSession();
if (!session) {
return redirect("/auth/login");
}
return (
<SettingsList>
<DeleteAccountForm />
</SettingsList>
);
}

View file

@ -0,0 +1,34 @@
import { config } from "@repo/config";
import { getSession } from "@saas/auth/lib/server";
import { ChangeEmailForm } from "@saas/settings/components/ChangeEmailForm";
import { ChangeNameForm } from "@saas/settings/components/ChangeNameForm";
import { UserAvatarForm } from "@saas/settings/components/UserAvatarForm";
import { UserLanguageForm } from "@saas/settings/components/UserLanguageForm";
import { SettingsList } from "@saas/shared/components/SettingsList";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: t("settings.account.title"),
};
}
export default async function AccountSettingsPage() {
const session = await getSession();
if (!session) {
return redirect("/auth/login");
}
return (
<SettingsList>
<UserAvatarForm />
{config.i18n.enabled && <UserLanguageForm />}
<ChangeNameForm />
<ChangeEmailForm />
</SettingsList>
);
}

View file

@ -0,0 +1,77 @@
import { config } from "@repo/config";
import { getSession } from "@saas/auth/lib/server";
import { SettingsMenu } from "@saas/settings/components/SettingsMenu";
import { PageHeader } from "@saas/shared/components/PageHeader";
import { SidebarContentLayout } from "@saas/shared/components/SidebarContentLayout";
import { UserAvatar } from "@shared/components/UserAvatar";
import {
CreditCardIcon,
LockKeyholeIcon,
SettingsIcon,
TriangleAlertIcon,
} from "lucide-react";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
import type { PropsWithChildren } from "react";
export default async function SettingsLayout({ children }: PropsWithChildren) {
const t = await getTranslations();
const session = await getSession();
if (!session) {
return redirect("/auth/login");
}
const menuItems = [
{
title: t("settings.menu.account.title"),
avatar: (
<UserAvatar
name={session.user.name ?? ""}
avatarUrl={session.user.image}
/>
),
items: [
{
title: t("settings.menu.account.general"),
href: "/app/settings/general",
icon: <SettingsIcon className="size-4 opacity-50" />,
},
{
title: t("settings.menu.account.security"),
href: "/app/settings/security",
icon: <LockKeyholeIcon className="size-4 opacity-50" />,
},
...(config.users.enableBilling
? [
{
title: t("settings.menu.account.billing"),
href: "/app/settings/billing",
icon: (
<CreditCardIcon className="size-4 opacity-50" />
),
},
]
: []),
{
title: t("settings.menu.account.dangerZone"),
href: "/app/settings/danger-zone",
icon: <TriangleAlertIcon className="size-4 opacity-50" />,
},
],
},
];
return (
<>
<PageHeader
title={t("settings.account.title")}
subtitle={t("settings.account.subtitle")}
/>
<SidebarContentLayout
sidebar={<SettingsMenu menuItems={menuItems} />}
>
{children}
</SidebarContentLayout>
</>
);
}

View file

@ -0,0 +1,68 @@
import { config } from "@repo/config";
import { userAccountQueryKey, userPasskeyQueryKey } from "@saas/auth/lib/api";
import {
getSession,
getUserAccounts,
getUserPasskeys,
} from "@saas/auth/lib/server";
import { ActiveSessionsBlock } from "@saas/settings/components/ActiveSessionsBlock";
import { ChangePasswordForm } from "@saas/settings/components/ChangePassword";
import { ConnectedAccountsBlock } from "@saas/settings/components/ConnectedAccountsBlock";
import { PasskeysBlock } from "@saas/settings/components/PasskeysBlock";
import { SetPasswordForm } from "@saas/settings/components/SetPassword";
import { TwoFactorBlock } from "@saas/settings/components/TwoFactorBlock";
import { SettingsList } from "@saas/shared/components/SettingsList";
import { getServerQueryClient } from "@shared/lib/server";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: t("settings.account.security.title"),
};
}
export default async function AccountSettingsPage() {
const session = await getSession();
if (!session) {
return redirect("/auth/login");
}
const userAccounts = await getUserAccounts();
const userHasPassword = userAccounts?.some(
(account) => account.provider === "credential",
);
const queryClient = getServerQueryClient();
await queryClient.prefetchQuery({
queryKey: userAccountQueryKey,
queryFn: () => getUserAccounts(),
});
if (config.auth.enablePasskeys) {
await queryClient.prefetchQuery({
queryKey: userPasskeyQueryKey,
queryFn: () => getUserPasskeys(),
});
}
return (
<SettingsList>
{config.auth.enablePasswordLogin &&
(userHasPassword ? (
<ChangePasswordForm />
) : (
<SetPasswordForm />
))}
{config.auth.enableSocialLogin && <ConnectedAccountsBlock />}
{config.auth.enablePasskeys && <PasskeysBlock />}
{config.auth.enableTwoFactor && <TwoFactorBlock />}
<ActiveSessionsBlock />
</SettingsList>
);
}

View file

@ -0,0 +1,8 @@
import { AuthWrapper } from "@saas/shared/components/AuthWrapper";
import type { PropsWithChildren } from "react";
export default function WithoutOrganizationSlugLayout({
children,
}: PropsWithChildren) {
return <AuthWrapper>{children}</AuthWrapper>;
}

View file

@ -0,0 +1,21 @@
import { config } from "@repo/config";
import { getOrganizationList } from "@saas/auth/lib/server";
import { CreateOrganizationForm } from "@saas/organizations/components/CreateOrganizationForm";
import { redirect } from "next/navigation";
export const dynamic = "force-dynamic";
export default async function NewOrganizationPage() {
const organizations = await getOrganizationList();
if (
!config.organizations.enable ||
(!config.organizations.enableUsersToCreateOrganizations &&
(!config.organizations.requireOrganization ||
organizations.length > 0))
) {
return redirect("/app");
}
return <CreateOrganizationForm />;
}

View file

@ -0,0 +1,35 @@
import { auth } from "@repo/auth";
import { getOrganizationById } from "@repo/database";
import { OrganizationInvitationModal } from "@saas/organizations/components/OrganizationInvitationModal";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export default async function OrganizationInvitationPage({
params,
}: {
params: Promise<{ invitationId: string }>;
}) {
const { invitationId } = await params;
const invitation = await auth.api.getInvitation({
query: {
id: invitationId,
},
headers: await headers(),
});
if (!invitation) {
redirect("/app");
}
const organization = await getOrganizationById(invitation.organizationId);
return (
<OrganizationInvitationModal
organizationName={invitation.organizationName}
organizationSlug={invitation.organizationSlug}
logoUrl={organization?.logo || undefined}
invitationId={invitationId}
/>
);
}

View file

@ -0,0 +1,72 @@
import { AiChat } from "@saas/ai/components/AiChat";
import { aiChatListQueryKey, aiChatQueryKey } from "@saas/ai/lib/api";
import { getActiveOrganization } from "@saas/auth/lib/server";
import { PageHeader } from "@saas/shared/components/PageHeader";
import { getServerApiClient, getServerQueryClient } from "@shared/lib/server";
import { redirect } from "next/navigation";
export default async function AiDemoPage({
params,
}: {
params: Promise<{ organizationSlug: string }>;
}) {
const { organizationSlug } = await params;
const organization = await getActiveOrganization(organizationSlug);
const queryClient = getServerQueryClient();
const apiClient = await getServerApiClient();
if (!organization) {
redirect("/app");
}
const organizationId = organization.id;
const chats = await (async () => {
const response = await apiClient.ai.chats.$get({
query: {
organizationId,
},
});
if (!response.ok) {
throw new Error("Failed to fetch chats");
}
return (await response.json()).chats;
})();
await queryClient.prefetchQuery({
queryKey: aiChatListQueryKey(organizationId),
queryFn: async () => chats,
});
if (chats.length > 0) {
await queryClient.prefetchQuery({
queryKey: aiChatQueryKey(chats[0].id),
queryFn: async () => {
const response = await apiClient.ai.chats[":id"].$get({
param: {
id: chats[0].id,
},
});
if (!response.ok) {
throw new Error("Failed to fetch chat");
}
return response.json();
},
});
}
return (
<>
<PageHeader
title="AI Chatbot"
subtitle="This is an example chatbot built with the OpenAI API"
/>
<AiChat organizationId={organizationId} />
</>
);
}

View file

@ -0,0 +1,42 @@
import { config } from "@repo/config";
import { getActiveOrganization } from "@saas/auth/lib/server";
import { activeOrganizationQueryKey } from "@saas/organizations/lib/api";
import { purchasesQueryKey } from "@saas/payments/lib/api";
import { getPurchases } from "@saas/payments/lib/server";
import { AppWrapper } from "@saas/shared/components/AppWrapper";
import { getServerQueryClient } from "@shared/lib/server";
import { notFound } from "next/navigation";
import type { PropsWithChildren } from "react";
export default async function OrganizationLayout({
children,
params,
}: PropsWithChildren<{
params: Promise<{
organizationSlug: string;
}>;
}>) {
const { organizationSlug } = await params;
const organization = await getActiveOrganization(organizationSlug);
if (!organization) {
return notFound();
}
const queryClient = getServerQueryClient();
await queryClient.prefetchQuery({
queryKey: activeOrganizationQueryKey(organizationSlug),
queryFn: () => organization,
});
if (config.users.enableBilling) {
await queryClient.prefetchQuery({
queryKey: purchasesQueryKey(organization.id),
queryFn: () => getPurchases(organization.id),
});
}
return <AppWrapper>{children}</AppWrapper>;
}

View file

@ -0,0 +1,47 @@
import { getActiveOrganization } from "@saas/auth/lib/server";
import OrganizationStart from "@saas/organizations/components/OrganizationStart";
import { PageHeader } from "@saas/shared/components/PageHeader";
import { getTranslations } from "next-intl/server";
import { notFound } from "next/navigation";
export async function generateMetadata({
params,
}: {
params: Promise<{ organizationSlug: string }>;
}) {
const { organizationSlug } = await params;
const activeOrganization = await getActiveOrganization(
organizationSlug as string,
);
return {
title: activeOrganization?.name,
};
}
export default async function OrganizationPage({
params,
}: { params: Promise<{ organizationSlug: string }> }) {
const { organizationSlug } = await params;
const t = await getTranslations();
const activeOrganization = await getActiveOrganization(
organizationSlug as string,
);
if (!activeOrganization) {
return notFound();
}
return (
<div>
<PageHeader
title={activeOrganization.name}
subtitle={t("organizations.start.subtitle")}
/>
<OrganizationStart />
</div>
);
}

View file

@ -0,0 +1,51 @@
import { createPurchasesHelper } from "@repo/payments/lib/helper";
import { getActiveOrganization } from "@saas/auth/lib/server";
import { ActivePlan } from "@saas/payments/components/ActivePlan";
import { ChangePlan } from "@saas/payments/components/ChangePlan";
import { purchasesQueryKey } from "@saas/payments/lib/api";
import { getPurchases } from "@saas/payments/lib/server";
import { SettingsList } from "@saas/shared/components/SettingsList";
import { getServerQueryClient } from "@shared/lib/server";
import { getTranslations } from "next-intl/server";
import { notFound } from "next/navigation";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: t("settings.billing.title"),
};
}
export default async function BillingSettingsPage({
params,
}: {
params: Promise<{ organizationSlug: string }>;
}) {
const { organizationSlug } = await params;
const organization = await getActiveOrganization(organizationSlug);
if (!organization) {
return notFound();
}
const purchases = await getPurchases(organization.id);
const queryClient = getServerQueryClient();
await queryClient.prefetchQuery({
queryKey: purchasesQueryKey(organization.id),
queryFn: () => purchases,
});
const { activePlan } = createPurchasesHelper(purchases);
return (
<SettingsList>
{activePlan && <ActivePlan organizationId={organization.id} />}
<ChangePlan
organizationId={organization.id}
activePlanId={activePlan?.id}
/>
</SettingsList>
);
}

View file

@ -0,0 +1,19 @@
import { DeleteOrganizationForm } from "@saas/organizations/components/DeleteOrganizationForm";
import { SettingsList } from "@saas/shared/components/SettingsList";
import { getTranslations } from "next-intl/server";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: t("organizations.settings.dangerZone.title"),
};
}
export default function OrganizationSettingsPage() {
return (
<SettingsList>
<DeleteOrganizationForm />
</SettingsList>
);
}

View file

@ -0,0 +1,21 @@
import { ChangeOrganizationNameForm } from "@saas/organizations/components/ChangeOrganizationNameForm";
import { OrganizationLogoForm } from "@saas/organizations/components/OrganizationLogoForm";
import { SettingsList } from "@saas/shared/components/SettingsList";
import { getTranslations } from "next-intl/server";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: t("organizations.settings.title"),
};
}
export default function OrganizationSettingsPage() {
return (
<SettingsList>
<OrganizationLogoForm />
<ChangeOrganizationNameForm />
</SettingsList>
);
}

View file

@ -0,0 +1,99 @@
import { isOrganizationAdmin } from "@repo/auth/lib/helper";
import { config } from "@repo/config";
import { getActiveOrganization, getSession } from "@saas/auth/lib/server";
import { OrganizationLogo } from "@saas/organizations/components/OrganizationLogo";
import { SettingsMenu } from "@saas/settings/components/SettingsMenu";
import { PageHeader } from "@saas/shared/components/PageHeader";
import { SidebarContentLayout } from "@saas/shared/components/SidebarContentLayout";
import {
CreditCardIcon,
Settings2Icon,
TriangleAlertIcon,
Users2Icon,
} from "lucide-react";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
import type { PropsWithChildren } from "react";
export default async function SettingsLayout({
children,
params,
}: PropsWithChildren<{
params: Promise<{ organizationSlug: string }>;
}>) {
const t = await getTranslations();
const session = await getSession();
const { organizationSlug } = await params;
const organization = await getActiveOrganization(organizationSlug);
if (!organization) {
redirect("/app");
}
const userIsOrganizationAdmin = isOrganizationAdmin(
organization,
session?.user,
);
const organizationSettingsBasePath = `/app/${organizationSlug}/settings`;
const menuItems = [
{
title: t("settings.menu.organization.title"),
avatar: (
<OrganizationLogo
name={organization.name}
logoUrl={organization.logo}
/>
),
items: [
{
title: t("settings.menu.organization.general"),
href: `${organizationSettingsBasePath}/general`,
icon: <Settings2Icon className="size-4 opacity-50" />,
},
{
title: t("settings.menu.organization.members"),
href: `${organizationSettingsBasePath}/members`,
icon: <Users2Icon className="size-4 opacity-50" />,
},
...(config.organizations.enable &&
config.organizations.enableBilling &&
userIsOrganizationAdmin
? [
{
title: t("settings.menu.organization.billing"),
href: `${organizationSettingsBasePath}/billing`,
icon: (
<CreditCardIcon className="size-4 opacity-50" />
),
},
{
title: t(
"settings.menu.organization.dangerZone",
),
href: `${organizationSettingsBasePath}/danger-zone`,
icon: (
<TriangleAlertIcon className="size-4 opacity-50" />
),
},
]
: []),
],
},
];
return (
<>
<PageHeader
title={t("organizations.settings.title")}
subtitle={t("organizations.settings.subtitle")}
/>
<SidebarContentLayout
sidebar={<SettingsMenu menuItems={menuItems} />}
>
{children}
</SidebarContentLayout>
</>
);
}

View file

@ -0,0 +1,37 @@
import { isOrganizationAdmin } from "@repo/auth/lib/helper";
import { getActiveOrganization, getSession } from "@saas/auth/lib/server";
import { InviteMemberForm } from "@saas/organizations/components/InviteMemberForm";
import { OrganizationMembersBlock } from "@saas/organizations/components/OrganizationMembersBlock";
import { SettingsList } from "@saas/shared/components/SettingsList";
import { getTranslations } from "next-intl/server";
import { notFound } from "next/navigation";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: t("organizations.settings.title"),
};
}
export default async function OrganizationSettingsPage({
params,
}: {
params: Promise<{ organizationSlug: string }>;
}) {
const session = await getSession();
const { organizationSlug } = await params;
const organization = await getActiveOrganization(organizationSlug);
if (!organization) {
return notFound();
}
return (
<SettingsList>
{isOrganizationAdmin(organization, session?.user) && (
<InviteMemberForm organizationId={organization.id} />
)}
<OrganizationMembersBlock organizationId={organization.id} />
</SettingsList>
);
}

View file

@ -0,0 +1,5 @@
import { notFound } from "next/navigation";
export default function CatchAll() {
notFound();
}

View file

@ -0,0 +1,71 @@
import { config } from "@repo/config";
import { createPurchasesHelper } from "@repo/payments/lib/helper";
import { getOrganizationList, getSession } from "@saas/auth/lib/server";
import { PricingTable } from "@saas/payments/components/PricingTable";
import { getPurchases } from "@saas/payments/lib/server";
import { AuthWrapper } from "@saas/shared/components/AuthWrapper";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
export const dynamic = "force-dynamic";
export const revalidate = 0;
export async function generateMetadata() {
const t = await getTranslations();
return {
title: t("choosePlan.title"),
};
}
export default async function ChoosePlanPage() {
const t = await getTranslations();
const session = await getSession();
if (!session) {
return redirect("/auth/login");
}
let organizationId: string | undefined;
if (config.organizations.enable && config.organizations.enableBilling) {
const organization = (await getOrganizationList()).at(0);
if (!organization) {
redirect("/app/new-organization");
}
organizationId = organization.id;
}
const purchases = await getPurchases(organizationId);
const { activePlan } = createPurchasesHelper(purchases);
if (activePlan) {
return redirect("/app");
}
return (
<AuthWrapper contentClass="max-w-5xl">
<div className="mb-4 text-center">
<h1 className="text-center font-bold text-2xl lg:text-3xl">
{t("choosePlan.title")}
</h1>
<p className="text-muted-foreground text-sm lg:text-base">
{t("choosePlan.description")}
</p>
</div>
<div>
<PricingTable
{...(organizationId
? {
organizationId,
}
: {
userId: session.user.id,
})}
/>
</div>
</AuthWrapper>
);
}

View file

@ -0,0 +1,52 @@
import { config } from "@repo/config";
import { SessionProvider } from "@saas/auth/components/SessionProvider";
import { sessionQueryKey } from "@saas/auth/lib/api";
import { getOrganizationList, getSession } from "@saas/auth/lib/server";
import { ActiveOrganizationProvider } from "@saas/organizations/components/ActiveOrganizationProvider";
import { organizationListQueryKey } from "@saas/organizations/lib/api";
import { purchasesQueryKey } from "@saas/payments/lib/api";
import { getPurchases } from "@saas/payments/lib/server";
import { ConfirmationAlertProvider } from "@saas/shared/components/ConfirmationAlertProvider";
import { getServerQueryClient } from "@shared/lib/server";
import { HydrationBoundary, dehydrate } from "@tanstack/react-query";
import type { PropsWithChildren } from "react";
export const dynamic = "force-dynamic";
export const revalidate = 0;
export default async function Layout({ children }: PropsWithChildren) {
const session = await getSession();
const queryClient = getServerQueryClient();
await queryClient.prefetchQuery({
queryKey: sessionQueryKey,
queryFn: () => session,
});
if (config.organizations.enable) {
await queryClient.prefetchQuery({
queryKey: organizationListQueryKey,
queryFn: getOrganizationList,
});
}
if (config.users.enableBilling) {
await queryClient.prefetchQuery({
queryKey: purchasesQueryKey(),
queryFn: () => getPurchases(),
});
}
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<SessionProvider>
<ActiveOrganizationProvider>
<ConfirmationAlertProvider>
{children}
</ConfirmationAlertProvider>
</ActiveOrganizationProvider>
</SessionProvider>
</HydrationBoundary>
);
}

View file

@ -0,0 +1,5 @@
import { NotFound } from "@marketing/shared/components/NotFound";
export default async function NotFoundPage() {
return <NotFound />;
}

View file

@ -0,0 +1,35 @@
import { config } from "@repo/config";
import { getSession } from "@saas/auth/lib/server";
import { OnboardingForm } from "@saas/onboarding/components/OnboardingForm";
import { AuthWrapper } from "@saas/shared/components/AuthWrapper";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
export const dynamic = "force-dynamic";
export const revalidate = 0;
export async function generateMetadata() {
const t = await getTranslations();
return {
title: t("onboarding.title"),
};
}
export default async function OnboardingPage() {
const session = await getSession();
if (!session) {
return redirect("/auth/login");
}
if (!config.users.enableOnboarding || session.user.onboardingComplete) {
return redirect("/app");
}
return (
<AuthWrapper>
<OnboardingForm />
</AuthWrapper>
);
}

View file

@ -0,0 +1,17 @@
import { ForgotPasswordForm } from "@saas/auth/components/ForgotPasswordForm";
import { getTranslations } from "next-intl/server";
export const dynamic = "force-dynamic";
export const revalidate = 0;
export async function generateMetadata() {
const t = await getTranslations();
return {
title: t("auth.forgotPassword.title"),
};
}
export default function ForgotPasswordPage() {
return <ForgotPasswordForm />;
}

View file

@ -0,0 +1,14 @@
import { SessionProvider } from "@saas/auth/components/SessionProvider";
import { AuthWrapper } from "@saas/shared/components/AuthWrapper";
import type { PropsWithChildren } from "react";
export const dynamic = "force-dynamic";
export const revalidate = 0;
export default function AuthLayout({ children }: PropsWithChildren) {
return (
<SessionProvider>
<AuthWrapper>{children}</AuthWrapper>
</SessionProvider>
);
}

View file

@ -0,0 +1,17 @@
import { LoginForm } from "@saas/auth/components/LoginForm";
import { getTranslations } from "next-intl/server";
export const dynamic = "force-dynamic";
export const revalidate = 0;
export async function generateMetadata() {
const t = await getTranslations();
return {
title: t("auth.login.title"),
};
}
export default function LoginPage() {
return <LoginForm />;
}

View file

@ -0,0 +1,17 @@
import { ResetPasswordForm } from "@saas/auth/components/ResetPasswordForm";
import { getTranslations } from "next-intl/server";
export const dynamic = "force-dynamic";
export const revalidate = 0;
export async function generateMetadata() {
const t = await getTranslations();
return {
title: t("auth.resetPassword.title"),
};
}
export default function ResetPasswordPage() {
return <ResetPasswordForm />;
}

View file

@ -0,0 +1,48 @@
import { config } from "@repo/config";
import { SignupForm } from "@saas/auth/components/SignupForm";
import { getInvitation } from "@saas/auth/lib/server";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
import { withQuery } from "ufo";
export const dynamic = "force-dynamic";
export const revalidate = 0;
export async function generateMetadata() {
const t = await getTranslations();
return {
title: t("auth.signup.title"),
};
}
export default async function SignupPage({
searchParams,
}: {
searchParams: Promise<{
[key: string]: string | string[] | undefined;
invitationId?: string;
}>;
}) {
const params = await searchParams;
const { invitationId } = params;
if (!(config.auth.enableSignup || invitationId)) {
return redirect(withQuery("/auth/login", params));
}
if (invitationId) {
const invitation = await getInvitation(invitationId);
if (
!invitation ||
invitation.status !== "pending" ||
invitation.expiresAt.getTime() < new Date().getTime()
) {
return redirect(withQuery("/auth/login", params));
}
return <SignupForm prefillEmail={invitation.email} />;
}
return <SignupForm />;
}

View file

@ -0,0 +1,17 @@
import { OtpForm } from "@saas/auth/components/OtpForm";
import { getTranslations } from "next-intl/server";
export const dynamic = "force-dynamic";
export const revalidate = 0;
export async function generateMetadata() {
const t = await getTranslations();
return {
title: t("auth.verify.title"),
};
}
export default function VerifyPage() {
return <OtpForm />;
}

View file

@ -0,0 +1,17 @@
import { Document } from "@shared/components/Document";
import { NextIntlClientProvider } from "next-intl";
import { getLocale, getMessages } from "next-intl/server";
import type { PropsWithChildren } from "react";
export default async function LocaleLayout({ children }: PropsWithChildren) {
const locale = await getLocale();
const messages = await getMessages();
return (
<Document locale={locale}>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</Document>
);
}