From 95b0e099aecd095d0a01ff5badcf4b30740c57f3 Mon Sep 17 00:00:00 2001 From: SimonFJ20 Date: Tue, 24 Sep 2024 02:26:55 +0200 Subject: [PATCH] fix login and files --- .gitignore | 3 + database.sql | 18 ++++ main.ts | 243 ++++++++++++++++++++++++++++++++++------------ public/index.html | 3 + public/login.html | 2 + public/login.js | 10 +- test.db | 0 7 files changed, 214 insertions(+), 65 deletions(-) create mode 100644 .gitignore create mode 100644 database.sql delete mode 100644 test.db diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1f33a07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +bunker.db +files/ + diff --git a/database.sql b/database.sql new file mode 100644 index 0000000..4b4c26b --- /dev/null +++ b/database.sql @@ -0,0 +1,18 @@ +PRAGMA foreign_keys=OFF; +BEGIN TRANSACTION; + +CREATE TABLE users ( + username text not null, + passwordHash text not null +); + +CREATE TABLE sessions ( + token text not null, + userId int not null, + username text not null +); + +INSERT INTO USERS (username, passwordHash) + VALUES ('sfj', '$2a$10$7lxFF6MWeWzn8PZDpveuD.KFQHWFutLUg9cjHKG6HR7wVo.p81NTG'); + +COMMIT; diff --git a/main.ts b/main.ts index 77bd582..5d27d7d 100644 --- a/main.ts +++ b/main.ts @@ -1,39 +1,85 @@ -// import * as sqlite from "jsr:@db/sqlite@0.11"; +import * as sqlite from "jsr:@db/sqlite@0.11"; import * as oak from "jsr:@oak/oak@14"; import * as bcrypt from "https://deno.land/x/bcrypt@v0.4.1/mod.ts"; -const app = new oak.Application(); - type User = { + id: number; username: string; passwordHash: string; }; -const users: { [username: string]: User } = { - ["sfj"]: { - username: "sfj", - passwordHash: await bcrypt.hash("sfj"), - }, -}; - type Session = { + id: number; token: string; + userId: number; username: string; }; -const sessions: { [token: string]: Session } = {}; +interface Db { + userWhereUsername(username: string): Promise; + sessionWhereToken(token: string): Promise; + createSession(init: Omit): Promise; +} -const publicRouter = new oak.Router(); +class SqliteDb implements Db { + private db = new sqlite.Database("bunker.db", { create: false }); -publicRouter.get("/favicon.ico", async (ctx) => { - await ctx.send({ path: ctx.request.url.pathname, root: "./public" }); -}); -publicRouter.get("/login.html", async (ctx) => { - await ctx.send({ path: ctx.request.url.pathname, root: "./public" }); -}); -publicRouter.get("/login.js", async (ctx) => { - await ctx.send({ path: ctx.request.url.pathname, root: "./public" }); -}); + public async userWhereUsername(username: string): Promise { + await Promise.resolve(); + const res = this.db.prepare( + "select rowid as id, * from users where username=?", + ) + .value<[User]>(username); + if (!res) { + return null; + } + return ["id", "username", "passwordHash"] + .map((key, index) => ({ [key]: res[index] })) + .reduce>( + (acc, kv) => ({ ...acc, ...kv }), + {}, + ) as User; + } + public async sessionWhereToken(token: string): Promise { + await Promise.resolve(); + const res = this.db.prepare( + "select rowid as id, * from sessions where token=?", + ) + .value<[Session]>(token); + if (!res) { + return null; + } + return ["id", "token", "userId", "username"] + .map((key, index) => ({ [key]: res[index] })) + .reduce>( + (acc, kv) => ({ ...acc, ...kv }), + {}, + ) as Session; + } + public async deleteSessionsWithUsername(username: string): Promise { + await Promise.resolve(); + this.db.exec("delete from sessions where username=?", username); + } + public async createSession(init: Omit): Promise { + await Promise.resolve(); + this.db.exec( + "insert into sessions (token, userId, username) values (:token, :userId, :username)", + init, + ); + const res = this.db.prepare( + "select rowid as id, * from sessions where username=? and token=?", + ) + .value<[Session]>(init.username, init.token)!; + return ["id", "token", "userId", "username"] + .map((key, index) => ({ [key]: res[index] })) + .reduce>( + (acc, kv) => ({ ...acc, ...kv }), + {}, + ) as Session; + } +} + +const db = new SqliteDb(); type LoginReq = { username: string; @@ -63,41 +109,46 @@ function generateToken(): string { return token; } -publicRouter.post("/api/login", async (ctx) => { - const body = await ctx.request.body.json() as LoginReq; - if ( - typeof body.username !== "string" || typeof body.password !== "string" - ) { - return respond(ctx, 400, { ok: false, msg: "malformed request" }); - } - if (!(body.username in users)) { - return respond(ctx, 401, { ok: false, msg: "bad creds" }); - } - const user = users[body.username]; - if (!await bcrypt.compare(body.password, user.passwordHash)) { - return respond(ctx, 401, { ok: false, msg: "bad creds" }); - } - for (const token in sessions) { - if (sessions[token].username === body.username) { - delete sessions[token]; +const publicRouter = new oak.Router() + .get("/favicon.ico", async (ctx) => { + await ctx.send({ path: ctx.request.url.pathname, root: "./public" }); + }) + .get("/login.html", async (ctx) => { + await ctx.send({ path: ctx.request.url.pathname, root: "./public" }); + }) + .get("/login.js", async (ctx) => { + await ctx.send({ path: ctx.request.url.pathname, root: "./public" }); + }) + .post("/api/login", async (ctx) => { + const body = await ctx.request.body.json() as LoginReq; + if ( + typeof body.username !== "string" || + typeof body.password !== "string" + ) { + return respond(ctx, 400, { ok: false, msg: "malformed request" }); } - } - const token = generateToken(); - sessions[token] = { - token, - username: body.username, - }; - await ctx.cookies.set("Authorization", token, { sameSite: "strict" }); - return respond(ctx, 200, { ok: true }); -}); - -app.use(publicRouter.routes()); -app.use(publicRouter.allowedMethods()); + const user = await db.userWhereUsername(body.username); + if (!user) { + return respond(ctx, 401, { ok: false, msg: "bad creds1" }); + } + if (!await bcrypt.compare(body.password, user.passwordHash)) { + return respond(ctx, 401, { ok: false, msg: "bad creds2" }); + } + await db.deleteSessionsWithUsername(user.username); + const token = generateToken(); + await db.createSession({ + token, + userId: user.id, + username: user.username, + }); + await ctx.cookies.set("Authorization", token, { sameSite: "strict" }); + return respond(ctx, 200, { ok: true }); + }); function authMiddleware(): oak.Middleware { return async (ctx, next) => { const token = await ctx.cookies.get("Authorization"); - if (!token || !(token in sessions)) { + if (!token || !await db.sessionWhereToken(token)) { ctx.response.redirect("/login.html"); return; } @@ -105,18 +156,82 @@ function authMiddleware(): oak.Middleware { }; } -const authRouter = new oak.Router(); -app.use(authMiddleware(), authRouter.routes()); -app.use(authMiddleware(), authRouter.allowedMethods()); - -app.use(authMiddleware(), async (ctx) => { - console.log(ctx.request.url.pathname); - const path = ((p) => p === "/" ? "/index.html" : p)( - ctx.request.url.pathname, - ); - await oak.send(ctx, path, { - root: "./public", +async function listFilesInFolder(folderPath: string): Promise { + if (!/^\/files\//.test(folderPath)) { + throw new Error("malformed path"); + } + folderPath = folderPath.replace(/^\/files\//, "./files/"); + const command = new Deno.Command("ls", { + args: ["-1F", "--group-directories-first", folderPath], + stdout: "piped", }); -}); + const output = await command.output(); + const text = new TextDecoder().decode(output.stdout); + return text + .split("\n") + .filter((v) => v); +} -app.listen({ port: 8000 }); +function listPage(folderPath: string, files: string[]): string { + const filesHtml = files.map((filename) => /*html*/ ` +
  • ${filename}
  • + `).join("\n"); + return /*html*/ ` + + + + + Bunker ${folderPath} + + + +

    Home

    +

    ${folderPath}

    +
      +
    • ..
    • + ${filesHtml} +
    + + + `.replace(/^ {8}/gm, ""); +} + +const authRouter = new oak.Router(); + +new oak.Application() + .use(publicRouter.routes()) + .use(publicRouter.allowedMethods()) + .use(authMiddleware(), authRouter.routes()) + .use(authMiddleware(), authRouter.allowedMethods()) + .use(authMiddleware(), async (ctx) => { + const { request: { url: { pathname } } } = ctx; + if (pathname.startsWith("/files/")) { + const name = decodeURIComponent(pathname); + const info = await Deno.stat( + name.replace(/^\/files\//, "files/"), + ); + if (name.endsWith("/") || info.isDirectory) { + const files = await listFilesInFolder(name); + ctx.response.type = "text/html"; + ctx.response.body = listPage(name, files); + ctx.response.status = 200; + ctx.respond = true; + return; + } + await oak.send( + ctx, + name + .replace(/^\/files\//, "") + .replaceAll("..", "."), + { + root: "./files", + }, + ); + return; + } + await oak.send(ctx, pathname, { + root: "./public", + index: "index.html", + }); + }) + .listen({ port: 8000 }); diff --git a/public/index.html b/public/index.html index 2c2a950..5f20c97 100644 --- a/public/index.html +++ b/public/index.html @@ -7,5 +7,8 @@

    Bunker !!!

    + diff --git a/public/login.html b/public/login.html index a679758..56ccbc2 100644 --- a/public/login.html +++ b/public/login.html @@ -10,6 +10,8 @@

    Login

    +
    +

    diff --git a/public/login.js b/public/login.js index 4923dd3..e4474f2 100644 --- a/public/login.js +++ b/public/login.js @@ -1,5 +1,8 @@ +const loginErrorSpan = document.querySelector("#login-error"); + document.querySelector("#login").onsubmit = async (event) => { event.preventDefault(); + loginErrorSpan.hidden = true; const form = new FormData(event.target); const body = JSON.stringify({ username: form.get("username"), @@ -10,6 +13,11 @@ document.querySelector("#login").onsubmit = async (event) => { headers: new Headers({ "Content-Type": "application/json" }), body, }).then((res) => res.json()); - console.log(res); + if (!res.ok) { + loginErrorSpan.innerText = res.msg; + loginErrorSpan.hidden = false; + return; + } + window.location.pathname = "/"; }; diff --git a/test.db b/test.db deleted file mode 100644 index e69de29..0000000