Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐ซ Status page with uptime monitoring & API monitoring as code ๐ซ openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259import { Events, setupAnalytics } from "@openstatus/analytics";import { db, eq } from "@openstatus/db";import { type User, user } from "@openstatus/db/src/schema";import { getCurrency } from "@openstatus/db/src/schema/plan/utils";import { WelcomeEmail, sendEmail } from "@openstatus/emails";import type { DefaultSession } from "next-auth";import NextAuth from "next-auth";import { headers } from "next/headers";import { after } from "next/server";import { cache } from "react";
import { adapter } from "./adapter";import { logger } from "./logger";import { GitHubProvider, GoogleProvider, OIDCProvider, ResendProvider, WorkOSProvider,} from "./providers";import { authorizeSsoSignIn, joinWorkspaceViaSso, readWorkOSProfile,} from "./sso";
const hasWorkOS = Boolean( process.env.AUTH_WORKOS_ID && process.env.AUTH_WORKOS_SECRET,);
export type { DefaultSession };
async function syncUser( userId: number, fields: { firstName?: string | null; lastName?: string | null; photoUrl?: string | null; name?: string | null; },) { await db .update(user) .set({ ...fields, updatedAt: new Date() }) .where(eq(user.id, userId)) .run();}
// Runs from the `signIn` event, not `createUser`: only `signIn` carries the// account, and the trial must know the provider to skip SSO signups. It also// fires after the account row is linked, so nothing races the adapter.// Scheduled with `after()`: the Stripe calls take seconds and would otherwise// hold the sign-in redirect. Headers are read up front, before the response.async function onNewUser( newUser: Partial<User>, provider: string | undefined, requestHeaders: Headers,) { if (!newUser.id || !newUser.email) { throw new Error("User id & email is required"); }
// this means the user has already been created with clerk if (newUser.tenantId) return;
const currency = getCurrency({ continent: requestHeaders.get("x-vercel-ip-continent") || "NA", country: requestHeaders.get("x-vercel-ip-country") || "US", });
// Imported lazily to keep Stripe out of the proxy bundle, which also loads this module. const { maybeStartSignupTrial } = await import("@openstatus/api/src/router/stripe/trial"); const trial = await maybeStartSignupTrial({ userId: newUser.id, email: newUser.email, provider, currency, }).catch((error: Error) => { console.error("signup trial failed", { userId: newUser.id, error }); return { started: false, reason: "stripe_error" } as const; });
await sendEmail({ from: "Thibault from openstatus <thibault@openstatus.dev>", subject: "Welcome to openstatus.", to: [newUser.email], react: WelcomeEmail({ trialEndsAt: trial.started ? trial.trialEndsAt : undefined, }), });
const analytics = await setupAnalytics({ userId: `usr_${newUser.id}`, email: newUser.email, location: requestHeaders.get("x-forwarded-for") ?? undefined, userAgent: requestHeaders.get("user-agent") ?? undefined, });
await analytics.track(Events.CreateUser); if (trial.started) { await analytics.track({ ...Events.StartTrial, currency }); } else if (trial.reason !== "disabled") { await analytics.track({ ...Events.SkipTrial, reason: trial.reason }); }}
const { handlers, signIn, signOut, auth: nextAuth,} = NextAuth({ // debug: true, adapter, logger, providers: [ GitHubProvider, GoogleProvider, ...(process.env.AUTH_OIDC_ISSUER ? [OIDCProvider] : []), ...(hasWorkOS ? [WorkOSProvider] : []), ...(process.env.NODE_ENV === "development" || process.env.SELF_HOST === "true" ? [ResendProvider] : []), ], callbacks: { async redirect({ url, baseUrl }) { // Allow relative URLs, but not protocol-relative `//evil.com` which the // browser would resolve off-origin. if (url.startsWith("/") && !url.startsWith("//")) { return `${baseUrl}${url}`; } // Same-origin absolute URLs only โ compare parsed origins, not a string // prefix, so `https://<baseUrl>.evil.com` cannot pass as trusted. // Preserves the original callback URL so invite links keep working. try { if (new URL(url).origin === new URL(baseUrl).origin) { return url; } } catch { // malformed url โ fall through to baseUrl } return baseUrl; }, async signIn(params) { // We keep updating the user info when we loggin in
if (params.account?.provider === "google") { if (!params.profile) return true; if (Number.isNaN(Number(params.user.id))) return true;
await syncUser(Number(params.user.id), { firstName: params.profile.given_name, lastName: params.profile.family_name || "", photoUrl: params.profile.picture, // keep the name in sync name: `${params.profile.given_name} ${ params.profile.family_name || "" }`.trim(), }); } if (params.account?.provider === "github") { if (!params.profile) return true; if (Number.isNaN(Number(params.user.id))) return true;
await syncUser(Number(params.user.id), { name: params.profile.name, photoUrl: String(params.profile.avatar_url), }); }
if (params.account?.provider === "oidc") { if (!params.profile) return true; if (Number.isNaN(Number(params.user.id))) return true;
const { given_name, family_name, name, picture } = params.profile; // some IdPs only send a combined `name` claim const [nameFirst, ...nameRest] = name?.split(" ") ?? []; const fullName = [given_name, family_name].filter(Boolean).join(" ");
await syncUser(Number(params.user.id), { firstName: given_name ?? nameFirst, lastName: family_name || nameRest.join(" "), photoUrl: picture, name: fullName || name, }); }
if (params.account?.provider === "workos") { return authorizeSsoSignIn(readWorkOSProfile(params.profile)); }
// REMINDER: only used in dev mode if (params.account?.provider === "resend") { if (Number.isNaN(Number(params.user.id))) return true; await db .update(user) .set({ updatedAt: new Date() }) .where(eq(user.id, Number(params.user.id))) .run(); }
return true; }, async session(params) { return params.session; }, }, events: { async signIn(params) { if (params.account?.provider === "workos") { const { organization_id: organizationId } = readWorkOSProfile( params.profile, ); const userId = Number(params.user.id); if (organizationId && !Number.isNaN(userId)) { await joinWorkspaceViaSso({ organizationId, userId }); } }
if (params.isNewUser) { const newUser = { ...params.user, id: Number(params.user.id) }; const provider = params.account?.provider; const requestHeaders = new Headers(await headers()); after(() => onNewUser(newUser, provider, requestHeaders).catch((error) => { console.error("onNewUser failed", { userId: newUser.id, error }); }), ); return; } if (!params.user.id || !params.user.email) return;
const analytics = await setupAnalytics({ userId: `usr_${params.user.id}`, email: params.user.email, location: (await headers()).get("x-forwarded-for") ?? undefined, userAgent: (await headers()).get("user-agent") ?? undefined, });
await analytics.track(Events.SignInUser); }, }, pages: { signIn: "/login", newUser: "/onboarding", }, // basePath: "/api/auth", // default is `/api/auth` // secret: process.env.AUTH_SECRET, // default is `AUTH_SECRET` debug: process.env.NODE_ENV === "development",});
// The root layout and the tRPC RSC context both need the session; `cache`// collapses them into a single lookup per render. Outside a React render// (the proxy's `auth(handler)` form, route handlers) it is a pass-through.export const auth = cache(nextAuth);export { handlers, signIn, signOut };