Skip to content

Commit 8f16bef

Browse files
authored
feat: security (#491)
* refactor: Requirements -> Context type helper * feat: security
1 parent 9d4b5c6 commit 8f16bef

35 files changed

+831
-650
lines changed

packages/effect-http-node/examples/example.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Schema } from "@effect/schema"
22
import { Context, Effect, Layer, pipe } from "effect"
3-
import { Api, RouterBuilder } from "effect-http"
3+
import { Api, RouterBuilder, Security } from "effect-http"
44

55
import { NodeRuntime } from "@effect/platform-node"
66
import { NodeServer } from "effect-http-node"
@@ -33,11 +33,7 @@ const dummyStuff = pipe(
3333
const getLesnek = Api.get("getLesnek", "/lesnek").pipe(
3434
Api.setResponseBody(Schema.string),
3535
Api.setRequestQuery(Lesnek),
36-
Api.addSecurity("myAwesomeBearerAuth", {
37-
type: "http",
38-
options: { scheme: "bearer", bearerFormat: "JWT" },
39-
schema: Schema.Secret
40-
})
36+
Api.setSecurity(Security.bearer({ name: "myAwesomeBearerAuth", bearerFormat: "JWT" }))
4137
)
4238

4339
const api = pipe(

packages/effect-http-node/examples/new-api.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Schema } from "@effect/schema"
22
import { pipe } from "effect"
3-
import { Api, ApiGroup, ApiResponse, SecurityScheme } from "effect-http"
3+
import { Api, ApiGroup, ApiResponse, Security } from "effect-http"
44

55
interface MyRequirement {}
66
interface AnotherDep {}
@@ -31,12 +31,8 @@ const test = pipe(
3131
Api.get("test", "/test"),
3232
Api.setRequestBody(MyRequest),
3333
Api.setRequestPath(TestPathParams),
34-
Api.addSecurity(
35-
"mySecurity",
36-
SecurityScheme.bearer({
37-
description: "test",
38-
tokenSchema: Schema.string
39-
})
34+
Api.setSecurity(
35+
Security.bearer({ name: "mySecurity", description: "test" })
4036
),
4137
Api.setResponseBody(MyRequest),
4238
Api.addResponse(ApiResponse.make(201, TestPathParams, MyRequest))

packages/effect-http-node/examples/readme-security.ts

+4-17
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,21 @@
11
import { NodeRuntime } from "@effect/platform-node"
22
import { Schema } from "@effect/schema"
33
import { Effect } from "effect"
4-
import { Api, RouterBuilder } from "effect-http"
4+
import { Api, RouterBuilder, Security } from "effect-http"
55
import { NodeServer } from "effect-http-node"
66

77
const mySecuredEnpoint = Api.post("security", "/testSecurity", { description: "" }).pipe(
88
Api.setResponseBody(Schema.string),
9-
Api.addSecurity(
10-
"myAwesomeBearerAuth", // arbitrary name for the security scheme
11-
{
12-
type: "http",
13-
options: {
14-
scheme: "bearer",
15-
bearerFormat: "JWT"
16-
},
17-
// Schema<any, string> for decoding-encoding the significant part
18-
// "Authorization: Bearer <significant part>"
19-
schema: Schema.Secret
20-
}
21-
)
9+
Api.setSecurity(Security.bearer())
2210
)
2311

2412
const api = Api.make().pipe(
2513
Api.addEndpoint(mySecuredEnpoint)
2614
)
2715

2816
const app = RouterBuilder.make(api).pipe(
29-
RouterBuilder.handle("security", (_, security) => {
30-
const token = security.myAwesomeBearerAuth.token // Secret
31-
return Effect.succeed(`your token ${token}`)
17+
RouterBuilder.handle("security", (_, token) => {
18+
return Effect.succeed(`your token ${token}`) // Secret
3219
}),
3320
RouterBuilder.build
3421
)

packages/effect-http-node/test/server.test.ts

+116-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { HttpServer } from "@effect/platform"
1+
import { HttpClient, HttpServer } from "@effect/platform"
2+
import { Schema } from "@effect/schema"
23
import * as it from "@effect/vitest"
3-
import { Context, Effect, Either, Layer, Option, pipe, ReadonlyArray } from "effect"
4-
import { Api, ApiResponse, ClientError, RouterBuilder, ServerError } from "effect-http"
4+
import { Context, Effect, Either, Encoding, Layer, Option, pipe, ReadonlyArray } from "effect"
5+
import { Api, ApiResponse, ClientError, RouterBuilder, Security, ServerError } from "effect-http"
56
import { NodeTesting } from "effect-http-node"
67
import { createHash } from "node:crypto"
78
import { describe, expect, test } from "vitest"
@@ -286,7 +287,7 @@ it.scoped(
286287

287288
const result = yield* _(
288289
NodeTesting.make(app, exampleApiOptional),
289-
Effect.flatMap((client) => Effect.all(ReadonlyArray.map(params, client.hello)))
290+
Effect.flatMap((client) => Effect.all(ReadonlyArray.map(params, (params) => client.hello(params))))
290291
)
291292

292293
expect(result).toStrictEqual(params)
@@ -309,7 +310,7 @@ it.scoped(
309310

310311
const result = yield* _(
311312
NodeTesting.make(app, exampleApiOptionalParams),
312-
Effect.flatMap((client) => Effect.all(ReadonlyArray.map(params, client.hello)))
313+
Effect.flatMap((client) => Effect.all(ReadonlyArray.map(params, (params) => client.hello(params))))
313314
)
314315

315316
expect(result).toStrictEqual(params)
@@ -364,3 +365,113 @@ it.scoped(
364365
expect(result).toBe(undefined)
365366
})
366367
)
368+
369+
it.scoped(
370+
"optional parameters",
371+
Effect.gen(function*(_) {
372+
const app = pipe(
373+
RouterBuilder.make(exampleApiOptionalParams),
374+
RouterBuilder.handle("hello", ({ path }) => Effect.succeed({ path })),
375+
RouterBuilder.build
376+
)
377+
378+
const params = [
379+
{ path: { value: 12 } },
380+
{ path: { value: 12, another: "another" } }
381+
] as const
382+
383+
const result = yield* _(
384+
NodeTesting.make(app, exampleApiOptionalParams),
385+
Effect.flatMap((client) => Effect.all(ReadonlyArray.map(params, (params) => client.hello(params))))
386+
)
387+
388+
expect(result).toStrictEqual(params)
389+
})
390+
)
391+
392+
it.scoped(
393+
"single full response",
394+
Effect.gen(function*(_) {
395+
const app = pipe(
396+
RouterBuilder.make(exampleApiFullResponse),
397+
RouterBuilder.handle("hello", () =>
398+
Effect.succeed({
399+
body: 12,
400+
headers: { "my-header": "test" },
401+
status: 200 as const
402+
})),
403+
RouterBuilder.handle("another", () => Effect.succeed(12)),
404+
RouterBuilder.build
405+
)
406+
407+
const result = yield* _(
408+
NodeTesting.make(app, exampleApiFullResponse),
409+
Effect.flatMap((client) => Effect.all([client.hello({}), client.another({})]))
410+
)
411+
412+
expect(result).toMatchObject([
413+
{
414+
status: 200,
415+
body: 12,
416+
headers: { "my-header": "test" }
417+
},
418+
12
419+
])
420+
})
421+
)
422+
423+
it.scoped(
424+
"empty response",
425+
Effect.gen(function*(_) {
426+
const app = pipe(
427+
RouterBuilder.make(exampleApiEmptyResponse),
428+
RouterBuilder.handle("test", () => Effect.unit),
429+
RouterBuilder.build
430+
)
431+
432+
const result = yield* _(
433+
NodeTesting.make(app, exampleApiEmptyResponse),
434+
Effect.flatMap((client) => client.test({ body: "test" }))
435+
)
436+
437+
expect(result).toBe(undefined)
438+
})
439+
)
440+
441+
it.scoped(
442+
"complex security example",
443+
Effect.gen(function*(_) {
444+
class MyService extends Effect.Tag("MyService")<MyService, { value: string }>() {
445+
static live = Layer.succeed(MyService, { value: "hello" })
446+
}
447+
448+
const security = pipe(
449+
Security.basic(),
450+
Security.mapEffect((creds) => MyService.value.pipe(Effect.map((value) => `${value}-${creds.user}-${creds.pass}`)))
451+
)
452+
453+
const api = Api.make().pipe(
454+
Api.addEndpoint(
455+
Api.post("test", "/test").pipe(Api.setResponseBody(Schema.string), Api.setSecurity(security))
456+
)
457+
)
458+
459+
const app = pipe(
460+
RouterBuilder.make(api),
461+
RouterBuilder.handle("test", (_, security) => Effect.succeed(security)),
462+
RouterBuilder.build
463+
)
464+
465+
const credentials = Encoding.encodeBase64("user:pass")
466+
467+
const result = yield* _(
468+
NodeTesting.make(app, api),
469+
Effect.flatMap((client) =>
470+
client.test({}, HttpClient.request.setHeader("authorization", `basic ${credentials}`))
471+
),
472+
Effect.provide(MyService.live)
473+
)
474+
475+
expect(result).toBe("hello-user-pass")
476+
})
477+
)

0 commit comments

Comments
 (0)