-
Notifications
You must be signed in to change notification settings - Fork 167
Expand file tree
/
Copy pathhandlers.ts
More file actions
77 lines (72 loc) · 2.95 KB
/
Copy pathhandlers.ts
File metadata and controls
77 lines (72 loc) · 2.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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)),
),
);
};