Backend: DB schema, login API
This commit is contained in:
96
packages/backend/src/services/database.ts
Normal file
96
packages/backend/src/services/database.ts
Normal 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;
|
||||
}),
|
||||
});
|
||||
}),
|
||||
));
|
||||
@@ -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>() { }
|
||||
|
||||
Reference in New Issue
Block a user