refactor: api routes
This commit is contained in:
23
apps/api/src/auth/better-auth.ts
Normal file
23
apps/api/src/auth/better-auth.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Effect, Layer } from "effect";
|
||||||
|
import { Authorization } from "./context";
|
||||||
|
import { auth } from "./config";
|
||||||
|
import { AuthorizationError, AuthorizationUnknownError } from "./errors";
|
||||||
|
|
||||||
|
export const make = () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
return Authorization.of({
|
||||||
|
use: (fn) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const data = yield* Effect.tryPromise({
|
||||||
|
try: () => fn(auth),
|
||||||
|
catch: (error) =>
|
||||||
|
error instanceof Error
|
||||||
|
? new AuthorizationError({ message: error.message })
|
||||||
|
: new AuthorizationUnknownError(),
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BetterAuthLive = Layer.scoped(Authorization, make());
|
||||||
@@ -3,7 +3,7 @@ import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|||||||
import { bearer, deviceAuthorization, genericOAuth } from "better-auth/plugins";
|
import { bearer, deviceAuthorization, genericOAuth } from "better-auth/plugins";
|
||||||
import { expo } from "@better-auth/expo";
|
import { expo } from "@better-auth/expo";
|
||||||
import { drizzleSchema } from "@money/shared/db";
|
import { drizzleSchema } from "@money/shared/db";
|
||||||
import { db } from "./db";
|
import { db } from "../db";
|
||||||
import { BASE_URL, HOST } from "@money/shared";
|
import { BASE_URL, HOST } from "@money/shared";
|
||||||
|
|
||||||
export const auth = betterAuth({
|
export const auth = betterAuth({
|
||||||
14
apps/api/src/auth/context.ts
Normal file
14
apps/api/src/auth/context.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Context, type Effect } from "effect";
|
||||||
|
import type { AuthorizationError, AuthorizationUnknownError } from "./errors";
|
||||||
|
import type { auth } from "./config";
|
||||||
|
|
||||||
|
export class Authorization extends Context.Tag("Authorization")<
|
||||||
|
Authorization,
|
||||||
|
AuthorizationImpl
|
||||||
|
>() {}
|
||||||
|
|
||||||
|
export interface AuthorizationImpl {
|
||||||
|
use: <T>(
|
||||||
|
fn: (client: typeof auth) => Promise<T>,
|
||||||
|
) => Effect.Effect<T, AuthorizationUnknownError | AuthorizationError, never>;
|
||||||
|
}
|
||||||
8
apps/api/src/auth/errors.ts
Normal file
8
apps/api/src/auth/errors.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Data } from "effect";
|
||||||
|
|
||||||
|
export class AuthorizationUnknownError extends Data.TaggedError(
|
||||||
|
"AuthClientUnknownError",
|
||||||
|
) {}
|
||||||
|
export class AuthorizationError extends Data.TaggedError("AuthorizationError")<{
|
||||||
|
message: string;
|
||||||
|
}> {}
|
||||||
70
apps/api/src/auth/handler.ts
Normal file
70
apps/api/src/auth/handler.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
HttpApi,
|
||||||
|
HttpApiBuilder,
|
||||||
|
HttpApiEndpoint,
|
||||||
|
HttpApiGroup,
|
||||||
|
HttpLayerRouter,
|
||||||
|
HttpServerRequest,
|
||||||
|
} from "@effect/platform";
|
||||||
|
import * as NodeHttpServerRequest from "@effect/platform-node/NodeHttpServerRequest";
|
||||||
|
import { toNodeHandler } from "better-auth/node";
|
||||||
|
import { Effect, Layer } from "effect";
|
||||||
|
import { AuthorizationError } from "./errors";
|
||||||
|
import { auth } from "./config";
|
||||||
|
|
||||||
|
const authHandler = Effect.gen(function* () {
|
||||||
|
const request = yield* HttpServerRequest.HttpServerRequest;
|
||||||
|
const nodeRequest = NodeHttpServerRequest.toIncomingMessage(request);
|
||||||
|
const nodeResponse = NodeHttpServerRequest.toServerResponse(request);
|
||||||
|
|
||||||
|
nodeResponse.setHeader("Access-Control-Allow-Origin", "http://laptop:8081");
|
||||||
|
nodeResponse.setHeader(
|
||||||
|
"Access-Control-Allow-Methods",
|
||||||
|
"GET, POST, PUT, DELETE, OPTIONS",
|
||||||
|
);
|
||||||
|
nodeResponse.setHeader(
|
||||||
|
"Access-Control-Allow-Headers",
|
||||||
|
"Content-Type, Authorization, B3, traceparent, Cookie",
|
||||||
|
);
|
||||||
|
nodeResponse.setHeader("Access-Control-Max-Age", "600");
|
||||||
|
nodeResponse.setHeader("Access-Control-Allow-Credentials", "true");
|
||||||
|
|
||||||
|
// Handle preflight requests
|
||||||
|
if (nodeRequest.method === "OPTIONS") {
|
||||||
|
nodeResponse.statusCode = 200;
|
||||||
|
nodeResponse.end();
|
||||||
|
|
||||||
|
return;
|
||||||
|
// return nodeResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield* Effect.tryPromise({
|
||||||
|
try: () => toNodeHandler(auth)(nodeRequest, nodeResponse),
|
||||||
|
catch: (error) => {
|
||||||
|
return new AuthorizationError({ message: `${error}` });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// return nodeResponse;
|
||||||
|
});
|
||||||
|
|
||||||
|
export class AuthContractGroup extends HttpApiGroup.make("auth")
|
||||||
|
.add(HttpApiEndpoint.get("get", "/*"))
|
||||||
|
.add(HttpApiEndpoint.post("post", "/*"))
|
||||||
|
.add(HttpApiEndpoint.options("options", "/*"))
|
||||||
|
.prefix("/api/auth") {}
|
||||||
|
|
||||||
|
export class DomainApi extends HttpApi.make("domain").add(AuthContractGroup) {}
|
||||||
|
|
||||||
|
export const Api = HttpApi.make("api").addHttpApi(DomainApi);
|
||||||
|
|
||||||
|
const AuthLive = HttpApiBuilder.group(Api, "auth", (handlers) =>
|
||||||
|
handlers
|
||||||
|
.handle("get", () => authHandler.pipe(Effect.orDie))
|
||||||
|
.handle("post", () => authHandler.pipe(Effect.orDie))
|
||||||
|
.handle("options", () => authHandler.pipe(Effect.orDie)),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AuthRoute = HttpLayerRouter.addHttpApi(Api).pipe(
|
||||||
|
Layer.provide(AuthLive),
|
||||||
|
);
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import type { AuthData } from "@money/shared/auth";
|
|
||||||
import { Hono } from "hono";
|
|
||||||
|
|
||||||
export const getHono = () =>
|
|
||||||
new Hono<{ Variables: { auth: AuthData | null } }>();
|
|
||||||
@@ -1,123 +1,15 @@
|
|||||||
import * as Layer from "effect/Layer";
|
import * as Layer from "effect/Layer";
|
||||||
import * as Effect from "effect/Effect";
|
import * as Effect from "effect/Effect";
|
||||||
import * as Context from "effect/Context";
|
|
||||||
import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter";
|
import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter";
|
||||||
import * as HttpServerResponse from "@effect/platform/HttpServerResponse";
|
import * as HttpServerResponse from "@effect/platform/HttpServerResponse";
|
||||||
import * as HttpServerRequest from "@effect/platform/HttpServerRequest";
|
|
||||||
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
|
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
|
||||||
import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
|
import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
|
||||||
import * as NodeHttpServerRequest from "@effect/platform-node/NodeHttpServerRequest";
|
|
||||||
import { createServer } from "http";
|
import { createServer } from "http";
|
||||||
import {
|
import { CorsMiddleware } from "./middleware/cors";
|
||||||
HttpApi,
|
import { AuthRoute } from "./auth/handler";
|
||||||
HttpApiBuilder,
|
import { BetterAuthLive } from "./auth/better-auth";
|
||||||
HttpApiEndpoint,
|
import { WebhookReceiverRoute } from "./webhook";
|
||||||
HttpApiGroup,
|
import { ZeroMutateRoute, ZeroQueryRoute } from "./zero/handler";
|
||||||
HttpApiSchema,
|
|
||||||
HttpApiSecurity,
|
|
||||||
HttpMiddleware,
|
|
||||||
} from "@effect/platform";
|
|
||||||
import { Schema, Data, Console } from "effect";
|
|
||||||
import { auth } from "./auth";
|
|
||||||
import { AuthSchema } from "@money/shared/auth";
|
|
||||||
import { toNodeHandler } from "better-auth/node";
|
|
||||||
import { BASE_URL } from "@money/shared";
|
|
||||||
|
|
||||||
class CurrentSession extends Context.Tag("CurrentSession")<
|
|
||||||
CurrentSession,
|
|
||||||
{ readonly auth: Schema.Schema.Type<typeof AuthSchema> | null }
|
|
||||||
>() {}
|
|
||||||
|
|
||||||
class Unauthorized extends Schema.TaggedError<Unauthorized>()(
|
|
||||||
"Unauthorized",
|
|
||||||
{},
|
|
||||||
HttpApiSchema.annotations({ status: 401 }),
|
|
||||||
) {}
|
|
||||||
|
|
||||||
const parseAuthorization = (input: string) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const m = /^Bearer\s+(.+)$/.exec(input);
|
|
||||||
if (!m) {
|
|
||||||
return yield* Effect.fail(
|
|
||||||
new Unauthorized({ message: "Invalid Authorization header" }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return m[1];
|
|
||||||
});
|
|
||||||
|
|
||||||
export class Authorization extends Context.Tag("Authorization")<
|
|
||||||
Authorization,
|
|
||||||
AuthorizationImpl
|
|
||||||
>() {}
|
|
||||||
|
|
||||||
export interface AuthorizationImpl {
|
|
||||||
use: <T>(
|
|
||||||
fn: (client: typeof auth) => Promise<T>,
|
|
||||||
) => Effect.Effect<T, AuthorizationUnknownError | AuthorizationError, never>;
|
|
||||||
}
|
|
||||||
|
|
||||||
class AuthorizationUnknownError extends Data.TaggedError(
|
|
||||||
"AuthClientUnknownError",
|
|
||||||
) {}
|
|
||||||
class AuthorizationError extends Data.TaggedError("AuthorizationError")<{
|
|
||||||
message: string;
|
|
||||||
}> {}
|
|
||||||
|
|
||||||
export const make = () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
return Authorization.of({
|
|
||||||
use: (fn) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const data = yield* Effect.tryPromise({
|
|
||||||
try: () => fn(auth),
|
|
||||||
catch: (error) =>
|
|
||||||
error instanceof Error
|
|
||||||
? new AuthorizationError({ message: error.message })
|
|
||||||
: new AuthorizationUnknownError(),
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export const AuthorizationLayer = Layer.scoped(Authorization, make());
|
|
||||||
|
|
||||||
const SessionMiddleware = HttpLayerRouter.middleware<{
|
|
||||||
provides: CurrentSession;
|
|
||||||
}>()(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const auth = yield* Authorization;
|
|
||||||
|
|
||||||
return (httpEffect) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
||||||
const headers = request.headers;
|
|
||||||
|
|
||||||
const session = yield* auth
|
|
||||||
.use((auth) => auth.api.getSession({ headers }))
|
|
||||||
.pipe(
|
|
||||||
Effect.flatMap((s) =>
|
|
||||||
s == null ? Effect.succeed(null) : Schema.decode(AuthSchema)(s),
|
|
||||||
),
|
|
||||||
Effect.tap((s) => Console.debug("Auth result", s)),
|
|
||||||
);
|
|
||||||
|
|
||||||
return yield* Effect.provideService(httpEffect, CurrentSession, {
|
|
||||||
auth: session,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const HelloRoute = HttpLayerRouter.add(
|
|
||||||
"GET",
|
|
||||||
"/hello",
|
|
||||||
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const { auth } = yield* CurrentSession;
|
|
||||||
return HttpServerResponse.text(`Hello, your name is ${auth?.user.name}`);
|
|
||||||
}),
|
|
||||||
).pipe(Layer.provide(SessionMiddleware.layer));
|
|
||||||
|
|
||||||
const RootRoute = HttpLayerRouter.add(
|
const RootRoute = HttpLayerRouter.add(
|
||||||
"GET",
|
"GET",
|
||||||
@@ -127,78 +19,17 @@ const RootRoute = HttpLayerRouter.add(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const authHandler = Effect.gen(function* () {
|
const AllRoutes = Layer.mergeAll(
|
||||||
const request = yield* HttpServerRequest.HttpServerRequest;
|
RootRoute,
|
||||||
const nodeRequest = NodeHttpServerRequest.toIncomingMessage(request);
|
AuthRoute,
|
||||||
const nodeResponse = NodeHttpServerRequest.toServerResponse(request);
|
ZeroQueryRoute,
|
||||||
|
ZeroMutateRoute,
|
||||||
nodeResponse.setHeader("Access-Control-Allow-Origin", "http://laptop:8081");
|
WebhookReceiverRoute,
|
||||||
nodeResponse.setHeader(
|
|
||||||
"Access-Control-Allow-Methods",
|
|
||||||
"GET, POST, PUT, DELETE, OPTIONS",
|
|
||||||
);
|
);
|
||||||
nodeResponse.setHeader(
|
|
||||||
"Access-Control-Allow-Headers",
|
|
||||||
"Content-Type, Authorization, B3, traceparent, Cookie",
|
|
||||||
);
|
|
||||||
nodeResponse.setHeader("Access-Control-Max-Age", "600");
|
|
||||||
nodeResponse.setHeader("Access-Control-Allow-Credentials", "true");
|
|
||||||
|
|
||||||
// Handle preflight requests
|
|
||||||
if (nodeRequest.method === "OPTIONS") {
|
|
||||||
nodeResponse.statusCode = 200;
|
|
||||||
nodeResponse.end();
|
|
||||||
|
|
||||||
return;
|
|
||||||
// return nodeResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield* Effect.tryPromise({
|
|
||||||
try: () => toNodeHandler(auth)(nodeRequest, nodeResponse),
|
|
||||||
catch: (error) => {
|
|
||||||
return new AuthorizationError({ message: `${error}` });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// return nodeResponse;
|
|
||||||
});
|
|
||||||
|
|
||||||
export class AuthContractGroup extends HttpApiGroup.make("auth")
|
|
||||||
.add(HttpApiEndpoint.get("get", "/*"))
|
|
||||||
.add(HttpApiEndpoint.post("post", "/*"))
|
|
||||||
.add(HttpApiEndpoint.options("options", "/*"))
|
|
||||||
.prefix("/api/auth") {}
|
|
||||||
|
|
||||||
export class DomainApi extends HttpApi.make("domain").add(AuthContractGroup) {}
|
|
||||||
|
|
||||||
export const Api = HttpApi.make("api").addHttpApi(DomainApi);
|
|
||||||
|
|
||||||
const AuthLive = HttpApiBuilder.group(Api, "auth", (handlers) =>
|
|
||||||
handlers
|
|
||||||
.handle("get", () => authHandler.pipe(Effect.orDie))
|
|
||||||
.handle("post", () => authHandler.pipe(Effect.orDie))
|
|
||||||
.handle("options", () => authHandler.pipe(Effect.orDie)),
|
|
||||||
);
|
|
||||||
|
|
||||||
const CorsMiddleware = HttpLayerRouter.middleware(
|
|
||||||
HttpMiddleware.cors({
|
|
||||||
allowedOrigins: ["https://money.koon.us", `${BASE_URL}:8081`],
|
|
||||||
allowedMethods: ["POST", "GET", "OPTIONS"],
|
|
||||||
allowedHeaders: ["Content-Type", "Authorization"],
|
|
||||||
credentials: true,
|
|
||||||
}),
|
|
||||||
// {
|
|
||||||
// global: true,
|
|
||||||
// },
|
|
||||||
);
|
|
||||||
|
|
||||||
const AuthRoute = HttpLayerRouter.addHttpApi(Api).pipe(Layer.provide(AuthLive));
|
|
||||||
|
|
||||||
const AllRoutes = Layer.mergeAll(RootRoute, AuthRoute, HelloRoute);
|
|
||||||
|
|
||||||
HttpLayerRouter.serve(AllRoutes).pipe(
|
HttpLayerRouter.serve(AllRoutes).pipe(
|
||||||
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })),
|
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })),
|
||||||
Layer.provide(AuthorizationLayer),
|
Layer.provide(BetterAuthLive),
|
||||||
Layer.provide(CorsMiddleware.layer),
|
Layer.provide(CorsMiddleware.layer),
|
||||||
Layer.launch,
|
Layer.launch,
|
||||||
NodeRuntime.runMain,
|
NodeRuntime.runMain,
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
import { serve } from "@hono/node-server";
|
|
||||||
import { authDataSchema } from "@money/shared/auth";
|
|
||||||
import { BASE_URL } from "@money/shared";
|
|
||||||
import { cors } from "hono/cors";
|
|
||||||
import { auth } from "./auth";
|
|
||||||
import { getHono } from "./hono";
|
|
||||||
import { zero } from "./zero";
|
|
||||||
import { webhook } from "./webhook";
|
|
||||||
|
|
||||||
const app = getHono();
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
"/api/*",
|
|
||||||
cors({
|
|
||||||
origin: ["https://money.koon.us", `${BASE_URL}:8081`],
|
|
||||||
allowMethods: ["POST", "GET", "OPTIONS"],
|
|
||||||
allowHeaders: ["Content-Type", "Authorization"],
|
|
||||||
credentials: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
app.on(["GET", "POST"], "/api/auth/*", (c) => auth.handler(c.req.raw));
|
|
||||||
|
|
||||||
app.use("*", async (c, next) => {
|
|
||||||
const authHeader = c.req.raw.headers.get("Authorization");
|
|
||||||
const cookie = authHeader?.split("Bearer ")[1];
|
|
||||||
|
|
||||||
const newHeaders = new Headers(c.req.raw.headers);
|
|
||||||
|
|
||||||
if (cookie) {
|
|
||||||
newHeaders.set("Cookie", cookie);
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = await auth.api.getSession({ headers: newHeaders });
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
c.set("auth", null);
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
c.set("auth", authDataSchema.parse(session));
|
|
||||||
return next();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.route("/api/zero", zero);
|
|
||||||
|
|
||||||
app.get("/api", (c) => c.text("OK"));
|
|
||||||
app.get("/api/webhook_receiver", webhook);
|
|
||||||
app.get("/", (c) => c.text("OK"));
|
|
||||||
|
|
||||||
serve(
|
|
||||||
{
|
|
||||||
fetch: app.fetch,
|
|
||||||
port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
|
|
||||||
},
|
|
||||||
(info) => {
|
|
||||||
console.log(`Server is running on ${info.address}:${info.port}`);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
11
apps/api/src/middleware/cors.ts
Normal file
11
apps/api/src/middleware/cors.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { HttpLayerRouter, HttpMiddleware } from "@effect/platform";
|
||||||
|
import { BASE_URL } from "@money/shared";
|
||||||
|
|
||||||
|
export const CorsMiddleware = HttpLayerRouter.middleware(
|
||||||
|
HttpMiddleware.cors({
|
||||||
|
allowedOrigins: ["https://money.koon.us", `${BASE_URL}:8081`],
|
||||||
|
allowedMethods: ["POST", "GET", "OPTIONS"],
|
||||||
|
allowedHeaders: ["Content-Type", "Authorization"],
|
||||||
|
credentials: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
61
apps/api/src/middleware/session.ts
Normal file
61
apps/api/src/middleware/session.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { AuthSchema } from "@money/shared/auth";
|
||||||
|
import { Context, Effect, Schema, Console } from "effect";
|
||||||
|
import { Authorization } from "../auth/context";
|
||||||
|
import { HttpLayerRouter, HttpServerRequest } from "@effect/platform";
|
||||||
|
|
||||||
|
export class CurrentSession extends Context.Tag("CurrentSession")<
|
||||||
|
CurrentSession,
|
||||||
|
{ readonly auth: Schema.Schema.Type<typeof AuthSchema> | null }
|
||||||
|
>() {}
|
||||||
|
|
||||||
|
export const SessionMiddleware = HttpLayerRouter.middleware<{
|
||||||
|
provides: CurrentSession;
|
||||||
|
}>()(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const auth = yield* Authorization;
|
||||||
|
|
||||||
|
return (httpEffect) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const request = yield* HttpServerRequest.HttpServerRequest;
|
||||||
|
const headers = { ...request.headers };
|
||||||
|
|
||||||
|
const token = yield* HttpServerRequest.schemaHeaders(
|
||||||
|
Schema.Struct({
|
||||||
|
authorization: Schema.optional(Schema.String),
|
||||||
|
}),
|
||||||
|
).pipe(
|
||||||
|
Effect.tap(Console.debug),
|
||||||
|
Effect.flatMap(({ authorization }) =>
|
||||||
|
authorization != undefined
|
||||||
|
? parseAuthorization(authorization)
|
||||||
|
: Effect.succeed(undefined),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (token) {
|
||||||
|
headers["cookie"] = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = yield* auth
|
||||||
|
.use((auth) => auth.api.getSession({ headers }))
|
||||||
|
.pipe(
|
||||||
|
Effect.flatMap((s) =>
|
||||||
|
s == null ? Effect.succeed(null) : Schema.decode(AuthSchema)(s),
|
||||||
|
),
|
||||||
|
// Effect.tap((s) => Console.debug("Auth result", s)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return yield* Effect.provideService(httpEffect, CurrentSession, {
|
||||||
|
auth: session,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const parseAuthorization = (input: string) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const m = /^Bearer\s+(.+)$/.exec(input);
|
||||||
|
if (!m) {
|
||||||
|
return yield* Effect.fail(new Error("Invalid token"));
|
||||||
|
}
|
||||||
|
return m[1];
|
||||||
|
});
|
||||||
@@ -1,11 +1,21 @@
|
|||||||
import type { Context } from "hono";
|
import {
|
||||||
|
HttpLayerRouter,
|
||||||
|
HttpServerRequest,
|
||||||
|
HttpServerResponse,
|
||||||
|
} from "@effect/platform";
|
||||||
|
import { Effect } from "effect";
|
||||||
|
|
||||||
import { plaidClient } from "./plaid";
|
import { plaidClient } from "./plaid";
|
||||||
// import { LinkSessionFinishedWebhook, WebhookType } from "plaid";
|
// import { LinkSessionFinishedWebhook, WebhookType } from "plaid";
|
||||||
|
|
||||||
export const webhook = async (c: Context) => {
|
export const WebhookReceiverRoute = HttpLayerRouter.add(
|
||||||
console.log("Got webhook");
|
"*",
|
||||||
const b = await c.req.text();
|
"/api/webhook_receiver",
|
||||||
console.log("body:", b);
|
|
||||||
|
|
||||||
return c.text("Hi");
|
Effect.gen(function* () {
|
||||||
};
|
const request = yield* HttpServerRequest.HttpServerRequest;
|
||||||
|
const body = yield* request.json;
|
||||||
|
Effect.log("Got a webhook!", body);
|
||||||
|
return HttpServerResponse.text("HELLO THERE");
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|||||||
6
apps/api/src/zero/errors.ts
Normal file
6
apps/api/src/zero/errors.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { Data } from "effect";
|
||||||
|
|
||||||
|
export class ZeroUnknownError extends Data.TaggedError("ZeroUnknownError") {}
|
||||||
|
export class ZeroError extends Data.TaggedError("ZeroError")<{
|
||||||
|
error: Error;
|
||||||
|
}> {}
|
||||||
106
apps/api/src/zero/handler.ts
Normal file
106
apps/api/src/zero/handler.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import {
|
||||||
|
HttpLayerRouter,
|
||||||
|
HttpServerRequest,
|
||||||
|
HttpServerResponse,
|
||||||
|
Url,
|
||||||
|
} from "@effect/platform";
|
||||||
|
import { Console, Effect, Layer } from "effect";
|
||||||
|
import { CurrentSession, SessionMiddleware } from "../middleware/session";
|
||||||
|
import { ZeroError, ZeroUnknownError } from "./errors";
|
||||||
|
import { withValidation, type ReadonlyJSONValue } from "@rocicorp/zero";
|
||||||
|
import {
|
||||||
|
handleGetQueriesRequest,
|
||||||
|
PushProcessor,
|
||||||
|
ZQLDatabase,
|
||||||
|
} from "@rocicorp/zero/server";
|
||||||
|
import { BASE_URL, queries, schema } from "@money/shared";
|
||||||
|
import type { AuthSchemaType } from "@money/shared/auth";
|
||||||
|
import { PostgresJSConnection } from "@rocicorp/zero/pg";
|
||||||
|
import postgres from "postgres";
|
||||||
|
import { createMutators } from "./mutators";
|
||||||
|
|
||||||
|
const processor = new PushProcessor(
|
||||||
|
new ZQLDatabase(
|
||||||
|
new PostgresJSConnection(postgres(process.env.ZERO_UPSTREAM_DB! as string)),
|
||||||
|
schema,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ZeroQueryRoute = HttpLayerRouter.add(
|
||||||
|
"POST",
|
||||||
|
"/api/zero/get-queries",
|
||||||
|
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const { auth } = yield* CurrentSession;
|
||||||
|
|
||||||
|
const request = yield* HttpServerRequest.HttpServerRequest;
|
||||||
|
const body = yield* request.json;
|
||||||
|
|
||||||
|
const result = yield* Effect.tryPromise({
|
||||||
|
try: () =>
|
||||||
|
handleGetQueriesRequest(
|
||||||
|
(name, args) => ({ query: getQuery(auth, name, args) }),
|
||||||
|
schema,
|
||||||
|
body as ReadonlyJSONValue,
|
||||||
|
),
|
||||||
|
catch: (error) =>
|
||||||
|
error instanceof Error
|
||||||
|
? new ZeroError({ error })
|
||||||
|
: new ZeroUnknownError(),
|
||||||
|
}).pipe(
|
||||||
|
Effect.tapErrorTag("ZeroError", (err) =>
|
||||||
|
Console.error("Zero Error", err.error),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return yield* HttpServerResponse.json(result);
|
||||||
|
}),
|
||||||
|
).pipe(Layer.provide(SessionMiddleware.layer));
|
||||||
|
|
||||||
|
function getQuery(
|
||||||
|
authData: AuthSchemaType | null,
|
||||||
|
name: string,
|
||||||
|
args: readonly ReadonlyJSONValue[],
|
||||||
|
) {
|
||||||
|
if (name in validatedQueries) {
|
||||||
|
const q = validatedQueries[name];
|
||||||
|
return q(authData, ...args);
|
||||||
|
}
|
||||||
|
throw new Error(`Unknown query: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validatedQueries = Object.fromEntries(
|
||||||
|
Object.values(queries).map((q) => [q.queryName, withValidation(q)]),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ZeroMutateRoute = HttpLayerRouter.add(
|
||||||
|
"POST",
|
||||||
|
"/api/zero/mutate",
|
||||||
|
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const { auth } = yield* CurrentSession;
|
||||||
|
|
||||||
|
const request = yield* HttpServerRequest.HttpServerRequest;
|
||||||
|
const url = yield* Url.fromString(`${BASE_URL}${request.url}`);
|
||||||
|
const body = yield* request.json;
|
||||||
|
|
||||||
|
const result = yield* Effect.tryPromise({
|
||||||
|
try: () =>
|
||||||
|
processor.process(
|
||||||
|
createMutators(auth),
|
||||||
|
url.searchParams,
|
||||||
|
body as ReadonlyJSONValue,
|
||||||
|
),
|
||||||
|
catch: (error) =>
|
||||||
|
error instanceof Error
|
||||||
|
? new ZeroError({ error })
|
||||||
|
: new ZeroUnknownError(),
|
||||||
|
}).pipe(
|
||||||
|
Effect.tapErrorTag("ZeroError", (err) =>
|
||||||
|
Console.error("Zero Error", err.error),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return yield* HttpServerResponse.json(result);
|
||||||
|
}),
|
||||||
|
).pipe(Layer.provide(SessionMiddleware.layer));
|
||||||
@@ -1,15 +1,3 @@
|
|||||||
import {
|
|
||||||
type ReadonlyJSONValue,
|
|
||||||
type Transaction,
|
|
||||||
withValidation,
|
|
||||||
} from "@rocicorp/zero";
|
|
||||||
import {
|
|
||||||
handleGetQueriesRequest,
|
|
||||||
PushProcessor,
|
|
||||||
ZQLDatabase,
|
|
||||||
} from "@rocicorp/zero/server";
|
|
||||||
import { PostgresJSConnection } from "@rocicorp/zero/pg";
|
|
||||||
import postgres from "postgres";
|
|
||||||
import {
|
import {
|
||||||
createMutators as createMutatorsShared,
|
createMutators as createMutatorsShared,
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
@@ -18,36 +6,27 @@ import {
|
|||||||
type Mutators,
|
type Mutators,
|
||||||
type Schema,
|
type Schema,
|
||||||
} from "@money/shared";
|
} from "@money/shared";
|
||||||
import type { AuthData } from "@money/shared/auth";
|
import type { AuthSchemaType } from "@money/shared/auth";
|
||||||
import { getHono } from "./hono";
|
|
||||||
import {
|
import {
|
||||||
Configuration,
|
type ReadonlyJSONValue,
|
||||||
CountryCode,
|
type Transaction,
|
||||||
PlaidApi,
|
withValidation,
|
||||||
PlaidEnvironments,
|
} from "@rocicorp/zero";
|
||||||
Products,
|
import { plaidClient } from "../plaid";
|
||||||
} from "plaid";
|
import { CountryCode, Products } from "plaid";
|
||||||
import { randomUUID } from "crypto";
|
|
||||||
import { db } from "./db";
|
|
||||||
import {
|
import {
|
||||||
balance,
|
balance,
|
||||||
plaidAccessTokens,
|
plaidAccessTokens,
|
||||||
plaidLink,
|
plaidLink,
|
||||||
transaction,
|
transaction,
|
||||||
} from "@money/shared/db";
|
} from "@money/shared/db";
|
||||||
|
import { db } from "../db";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
import { and, eq, inArray, sql, type InferInsertModel } from "drizzle-orm";
|
import { and, eq, inArray, sql, type InferInsertModel } from "drizzle-orm";
|
||||||
import { plaidClient } from "./plaid";
|
|
||||||
|
|
||||||
const processor = new PushProcessor(
|
|
||||||
new ZQLDatabase(
|
|
||||||
new PostgresJSConnection(postgres(process.env.ZERO_UPSTREAM_DB! as string)),
|
|
||||||
schema,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
type Tx = Transaction<Schema>;
|
type Tx = Transaction<Schema>;
|
||||||
|
|
||||||
const createMutators = (authData: AuthData | null) => {
|
export const createMutators = (authData: AuthSchemaType | null) => {
|
||||||
const mutators = createMutatorsShared(authData);
|
const mutators = createMutatorsShared(authData);
|
||||||
return {
|
return {
|
||||||
...mutators,
|
...mutators,
|
||||||
@@ -221,41 +200,3 @@ const createMutators = (authData: AuthData | null) => {
|
|||||||
},
|
},
|
||||||
} as const satisfies Mutators;
|
} as const satisfies Mutators;
|
||||||
};
|
};
|
||||||
|
|
||||||
const zero = getHono()
|
|
||||||
.post("/mutate", async (c) => {
|
|
||||||
const authData = c.get("auth");
|
|
||||||
|
|
||||||
const result = await processor.process(createMutators(authData), c.req.raw);
|
|
||||||
|
|
||||||
return c.json(result);
|
|
||||||
})
|
|
||||||
.post("/get-queries", async (c) => {
|
|
||||||
const authData = c.get("auth");
|
|
||||||
|
|
||||||
const result = await handleGetQueriesRequest(
|
|
||||||
(name, args) => ({ query: getQuery(authData, name, args) }),
|
|
||||||
schema,
|
|
||||||
c.req.raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
return c.json(result);
|
|
||||||
});
|
|
||||||
|
|
||||||
const validatedQueries = Object.fromEntries(
|
|
||||||
Object.values(queries).map((q) => [q.queryName, withValidation(q)]),
|
|
||||||
);
|
|
||||||
|
|
||||||
function getQuery(
|
|
||||||
authData: AuthData | null,
|
|
||||||
name: string,
|
|
||||||
args: readonly ReadonlyJSONValue[],
|
|
||||||
) {
|
|
||||||
if (name in validatedQueries) {
|
|
||||||
const q = validatedQueries[name];
|
|
||||||
return q(authData, ...args);
|
|
||||||
}
|
|
||||||
throw new Error(`Unknown query: ${name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { zero };
|
|
||||||
@@ -14,6 +14,7 @@ import { config } from "./config";
|
|||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import type { BetterFetchResponse } from "@better-fetch/fetch";
|
import type { BetterFetchResponse } from "@better-fetch/fetch";
|
||||||
import { AuthSchema } from "@money/shared/auth";
|
import { AuthSchema } from "@money/shared/auth";
|
||||||
|
import { encode } from "node:punycode";
|
||||||
|
|
||||||
class AuthClientUnknownError extends Data.TaggedError(
|
class AuthClientUnknownError extends Data.TaggedError(
|
||||||
"AuthClientUnknownError",
|
"AuthClientUnknownError",
|
||||||
@@ -161,9 +162,10 @@ const requestAuth = Effect.gen(function* () {
|
|||||||
if (sessionData == null) return yield* Effect.fail(new AuthClientNoData());
|
if (sessionData == null) return yield* Effect.fail(new AuthClientNoData());
|
||||||
|
|
||||||
const result = yield* Schema.decodeUnknown(AuthSchema)(sessionData);
|
const result = yield* Schema.decodeUnknown(AuthSchema)(sessionData);
|
||||||
|
const encoded = yield* Schema.encode(AuthSchema)(result);
|
||||||
|
|
||||||
const fs = yield* FileSystem.FileSystem;
|
const fs = yield* FileSystem.FileSystem;
|
||||||
yield* fs.writeFileString(config.authPath, JSON.stringify(result));
|
yield* fs.writeFileString(config.authPath, JSON.stringify(encoded));
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user