Logout API and implementation
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
import { Schema as S } from "@effect/schema";
|
import { Schema as S } from "@effect/schema";
|
||||||
import { SessionId } from "common";
|
import { SessionId } from "common";
|
||||||
import { Login, Me } from "common/api";
|
import { Login, Logout, Me } from "common/api";
|
||||||
import { AccessLog, SessionData } from "common/db";
|
import { AccessLog, SessionData } from "common/db";
|
||||||
import { DateTime, Duration, Either as E, Effect, Match, Option as O, pipe } from "effect";
|
import { DateTime, Duration, Either as E, Effect, Match, Option as O, pipe } from "effect";
|
||||||
|
import { constant } from "effect/Function";
|
||||||
import { Api } from "make-api";
|
import { Api } from "make-api";
|
||||||
import { brotliCompress } from "node:zlib";
|
import { brotliCompress } from "node:zlib";
|
||||||
import { RequestError } from "./RequestError";
|
import { RequestError } from "./RequestError";
|
||||||
@@ -170,6 +171,26 @@ export const app = pipe(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else if (yield* match(Logout)) {
|
||||||
|
|
||||||
|
const sessionId = yield* pipe(
|
||||||
|
getCookie("sessionId"),
|
||||||
|
Effect.flatten,
|
||||||
|
Effect.flatMap(S.decodeUnknown(SessionId)),
|
||||||
|
Effect.catchAll(constant(Effect.succeed(null))),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sessionId !== null) {
|
||||||
|
db.deleteSession(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const setCookie = `sessionId=; Expires=${new Date(0).toUTCString()}; HttpOnly; SameSite=None; Secure`;
|
||||||
|
return new Response(null, {
|
||||||
|
status: 204,
|
||||||
|
headers: {
|
||||||
|
"Set-Cookie": setCookie,
|
||||||
|
},
|
||||||
|
});
|
||||||
} else if (yield* match(Me)) {
|
} else if (yield* match(Me)) {
|
||||||
|
|
||||||
const sessionId = yield* pipe(
|
const sessionId = yield* pipe(
|
||||||
|
|||||||
@@ -3,6 +3,21 @@ import { Schema as S } from "@effect/schema";
|
|||||||
import { UserId } from "common";
|
import { UserId } from "common";
|
||||||
import { pipe } from "effect";
|
import { pipe } from "effect";
|
||||||
|
|
||||||
|
export const Me = pipe(
|
||||||
|
Api.make("GET", "me"),
|
||||||
|
Api.responseBodyJson(200, S.Struct({
|
||||||
|
userId: UserId,
|
||||||
|
username: S.NonEmptyString,
|
||||||
|
admin: S.Boolean,
|
||||||
|
})),
|
||||||
|
Api.responseBodyText(401, S.String),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Logout = pipe(
|
||||||
|
Api.make("POST", "logout"),
|
||||||
|
Api.responseBodyNone(204),
|
||||||
|
);
|
||||||
|
|
||||||
export const Login = pipe(
|
export const Login = pipe(
|
||||||
Api.make("POST", "login"),
|
Api.make("POST", "login"),
|
||||||
Api.requestBodyJson(S.Struct({
|
Api.requestBodyJson(S.Struct({
|
||||||
@@ -17,13 +32,3 @@ export const Login = pipe(
|
|||||||
Api.responseBodyText(400, S.String),
|
Api.responseBodyText(400, S.String),
|
||||||
Api.responseBodyText(401, S.String),
|
Api.responseBodyText(401, S.String),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Me = pipe(
|
|
||||||
Api.make("GET", "me"),
|
|
||||||
Api.responseBodyJson(200, S.Struct({
|
|
||||||
userId: UserId,
|
|
||||||
username: S.NonEmptyString,
|
|
||||||
admin: S.Boolean,
|
|
||||||
})),
|
|
||||||
Api.responseBodyText(401, S.String),
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Schema as S } from "@effect/schema";
|
|||||||
import { Me } from "common/api";
|
import { Me } from "common/api";
|
||||||
import { Effect, Fiber, Option as O, Option, pipe } from "effect";
|
import { Effect, Fiber, Option as O, Option, pipe } from "effect";
|
||||||
import { useLocation } from "preact-iso";
|
import { useLocation } from "preact-iso";
|
||||||
import { useEffect } from "preact/hooks";
|
import { useEffect, useMemo } from "preact/hooks";
|
||||||
import { useStore } from "../store";
|
import { useStore } from "../store";
|
||||||
import * as style from "./Home.css";
|
import * as style from "./Home.css";
|
||||||
|
|
||||||
@@ -41,6 +41,26 @@ export const Home = () => {
|
|||||||
return () => Effect.runFork(Fiber.interrupt(fiber), { immediate: true });
|
return () => Effect.runFork(Fiber.interrupt(fiber), { immediate: true });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const logoutEffect = useMemo(() => Effect.gen(function* () {
|
||||||
|
const res = yield* Effect.promise((signal) => fetch("http://localhost:3000/logout", {
|
||||||
|
method: "POST",
|
||||||
|
signal,
|
||||||
|
credentials: "include",
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
yield* Effect.die(new Error("Response was not ok"));
|
||||||
|
}
|
||||||
|
|
||||||
|
setUser(O.none());
|
||||||
|
|
||||||
|
route("/login");
|
||||||
|
}), []);
|
||||||
|
|
||||||
|
const onLogoutClick = () => {
|
||||||
|
Effect.runFork(logoutEffect);
|
||||||
|
};
|
||||||
|
|
||||||
if (user === null) {
|
if (user === null) {
|
||||||
return (
|
return (
|
||||||
<div class={style.container}>
|
<div class={style.container}>
|
||||||
@@ -53,6 +73,7 @@ export const Home = () => {
|
|||||||
<div class={style.container}>
|
<div class={style.container}>
|
||||||
<div class={style.content}>
|
<div class={style.content}>
|
||||||
Użytkownik: {user.username}
|
Użytkownik: {user.username}
|
||||||
|
<button type="button" onClick={onLogoutClick}>Wyloguj się</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { Method } from "./Method";
|
|||||||
import type { MimeTypePredicate } from "./MimeType";
|
import type { MimeTypePredicate } from "./MimeType";
|
||||||
import { QueryEmpty, type Query } from "./Query";
|
import { QueryEmpty, type Query } from "./Query";
|
||||||
import { RequestBodyFile, RequestBodyJson, RequestBodyMultipart, RequestBodyNone, RequestBodyText, RequestBodyUrlEncoded, type RequestBody } from "./RequestBody";
|
import { RequestBodyFile, RequestBodyJson, RequestBodyMultipart, RequestBodyNone, RequestBodyText, RequestBodyUrlEncoded, type RequestBody } from "./RequestBody";
|
||||||
import { ResponseBodyFile, ResponseBodyJson, ResponseBodyText, ResponseEmpty, type ResponseBody } from "./ResponseBody";
|
import { ResponseBodyFile, ResponseBodyJson, ResponseBodyNone, ResponseBodyText, ResponseEmpty, type ResponseBody } from "./ResponseBody";
|
||||||
import { Route, RouteLiteral, RouteParam } from "./Route";
|
import { Route, RouteLiteral, RouteParam } from "./Route";
|
||||||
|
|
||||||
export interface Api<Props extends Api.PropsAny> {
|
export interface Api<Props extends Api.PropsAny> {
|
||||||
@@ -109,6 +109,14 @@ export const requestBodyFile = <const MimeType extends string>(mimeType: MimeTyp
|
|||||||
readonly query: T["props"]["query"],
|
readonly query: T["props"]["query"],
|
||||||
}> => Api(method, { ...props, request: RequestBodyFile(mimeType) });
|
}> => Api(method, { ...props, request: RequestBodyFile(mimeType) });
|
||||||
|
|
||||||
|
export const responseBodyNone = <const Status extends number>(status: Status) =>
|
||||||
|
<const T extends Api.Any>({ method, props: { response, ...props } }: T): Api<{
|
||||||
|
readonly route: T["props"]["route"],
|
||||||
|
readonly request: T["props"]["request"],
|
||||||
|
readonly response: { readonly [K in keyof T["props"]["response"] | Status]: K extends Status ? ResponseBodyNone : T["props"]["response"][K] },
|
||||||
|
readonly query: T["props"]["query"],
|
||||||
|
}> => Api(method, { ...props, response: Object.freeze<any>({ ...response, [status]: ResponseBodyNone }) });
|
||||||
|
|
||||||
export const responseBodyText = <const Status extends number, const Schema extends S.Schema<any, string>>(status: Status, schema: Schema) =>
|
export const responseBodyText = <const Status extends number, const Schema extends S.Schema<any, string>>(status: Status, schema: Schema) =>
|
||||||
<const T extends Api.Any>({ method, props: { response, ...props } }: T): Api<{
|
<const T extends Api.Any>({ method, props: { response, ...props } }: T): Api<{
|
||||||
readonly route: T["props"]["route"],
|
readonly route: T["props"]["route"],
|
||||||
|
|||||||
Reference in New Issue
Block a user