77 lines · 2.9 KB · TypeScript
RawDownload
import { HttpApiBuilder } from "effect/unstable/httpapi";import { HttpRouter } from "effect/unstable/http";import { Effect, Layer } from "effect"; import { SystemError, SystemHttpApi } from "./api";import { BetterAuth, countOrgMembers, type BetterAuthHandle } from "../auth/better-auth";import { SelfHostDb, type SelfHostDbHandle } from "../db/self-host-db";import { findRedeemableCode } from "../auth/invites"; // ---------------------------------------------------------------------------// Handlers for the public system API. Unauthenticated; every DB touch is an// Effect.tryPromise. `health` fails soft (a DB hiccup reports "degraded", it// never throws); `setup-status` reports whether the one org has zero members.// --------------------------------------------------------------------------- export const SystemHandlers = HttpApiBuilder.group(SystemHttpApi, "system", (handlers) =>  handlers    .handle("health", () =>      Effect.gen(function* () {        const { client } = yield* SelfHostDb;        const status = yield* Effect.tryPromise({          try: () => client.execute("SELECT 1"),          catch: () => new SystemError({ message: "database unreachable" }),        }).pipe(          Effect.as("ok"),          Effect.orElseSucceed(() => "degraded"),        );        return { status };      }),    )    .handle("setupStatus", () =>      Effect.gen(function* () {        const { auth, organizationId } = yield* BetterAuth;        // Count via Better Auth's adapter (see countOrgMembers) so this read is        // consistent with how memberships are written.        const count = yield* Effect.tryPromise({          try: () => countOrgMembers(auth, organizationId),          catch: () => new SystemError({ message: "failed to read setup status" }),        });        return { needsSetup: count === 0 };      }),    )    .handle("inviteStatus", ({ params }) =>      Effect.gen(function* () {        const { client } = yield* SelfHostDb;        const code = yield* Effect.tryPromise({          try: () => findRedeemableCode(client, params.code),          catch: () => new SystemError({ message: "failed to read invite status" }),        });        return { valid: code !== null };      }),    ),); export interface SelfHostSystemApiDeps {  readonly betterAuth: BetterAuthHandle;  readonly db: SelfHostDbHandle;  readonly mountPrefix: `/${string}`;} /** Mountable extension route layer (see makeSelfHostAdminApiLayer). */export const makeSelfHostSystemApiLayer = ({  betterAuth,  db,  mountPrefix,}: SelfHostSystemApiDeps) => {  const prefixedRouter = Layer.effect(HttpRouter.HttpRouter)(    Effect.map(HttpRouter.HttpRouter.asEffect(), (router) => router.prefixed(mountPrefix)),  );  return HttpApiBuilder.layer(SystemHttpApi).pipe(    Layer.provide(SystemHandlers),    Layer.provide(prefixedRouter),    HttpRouter.provideRequest(      Layer.mergeAll(Layer.succeed(BetterAuth)(betterAuth), Layer.succeed(SelfHostDb)(db)),    ),  );};