fix login and files
All checks were successful
Validate / Validate (push) Successful in 11s

This commit is contained in:
SimonFJ20 2024-09-24 02:26:55 +02:00
parent a03ad035fb
commit 95b0e099ae
7 changed files with 214 additions and 65 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
bunker.db
files/

18
database.sql Normal file
View File

@ -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;

221
main.ts
View File

@ -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 oak from "jsr:@oak/oak@14";
import * as bcrypt from "https://deno.land/x/bcrypt@v0.4.1/mod.ts"; import * as bcrypt from "https://deno.land/x/bcrypt@v0.4.1/mod.ts";
const app = new oak.Application();
type User = { type User = {
id: number;
username: string; username: string;
passwordHash: string; passwordHash: string;
}; };
const users: { [username: string]: User } = {
["sfj"]: {
username: "sfj",
passwordHash: await bcrypt.hash("sfj"),
},
};
type Session = { type Session = {
id: number;
token: string; token: string;
userId: number;
username: string; username: string;
}; };
const sessions: { [token: string]: Session } = {}; interface Db {
userWhereUsername(username: string): Promise<User | null>;
sessionWhereToken(token: string): Promise<Session | null>;
createSession(init: Omit<Session, "id">): Promise<Session>;
}
const publicRouter = new oak.Router(); class SqliteDb implements Db {
private db = new sqlite.Database("bunker.db", { create: false });
publicRouter.get("/favicon.ico", async (ctx) => { public async userWhereUsername(username: string): Promise<User | null> {
await ctx.send({ path: ctx.request.url.pathname, root: "./public" }); await Promise.resolve();
}); const res = this.db.prepare(
publicRouter.get("/login.html", async (ctx) => { "select rowid as id, * from users where username=?",
await ctx.send({ path: ctx.request.url.pathname, root: "./public" }); )
}); .value<[User]>(username);
publicRouter.get("/login.js", async (ctx) => { if (!res) {
await ctx.send({ path: ctx.request.url.pathname, root: "./public" }); return null;
}); }
return ["id", "username", "passwordHash"]
.map((key, index) => ({ [key]: res[index] }))
.reduce<Partial<User>>(
(acc, kv) => ({ ...acc, ...kv }),
{},
) as User;
}
public async sessionWhereToken(token: string): Promise<Session | null> {
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<Partial<User>>(
(acc, kv) => ({ ...acc, ...kv }),
{},
) as Session;
}
public async deleteSessionsWithUsername(username: string): Promise<void> {
await Promise.resolve();
this.db.exec("delete from sessions where username=?", username);
}
public async createSession(init: Omit<Session, "id">): Promise<Session> {
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<Partial<User>>(
(acc, kv) => ({ ...acc, ...kv }),
{},
) as Session;
}
}
const db = new SqliteDb();
type LoginReq = { type LoginReq = {
username: string; username: string;
@ -63,41 +109,46 @@ function generateToken(): string {
return token; return token;
} }
publicRouter.post("/api/login", async (ctx) => { 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; const body = await ctx.request.body.json() as LoginReq;
if ( if (
typeof body.username !== "string" || typeof body.password !== "string" typeof body.username !== "string" ||
typeof body.password !== "string"
) { ) {
return respond(ctx, 400, { ok: false, msg: "malformed request" }); return respond(ctx, 400, { ok: false, msg: "malformed request" });
} }
if (!(body.username in users)) { const user = await db.userWhereUsername(body.username);
return respond(ctx, 401, { ok: false, msg: "bad creds" }); if (!user) {
return respond(ctx, 401, { ok: false, msg: "bad creds1" });
} }
const user = users[body.username];
if (!await bcrypt.compare(body.password, user.passwordHash)) { if (!await bcrypt.compare(body.password, user.passwordHash)) {
return respond(ctx, 401, { ok: false, msg: "bad creds" }); return respond(ctx, 401, { ok: false, msg: "bad creds2" });
}
for (const token in sessions) {
if (sessions[token].username === body.username) {
delete sessions[token];
}
} }
await db.deleteSessionsWithUsername(user.username);
const token = generateToken(); const token = generateToken();
sessions[token] = { await db.createSession({
token, token,
username: body.username, userId: user.id,
}; username: user.username,
});
await ctx.cookies.set("Authorization", token, { sameSite: "strict" }); await ctx.cookies.set("Authorization", token, { sameSite: "strict" });
return respond(ctx, 200, { ok: true }); return respond(ctx, 200, { ok: true });
}); });
app.use(publicRouter.routes());
app.use(publicRouter.allowedMethods());
function authMiddleware(): oak.Middleware { function authMiddleware(): oak.Middleware {
return async (ctx, next) => { return async (ctx, next) => {
const token = await ctx.cookies.get("Authorization"); const token = await ctx.cookies.get("Authorization");
if (!token || !(token in sessions)) { if (!token || !await db.sessionWhereToken(token)) {
ctx.response.redirect("/login.html"); ctx.response.redirect("/login.html");
return; return;
} }
@ -105,18 +156,82 @@ function authMiddleware(): oak.Middleware {
}; };
} }
const authRouter = new oak.Router(); async function listFilesInFolder(folderPath: string): Promise<string[]> {
app.use(authMiddleware(), authRouter.routes()); if (!/^\/files\//.test(folderPath)) {
app.use(authMiddleware(), authRouter.allowedMethods()); throw new Error("malformed path");
}
app.use(authMiddleware(), async (ctx) => { folderPath = folderPath.replace(/^\/files\//, "./files/");
console.log(ctx.request.url.pathname); const command = new Deno.Command("ls", {
const path = ((p) => p === "/" ? "/index.html" : p)( args: ["-1F", "--group-directories-first", folderPath],
ctx.request.url.pathname, stdout: "piped",
);
await oak.send(ctx, path, {
root: "./public",
}); });
}); 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*/ `
<li><a href="./${filename}">${filename}</a></li>
`).join("\n");
return /*html*/ `
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bunker ${folderPath}</title>
<head>
</head>
<body>
<p><a href="/">Home</a></p>
<p>${folderPath}</p>
<ul>
<li><a href="..">..</a></li>
${filesHtml}
</ul>
</body>
</html>
`.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 });

View File

@ -7,5 +7,8 @@
</head> </head>
<body> <body>
<h1>Bunker !!!</h1> <h1>Bunker !!!</h1>
<ul>
<li><a href="/files/">Files</a></li>
</ul>
</body> </body>
</html> </html>

View File

@ -10,6 +10,8 @@
<h1>Login</h1> <h1>Login</h1>
<form id="login"> <form id="login">
<span id="login-error" hidden style="color: red;">Hidden.</span><br>
<label for="username">Username: </label><br> <label for="username">Username: </label><br>
<input type="text" id="username" name="username"><br> <input type="text" id="username" name="username"><br>

View File

@ -1,5 +1,8 @@
const loginErrorSpan = document.querySelector("#login-error");
document.querySelector("#login").onsubmit = async (event) => { document.querySelector("#login").onsubmit = async (event) => {
event.preventDefault(); event.preventDefault();
loginErrorSpan.hidden = true;
const form = new FormData(event.target); const form = new FormData(event.target);
const body = JSON.stringify({ const body = JSON.stringify({
username: form.get("username"), username: form.get("username"),
@ -10,6 +13,11 @@ document.querySelector("#login").onsubmit = async (event) => {
headers: new Headers({ "Content-Type": "application/json" }), headers: new Headers({ "Content-Type": "application/json" }),
body, body,
}).then((res) => res.json()); }).then((res) => res.json());
console.log(res); if (!res.ok) {
loginErrorSpan.innerText = res.msg;
loginErrorSpan.hidden = false;
return;
}
window.location.pathname = "/";
}; };

View File