This commit is contained in:
HRG @ SCExC 2024-02-24 11:12:14 -05:00
parent f370a2fe68
commit e335c65506
21 changed files with 33471 additions and 86 deletions

248
index.js
View file

@ -1,22 +1,34 @@
const fs = require('fs'); const fs = require("fs");
const path = require('path'); const path = require("path");
const express = require('express'); const express = require("express");
const session = require('express-session'); const session = require("express-session");
const SQLiteStore = require('better-sqlite3-session-store')(session); const SQLiteStore = require("better-sqlite3-session-store")(session);
const betterSqlite3 = require('better-sqlite3'); const betterSqlite3 = require("better-sqlite3");
const cheerio = require("cheerio");
const { Eta } = require("eta");
const bcrypt = require("bcrypt");
const app = express(); const app = express();
const bodyParser = require("body-parser");
const PORT = 3000; const PORT = 3000;
const db = betterSqlite3('prime.db'); let viewpath = path.join(__dirname, "views");
let eta = new Eta({ views: viewpath, cache: false, autoEscape: false });
app.use(session({ app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static("public"));
const db = betterSqlite3("prime.db");
db.pragma("journal_mode = WAL");
app.use(
session({
store: new SQLiteStore({ client: db, expired: { clear: true } }), store: new SQLiteStore({ client: db, expired: { clear: true } }),
secret: 'your secret key', secret: "your secret key",
resave: false, resave: false,
saveUninitialized: true, saveUninitialized: true,
cookie: { secure: false } cookie: { secure: false },
})); })
);
function applyMigrations() { function applyMigrations() {
db.exec(` db.exec(`
@ -27,18 +39,20 @@ function applyMigrations() {
); );
`); `);
const migrationsDir = path.join(__dirname, "/migrations");
const migrationFiles = fs
.readdirSync(migrationsDir)
.filter((file) => file.endsWith(".sql"));
const migrationsDir = path.join(__dirname, '/migrations'); migrationFiles.forEach((file) => {
const migrationFiles = fs.readdirSync(migrationsDir).filter(file => file.endsWith('.sql')); const isApplied = db
.prepare("SELECT filename FROM migrations WHERE filename = ?")
.get(file);
migrationFiles.forEach(file => {
const isApplied = db.prepare('SELECT filename FROM migrations WHERE filename = ?').get(file);
console.log(!isApplied, file)
if (!isApplied) { if (!isApplied) {
const sql = fs.readFileSync(path.join(migrationsDir, file), 'utf-8'); const sql = fs.readFileSync(path.join(migrationsDir, file), "utf-8");
db.exec(sql); db.exec(sql);
db.prepare('INSERT INTO migrations (filename) VALUES (?)').run(file); db.prepare("INSERT INTO migrations (filename) VALUES (?)").run(file);
console.log(`Migration applied: ${file}`); console.log(`Migration applied: ${file}`);
} }
}); });
@ -46,21 +60,203 @@ function applyMigrations() {
applyMigrations(); applyMigrations();
app.get('*', (req, res) => { // __ __ _______ _______ ______
// | | | || || || _ |
// | | | || _____|| ___|| | ||
// | |_| || |_____ | |___ | |_||_
// | ||_____ || ___|| __ |
// | | _____| || |___ | | | |
// |_______||_______||_______||___| |_|
function createUser(db, username, hashedPassword) {
return db
.prepare("INSERT INTO users (username, password) VALUES (?, ?)")
.run(username, hashedPassword).lastInsertRowId;
}
function getUser(db, username) {
return db.prepare("SELECT * from users where username = ?").get(username);
}
app.post("/register", async (req, res) => {
try {
const { username, password } = req.body;
const hashedPassword = await bcrypt.hash(password, 10); // 10 is the saltRounds
createUser(db, username, hashedPassword);
res.redirect("/workshop");
} catch (e) {
res.render("auth/register.html", { error: e });
}
});
app.post("/login", async (req, res) => {
const { username, password } = req.body;
const user = getUser(db, username);
if (user && (await bcrypt.compare(password, user.password))) {
req.session.userId = user.id;
return res.redirect("/");
}
return res.send(
eta.render("auth/login", {
error: "are you sure you entered that right?",
})
);
});
app.get("/register", async (req, res) => {
if (req.session.userId) {
return res.redirect("/");
}
return res.render("auth/register", { error: null });
});
app.get("/login", async (req, res) => {
console.log(req.session.userId);
if (req.session.userId) {
return res.redirect("/");
}
return res.send(eta.render("auth/login", { error: null }));
});
app.all("/logout", async (req, res) => {
return req.session.destroy(() => {
res.redirect("/");
});
});
// _ _ _______ ______ ___ _ _______ __ __ _______ _______
// | | _ | || || _ | | | | || || | | || || |
// | || || || _ || | || | |_| || _____|| |_| || _ || _ |
// | || | | || |_||_ | _|| |_____ | || | | || |_| |
// | || |_| || __ || |_ |_____ || || |_| || ___|
// | _ || || | | || _ | _____| || _ || || |
// |__| |__||_______||___| |_||___| |_||_______||__| |__||_______||___|
function getStructures(db) {
return db.prepare("SELECT * from structures").all();
}
function getStructure(db, id) {
return db.prepare("SELECT * from structures where ID = ?").run(id);
}
function createStructure(db, name, userId) {
const stmt = db.prepare(
"INSERT INTO structures (name, user_id) VALUES (?, ?)"
);
const info = stmt.run(name, userId);
return info.lastInsertRowid; // Returns the structure_id of the newly created structure
}
function createRoute(db, verb, path, structureId, userId, handler) {
const stmt = db.prepare(
"INSERT INTO routes (verb, path, structure_id, user_id, handler) VALUES (?, ?, ?, ?, ?)"
);
const info = stmt.run(verb, path, structureId, userId, handler);
return info.lastInsertRowid; // Returns the route_id of the newly created route
}
function getTemplateString(db, templateAlias, structId) {
const stmt = db.prepare(`
SELECT t.path
FROM templates AS t
JOIN structure_templates AS st ON t.id = st.template_id
WHERE st.structure_id = ? AND st.alias = ?
`);
const path = stmt.get(structId, templateAlias);
return fs.readFileSync(path).toString();
}
const { LRUCache } = require("lru-cache");
// we'll figure out max size later
const cache = new LRUCache({ max: 10000 });
function getTemplater(structId) {
let etaInstance = cache.get(structId);
if (!etaInstance) {
etaInstance = new Eta({});
etaInstance.resolvePath = function (path, _) {
return path;
};
etaInstance.readFile = function (templateAlias) {
return getTemplateString(db, templateAlias, structId);
};
cache.set(structId, etaInstance);
}
return etaInstance;
}
function bootstrapTemplateWithHTMXetc(htmlString) {
const $ = cheerio.load(htmlString);
if ($("html").length > 0) {
console.log("waddup");
let head = $("head");
// If <head> does not exist, prepend it to <html>
if (head.length === 0) {
$("html").prepend("<head></head>");
head = $("head");
}
head.append(`
<script src="https://unpkg.com/htmx.org"></script>
<script src="https://unpkg.com/hyperscript.org"></script>
<script src="https://cdn.tailwindcss.com"></script>
`);
}
return $.html();
}
app.post("/workshop", (req, res) => {
let structId = createStructure(db, req.body.name);
return res.redirect("/workshop/" + structId);
});
app.get("/workshop", (req, res) => {
return res.send(
bootstrapTemplateWithHTMXetc(
eta.render("workshop/index", {
structures: getStructures(db),
})
)
);
});
app.get("/workshop/:structure_id", (req, res) => {
return res.send(
bootstrapTemplateWithHTMXetc(
eta.render("workshop/editor", {
structure: getStructure(db, req.params.structure_id),
})
)
);
});
app.all("*", (req, res) => {
const path = req.path; const path = req.path;
const verb = req.method;
try { try {
const stmt = db.prepare('SELECT * FROM routes WHERE path = ?'); const stmt = db.prepare("SELECT * FROM routes WHERE path = ? AND verb = ?");
const route = stmt.get(path); const route = stmt.get(path, verb);
if (route) { if (route) {
res.json({ success: true, message: 'Path found', data: route }); return res.send(
eval(route.handler)({ ...this, ...handlerContext(req, res) })
);
} else { } else {
res.status(404).json({ success: false, message: 'Path not found' }); res.status(404).json({ success: false, message: "Path not found" });
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ success: false, message: 'Internal server error' }); res.status(500).json({ success: false, message: "Internal server error" });
} }
}); });

View file

@ -1,51 +1,63 @@
CREATE TABLE users ( CREATE TABLE users (
user_id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL username TEXT NOT NULL,
password TEXT NOT NULL,
UNIQUE(username)
); );
CREATE TABLE structures ( CREATE TABLE structures (
structure_id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, name TEXT NOT NULL,
user_id INTEGER, user_id INTEGER,
FOREIGN KEY(user_id) REFERENCES users(user_id) FOREIGN KEY(user_id) REFERENCES users(id),
UNIQUE(name, user_id)
); );
CREATE TABLE dbs ( CREATE TABLE dbs (
db_id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, name TEXT NOT NULL,
belongs_to_struct INTEGER, belongs_to_struct INTEGER,
FOREIGN KEY(belongs_to_struct) REFERENCES structures(structure_id) FOREIGN KEY(belongs_to_struct) REFERENCES structures(id),
UNIQUE(name, belongs_to_struct)
); );
CREATE TABLE templates ( CREATE TABLE templates (
template_id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL, file_path TEXT NOT NULL,
user_id INTEGER, user_id INTEGER,
belongs_to_struct INTEGER, belongs_to_struct INTEGER,
FOREIGN KEY(user_id) REFERENCES users(user_id), engine TEXT NOT NULL,
FOREIGN KEY(belongs_to_struct) REFERENCES structures(structure_id) FOREIGN KEY(user_id) REFERENCES users(id),
FOREIGN KEY(belongs_to_struct) REFERENCES structures(id),
UNIQUE(name, belongs_to_struct)
); );
CREATE TABLE routes ( CREATE TABLE routes (
route_id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
verb TEXT CHECK(verb IN ('POST', 'GET', 'PUT', 'DELETE')), verb TEXT CHECK(verb IN ('POST', 'GET', 'PUT', 'DELETE')),
path TEXT NOT NULL, path TEXT NOT NULL,
structure_id INTEGER, structure_id INTEGER,
user_id INTEGER, user_id INTEGER,
handler TEXT NOT NULL, handler TEXT NOT NULL,
FOREIGN KEY(structure_id) REFERENCES structures(structure_id), language TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(user_id) FOREIGN KEY(structure_id) REFERENCES structures(id),
FOREIGN KEY(user_id) REFERENCES users(id) UNIQUE(verb, path)
); );
CREATE TABLE structure_dbs ( CREATE TABLE structure_dbs (
db_id INTEGER, db_id INTEGER,
structure_id INTEGER, structure_id INTEGER,
PRIMARY KEY(db_id, structure_id), PRIMARY KEY(db_id, structure_id),
FOREIGN KEY(db_id) REFERENCES dbs(db_id), FOREIGN KEY(db_id) REFERENCES dbs(id),
FOREIGN KEY(structure_id) REFERENCES structures(structure_id) FOREIGN KEY(structure_id) REFERENCES structures(id)
); );
CREATE TABLE structure_templates ( CREATE TABLE structure_templates (
template_id INTEGER, template_id INTEGER,
structure_id INTEGER, structure_id INTEGER,
alias TEXT NOT NULL,
PRIMARY KEY(template_id, structure_id), PRIMARY KEY(template_id, structure_id),
FOREIGN KEY(template_id) REFERENCES templates(template_id), FOREIGN KEY(template_id) REFERENCES templates(id),
FOREIGN KEY(structure_id) REFERENCES structures(structure_id) FOREIGN KEY(structure_id) REFERENCES structures(id),
UNIQUE(structure_id, alias)
); );

1647
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,9 +9,17 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"bcrypt": "^5.1.1",
"better-sqlite3": "^9.4.0", "better-sqlite3": "^9.4.0",
"better-sqlite3-session-store": "^0.1.0", "better-sqlite3-session-store": "^0.1.0",
"body-parser": "^1.20.2",
"cheerio": "^1.0.0-rc.12",
"dot": "^1.1.3",
"ejs": "^3.1.9",
"eta": "^3.2.0",
"express": "^4.18.2", "express": "^4.18.2",
"express-session": "^1.18.0" "express-session": "^1.18.0",
"lru-cache": "^10.2.0",
"nunjucks": "^3.2.4"
} }
} }

BIN
prime.db Normal file

Binary file not shown.

BIN
public/.DS_Store vendored Normal file

Binary file not shown.

BIN
public/icons/.DS_Store vendored Normal file

Binary file not shown.

BIN
public/icons/model.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
public/icons/route.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
public/icons/template.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

6491
public/js/_hyperscript.min.js vendored Normal file

File diff suppressed because it is too large Load diff

3382
public/js/htmx.min.js vendored Normal file

File diff suppressed because it is too large Load diff

21625
public/js/tailwind.js Normal file

File diff suppressed because it is too large Load diff

12
views/auth/login.eta Normal file
View file

@ -0,0 +1,12 @@
<form action="/login" method="POST">
<input name="username" placeholder="username">
<input name="password" placeholder="password">
<button type="submit">
come home
</button>
<% if (error) { %>
<%= error %>
<% } %>
<br>
<div> or you can <a href="/register">move in here</a>.
</form>

10
views/auth/register.eta Normal file
View file

@ -0,0 +1,10 @@
<form action="/register" method="POST">
<input name="username" placeholder="username">
<input name="password" placeholder="password">
<button type="submit">
move in
</button>
<% if (error) { %>
<%= error %>
<% } %>
</form>

0
views/index.html Normal file
View file

29
views/workshop/editor.eta Normal file
View file

@ -0,0 +1,29 @@
<html>
<head>
<link rel="stylesheet" href="https://unpkg.com/xp.css">
</head>
<body>
<div class="flex flex-col w-full h-full window">
<div class="title-bar">
<div class="title-bar-text">Workshop - <%- include('/workshop/structure_name', {structure: it.structure}) %>
</div>
</div>
<div class="flex flex-grow window-body">
<div id="left-pane" class="flex w-1/2 bg-blue-500 h-full">
<div id="toggle" class="w-1/5 bg-red-500 h-full">
<%- include('/workshop/sidebar') %>
</div>
<div id="code-area" class="flex-grow bg-black h-full">
</div>
</div>
<div id="preview-area" class="w-1/2 bg-green-500 h-full">
</div>
</div>
</div>
</body>
</html>

24
views/workshop/index.eta Normal file
View file

@ -0,0 +1,24 @@
<html>
<head>
<script src="https://unpkg.com/htmx.org"></script>
<script src="https://unpkg.com/hyperscript.org"></script>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<button _="on click toggle .hidden on #new-structure">+ new structure</button>
<div id="new-structure" class="hidden">
<form action="/workshop" method="POST">
<label for="name">come up with a name for your structure and enter it here.</label>
<input name="name">
<button type="submit">
erect structure
</button>
</form>
</div>
<% structures.map(structure => { %>
<a href="/workshop/<%= structure.id %>">
<%= structure.name %>
</a>
<% }) %>
</body>
</html>

View file

@ -0,0 +1 @@
sdf

View file

@ -0,0 +1,2 @@
<div id="name" _="">structure<%= it.structure.name %><div _="on click toggle .hidden on me then toggle .hidden on the next <div/>">✎</div></div>
<div class="hidden" _=""><input placeholder="structure name" value="<%= it.structure.name %>"></div>