diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index fe6165a..1070357 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -11,11 +11,14 @@ import { WebhookReceiverRoute } from "./webhook"; import { ZeroMutateRoute, ZeroQueryRoute } from "./zero/handler"; import { RpcRoute } from "./rpc/handler"; import { BASE_URL } from "@money/shared"; +import { CurrentSession, SessionMiddleware } from "./middleware/session"; const RootRoute = HttpLayerRouter.add( "GET", "/", Effect.gen(function* () { + const d = yield* CurrentSession; + return HttpServerResponse.text("OK"); }), ); @@ -28,11 +31,11 @@ const AllRoutes = Layer.mergeAll( RpcRoute, WebhookReceiverRoute, ).pipe( + Layer.provide(SessionMiddleware.layer), Layer.provide( HttpLayerRouter.cors({ allowedOrigins: ["https://money.koon.us", `${BASE_URL}:8081`], allowedMethods: ["POST", "GET", "OPTIONS"], - // allowedHeaders: ["Content-Type", "Authorization", ""], credentials: true, }), ), diff --git a/apps/api/src/rpc/handler.ts b/apps/api/src/rpc/handler.ts index 26e52b1..06547c9 100644 --- a/apps/api/src/rpc/handler.ts +++ b/apps/api/src/rpc/handler.ts @@ -1,13 +1,76 @@ import { RpcSerialization, RpcServer } from "@effect/rpc"; -import { Effect, Layer, Schema } from "effect"; -import { LinkRpcs, Link } from "@money/shared/rpc"; +import { Console, Effect, Layer, Schema } from "effect"; +import { LinkRpcs, Link, AuthMiddleware } from "@money/shared/rpc"; +import { CurrentSession } from "../middleware/session"; +import { Authorization } from "../auth/context"; +import { HttpServerRequest } from "@effect/platform"; +import { AuthSchema } from "@money/shared/auth"; + +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]; + }); + +export const AuthLive = Layer.scoped( + AuthMiddleware, + Effect.gen(function* () { + const auth = yield* Authorization; + + return AuthMiddleware.of(({ headers, payload, rpc }) => + Effect.gen(function* () { + const newHeaders = { ...headers }; + + const token = yield* Schema.decodeUnknown( + Schema.Struct({ + authorization: Schema.optional(Schema.String), + }), + )(headers).pipe( + // Effect.tap(Console.debug), + Effect.flatMap(({ authorization }) => + authorization != undefined + ? parseAuthorization(authorization) + : Effect.succeed(undefined), + ), + ); + + if (token) { + newHeaders["cookie"] = token; + } + + const session = yield* auth + .use((auth) => auth.api.getSession({ headers: newHeaders })) + .pipe( + Effect.flatMap((s) => + s == null ? Effect.succeed(null) : Schema.decode(AuthSchema)(s), + ), + Effect.tap((s) => Console.debug("Auth result", s)), + ); + + return { auth: session }; + }).pipe(Effect.orDie), + ); + }), +); const LinkHandlers = LinkRpcs.toLayer({ - CreateLink: () => Effect.succeed(new Link({ href: "hi" })), + CreateLink: () => + Effect.gen(function* () { + const session = yield* CurrentSession; + + return new Link({ href: session.auth?.user.name || "anon" }); + }), }); export const RpcRoute = RpcServer.layerHttpRouter({ group: LinkRpcs, path: "/rpc", protocol: "http", -}).pipe(Layer.provide(LinkHandlers), Layer.provide(RpcSerialization.layerJson)); +}).pipe( + Layer.provide(LinkHandlers), + Layer.provide(RpcSerialization.layerJson), + Layer.provide(AuthLive), +); diff --git a/apps/expo/app/_layout.tsx b/apps/expo/app/_layout.tsx index a8cff7c..0711ebc 100644 --- a/apps/expo/app/_layout.tsx +++ b/apps/expo/app/_layout.tsx @@ -4,7 +4,7 @@ import "react-native-reanimated"; import { authClient } from "@/lib/auth-client"; import { ZeroProvider } from "@rocicorp/zero/react"; import { useMemo } from "react"; -import { authDataSchema } from "@money/shared/auth"; +import { AuthSchema } from "@money/shared/auth"; import { Platform } from "react-native"; import type { ZeroOptions } from "@rocicorp/zero"; import { @@ -15,6 +15,7 @@ import { BASE_URL, } from "@money/shared"; import { expoSQLiteStoreProvider } from "@rocicorp/zero/react-native"; +import { Schema as S } from "effect"; export const unstable_settings = { anchor: "index", @@ -26,8 +27,8 @@ export default function RootLayout() { const { data: session, isPending } = authClient.useSession(); const authData = useMemo(() => { - const result = authDataSchema.safeParse(session); - return result.success ? result.data : null; + const result = session ? S.decodeSync(AuthSchema)(session) : null; + return result ? result : null; }, [session]); const cookie = useMemo(() => { diff --git a/apps/expo/app/rpc.tsx b/apps/expo/app/rpc.tsx deleted file mode 100644 index 65ab3ce..0000000 --- a/apps/expo/app/rpc.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Button, Text, View } from "react-native"; -import { AtomRpc, useAtomSet } from "@effect-atom/atom-react"; -import { Layer } from "effect"; -import { LinkRpcs } from "@money/shared/rpc"; -import { FetchHttpClient } from "@effect/platform"; -import { RpcClient, RpcSerialization } from "@effect/rpc"; - -class Client extends AtomRpc.Tag()("RpcClient", { - group: LinkRpcs, - protocol: RpcClient.layerProtocolHttp({ - url: "http://laptop:3000/rpc", - }).pipe(Layer.provide([RpcSerialization.layerJson, FetchHttpClient.layer])), -}) {} - -export default function Page() { - const create = useAtomSet(Client.mutation("CreateLink")); - - const onPress = () => { - create({ - payload: void 0, - }); - }; - - return ( - - RPC Test - - {link ? ( - <> - - Please click the button to complete setup. - - - - - ) : ( - Loading Plaid Link - )} + {/* {link ? ( */} + {/* <> */} + {/* */} + {/* Please click the button to complete setup. */} + {/* */} + {/**/} + {/* */} + {/* */} + {/* ) : ( */} + {/* Loading Plaid Link */} + {/* )} */} ); }