This commit is contained in:
parent
a03ad035fb
commit
95b0e099ae
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
bunker.db
|
||||
files/
|
||||
|
18
database.sql
Normal file
18
database.sql
Normal 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;
|
213
main.ts
213
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<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) => {
|
||||
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<User | null> {
|
||||
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<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 = {
|
||||
username: string;
|
||||
@ -63,41 +109,46 @@ function generateToken(): string {
|
||||
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;
|
||||
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" });
|
||||
}
|
||||
if (!(body.username in users)) {
|
||||
return respond(ctx, 401, { ok: false, msg: "bad creds" });
|
||||
const user = await db.userWhereUsername(body.username);
|
||||
if (!user) {
|
||||
return respond(ctx, 401, { ok: false, msg: "bad creds1" });
|
||||
}
|
||||
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];
|
||||
}
|
||||
return respond(ctx, 401, { ok: false, msg: "bad creds2" });
|
||||
}
|
||||
await db.deleteSessionsWithUsername(user.username);
|
||||
const token = generateToken();
|
||||
sessions[token] = {
|
||||
await db.createSession({
|
||||
token,
|
||||
username: body.username,
|
||||
};
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
});
|
||||
await ctx.cookies.set("Authorization", token, { sameSite: "strict" });
|
||||
return respond(ctx, 200, { ok: true });
|
||||
});
|
||||
|
||||
app.use(publicRouter.routes());
|
||||
app.use(publicRouter.allowedMethods());
|
||||
|
||||
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 {
|
||||
};
|
||||
}
|
||||
|
||||
async function listFilesInFolder(folderPath: string): Promise<string[]> {
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
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,
|
||||
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/"),
|
||||
);
|
||||
await oak.send(ctx, path, {
|
||||
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",
|
||||
});
|
||||
});
|
||||
|
||||
app.listen({ port: 8000 });
|
||||
})
|
||||
.listen({ port: 8000 });
|
||||
|
@ -7,5 +7,8 @@
|
||||
</head>
|
||||
<body>
|
||||
<h1>Bunker !!!</h1>
|
||||
<ul>
|
||||
<li><a href="/files/">Files</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -10,6 +10,8 @@
|
||||
<h1>Login</h1>
|
||||
|
||||
<form id="login">
|
||||
<span id="login-error" hidden style="color: red;">Hidden.</span><br>
|
||||
|
||||
<label for="username">Username: </label><br>
|
||||
<input type="text" id="username" name="username"><br>
|
||||
|
||||
|
@ -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 = "/";
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user