forked from effect-app/boilerplate
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRequestEnv.ts
More file actions
157 lines (138 loc) · 4.83 KB
/
RequestEnv.ts
File metadata and controls
157 lines (138 loc) · 4.83 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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/ban-types */
import { Role } from "models/User"
import { HttpServerRequest } from "@effect-app/infra/api/http"
import { JWTError, type RequestHandler } from "@effect-app/infra/api/routing"
import type { RequestContext } from "@effect-app/infra/RequestContext"
import { RequestContextContainer } from "@effect-app/infra/services/RequestContextContainer"
import type { StructFields } from "@effect-app/schema"
import { Req as Req_ } from "@effect-app/schema/REST"
import { NotLoggedInError, UnauthorizedError } from "api/errors"
import { Auth0Config, checkJWTI } from "api/middleware/auth"
import { Duration, Effect, Exit, Layer, Option, Request } from "effect-app"
import {
makeUserProfileFromAuthorizationHeader,
makeUserProfileFromUserHeader,
UserProfile
} from "../services/UserProfile"
// Workaround for the error when using
// import type { AllowAnonymous, RequestConfig } from "resources/lib"
export type RequestConfig = { allowAnonymous?: true; allowedRoles?: readonly Role[] }
export type AllowAnonymous<A> = A extends { allowAnonymous: true } ? true : false
export function Req<C extends RequestConfig>(config?: C) {
return Req_(config)
}
export interface CTX {
context: RequestContext
}
export type GetCTX<Req> =
& CTX
& (AllowAnonymous<Req> extends true ? {
userProfile?: UserProfile
}
// eslint-disable-next-line @typescript-eslint/ban-types
: { userProfile: UserProfile })
export type GetContext<Req> = AllowAnonymous<Req> extends true ? never
// eslint-disable-next-line @typescript-eslint/ban-types
: UserProfile
export const RequestCacheLayers = Layer.mergeAll(
Layer.setRequestCache(
Request.makeCache({ capacity: 500, timeToLive: Duration.hours(8) })
),
Layer.setRequestCaching(true),
Layer.setRequestBatching(true)
)
const authConfig = Auth0Config.runSync
const EmptyLayer = Effect.unit.pipe(Layer.scopedDiscard)
const fakeLogin = true
const checkRoles = (request: any, userProfile: Option<UserProfile>) =>
Effect.gen(function*($) {
const userRoles = userProfile
.map((_) => _.roles.includes("manager") ? [Role("manager"), Role("user")] : [Role("user")])
.getOrElse(() => [Role("user")])
const allowedRoles: readonly Role[] = request.allowedRoles ?? ["user"]
if (!allowedRoles.some((_) => userRoles.includes(_))) {
return yield* $(new UnauthorizedError())
}
})
const UserAuthorizationLive = <Req extends RequestConfig>(request: Req) =>
Effect
.gen(function*($) {
if (!fakeLogin && !request.allowAnonymous) {
yield* $(Effect.catchAll(checkJWTI(authConfig), (err) => Effect.fail(new JWTError({ error: err }))))
}
const req = yield* $(HttpServerRequest)
const r = (fakeLogin
? makeUserProfileFromUserHeader(req.headers["x-user"])
: makeUserProfileFromAuthorizationHeader(
req.headers["authorization"]
))
.pipe(Effect.exit)
.runSync
if (!Exit.isSuccess(r)) {
yield* $(Effect.logWarning("Parsing userInfo failed").pipe(Effect.annotateLogs("r", r)))
}
const userProfile = Option.fromNullable(Exit.isSuccess(r) ? r.value : undefined)
const rcc = yield* $(RequestContextContainer)
yield* $(rcc.update((_): RequestContext => ({ ..._, userProfile: userProfile.value })))
const up = userProfile.value
if (!request.allowAnonymous && !up) {
return yield* $(new NotLoggedInError())
}
yield* $(checkRoles(request, userProfile))
if (up) {
return Layer.succeed(UserProfile, up)
}
return EmptyLayer
})
.pipe(Effect.withSpan("middleware"), Layer.unwrapEffect)
export const RequestEnv = <Req extends RequestConfig>(handler: { Request: Req }) =>
Layer.mergeAll(UserAuthorizationLive(handler.Request))
export type RequestEnv = Layer.Layer.Success<ReturnType<typeof RequestEnv>>
export function handleRequestEnv<
R,
M,
PathA extends StructFields,
CookieA extends StructFields,
QueryA extends StructFields,
BodyA extends StructFields,
HeaderA extends StructFields,
ReqA extends PathA & QueryA & BodyA,
ResA extends StructFields,
ResE,
PPath extends `/${string}`,
CTX,
Context
>(
handler: RequestHandler<
R,
M,
PathA,
CookieA,
QueryA,
BodyA,
HeaderA,
ReqA,
ResA,
ResE,
PPath,
CTX,
Context,
RequestConfig
>
) {
return {
handler: {
...handler,
h: (pars: any) =>
Effect
.all({
context: RequestContextContainer.get,
userProfile: Effect.serviceOption(UserProfile).andThen((_) => _.value)
})
.andThen((ctx) => (handler.h as (i: any, ctx: CTX) => Effect<ResA, ResE, R>)(pars, ctx as any /* TODO */))
.pipe(Effect.provide(RequestCacheLayers))
},
makeRequestLayer: RequestEnv(handler)
}
}