Extend and fix session management, /me API

This commit is contained in:
2024-08-11 22:54:53 +02:00
parent a0a6681ae0
commit 1a60ecb48a
9 changed files with 287 additions and 35 deletions

View File

@@ -1,7 +1,7 @@
import { Schema as S } from "@effect/schema";
import { Database as SqliteDatabase } from "bun:sqlite";
import { SessionId, UserId } from "common";
import { User } from "common/db";
import { AccessLog, SessionData, User } from "common/db";
import { Context, Effect, Layer, pipe } from "effect";
import { NoSuchElementException } from "effect/Cause";
import { ulid } from "ulid";
@@ -14,8 +14,14 @@ export function generateSessionId(byteLength: number = 12): SessionId {
}
export interface DatabaseInterface {
readonly createAccessLog: (accessLog: AccessLog) => Effect.Effect<void>;
readonly getUserByUsername: (username: string) => Effect.Effect<User, NoSuchElementException>;
readonly createSession: (userId: UserId) => Effect.Effect<SessionId>;
readonly getUserById: (userId: UserId) => Effect.Effect<User, NoSuchElementException>;
readonly createSession: (sessionData: SessionData) => Effect.Effect<SessionId>;
readonly getAndRefreshSessionData: (sessionId: SessionId) => Effect.Effect<SessionData, NoSuchElementException>;
readonly deleteSession: (sessionId: SessionId) => Effect.Effect<void>;
}
export class Database extends Context.Tag("Database")<Database, DatabaseInterface>() { }
@@ -85,23 +91,64 @@ export const DatabaseLive = (filePath: string = "db.sqlite3") => Layer.effect(Da
}
})),
Effect.map((db) => {
const getUserByUsername = db.prepare("SELECT userId, username, password, admin FROM User WHERE username = ?");
const cleanupSessions = db.prepare("DELETE FROM Session WHERE expiresAt >= datetime()");
const createSession = db.prepare("INSERT INTO Session (sessionId, userId, expiresAt) VALUES (?, ?, datetime('now', '+7 days'))");
const updateSession = db.prepare("UPDATE Session SET expiresAt = datetime('now', '+7 days') WHERE sessionId = ?");
const createAccessLog = db.prepare<
never,
[timestamp: string, requestId: string, method: string, pathname: string, query: string, ip: string | null]
>("INSERT INTO AccessLog (timestamp, requestId, method, pathname, query, ip) VALUES (?, ?, ?, ?, ?, ?)");
const getUserByUsername = db.prepare<
{ userId: string, username: string, password: string, admin: number },
[username: string]
>("SELECT userId, username, password, admin FROM User WHERE username = ?");
const getUserById = db.prepare<
{ userId: string, username: string, password: string, admin: number },
[userId: UserId]
>("SELECT userId, username, password, admin FROM User WHERE userId = ?");
const cleanupSessions = db.prepare<never, []>("DELETE FROM Session WHERE datetime() >= expiresAt");
const createSession = db.prepare<
never,
[sessionId: SessionId, userId: UserId]
>("INSERT INTO Session (sessionId, userId, expiresAt) VALUES (?, ?, datetime('now', '+7 days'))");
const updateSession = db.prepare<typeof SessionData.Encoded, [sessionId: SessionId]>("UPDATE Session SET expiresAt = datetime('now', '+7 days') WHERE sessionId = ? RETURNING userId");
const deleteSession = db.prepare<never, [sessionId: SessionId]>("DELETE FROM Session WHERE sessionId = ?");
return Object.freeze<DatabaseInterface>({
createAccessLog: (accessLog) => Effect.sync(() => {
const { timestamp, requestId, method, pathname, query, ip } = S.encodeSync(AccessLog)(accessLog);
createAccessLog.run(timestamp, requestId, method, pathname, query, ip);
}),
getUserByUsername: (username) => Effect.suspend(() => {
const res = getUserByUsername.get(username);
if (res === null) return Effect.fail(new NoSuchElementException());
return Effect.succeed(S.decodeUnknownSync(User)(res));
return Effect.succeed(S.decodeSync(User)(res));
}),
createSession: (userId) => Effect.sync(() => {
getUserById: (userId) => Effect.suspend(() => {
const res = getUserById.get(userId);
if (res === null) return Effect.fail(new NoSuchElementException());
return Effect.succeed(S.decodeSync(User)(res));
}),
createSession: ({ userId }) => Effect.sync(() => {
cleanupSessions.run();
const sessionId = generateSessionId();
createSession.run(sessionId, userId);
return sessionId;
}),
getAndRefreshSessionData: (sessionId) => Effect.suspend(() => {
cleanupSessions.run();
const res = updateSession.get(sessionId);
if (res === null) return Effect.fail(new NoSuchElementException());
return Effect.succeed(S.decodeUnknownSync(SessionData)(res));
}),
deleteSession: (sessionId) => Effect.sync(() => {
cleanupSessions.run();
deleteSession.run(sessionId);
}),
});
}),
));