Skip to content

Commit a9767d3

Browse files
juliusmarmingecodex
andcommitted
Use typed Environment HttpApi clients
Co-authored-by: codex <codex@users.noreply.github.com>
1 parent 9a20e0b commit a9767d3

16 files changed

Lines changed: 866 additions & 687 deletions

File tree

apps/server/src/auth/http.ts

Lines changed: 133 additions & 221 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import {
2-
type AuthBearerBootstrapResult,
3-
AuthBootstrapInput,
4-
AuthCreatePairingCredentialInput,
5-
AuthRevokeClientSessionInput,
6-
AuthRevokePairingLinkInput,
7-
type AuthWebSocketTokenResult,
2+
EnvironmentHttpApi,
3+
EnvironmentHttpBadRequestError,
4+
EnvironmentHttpForbiddenError,
5+
EnvironmentHttpInternalServerError,
6+
EnvironmentHttpUnauthorizedError,
87
} from "@t3tools/contracts";
98
import * as DateTime from "effect/DateTime";
109
import * as Effect from "effect/Effect";
11-
import * as Schema from "effect/Schema";
12-
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
10+
import * as Cookies from "effect/unstable/http/Cookies";
11+
import * as HttpEffect from "effect/unstable/http/HttpEffect";
12+
import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
13+
import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder";
1314

1415
import { AuthError, ServerAuth } from "./Services/ServerAuth.ts";
1516
import { SessionCredentialService } from "./Services/SessionCredentialService.ts";
1617
import { deriveAuthClientMetadata } from "./utils.ts";
17-
import { browserApiCorsHeaders } from "../httpCors.ts";
1818

1919
export const respondToAuthError = (error: AuthError) =>
2020
Effect.gen(function* () {
@@ -28,158 +28,30 @@ export const respondToAuthError = (error: AuthError) =>
2828
{
2929
error: error.message,
3030
},
31-
{ status: error.status ?? 500, headers: browserApiCorsHeaders },
31+
{ status: error.status ?? 500 },
3232
);
3333
});
3434

35-
export const authSessionRouteLayer = HttpRouter.add(
36-
"GET",
37-
"/api/auth/session",
35+
export const failEnvironmentHttpAuthError = (error: AuthError) =>
3836
Effect.gen(function* () {
39-
const request = yield* HttpServerRequest.HttpServerRequest;
40-
const serverAuth = yield* ServerAuth;
41-
const session = yield* serverAuth.getSessionState(request);
42-
return HttpServerResponse.jsonUnsafe(session, {
43-
status: 200,
44-
headers: browserApiCorsHeaders,
45-
});
46-
}),
47-
);
48-
49-
const PairingCredentialRequestHeaders = Schema.Struct({
50-
"content-length": Schema.optionalKey(Schema.String),
51-
"content-type": Schema.optionalKey(Schema.String),
52-
"transfer-encoding": Schema.optionalKey(Schema.String),
53-
});
54-
55-
function hasRequestBody(headers: typeof PairingCredentialRequestHeaders.Type) {
56-
const contentLengthHeader = headers["content-length"];
57-
if (typeof contentLengthHeader === "string") {
58-
const contentLength = Number.parseInt(contentLengthHeader, 10);
59-
if (Number.isFinite(contentLength)) {
60-
return contentLength > 0;
37+
if ((error.status ?? 500) >= 500) {
38+
yield* Effect.logError("auth route failed", {
39+
message: error.message,
40+
cause: error.cause,
41+
});
6142
}
62-
}
63-
return typeof headers["transfer-encoding"] === "string";
64-
}
65-
66-
export const authBootstrapRouteLayer = HttpRouter.add(
67-
"POST",
68-
"/api/auth/bootstrap",
69-
Effect.gen(function* () {
70-
const request = yield* HttpServerRequest.HttpServerRequest;
71-
const serverAuth = yield* ServerAuth;
72-
const sessions = yield* SessionCredentialService;
73-
const payload = yield* HttpServerRequest.schemaBodyJson(AuthBootstrapInput).pipe(
74-
Effect.mapError(
75-
(cause) =>
76-
new AuthError({
77-
message: "Invalid bootstrap payload.",
78-
status: 400,
79-
cause,
80-
}),
81-
),
82-
);
83-
const result = yield* serverAuth.exchangeBootstrapCredential(
84-
payload.credential,
85-
deriveAuthClientMetadata({ request }),
86-
);
8743

88-
return yield* HttpServerResponse.jsonUnsafe(result.response, {
89-
status: 200,
90-
headers: browserApiCorsHeaders,
91-
}).pipe(
92-
HttpServerResponse.setCookie(sessions.cookieName, result.sessionToken, {
93-
expires: DateTime.toDate(result.response.expiresAt),
94-
httpOnly: true,
95-
path: "/",
96-
sameSite: "lax",
97-
}),
98-
);
99-
}).pipe(Effect.catchTag("AuthError", (error) => respondToAuthError(error))),
100-
);
101-
102-
export const authBearerBootstrapRouteLayer = HttpRouter.add(
103-
"POST",
104-
"/api/auth/bootstrap/bearer",
105-
Effect.gen(function* () {
106-
const request = yield* HttpServerRequest.HttpServerRequest;
107-
const serverAuth = yield* ServerAuth;
108-
const payload = yield* HttpServerRequest.schemaBodyJson(AuthBootstrapInput).pipe(
109-
Effect.mapError(
110-
(cause) =>
111-
new AuthError({
112-
message: "Invalid bootstrap payload.",
113-
status: 400,
114-
cause,
115-
}),
116-
),
117-
);
118-
const result = yield* serverAuth.exchangeBootstrapCredentialForBearerSession(
119-
payload.credential,
120-
deriveAuthClientMetadata({ request }),
121-
);
122-
return HttpServerResponse.jsonUnsafe(result satisfies AuthBearerBootstrapResult, {
123-
status: 200,
124-
headers: browserApiCorsHeaders,
125-
});
126-
}).pipe(Effect.catchTag("AuthError", (error) => respondToAuthError(error))),
127-
);
128-
129-
export const authWebSocketTokenRouteLayer = HttpRouter.add(
130-
"POST",
131-
"/api/auth/ws-token",
132-
Effect.gen(function* () {
133-
const request = yield* HttpServerRequest.HttpServerRequest;
134-
const serverAuth = yield* ServerAuth;
135-
const session = yield* serverAuth.authenticateHttpRequest(request);
136-
const result = yield* serverAuth.issueWebSocketToken(session);
137-
return HttpServerResponse.jsonUnsafe(result satisfies AuthWebSocketTokenResult, {
138-
status: 200,
139-
headers: browserApiCorsHeaders,
140-
});
141-
}).pipe(Effect.catchTag("AuthError", (error) => respondToAuthError(error))),
142-
);
143-
144-
export const authPairingCredentialRouteLayer = HttpRouter.add(
145-
"POST",
146-
"/api/auth/pairing-token",
147-
Effect.gen(function* () {
148-
const serverAuth = yield* ServerAuth;
149-
const request = yield* HttpServerRequest.HttpServerRequest;
150-
const session = yield* serverAuth.authenticateHttpRequest(request);
151-
if (session.role !== "owner") {
152-
return yield* new AuthError({
153-
message: "Only owner sessions can create pairing credentials.",
154-
status: 403,
155-
});
44+
switch (error.status) {
45+
case 400:
46+
return yield* new EnvironmentHttpBadRequestError({ message: error.message });
47+
case 401:
48+
return yield* new EnvironmentHttpUnauthorizedError({ message: error.message });
49+
case 403:
50+
return yield* new EnvironmentHttpForbiddenError({ message: error.message });
51+
default:
52+
return yield* new EnvironmentHttpInternalServerError({ message: error.message });
15653
}
157-
const headers = yield* HttpServerRequest.schemaHeaders(PairingCredentialRequestHeaders).pipe(
158-
Effect.mapError(
159-
(cause) =>
160-
new AuthError({
161-
message: "Invalid pairing credential request headers.",
162-
status: 400,
163-
cause,
164-
}),
165-
),
166-
);
167-
const payload = hasRequestBody(headers)
168-
? yield* HttpServerRequest.schemaBodyJson(AuthCreatePairingCredentialInput).pipe(
169-
Effect.mapError(
170-
(cause) =>
171-
new AuthError({
172-
message: "Invalid pairing credential payload.",
173-
status: 400,
174-
cause,
175-
}),
176-
),
177-
)
178-
: {};
179-
const result = yield* serverAuth.issuePairingCredential(payload);
180-
return HttpServerResponse.jsonUnsafe(result, { status: 200 });
181-
}).pipe(Effect.catchTag("AuthError", (error) => respondToAuthError(error))),
182-
);
54+
});
18355

18456
const authenticateOwnerSession = Effect.gen(function* () {
18557
const request = yield* HttpServerRequest.HttpServerRequest;
@@ -194,72 +66,112 @@ const authenticateOwnerSession = Effect.gen(function* () {
19466
return { serverAuth, session } as const;
19567
});
19668

197-
export const authPairingLinksRouteLayer = HttpRouter.add(
198-
"GET",
199-
"/api/auth/pairing-links",
200-
Effect.gen(function* () {
201-
const { serverAuth } = yield* authenticateOwnerSession;
202-
const pairingLinks = yield* serverAuth.listPairingLinks();
203-
return HttpServerResponse.jsonUnsafe(pairingLinks, { status: 200 });
204-
}).pipe(Effect.catchTag("AuthError", (error) => respondToAuthError(error))),
205-
);
206-
207-
export const authPairingLinksRevokeRouteLayer = HttpRouter.add(
208-
"POST",
209-
"/api/auth/pairing-links/revoke",
210-
Effect.gen(function* () {
211-
const { serverAuth } = yield* authenticateOwnerSession;
212-
const payload = yield* HttpServerRequest.schemaBodyJson(AuthRevokePairingLinkInput).pipe(
213-
Effect.mapError(
214-
(cause) =>
215-
new AuthError({
216-
message: "Invalid revoke pairing link payload.",
217-
status: 400,
218-
cause,
219-
}),
220-
),
221-
);
222-
const revoked = yield* serverAuth.revokePairingLink(payload.id);
223-
return HttpServerResponse.jsonUnsafe({ revoked }, { status: 200 });
224-
}).pipe(Effect.catchTag("AuthError", (error) => respondToAuthError(error))),
225-
);
226-
227-
export const authClientsRouteLayer = HttpRouter.add(
228-
"GET",
229-
"/api/auth/clients",
230-
Effect.gen(function* () {
231-
const { serverAuth, session } = yield* authenticateOwnerSession;
232-
const clients = yield* serverAuth.listClientSessions(session.sessionId);
233-
return HttpServerResponse.jsonUnsafe(clients, { status: 200 });
234-
}).pipe(Effect.catchTag("AuthError", (error) => respondToAuthError(error))),
235-
);
236-
237-
export const authClientsRevokeRouteLayer = HttpRouter.add(
238-
"POST",
239-
"/api/auth/clients/revoke",
240-
Effect.gen(function* () {
241-
const { serverAuth, session } = yield* authenticateOwnerSession;
242-
const payload = yield* HttpServerRequest.schemaBodyJson(AuthRevokeClientSessionInput).pipe(
243-
Effect.mapError(
244-
(cause) =>
245-
new AuthError({
246-
message: "Invalid revoke client payload.",
247-
status: 400,
248-
cause,
69+
export const authHttpApiLayer = HttpApiBuilder.group(EnvironmentHttpApi, "auth", (handlers) =>
70+
handlers
71+
.handle("session", () =>
72+
Effect.gen(function* () {
73+
const request = yield* HttpServerRequest.HttpServerRequest;
74+
const serverAuth = yield* ServerAuth;
75+
return yield* serverAuth.getSessionState(request);
76+
}),
77+
)
78+
.handle("bootstrap", ({ payload }) =>
79+
Effect.gen(function* () {
80+
const request = yield* HttpServerRequest.HttpServerRequest;
81+
const serverAuth = yield* ServerAuth;
82+
const sessions = yield* SessionCredentialService;
83+
const result = yield* serverAuth.exchangeBootstrapCredential(
84+
payload.credential,
85+
deriveAuthClientMetadata({ request }),
86+
);
87+
const sessionCookies = yield* Effect.fromResult(
88+
Cookies.set(Cookies.empty, sessions.cookieName, result.sessionToken, {
89+
expires: DateTime.toDate(result.response.expiresAt),
90+
httpOnly: true,
91+
path: "/",
92+
sameSite: "lax",
24993
}),
250-
),
251-
);
252-
const revoked = yield* serverAuth.revokeClientSession(session.sessionId, payload.sessionId);
253-
return HttpServerResponse.jsonUnsafe({ revoked }, { status: 200 });
254-
}).pipe(Effect.catchTag("AuthError", (error) => respondToAuthError(error))),
255-
);
256-
257-
export const authClientsRevokeOthersRouteLayer = HttpRouter.add(
258-
"POST",
259-
"/api/auth/clients/revoke-others",
260-
Effect.gen(function* () {
261-
const { serverAuth, session } = yield* authenticateOwnerSession;
262-
const revokedCount = yield* serverAuth.revokeOtherClientSessions(session.sessionId);
263-
return HttpServerResponse.jsonUnsafe({ revokedCount }, { status: 200 });
264-
}).pipe(Effect.catchTag("AuthError", (error) => respondToAuthError(error))),
94+
).pipe(
95+
Effect.mapError(
96+
(cause) =>
97+
new AuthError({
98+
message: "Failed to create browser session response.",
99+
status: 500,
100+
cause,
101+
}),
102+
),
103+
);
104+
105+
yield* HttpEffect.appendPreResponseHandler((_request, response) =>
106+
Effect.succeed(HttpServerResponse.mergeCookies(response, sessionCookies)),
107+
);
108+
return result.response;
109+
}).pipe(Effect.catchTag("AuthError", failEnvironmentHttpAuthError)),
110+
)
111+
.handle("bootstrapBearer", ({ payload }) =>
112+
Effect.gen(function* () {
113+
const request = yield* HttpServerRequest.HttpServerRequest;
114+
const serverAuth = yield* ServerAuth;
115+
const result = yield* serverAuth.exchangeBootstrapCredentialForBearerSession(
116+
payload.credential,
117+
deriveAuthClientMetadata({ request }),
118+
);
119+
return result;
120+
}).pipe(Effect.catchTag("AuthError", failEnvironmentHttpAuthError)),
121+
)
122+
.handle("webSocketToken", () =>
123+
Effect.gen(function* () {
124+
const request = yield* HttpServerRequest.HttpServerRequest;
125+
const serverAuth = yield* ServerAuth;
126+
const session = yield* serverAuth.authenticateHttpRequest(request);
127+
return yield* serverAuth.issueWebSocketToken(session);
128+
}).pipe(Effect.catchTag("AuthError", failEnvironmentHttpAuthError)),
129+
)
130+
.handle("pairingCredential", ({ payload }) =>
131+
Effect.gen(function* () {
132+
const serverAuth = yield* ServerAuth;
133+
const request = yield* HttpServerRequest.HttpServerRequest;
134+
const session = yield* serverAuth.authenticateHttpRequest(request);
135+
if (session.role !== "owner") {
136+
return yield* new AuthError({
137+
message: "Only owner sessions can create pairing credentials.",
138+
status: 403,
139+
});
140+
}
141+
return yield* serverAuth.issuePairingCredential(payload);
142+
}).pipe(Effect.catchTag("AuthError", failEnvironmentHttpAuthError)),
143+
)
144+
.handle("pairingLinks", () =>
145+
Effect.gen(function* () {
146+
const { serverAuth } = yield* authenticateOwnerSession;
147+
return yield* serverAuth.listPairingLinks();
148+
}).pipe(Effect.catchTag("AuthError", failEnvironmentHttpAuthError)),
149+
)
150+
.handle("revokePairingLink", ({ payload }) =>
151+
Effect.gen(function* () {
152+
const { serverAuth } = yield* authenticateOwnerSession;
153+
const revoked = yield* serverAuth.revokePairingLink(payload.id);
154+
return { revoked };
155+
}).pipe(Effect.catchTag("AuthError", failEnvironmentHttpAuthError)),
156+
)
157+
.handle("clients", () =>
158+
Effect.gen(function* () {
159+
const { serverAuth, session } = yield* authenticateOwnerSession;
160+
return yield* serverAuth.listClientSessions(session.sessionId);
161+
}).pipe(Effect.catchTag("AuthError", failEnvironmentHttpAuthError)),
162+
)
163+
.handle("revokeClient", ({ payload }) =>
164+
Effect.gen(function* () {
165+
const { serverAuth, session } = yield* authenticateOwnerSession;
166+
const revoked = yield* serverAuth.revokeClientSession(session.sessionId, payload.sessionId);
167+
return { revoked };
168+
}).pipe(Effect.catchTag("AuthError", failEnvironmentHttpAuthError)),
169+
)
170+
.handle("revokeOtherClients", () =>
171+
Effect.gen(function* () {
172+
const { serverAuth, session } = yield* authenticateOwnerSession;
173+
const revokedCount = yield* serverAuth.revokeOtherClientSessions(session.sessionId);
174+
return { revokedCount };
175+
}).pipe(Effect.catchTag("AuthError", failEnvironmentHttpAuthError)),
176+
),
265177
);

0 commit comments

Comments
 (0)