Backend: DB schema, login API

This commit is contained in:
2024-08-03 15:29:29 +02:00
parent 777038e0b4
commit 903168a565
6 changed files with 288 additions and 33 deletions

View File

@@ -0,0 +1,96 @@
import { Schema as S } from "@effect/schema";
import { Database as SqliteDatabase } from "bun:sqlite";
import { SessionId, User, UserId } from "common";
import { Context, Effect, Layer, pipe } from "effect";
import { NoSuchElementException } from "effect/Cause";
export function generateSessionId(byteLength: number = 12): SessionId {
const array = new Uint8Array(byteLength);
crypto.getRandomValues(array);
const string = Buffer.from(array).toString("base64url");
return SessionId.make(string);
}
export interface DatabaseInterface {
readonly getUserByUsername: (username: string) => Effect.Effect<User, NoSuchElementException>;
readonly createSession: (userId: UserId) => Effect.Effect<SessionId>;
}
export class Database extends Context.Tag("Database")<Database, DatabaseInterface>() { }
export const DatabaseLive = (filePath: string = "db.sqlite3") => Layer.effect(Database, pipe(
Effect.acquireRelease(
Effect.sync(() => new SqliteDatabase(filePath, { create: true, readwrite: true, strict: true })),
(db) => Effect.sync(() => db.close()),
),
Effect.tap((db) => Effect.sync(() => {
const systemInformation = `
createdBy TEXT REFERENCES User (userId) ON DELETE SET NULL ON UPDATE CASCADE,
createdAt TEXT NOT NULL,
modifiedBy TEXT REFERENCES User (userId) ON DELETE SET NULL ON UPDATE CASCADE,
modifiedAt TEXT NUL NULL
`;
db.exec("PRAGMA foreign_keys = ON");
db.exec(`CREATE TABLE IF NOT EXISTS AccessLog (
timestamp TEXT NOT NULL,
requestId TEXT NOT NULL,
method TEXT NOT NULL,
pathname TEXT NOT NULL,
query TEXT NOT NULL,
ip TEXT
)`);
db.exec(`CREATE TABLE IF NOT EXISTS User (
userId TEXT NOT NULL PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
admin BOOLEAN NOT NULL
)`);
db.exec(`CREATE TABLE IF NOT EXISTS Piece (
pieceId TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
composer TEXT,
lyricist TEXT,
arranger TEXT,
${systemInformation}
)`);
db.exec(`CREATE TABLE IF NOT EXISTS Session (
sessionId TEXT NOT NULL PRIMARY KEY,
userId TEXT NOT NULL REFERENCES User (userId) ON DELETE CASCADE ON UPDATE CASCADE,
expiresAt TEXT NOT NULL,
)`);
db.exec(`CREATE TABLE IF NOT EXISTS Attachment (
attachmentId TEXT NOT NULL PRIMARY KEY,
pieceId TEXT NOT NULL REFERENCES Piece (pieceId) ON DELETE CASCADE ON UPDATE CASCADE,
sha256 BLOB NOT NULL,
filename TEXT NOT NULL,
mediaType TEXT NOT NULL,
${systemInformation}
)`);
})),
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 = ?");
return Object.freeze<DatabaseInterface>({
getUserByUsername: (username) => Effect.suspend(() => {
const res = getUserByUsername.get(username);
if (res === null) return Effect.fail(new NoSuchElementException());
return Effect.succeed(S.decodeUnknownSync(User)(res));
}),
createSession: (userId) => Effect.sync(() => {
const sessionId = generateSessionId();
createSession.run(sessionId, userId);
return sessionId;
}),
});
}),
));

View File

@@ -1,13 +1,16 @@
import { RequestId } from "common";
import { Context, Option as O } from "effect";
import { Context, Effect, Option as O } from "effect";
export interface RequestInterface {
readonly requestId: RequestId;
readonly method: string;
readonly pathname: string;
readonly path: readonly string[];
readonly query: { readonly [_: string]: string };
readonly headers: { readonly [_: string]: string };
readonly ip: O.Option<string>;
readonly json: Effect.Effect<unknown, SyntaxError>;
}
export class Request extends Context.Tag("Request")<Request, RequestInterface>() { }