From 014e911a96c8f2966ee00d023eac48a071790405 Mon Sep 17 00:00:00 2001 From: Antoine Grondin Date: Mon, 31 Aug 2026 20:21:18 +1000 Subject: [PATCH] chore: close signups and checkout, show a "taking a break" notice humanlog.io is paused (https://www.webscale.lol/blog/humanlog-retro): the log parser is being open-sourced and the hosted observability tool is moving to minitape. People who signed up to try the app were given a $0/month "Free" plan and are now emailing to have it turned off, so the front door needs to close. The real switch is in src/lib/auth.ts. The shared better-auth config is spread into ours, so the overrides have to come after that spread and merge into it rather than replace it, or the callbacks it carries go with them. Disabling emailAndPassword closes /api/auth/sign-in/email and /api/auth/sign-up/email; emptying socialProviders removes the GitHub and Google routes. That last part matters: signIn.social auto-creates an account for any unknown identity, and those buttons render on the sign-IN page too, so closing /sign-up alone would not have stopped new accounts. The sign-in and sign-up layouts now render the notice instead of gating their children behind the release_auth_temp PostHog flag, which covers the nested routes (email-verify, reset-password, ...) in one edit each. Pricing, purchase, create-org, onboarding and env-new render it too, so no live Stripe Elements checkout is reachable. The pages underneath are left in the tree for whenever the lights come back on. The notice reuses the getlaunchlist form as a minitape launch list, and the previously-unmounted DismissibleBanner as a site-wide notice. The hero CTA drops its signup framing but keeps the installer, since the CLI still works locally. Verified against a dev server: sign-up/email and sign-in/email return 400, social returns 404, and all seven auth/checkout routes render the notice with no js.stripe.com. The backend refuses account and session creation independently. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01YTkqPnYuuk4wirAoZAeuu6 --- src/app/[org]/env-new/page.tsx | 31 +- src/app/create-org/page.tsx | 25 +- src/app/layout.tsx | 6 +- src/app/onboarding/create-org/page.tsx | 27 +- src/app/onboarding/pricing/page.tsx | 19 +- src/app/pricing/page.tsx | 23 +- src/app/sign-in/layout.tsx | 46 +- src/app/sign-up/layout.tsx | 46 +- src/app/user/purchase/page.tsx | 412 +----------------- src/components/break-banner.tsx | 23 + src/components/header/user-actions.tsx | 20 +- .../landing-page/hero-marketing.tsx | 2 +- .../sections/cta-banner-section.tsx | 26 +- .../landing-page/sections/faq-section.tsx | 9 +- .../landing-page/shared/cloud-beta-signup.tsx | 2 +- .../landing-page/shared/install-cta.tsx | 2 +- src/components/taking-a-break.tsx | 72 +++ src/lib/auth.ts | 44 +- 18 files changed, 231 insertions(+), 604 deletions(-) create mode 100644 src/components/break-banner.tsx create mode 100644 src/components/taking-a-break.tsx diff --git a/src/app/[org]/env-new/page.tsx b/src/app/[org]/env-new/page.tsx index b77f3519..6a97f21c 100644 --- a/src/app/[org]/env-new/page.tsx +++ b/src/app/[org]/env-new/page.tsx @@ -1,25 +1,14 @@ -"use client"; - -import { EnvironmentCreationForm } from "@/components/env/env-creation-form"; -import { authClient } from "@/lib/auth-client"; -import { useRouter } from "next/navigation"; -import { useEffect } from "react"; - +import { TakingABreak } from "@/components/taking-a-break"; + +/** + * humanlog.io is taking a break, so no new environments — creating one used to + * open a Stripe subscription (OrganizationService.CreateEnvironment). The + * backend refuses that RPC independently; this is the front door only. + * + * See https://www.webscale.lol/blog/humanlog-retro + */ export default function NewEnvironmentPage() { - const router = useRouter(); - const { useActiveOrganization } = authClient; - const { data: activeOrganization } = useActiveOrganization(); - - useEffect(() => { - if (!activeOrganization) return; - const url = `/${activeOrganization.slug}/env-new`; - router.replace(url); - }, [activeOrganization]); - return ( -
-

Create a new environment

- -
+ ); } diff --git a/src/app/create-org/page.tsx b/src/app/create-org/page.tsx index 4e4446ac..2e50b96c 100644 --- a/src/app/create-org/page.tsx +++ b/src/app/create-org/page.tsx @@ -1,23 +1,12 @@ -"use client"; - -import { CreateOrg } from "@/components/organization/create-org"; -import { toast } from "sonner"; -import { useRouter } from "next/navigation"; -import { authClient } from "@/lib/auth-client"; +import { TakingABreak } from "@/components/taking-a-break"; +/** + * humanlog.io is taking a break, so organization creation is closed. + * + * See https://www.webscale.lol/blog/humanlog-retro + */ export default function CreateOrgPage() { - const router = useRouter(); - const { useActiveOrganization } = authClient; - const { data: activeOrganization } = useActiveOrganization(); - - const onSuccess = () => { - toast.success("Organization created successfully"); - router.push(`/${activeOrganization?.slug}/localhost/query`); - }; - return ( -
- -
+ ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2faf2404..acb3809d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -13,6 +13,7 @@ import { OTELProvider } from "@/context/otel-provider"; import { PostHogProvider } from "@/context/posthog-provider"; import { TooltipProvider } from "@/components/ui/tooltip"; import { Header } from "@/components/header"; +import { BreakBanner } from "@/components/break-banner"; import { Suspense } from "react"; export const metadata: Metadata = { @@ -51,9 +52,10 @@ export default function RootLayout({
-
- {children} +
+
+
{children}
diff --git a/src/app/onboarding/create-org/page.tsx b/src/app/onboarding/create-org/page.tsx index ea19fd45..11a9ec48 100644 --- a/src/app/onboarding/create-org/page.tsx +++ b/src/app/onboarding/create-org/page.tsx @@ -1,25 +1,12 @@ -"use client"; - -import { toast } from "sonner"; -import { useRouter } from "next/navigation"; -import { CreateOrg } from "@/components/organization/create-org"; -import { authClient } from "@/lib/auth-client"; +import { TakingABreak } from "@/components/taking-a-break"; +/** + * humanlog.io is taking a break, so onboarding is closed. + * + * See https://www.webscale.lol/blog/humanlog-retro + */ export default function OnboardingCreateOrgPage() { - const { useActiveOrganization } = authClient; - const { data: activeOrganization } = useActiveOrganization(); - const router = useRouter(); - - const onSuccess = () => { - toast.success("Organization created successfully"); - router.push(`/${activeOrganization?.slug}/localhost/query`); - - // TODO - // router.push("/onboarding/pricing"); - }; return ( -
- -
+ ); } diff --git a/src/app/onboarding/pricing/page.tsx b/src/app/onboarding/pricing/page.tsx index 6be04a64..dd098be6 100644 --- a/src/app/onboarding/pricing/page.tsx +++ b/src/app/onboarding/pricing/page.tsx @@ -1,15 +1,12 @@ -"use client"; +import { TakingABreak } from "@/components/taking-a-break"; -import OnboardingPricing from "@/components/onboarding/pricing"; - -export default function OnboardingPage() { +/** + * humanlog.io is taking a break, so there is no plan to pick. + * + * See https://www.webscale.lol/blog/humanlog-retro + */ +export default function OnboardingPricingPage() { return ( -
-
- -
- -
-
+ ); } diff --git a/src/app/pricing/page.tsx b/src/app/pricing/page.tsx index a31711c0..e60b1454 100644 --- a/src/app/pricing/page.tsx +++ b/src/app/pricing/page.tsx @@ -1,17 +1,14 @@ -"use client"; - -import { useQuery } from "@connectrpc/connect-query"; -import { listProduct } from "api/js/svc/product/v1/service-ProductService_connectquery"; -import Pricing from "@/components/pricing"; - -export default function Page() { - const { isLoading, data } = useQuery(listProduct, { category: "logging" }); +import { TakingABreak } from "@/components/taking-a-break"; +/** + * humanlog.io is taking a break, so there are no plans to show and no checkout + * to enter. The pricing components are left in the tree for whenever the lights + * come back on. + * + * See https://www.webscale.lol/blog/humanlog-retro + */ +export default function PricingPage() { return ( - + ); } diff --git a/src/app/sign-in/layout.tsx b/src/app/sign-in/layout.tsx index 155d028e..be622355 100644 --- a/src/app/sign-in/layout.tsx +++ b/src/app/sign-in/layout.tsx @@ -1,38 +1,16 @@ -"use client"; - -import { useFeatureFlag } from "@/hooks/useFeatureFlag"; -import { ChevronLeft } from "lucide-react"; -import { useRouter } from "next/navigation"; -import { useEffect } from "react"; - -export default function SignInLayout({ - children, -}: { - children: React.ReactNode; -}) { - const router = useRouter(); - - const { flagEnabled: isAuthMigrationReady, isLoading } = - useFeatureFlag("release_auth_temp"); - - useEffect(() => { - if (!isLoading && !isAuthMigrationReady) { - router.replace("/"); - } - }, [isLoading, isAuthMigrationReady, router]); - - if (isLoading || !isAuthMigrationReady) return null; +import { TakingABreak } from "@/components/taking-a-break"; +/** + * humanlog.io is taking a break, so there is nothing to sign in to. + * + * The notice replaces `children` entirely rather than gating it, so every route + * under /sign-in (including the password-reset flow) lands on it. The + * underlying pages are left in place for whenever the lights come back on. + * + * See https://www.webscale.lol/blog/humanlog-retro + */ +export default function SignInLayout() { return ( -
- -
{children}
-
+ ); } diff --git a/src/app/sign-up/layout.tsx b/src/app/sign-up/layout.tsx index a732040c..1782283a 100644 --- a/src/app/sign-up/layout.tsx +++ b/src/app/sign-up/layout.tsx @@ -1,38 +1,16 @@ -"use client"; - -import { useFeatureFlag } from "@/hooks/useFeatureFlag"; -import { ChevronLeft } from "lucide-react"; -import { useRouter } from "next/navigation"; -import { useEffect } from "react"; - -export default function SignUpLayout({ - children, -}: { - children: React.ReactNode; -}) { - const router = useRouter(); - - const { flagEnabled: isAuthMigrationReady, isLoading } = - useFeatureFlag("release_auth_temp"); - - useEffect(() => { - if (!isLoading && !isAuthMigrationReady) { - router.replace("/"); - } - }, [isLoading, isAuthMigrationReady, router]); - - if (isLoading || !isAuthMigrationReady) return null; +import { TakingABreak } from "@/components/taking-a-break"; +/** + * humanlog.io is taking a break, so we're not accepting new signups. + * + * The notice replaces `children` entirely rather than gating it, so every route + * under /sign-up lands on it. The underlying pages are left in place for + * whenever the lights come back on. + * + * See https://www.webscale.lol/blog/humanlog-retro + */ +export default function SignUpLayout() { return ( -
- - {children} -
+ ); } diff --git a/src/app/user/purchase/page.tsx b/src/app/user/purchase/page.tsx index 2a1aa8d7..bafc924f 100644 --- a/src/app/user/purchase/page.tsx +++ b/src/app/user/purchase/page.tsx @@ -1,405 +1,13 @@ -"use client"; - -import { - ProductPane, - TotalPriceSummary, -} from "@/components/env/env-creation-form"; -import { Button } from "@/components/ui/button"; -import { Form, FormLabel } from "@/components/ui/form"; -import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; -import { cn } from "@/lib/utils"; -import { ConnectError } from "@connectrpc/connect"; -import { useForm } from "react-hook-form"; -import { useMutation, useQuery } from "@connectrpc/connect-query"; -import { - Elements, - PaymentElement, - useElements, - useStripe, -} from "@stripe/react-stripe-js"; -import { loadStripe, Stripe } from "@stripe/stripe-js"; -import { - createAddonSubscription, - createStripeCustomerSession, - getStripePublishableKey, -} from "api/js/svc/organization/v1/service-OrganizationService_connectquery"; -import { - CreateAddonSubscriptionRequest, - CreateAddonSubscriptionResponse, -} from "api/js/svc/organization/v1/service_pb"; -import { listProduct } from "api/js/svc/product/v1/service-ProductService_connectquery"; -import { Price as APIPrice } from "api/js/types/v1/price_pb"; -import { - Product as APIProduct, - Product_Scope, -} from "api/js/types/v1/product_pb"; -import { Loader } from "lucide-react"; -import Image from "next/image"; -import { useRouter, useSearchParams } from "next/navigation"; -import { Dispatch, SetStateAction, useMemo, useState } from "react"; -import { z } from "zod"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { getOrgEnvUrl } from "@/lib/utils/navigation"; -import { usePageStore } from "@/stores/page-store"; -import { useEnvironmentStore } from "@/stores/environment-store"; -import { authClient } from "@/lib/auth-client"; - -interface Product { - product: APIProduct; - prices: APIPrice[]; -} - -export default function UserAddonsPage() { - const searchParams = useSearchParams(); - const urlPlanId = searchParams.get("plan"); - const [isBilledYearly, setIsBilledYearly] = useState(true); - const [selectedProduct, setSelectedProduct] = useState(); - const [price, setPrice] = useState(); - const [stripePromise, setStripePromise] = useState< - Promise | undefined - >(); - - const stripeClientSecret = useQuery(createStripeCustomerSession).data - ?.customerSessionClientSecret; - const stripePK = useQuery(getStripePublishableKey).data?.stripePublishableKey; - - // fetch the product list, set the defaults - const { data } = useQuery(listProduct, { - category: "logging", - scope: Product_Scope.Organization, - }); - - const products = data?.items - .filter((el) => { - return ( - el.product?.scope == Product_Scope.Organization && el.prices.length > 0 - // && el.prices.filter((price) => Number(price.unitAmount) !== 0).length > 0 - ); - }) - .map((el): Product => { - return { product: el.product!, prices: el.prices }; - }); - - useMemo(() => { - if (selectedProduct || !data || !data.defaultProduct) { - return; - } - if (products?.length == 1) { - setSelectedProduct(products[0]); - return; - } - const defaultProduct = data.defaultProduct; - let item = data.items - .filter((el) => { - return el.product?.scope == Product_Scope.Organization; - }) - .find((el) => el.product?.stripeId === defaultProduct.stripeId); - item && setSelectedProduct({ product: item.product!, prices: item.prices }); - }, [products, data, selectedProduct]); - - useMemo(() => { - if (!selectedProduct) { - setPrice(data?.defaultProduct?.defaultPrice); - return; - } - - const selectedPrice = selectedProduct.prices.find((p) => { - if (isBilledYearly && p.recurring?.interval.includes("year")) { - return true; - } - if (!isBilledYearly && p.recurring?.interval.includes("month")) { - return true; - } - if (p.lookupKey === "free_personal_use") { - return true; - } - return false; - }); - - setPrice(selectedPrice); - }, [isBilledYearly, selectedProduct, data?.defaultProduct?.defaultPrice]); - - useMemo(() => { - if (selectedProduct || !data) return; - - const defaultProduct = urlPlanId - ? data.items.find((el) => el.product?.stripeId === urlPlanId) - : data.items.find( - (el) => el.product?.stripeId === data.defaultProduct?.stripeId, - ); - - defaultProduct && - setSelectedProduct({ - product: defaultProduct.product!, - prices: defaultProduct.prices, - }); - }, [data, selectedProduct, urlPlanId]); - - useMemo(() => { - if (!stripePK) { - return; - } - setStripePromise(loadStripe(stripePK)); - }, [stripePK]); - - if (!stripePromise || !price || !products) { - return ( -
- -
- ); - } - - return ( -
-

Subscribe

- - - -
- ); -} - -const formSchema = z.object({}); - -function CheckoutForm({ - selectedProduct, - setSelectedProduct, - price, - setPrice, - isBilledYearly, - setIsBilledYearly, - products, -}: { - selectedProduct: Product | undefined; - setSelectedProduct: Dispatch>; - price: APIPrice | undefined; - setPrice: Dispatch>; - isBilledYearly: boolean; - setIsBilledYearly: Dispatch>; - products: Product[]; -}) { - const router = useRouter(); - const stripe = useStripe(); - const elements = useElements(); - - const { useActiveOrganization } = authClient; - const { data: activeOrganization } = useActiveOrganization(); - - const { activeEnvironment } = useEnvironmentStore(); - const { activePage } = usePageStore(); - - const [errorMessage, setErrorMessage] = useState(); - - const handleError = (error: string) => { - setErrorMessage(error); - }; - - const { mutateAsync, isPending } = useMutation(createAddonSubscription); - - async function onSubmit(values: z.infer) { - if (!stripe || !elements || !price) { - return; - } - - const { error: submitError } = await elements.submit(); - if (submitError) { - console.log("submitError", submitError); - handleError(submitError.message!); - return; - } - let confirmationToken; - if (Number(price.unitAmount > 0)) { - const { error, confirmationToken: _confirmationToken } = - await stripe.createConfirmationToken({ - elements, - }); - - confirmationToken = _confirmationToken; - if (error) { - handleError(error.message!); - return; - } - } - - let res: CreateAddonSubscriptionResponse; - try { - res = await mutateAsync({ - payment: { - case: "stripe", - value: { - confirmationToken: confirmationToken?.id || "", - priceId: price?.stripeId, - }, - }, - }); - } catch (e) { - if (e instanceof ConnectError) { - handleError(e.message); - } else { - handleError("an unexpected problem occured"); - throw e; - } - return; - } - switch (res.payment.case) { - case "stripe": - if (res.payment.value.status === "requires_action") { - const { error } = await stripe.handleNextAction({ - clientSecret: res.payment.value.clientSecret, - }); - if (error) { - handleError(error.message!); - return; - } - } - } - console.log("checkout completed!"); - - if (!activeOrganization || !activeEnvironment) return; - - const url = getOrgEnvUrl(activeOrganization, activeEnvironment, activePage); - router.push(url); - } - - const form = useForm>({ - resolver: zodResolver(formSchema), - }); - +import { TakingABreak } from "@/components/taking-a-break"; + +/** + * humanlog.io is taking a break. This used to be the live Stripe Elements + * checkout; it must not load payment UI while the service is closed. + * + * See https://www.webscale.lol/blog/humanlog-retro + */ +export default function PurchasePage() { return ( -
- -
-
- Pick a product -
- - -
-
- - {/* Product Section */} - -
- {products.map((product) => { - const stripeID = product.product.stripeId; - const isSelected = selectedProduct?.product.stripeId === stripeID; - return ( - { - setSelectedProduct(product); - setPrice(price); - }} - /> - ); - })} -
-
- {/* Payment Section */} - {Number(price?.unitAmount) > 0 && ( -
- -
-
- Powered by Stripe - -
-
-
- )} - - {/* Submit Section */} -
- {price?.lookupKey == "free_personal_use" && ( - <> - -
- -
- - )} - {price?.lookupKey != "free_personal_use" && ( - <> - -
- -
- {/* Find the selected product */} - {price && ( - - )} -
- {errorMessage &&
{errorMessage}
} -
- - )} -
-
- + ); } diff --git a/src/components/break-banner.tsx b/src/components/break-banner.tsx new file mode 100644 index 00000000..fef03fe8 --- /dev/null +++ b/src/components/break-banner.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { DismissibleBanner } from "@/components/ui/dismissable-banner"; +import { RETRO_URL } from "@/components/taking-a-break"; + +/** + * Site-wide notice that humanlog.io is paused. + * + * See https://www.webscale.lol/blog/humanlog-retro + */ +export function BreakBanner() { + return ( + + ); +} + +export default BreakBanner; diff --git a/src/components/header/user-actions.tsx b/src/components/header/user-actions.tsx index 2f78c421..4599118b 100644 --- a/src/components/header/user-actions.tsx +++ b/src/components/header/user-actions.tsx @@ -15,7 +15,6 @@ import Link from "next/link"; import { ThemeToggle } from "@/components/theme-toggle"; import { User as BetterAuthUser } from "better-auth"; import { authClient } from "@/lib/auth-client"; -import { Button } from "@/components/ui/button"; import { NavigationMenuItem } from "@/components/ui/navigation-menu"; import { useEnvironmentStore } from "@/stores/environment-store"; import { usePageStore } from "@/stores/page-store"; @@ -48,19 +47,14 @@ export const UserActions = ({ user }: UserActionsProps) => { }); }; + // humanlog.io is taking a break, so there's nothing to sign in to and the + // "Sign In" button is gone. Signed-out visitors keep the theme toggle, which + // otherwise only exists inside the signed-in menu below. + // See https://www.webscale.lol/blog/humanlog-retro return !user ? ( - <> - - - - + + + ) : ( diff --git a/src/components/landing-page/hero-marketing.tsx b/src/components/landing-page/hero-marketing.tsx index 53889854..ddc2219b 100644 --- a/src/components/landing-page/hero-marketing.tsx +++ b/src/components/landing-page/hero-marketing.tsx @@ -281,7 +281,7 @@ const AboveFoldHero: React.FC = () => { {/* CTA Buttons */}
- + { return (

- Ready to Get Started? + Run it on your laptop

@@ -22,17 +21,18 @@ const CTABannerSection: React.FC = () => {
- -
-

- Need a hosted solution for your team? -

- -
-
+ {/* + The hosted service is on a break, so this is no longer gated behind + the `experiment_cloud_waitlist_temp` flag — everyone should see where + the hosted tool went. + See https://www.webscale.lol/blog/humanlog-retro + */} +
+

+ Looking for the hosted version? +

+ +
); diff --git a/src/components/landing-page/sections/faq-section.tsx b/src/components/landing-page/sections/faq-section.tsx index c72e175d..3f7434a4 100644 --- a/src/components/landing-page/sections/faq-section.tsx +++ b/src/components/landing-page/sections/faq-section.tsx @@ -158,14 +158,11 @@ const faqItems: FAQItem[] = [ answer: ( <>

- Yes! We{"'"}re working on a hosted solution for teams that need - managed infrastructure. + Not right now. humanlog.io is taking a break — the hosted tool is + moving to a new project called minitape, with no timeline promised.

- +
), diff --git a/src/components/landing-page/shared/cloud-beta-signup.tsx b/src/components/landing-page/shared/cloud-beta-signup.tsx index dfc7a503..6cab0910 100644 --- a/src/components/landing-page/shared/cloud-beta-signup.tsx +++ b/src/components/landing-page/shared/cloud-beta-signup.tsx @@ -18,7 +18,7 @@ interface CloudBetaSignupProps { } const CloudBetaSignup: React.FC = ({ - heading = "Signup for our private beta of Humanlog Cloud. Reuse everything you learn locally, skills transfer 1:1, spots limited to 50 engineers.", + heading = "humanlog.io is taking a break. The hosted tool is moving to a new project called minitape — leave your email to hear when it launches.", className = "", variant = "default", }) => { diff --git a/src/components/landing-page/shared/install-cta.tsx b/src/components/landing-page/shared/install-cta.tsx index b2d0d959..69db6269 100644 --- a/src/components/landing-page/shared/install-cta.tsx +++ b/src/components/landing-page/shared/install-cta.tsx @@ -35,7 +35,7 @@ interface InstallCTAProps { } const InstallCTA: React.FC = ({ - buttonText = "Install & Sign Up", + buttonText = "Install humanlog", buttonSize = "lg", buttonVariant = "default", className = "h-11", diff --git a/src/components/taking-a-break.tsx b/src/components/taking-a-break.tsx new file mode 100644 index 00000000..22ee7713 --- /dev/null +++ b/src/components/taking-a-break.tsx @@ -0,0 +1,72 @@ +"use client"; + +import React from "react"; +import { ArrowUpRight } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import CloudBetaSignup from "@/components/landing-page/shared/cloud-beta-signup"; +import { cn } from "@/lib/utils"; + +export const RETRO_URL = "https://www.webscale.lol/blog/humanlog-retro"; +export const CONTACT_EMAIL = "antoine@webscale.lol"; + +interface TakingABreakProps { + className?: string; + /** Shown under the heading. Defaults to the general "we're on a break" copy. */ + description?: React.ReactNode; +} + +/** + * The site-wide "humanlog.io is taking a break" notice. + * + * Rendered in place of anything that used to sign someone up, log them in, or + * take their money. See https://www.webscale.lol/blog/humanlog-retro + */ +export function TakingABreak({ className, description }: TakingABreakProps) { + return ( +
+ ); +} + +export default TakingABreak; diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 47f2f002..8589f962 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -30,6 +30,22 @@ const betterAuthCallbacksClient = createClient( ); const betterAuthAdapter = createBetterAuthAdapter(betterAuthAdapterClient); +const shared = createSharedBetterAuthConfig({ + callbacksClient: betterAuthCallbacksClient, + createInvitationUrl: (invitationId: string) => { + return `${getSelfURL()}/invitation-callback?invitationId=${invitationId}`; + }, + + github: { + clientId: process.env.GITHUB_CLIENT_ID || "", + clientSecret: process.env.GITHUB_CLIENT_SECRET || "", + }, + google: { + clientId: process.env.GOOGLE_CLIENT_ID || "", + clientSecret: process.env.GOOGLE_CLIENT_SECRET || "", + }, +}); + const config = { database: betterAuthAdapter, secret: process.env.BETTER_AUTH_SECRET || "", @@ -41,21 +57,21 @@ const config = { }, // telemetry: { enabled: true, debug: true }, - ...createSharedBetterAuthConfig({ - callbacksClient: betterAuthCallbacksClient, - createInvitationUrl: (invitationId: string) => { - return `${getSelfURL()}/invitation-callback?invitationId=${invitationId}`; - }, + ...shared, - github: { - clientId: process.env.GITHUB_CLIENT_ID || "", - clientSecret: process.env.GITHUB_CLIENT_SECRET || "", - }, - google: { - clientId: process.env.GOOGLE_CLIENT_ID || "", - clientSecret: process.env.GOOGLE_CLIENT_SECRET || "", - }, - }), + // humanlog.io is taking a break: https://www.webscale.lol/blog/humanlog-retro + // + // These overrides must stay *after* the spread above, and must merge into the + // shared config rather than replace it, or the callbacks it carries go with + // them. Disabling email/password closes /api/auth/sign-in/email and + // /api/auth/sign-up/email; emptying socialProviders removes the GitHub and + // Google routes, which otherwise auto-create an account for any unknown + // identity that hits `signIn.social`. + // + // The backend refuses account and session creation independently — see the + // closed-for-break interceptor in apisvc. This is the front door only. + emailAndPassword: { ...shared.emailAndPassword, enabled: false }, + socialProviders: {}, }; export const auth = betterAuth(config);