fuck it call it v0
This commit is contained in:
parent
e51ccfa79c
commit
2c24f0878f
@ -2,3 +2,4 @@ DEFAULT_USER_PASSWORD=
|
||||
ROOT_DOMAIN=
|
||||
DATA_ROOT=
|
||||
PORT=
|
||||
PROTO=
|
31
src/auth.ts
31
src/auth.ts
@ -59,3 +59,34 @@ export function sessionValidate(session: string): string|null {
|
||||
const row = sessionUserResponse.parse(rawrow);
|
||||
return row.user;
|
||||
}
|
||||
|
||||
const canCreateNewUsersResponse = z.object({
|
||||
allowNewUserCreation: z.number()
|
||||
});
|
||||
export function canCreateNewUsers(username: string): boolean {
|
||||
const rawrow = db.prepare("SELECT allowNewUserCreation FROM users WHERE username = ?").get(username);
|
||||
if (!rawrow) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const row = canCreateNewUsersResponse.parse(rawrow);
|
||||
|
||||
return row.allowNewUserCreation === 1;
|
||||
}
|
||||
|
||||
export async function createNewUser(username: string, password: string, allowNewUserCreation: boolean) {
|
||||
db.prepare(`INSERT INTO users(username, passwordHash, allowNewUserCreation) VALUES(?, ?, ?)
|
||||
ON CONFLICT DO NOTHING`).run(username, await argon2.hash(password, {
|
||||
timeCost: 5
|
||||
}), allowNewUserCreation ? 1 : 0);
|
||||
}
|
||||
|
||||
export async function updatePassword(username: string, password: string) {
|
||||
db.prepare(`UPDATE users SET passwordHash = ? WHERE username = ?`).run(await argon2.hash(password, {
|
||||
timeCost: 5
|
||||
}), username);
|
||||
}
|
||||
|
||||
export async function deleteSession(sid: string) {
|
||||
db.prepare(`DELETE FROM sessions WHERE id = ?`).run(sid);
|
||||
}
|
@ -20,3 +20,8 @@ if (!process.env["PORT"] || !parseInt(process.env["PORT"]) || parseInt(process.e
|
||||
throw new Error("PORT must be set and be an integer between 1 and 65535");
|
||||
}
|
||||
export const Port: number = parseInt(process.env["PORT"]);
|
||||
|
||||
if (!process.env["PROTO"]) {
|
||||
throw new Error("PROTO must be set");
|
||||
}
|
||||
export const Protocol: string = process.env["PROTO"];
|
@ -45,12 +45,12 @@ export function getGame(name: string): z.infer<typeof manifest> {
|
||||
return manifestCache.get(name)!;
|
||||
}
|
||||
|
||||
export function getLocalStorageDatabaseForUser(name: string, user: string): Database.Database {
|
||||
export function getStorageDatabaseForUser(storageName: string, name: string, user: string): Database.Database {
|
||||
if (!manifestCache.has(name)) throw new Error("no game (ps5 reference)");
|
||||
|
||||
mkdirSync(join(DataRoot, "storage", user, name), { recursive: true });
|
||||
|
||||
const db = new Database(join(DataRoot, "storage", user, name, "localStorage.db"));
|
||||
const db = new Database(join(DataRoot, "storage", user, name, storageName + ".db"));
|
||||
db.pragma("journal_mode = WAL");
|
||||
|
||||
db.exec("CREATE TABLE IF NOT EXISTS kv(key TEXT PRIMARY KEY, value TEXT)");
|
||||
|
@ -1,7 +1,7 @@
|
||||
import express from "express";
|
||||
import { sessionValidate } from "./auth.js";
|
||||
import "./game.js";
|
||||
import { getGame, getLocalStorageDatabaseForUser } from "./game.js";
|
||||
import { getGame, getStorageDatabaseForUser } from "./game.js";
|
||||
import path from "path";
|
||||
import { DataRoot, RootDomain } from "./env.js";
|
||||
import mime from "mime";
|
||||
@ -53,8 +53,13 @@ postauthRouter.get("/cgi-bin/rpc/dollhouse_authenticate", (req, res) => {
|
||||
|
||||
postauthRouter.use("/cgi-bin/static", express.static(path.join(base, "injections")));
|
||||
|
||||
postauthRouter.get("/cgi-bin/rpc/ls/read_all", (req, res) => {
|
||||
const db = getLocalStorageDatabaseForUser(req.game, req.active_user);
|
||||
postauthRouter.get("/cgi-bin/rpc/storage/:name/read_all", (req, res) => {
|
||||
if (!(["localStorage", "sessionStorage"]).includes(req.params.name)) {
|
||||
res.status(400).send("Bad Request");
|
||||
return;
|
||||
}
|
||||
|
||||
const db = getStorageDatabaseForUser(req.params.name, req.game, req.active_user);
|
||||
|
||||
const results = db.prepare("SELECT * FROM kv").all();
|
||||
db.close();
|
||||
@ -66,11 +71,19 @@ const upsertRequest = z.object({
|
||||
value: z.string()
|
||||
});
|
||||
|
||||
postauthRouter.post("/cgi-bin/rpc/ls/upsert", express.json({
|
||||
limit: 1024 * 1024 * 1024 * 16
|
||||
postauthRouter.post("/cgi-bin/rpc/storage/:name/upsert", express.json({
|
||||
limit: 1024 * 1024 * 1024 * 16,
|
||||
type: () => true
|
||||
}), (req, res) => {
|
||||
if (!(["localStorage", "sessionStorage"]).includes(req.params.name)) {
|
||||
res.status(400).send("Bad Request");
|
||||
return;
|
||||
}
|
||||
|
||||
const db = getStorageDatabaseForUser(req.params.name, req.game, req.active_user);
|
||||
const body = upsertRequest.parse(req.body);
|
||||
const db = getLocalStorageDatabaseForUser(req.game, req.active_user);
|
||||
|
||||
console.log("It is quite upserting", req.params.name, body.key);
|
||||
|
||||
db.prepare("INSERT INTO kv(key, value) VALUES(?, ?) ON CONFLICT (key) DO UPDATE SET value = ? WHERE key = ?").run(
|
||||
body.key,
|
||||
@ -87,9 +100,16 @@ const removeRequest = z.object({
|
||||
key: z.string()
|
||||
});
|
||||
|
||||
postauthRouter.post("/cgi-bin/rpc/ls/remove", express.json(), (req, res) => {
|
||||
postauthRouter.post("/cgi-bin/rpc/storage/:name/remove", express.json({
|
||||
type: () => true
|
||||
}), (req, res) => {
|
||||
if (!(["localStorage", "sessionStorage"]).includes(req.params.name)) {
|
||||
res.status(400).send("Bad Request");
|
||||
return;
|
||||
}
|
||||
|
||||
const db = getStorageDatabaseForUser(req.params.name, req.game, req.active_user);
|
||||
const body = removeRequest.parse(req.body);
|
||||
const db = getLocalStorageDatabaseForUser(req.game, req.active_user);
|
||||
|
||||
db.prepare("DELETE FROM kv WHERE key = ?").run(
|
||||
body.key
|
||||
@ -99,8 +119,13 @@ postauthRouter.post("/cgi-bin/rpc/ls/remove", express.json(), (req, res) => {
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
postauthRouter.post("/cgi-bin/rpc/ls/clear", (req, res) => {
|
||||
const db = getLocalStorageDatabaseForUser(req.game, req.active_user);
|
||||
postauthRouter.post("/cgi-bin/rpc/storage/:name/clear", (req, res) => {
|
||||
if (!(["localStorage", "sessionStorage"]).includes(req.params.name)) {
|
||||
res.status(400).send("Bad Request");
|
||||
return;
|
||||
}
|
||||
|
||||
const db = getStorageDatabaseForUser(req.params.name, req.game, req.active_user);
|
||||
|
||||
db.prepare("DELETE FROM kv").run();
|
||||
db.close();
|
||||
|
@ -2,160 +2,185 @@
|
||||
// Local Storage TODOs:
|
||||
// - Consistent order operation with the server
|
||||
// - Compressing requests
|
||||
// - websocket and firing storage events
|
||||
|
||||
const backingStore = new Map();
|
||||
let keys = [];
|
||||
function createStorage(name) {
|
||||
const backingStore = new Map();
|
||||
let keys = [];
|
||||
|
||||
function getKey(key) {
|
||||
console.log("[LocalStorageTracker] [G]", key);
|
||||
|
||||
if (backingStore.has(key.toString())) {
|
||||
return backingStore.get(key.toString());
|
||||
function post(endpoint, body) {
|
||||
fetch("/cgi-bin/rpc/storage/" + name + "/" + endpoint, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}).then(res => res.json()).then(r => {
|
||||
if (r.ok) {
|
||||
console.log("Sync ok");
|
||||
} else {
|
||||
console.error("Sync fucked");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function setKey(key, value) {
|
||||
console.log("[LocalStorageTracker] [S]", key, value);
|
||||
|
||||
fetch("/cgi-bin/rpc/ls/upsert", {
|
||||
body: JSON.stringify({
|
||||
key: key.toString(),
|
||||
value: value.toString()
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST"
|
||||
}).then(res => res.json()).then(r => {
|
||||
if (r.ok) {
|
||||
console.log("Sync ok");
|
||||
} else {
|
||||
console.error("Sync fucked");
|
||||
function getKey(key) {
|
||||
if (backingStore.has(key.toString())) {
|
||||
return backingStore.get(key.toString());
|
||||
}
|
||||
})
|
||||
|
||||
if (keys.indexOf(key.toString()) === -1) {
|
||||
keys.push(key.toString());
|
||||
return null;
|
||||
}
|
||||
|
||||
backingStore.set(key.toString(), value.toString());
|
||||
}
|
||||
function hasKey(key) {
|
||||
return backingStore.has(key.toString());
|
||||
}
|
||||
|
||||
function removeKey(key) {
|
||||
console.log("[LocalStorageTracker] [R]", key);
|
||||
function setKey(key, value) {
|
||||
// navigator.sendBeacon("/cgi-bin/rpc/storage/" + name + "/upsert", JSON.stringify({
|
||||
// key: key.toString(),
|
||||
// value: value.toString()
|
||||
// }))
|
||||
if (name === "sessionStorage") {
|
||||
try {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "/cgi-bin/rpc/storage/" + name + "/upsert", false);
|
||||
xhr.send(JSON.stringify({
|
||||
key: key.toString(),
|
||||
value: value.toString()
|
||||
}));
|
||||
} catch(e) {
|
||||
// fucking try anything idk
|
||||
post("upsert", {
|
||||
key: key.toString(),
|
||||
value: value.toString()
|
||||
})
|
||||
navigator.sendBeacon("/cgi-bin/rpc/storage/" + name + "/upsert", JSON.stringify({
|
||||
key: key.toString(),
|
||||
value: value.toString()
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
post("upsert", {
|
||||
key: key.toString(),
|
||||
value: value.toString()
|
||||
})
|
||||
}
|
||||
|
||||
fetch("/cgi-bin/rpc/ls/remove", {
|
||||
body: JSON.stringify({
|
||||
if (keys.indexOf(key.toString()) === -1) {
|
||||
keys.push(key.toString());
|
||||
}
|
||||
|
||||
backingStore.set(key.toString(), value.toString());
|
||||
}
|
||||
|
||||
function removeKey(key) {
|
||||
post("remove", {
|
||||
key: key.toString()
|
||||
})
|
||||
|
||||
if (keys.indexOf(key.toString()) !== -1) {
|
||||
keys = keys.filter(k => k !== key.toString());
|
||||
}
|
||||
|
||||
backingStore.delete(key);
|
||||
}
|
||||
|
||||
function clear() {
|
||||
post("clear", {});
|
||||
|
||||
keys = [];
|
||||
|
||||
backingStore.clear();
|
||||
}
|
||||
|
||||
function getKeyByIndex(index) {
|
||||
let realIndex = Number(index);
|
||||
if (Number.isNaN(realIndex)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (realIndex < 0 || realIndex >= keys.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return keys[realIndex];
|
||||
}
|
||||
|
||||
const api = {
|
||||
"clear": clear,
|
||||
"getItem": getKey,
|
||||
"setItem": setKey,
|
||||
"removeItem": removeKey,
|
||||
"key": getKeyByIndex,
|
||||
"__rdh_debug": function() {
|
||||
console.log(keys, backingStore);
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(window, name, {
|
||||
value: new Proxy({}, {
|
||||
set(_, key, value, __) {
|
||||
setKey(key, value);
|
||||
},
|
||||
get(_, k, __) {
|
||||
if (api[k]) {
|
||||
return api[k];
|
||||
}
|
||||
|
||||
if (k === "length") {
|
||||
return keys.length;
|
||||
}
|
||||
|
||||
return getKey(k);
|
||||
},
|
||||
ownKeys(_) {
|
||||
return [...keys];
|
||||
},
|
||||
has(_, k) {
|
||||
return hasKey(k);
|
||||
},
|
||||
getOwnPropertyDescriptor(_, k) {
|
||||
const exists = hasKey(k);
|
||||
if (!exists) return undefined;
|
||||
return {
|
||||
value: getKey(k),
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
}
|
||||
},
|
||||
isExtensible() {
|
||||
return true;
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST"
|
||||
}).then(res => res.json()).then(r => {
|
||||
if (r.ok) {
|
||||
console.log("Sync ok");
|
||||
} else {
|
||||
console.error("Sync fucked");
|
||||
}
|
||||
})
|
||||
writable: false,
|
||||
configurable: false
|
||||
});
|
||||
|
||||
if (keys.indexOf(key.toString()) !== -1) {
|
||||
keys = keys.filter(k => k !== key.toString());
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", "/cgi-bin/rpc/storage/" + name + "/read_all", false);
|
||||
xhr.send(null);
|
||||
|
||||
if (xhr.status !== 200) {
|
||||
alert("it appears to be fucked");
|
||||
window.stop();
|
||||
}
|
||||
|
||||
backingStore.delete(key);
|
||||
}
|
||||
|
||||
function clear() {
|
||||
console.log("[LocalStorageTracker] [C]");
|
||||
|
||||
fetch("/cgi-bin/rpc/ls/clear", {
|
||||
body: JSON.stringify({}),
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST"
|
||||
}).then(res => res.json()).then(r => {
|
||||
if (r.ok) {
|
||||
console.log("Sync ok");
|
||||
} else {
|
||||
console.error("Sync fucked");
|
||||
}
|
||||
})
|
||||
|
||||
keys = [];
|
||||
|
||||
backingStore.clear();
|
||||
}
|
||||
|
||||
function getKeyByIndex(index) {
|
||||
console.log("[LocalStorageTracker] [K]", index);
|
||||
|
||||
let realIndex = Number(index);
|
||||
if (Number.isNaN(realIndex)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (realIndex < 0 || realIndex >= keys.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return keys[realIndex];
|
||||
}
|
||||
|
||||
const api = {
|
||||
"clear": clear,
|
||||
"getItem": getKey,
|
||||
"setItem": setKey,
|
||||
"removeItem": removeKey,
|
||||
"key": getKeyByIndex,
|
||||
"__rdh_debug": function() {
|
||||
console.log(keys, backingStore);
|
||||
const responseBody = JSON.parse(xhr.responseText);
|
||||
for (let { key, value } of responseBody) {
|
||||
keys.push(key);
|
||||
backingStore.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(window, "localStorage", {
|
||||
value: new Proxy({}, {
|
||||
set(_, key, value, __) {
|
||||
setKey(key, value);
|
||||
},
|
||||
get(_, k, __) {
|
||||
if (api[k]) {
|
||||
return api[k];
|
||||
}
|
||||
|
||||
if (k === "length") {
|
||||
return keys.length;
|
||||
}
|
||||
let deadline = Date.now() + 1000;
|
||||
while (Date.now() < deadline){};
|
||||
|
||||
return getKey(k);
|
||||
},
|
||||
ownKeys(_) {
|
||||
return [...keys];
|
||||
}
|
||||
}),
|
||||
writable: false,
|
||||
configurable: false
|
||||
});
|
||||
createStorage("localStorage");
|
||||
createStorage("sessionStorage");
|
||||
|
||||
// Populate it with our initial values
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", "/cgi-bin/rpc/ls/read_all", false);
|
||||
xhr.send(null);
|
||||
|
||||
if (xhr.status !== 200) {
|
||||
alert("it appears to be fucked");
|
||||
window.stop();
|
||||
}
|
||||
|
||||
const responseBody = JSON.parse(xhr.responseText);
|
||||
for (let { key, value } of responseBody) {
|
||||
keys.push(key);
|
||||
backingStore.set(key, value);
|
||||
}
|
||||
|
||||
// DIE. DIE. DIE. (IndexedDB is hell to implement. I will do it later, for now zap it away so games fall back to localstorage.)
|
||||
Object.defineProperty(window, "indexedDB", {
|
||||
|
@ -1,9 +1,9 @@
|
||||
import express, { urlencoded } from "express";
|
||||
import { join } from "path";
|
||||
import z from "zod";
|
||||
import { authenticate, sessionValidate } from "./auth.js";
|
||||
import { authenticate, canCreateNewUsers, createNewUser, deleteSession, sessionValidate, updatePassword } from "./auth.js";
|
||||
import { getAllGames } from "./game.js";
|
||||
import { RootDomain } from "./env.js";
|
||||
import { Protocol, RootDomain } from "./env.js";
|
||||
|
||||
const router = express.Router();
|
||||
const preauthRouter = express.Router();
|
||||
@ -43,13 +43,75 @@ preauthRouter.post("/", async (req, res) => {
|
||||
postauthRouter.get("/", (req, res) => {
|
||||
let html = `<h1>Available games</h1><ul>`;
|
||||
for (let game of getAllGames()) {
|
||||
html = html + `<li><a href="${req.protocol}://${game.slug}.${RootDomain}/cgi-bin/rpc/dollhouse_authenticate?sid=${req.cookies["rdh_sid"]}">${game.name}</a></li>`
|
||||
html = html + `<li><a href="${Protocol}://${game.slug}.${RootDomain}/cgi-bin/rpc/dollhouse_authenticate?sid=${req.cookies["rdh_sid"]}">${game.name}</a></li>`
|
||||
}
|
||||
html = html + "</ul>"
|
||||
|
||||
if (canCreateNewUsers(req.active_user)) {
|
||||
html = html + `<br><h1>Create a new user</h1><form action='/create_user' method='POST'>
|
||||
<label>Username: <input type="username" name="username"></label><br/>
|
||||
<label>Password: <input type="password" name="password"></label><br/>
|
||||
<label>Allow new user creation: <input type="checkbox" name="allow"></label><br/>
|
||||
<input type="submit">
|
||||
</form>`
|
||||
}
|
||||
|
||||
html = html + `<br><h1>Change password</h1><form action='/change_password' method='POST'>
|
||||
<label>New Password: <input type="password" name="password"></label><br/>
|
||||
<input type="submit">
|
||||
</form>
|
||||
<form action='/delete_session' method='POST'>
|
||||
<input type="hidden" name="deletion" value="delete">
|
||||
<input type="submit" value="Delete Session">
|
||||
</form>`
|
||||
res.set("Content-Type", "text/html;charset=UTF-8");
|
||||
res.send(html);
|
||||
});
|
||||
|
||||
const createPayload = z.object({
|
||||
username: z.string(),
|
||||
password: z.string(),
|
||||
allow: z.optional(z.literal("on"))
|
||||
});
|
||||
postauthRouter.post('/create_user', urlencoded(), (req, res) => {
|
||||
if (!canCreateNewUsers(req.active_user)) {
|
||||
res.status(401).send("Unauthorized");
|
||||
return;
|
||||
}
|
||||
|
||||
const body = createPayload.parse(req.body);
|
||||
const allow = body.allow === "on";
|
||||
|
||||
createNewUser(body.username, body.password, allow);
|
||||
|
||||
res.set('Location', '/');
|
||||
res.status(303).send("Welcome");
|
||||
});
|
||||
|
||||
const changePasswordPayload = z.object({
|
||||
password: z.string()
|
||||
});
|
||||
postauthRouter.post('/change_password', urlencoded(), (req, res) => {
|
||||
const body = changePasswordPayload.parse(req.body);
|
||||
|
||||
updatePassword(req.active_user, body.password);
|
||||
|
||||
res.set('Location', '/');
|
||||
res.status(303).send("Welcome");
|
||||
})
|
||||
|
||||
const deleteSessionPayload = z.object({
|
||||
deletion: z.literal('delete')
|
||||
});
|
||||
postauthRouter.post('/delete_session', urlencoded(), (req, res) => {
|
||||
deleteSessionPayload.parse(req.body);
|
||||
|
||||
deleteSession(req.cookies["rdh_sid"]);
|
||||
|
||||
res.set('Location', '/');
|
||||
res.status(303).send("Welcome");
|
||||
})
|
||||
|
||||
|
||||
router.use((req, res, next) => {
|
||||
const session = req.cookies["rdh_sid"];
|
||||
|
Loading…
x
Reference in New Issue
Block a user