moving to server/db sameness and express-session

This commit is contained in:
Shoofle 2024-09-27 11:40:14 -04:00
parent ee5402ab44
commit 32913d582a
21 changed files with 1169 additions and 96 deletions

View File

@ -20,6 +20,7 @@
"graphology-layout-force": "^0.2.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
"react-router-dom": "^6.26.2",
"react-sigma": "^1.2.35",
"sigma": "^3.0.0-beta.29",
@ -3088,6 +3089,21 @@
"react": "^18.3.1"
}
},
"node_modules/react-hook-form": {
"version": "7.53.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.0.tgz",
"integrity": "sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ==",
"engines": {
"node": ">=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/react-hook-form"
},
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",

View File

@ -15,6 +15,7 @@
"graphology-layout-force": "^0.2.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
"react-router-dom": "^6.26.2",
"react-sigma": "^1.2.35",
"sigma": "^3.0.0-beta.29",

View File

@ -1,10 +1,12 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Landing from '/src/landing/Landing.jsx'
import PageView from '/src/page/PageView.jsx'
import PageEdit from '/src/page/PageEdit.jsx'
import Landing from '/src/landing/Landing.jsx';
import PageView from '/src/page/PageView.jsx';
import PageEdit from '/src/page/PageEdit.jsx';
import LogIn from '/src/login/LogIn.jsx';
import Register from '/src/login/Register.jsx';
import './App.css'
import './App.css';
const queryClient = new QueryClient();
@ -14,6 +16,8 @@ function App() {
<BrowserRouter>
<Routes>
<Route path="/" element={<Landing/>}/>
<Route path="/login" element={<LogIn/>}/>
<Route path="/register" element={<Register/>}/>
<Route path="/:pagenumber" element={<PageView/>}/>
<Route path="/:pagenumber/edit" element={<PageEdit/>}/>
</Routes>

View File

@ -4,8 +4,19 @@ import Graph from 'graphology';
export const apiUrl = `${window.location.origin}/api`;
//lil helper to throw errorrs from the promise when we get not-ok results
export const shoofetch = (url, config) => fetch(url, {...config, ...defaults})
.then(async (res) => {
if (!res.ok) {
throw new Error(`got an error from the server: ${await res.text()}`);
}
return res.json();
});
const defaults = {
headers: {'Content-Type': 'application/json'}
headers: {'Content-Type': 'application/json'},
credentials: 'include'
};
export async function postNewPage() {
@ -41,7 +52,6 @@ export async function postPage({id, title, description}) {
export async function deletePage(id) {
return fetch(`${apiUrl}/page/${id}`, {
method: 'DELETE',
headers: {'Content-Type': 'application/json'},
...defaults
})
}
@ -56,4 +66,22 @@ export async function fetchGraph() {
graph.import(serialized);
return graph;
})
}
export async function createAccount({name, password, nonce}) {
return shoofetch(`${apiUrl}/register`, {
method: 'POST',
body: JSON.stringify({name: name, password: password, nonce: nonce})
})
}
export async function logIn({name, password}) {
return shoofetch(`${apiUrl}/login`, {
method: 'POST',
body: JSON.stringify({name: name, password: password})
})
}
export async function logOut() {
return shoofetch(`${apiUrl}/logout`, { method: 'POST' });
}

View File

@ -1,7 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
@ -9,9 +8,4 @@ root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
);

View File

@ -1,7 +1,8 @@
.landing-page {
padding-left:10rem;
padding-right: 10rem;
width: 76ch;
margin-left: auto;
margin-right: auto;
padding-top: 3rem;
color: white;
}

View File

@ -1,7 +1,7 @@
import { useNavigate, Link } from 'react-router-dom'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { fetchPageList, postNewPage } from '../apiTools.jsx';
import { fetchPageList, postNewPage, logOut } from '../apiTools.jsx';
import { useFixLinks } from '../clientStuff.jsx';
import PageList from './PageList.jsx';
@ -37,9 +37,10 @@ function Landing() {
<PageList />
</section>
<section className="landing-section">
<button>Sign up</button><br/>
<button>Log in</button><br/>
<button onClick={makeNewPage.mutate} >Dig new room!</button><br/>
<button onClick={() => navigate('/register')}>Sign up</button><br/>
<button onClick={() => navigate('/login')}>Log in</button><br/>
<button onClick={makeNewPage.mutate}>Dig new room!</button><br/>
<button onClick={logOut}>Log Out</button><br/>
</section>
</div>
</div>

View File

@ -1 +1,32 @@
LogIn.jsx
import { useForm } from "react-hook-form";
import { useNavigate } from 'react-router-dom';
import { logIn } from '../apiTools.jsx';
function LogIn() {
const { register, handleSubmit } = useForm();
const navigate = useNavigate();
const onSubmit = (d) => {
logIn(d)
.then(() => navigate('/'))
.catch((error) => console.log('error logging in: ', error));
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>
User name:
<input {...register("name")} />
<br/>
</label>
<label>
Password:
<input {...register("password")} />
<br/>
</label>
<button type="submit">Log right in, buckaroo!</button>
</form>
);
}
export default LogIn;

View File

@ -0,0 +1,37 @@
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { createAccount } from "../apiTools.jsx";
function Register() {
const { register, handleSubmit } = useForm();
const navigate = useNavigate();
const onSubmit = (d) => {
createAccount(d)
.then(() => navigate("/login"))
.catch((error) => console.log("error: ", error));
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>
User name:
<input {...register("name")} />
<br/>
</label>
<label>
Password:
<input {...register("password")} />
<br/>
</label>
<label>
Nonce password from shoofle:
<input {...register("nonce")} />
<br/>
</label>
<button type="submit">Request Account!</button>
</form>
);
}
export default Register;

View File

@ -1,7 +1,8 @@
.page-container {
padding-left:10rem;
padding-right: 10rem;
width: 76ch;
margin-left: auto;
margin-right: auto;
padding-top: 3rem;
color: white;
}

View File

@ -1,13 +0,0 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@ -10,6 +10,18 @@ create table if not exists pages (
)
`;
const createUsers = `
create table if not exists users (
name varchar(64) primary key,
password varchar(128)
)
`;
const createTokens = `
create table if not exists tokens (
)
`;
async function initDb() {
const config = {
url: "http://127.0.0.1:8000"
@ -19,6 +31,7 @@ async function initDb() {
const converter = new showdown.Converter();
console.log("creating page table")
console.log(await db.execute(createPages));
console.log("finding pages that havent been rendered");
@ -35,7 +48,11 @@ async function initDb() {
sql: `replace into pages (id, title, description, html) values (?, ?, ?, ?)`,
args: [id, title, description, renderedPage]
});
});
console.log("creating user table");
console.log(await db.execute(createUsers));
}
initDb();

View File

@ -1,72 +1,122 @@
const express = require('express');
const showdown = require('showdown');
const argon2 = require('argon2');
const { query } = require('./dbHelper.js');
const { graphFromList } = require('./graphStuff.js');
const app = express.Router();
const converter = new showdown.Converter();
const sqlite = require("better-sqlite3");
const db = new sqlite('the_big_db.db', { verbose: console.log });
console.log(db);
app.get('/pages', (req, res) => {
query('select id, title from pages', [])
.then((r) => r.rows)
.then((row) => row.map(([id, title]) => {return {"id": id, "title": title};}))
.then((pages) => res.status(200).json(pages))
.catch((error) => res.status(500).json({"error": error}));
try {
const pages = db.prepare('select id, title from pages').all();
res.status(200).json(pages);
} catch (error) {
res.status(500).json({"error": error});
}
});
app.post('/page/new', (req, res) => {
query('insert into pages (title, description) values (?, ?) returning id',
["new page", "this page is new!"])
.then((r) => r.rows[0])
.then((row) => res.status(200).json({id: row[0]}))
.catch((error) => res.status(500).json({"error": error}))
try {
const newPage = db.prepare('insert into pages (title, description) values (?, ?) returning id')
.get("new page", "this page is new!");
res.status(200).json(newPage);
} catch (error) {
res.status(500).json({"error": error});
}
});
app.get('/page/:id', (req, res) => {
query('select * from pages where id=?', [req.params.id])
.then((r) => {
if (r.rows.length == 0) res.status(404).json({"error": "page not found in db"});
else return r.rows[0];
}).then((row) => res.status(200).json(row))
.catch((error) => res.status(500).json({"error": error}));
try {
const page = db.prepare('select * from pages where id=:id').get(req.params);
if (page === undefined) res.status(404).json({"error": "page not found"});
else res.status(200).json(page);
} catch (error) {
res.status(500).json({"error": error});
}
});
app.post('/page/:id', (req, res) => {
const html = converter.makeHtml(req.body.description);
query('replace into pages (id, title, description, html) values (?, ?, ?, ?)',
[req.params.id, req.body.title, req.body.description, html])
.then(() => res.status(200).json({}))
.catch((error) => res.status(500).json({"error": error}));
try {
const html = converter.makeHtml(req.body.description);
const changes = db.prepare('replace into pages (id, title, descriptioon, html) values (?, ?, ?, ?)')
.run(req.params.id, req.body.title, req.body.description, html);
res.status(200).json(changes);
} catch (error) {
res.status(500).json({"error": error});
}
});
app.delete('/page/:id', (req, res) => {
query('delete from pages where id = ?', [req.params.id])
.then(() => res.status(200).json({id: req.params.id}))
.catch((error) => res.status(500).json({"error": error}))
try {
const changes = db.prepare('delete from pages where id = ?').run(req.params.id);
res.status(200).json({id: req.params.id});
} catch (error) {
res.status(500).json({"error": error});
}
})
app.get('/graph', (req, res) => {
query('select id, html from pages', [])
.then((r) => r.rows)
.then((stuff) => graphFromList(stuff))
.then((graph) => res.json(graph))
.catch((error) => res.status(500).json({"error": error}));
try {
const rows = db.prepare('select id, html from pages').all();
const graph = graphFromList(rows);
res.status(200).json(graph);
} catch (error) {
res.status(500).json({"error": error});
}
});
// auth stuff
app.post('/register', (req, res) => {
// parse the body, which miight be url-encoded?
app.post('/register', async (req, res) => {
const {name, password, nonce} = req.body;
const oldUser = db.prepare('select name from users where name=?').get(name);
if (oldUser) return res.status(500).json({"error": "user name already in use"});
// check if the nonce password is correctt
// hash the password
// add tthe user to the users database
if (nonce != "a softer birdsong") return res.status(500).json({"error": "wrong nonce"});
try {
// i'm told argon2 is the good one nowatimes
const hash = await argon2.hash(password);
const inserted = db.prepare('insert into users (name, password) values (?, ?)').run(name, hash);
res.status(200).json(inserted);
} catch (error) {
res.status(500).json({"error": error});
}
});
app.post('/login', (req, res) => {
// hash the password
// check if the user/hashed pass matches
// if not, throw an error
// create a valid ttoken?
// return token
app.post('/login', async (req, res) => {
if (req.session.name) {
return res.status(200).json({message: "already logged in", name: req.session.name});
}
const {name, password} = req.body;
// fetch username and passswords from the db
const storedUser = db.prepare('select name, password from users where name = ?').get(name);
if (!storedUser) {
return res.status(401).json({"error": "password/username combo not found in database"});
}
//check if the passss hashes mattch and log in
if (!(await argon2.verify(storedUser.password, password))) {
return res.status(401).json({"error": "password/username combo not found in database"});
}
// set the session cookie and rreturn 200!
req.session.name = name;
return res.status(200).json({message: "successfully logged in!", name: name});
});
app.post('/logout', (req, res) => {
req.session.destroy();
res.status(200).json({message: "successfully logged out"});
});
module.exports = app;

View File

@ -1,19 +0,0 @@
// helper function(s?) to make a query to the database
const db_url = 'http://127.0.0.1:8000'
async function query(q, params) {
return fetch(db_url, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
statements: [{
q: q,
params: params
}]
})
}).then((res) => res.json())
.then((data) => data[0].results);
}
module.exports = { query: query };

View File

@ -0,0 +1,31 @@
const sqlite = require("better-sqlite3");
const sqlite3_db = new sqlite('./the_big_db.db', { verbose: console.log });
const libsql = require("@libsql/client");
const config = {url: "http://127.0.0.1:8000"};
const libsql_db = libsql.createClient(config);
async function copyPages() {
console.log("copying everythting from hte old db");
const pages = (await libsql_db.execute(`select id, title, description, html from pages`)).rows;
const insertPage = sqlite3_db.prepare(`replace into pages (id, title, description, html) values (:id, :title, :description, :html)`);
pages.forEach((page) => {
console.log(`inserting page ${page.id}`)
insertPage.run(page);
});
}
async function copyUsers() {
console.log("copying users now");
const users = (await libsql_db.execute(`select name, password from users`)).rows;
const insertUser = sqlite3_db.prepare(`replace into users (name, password) values (:name, :password)`);
users.forEach((user) => {
console.log(`inserting ${user.name}, ${user.password}`);
insertUser.run(user);
});
}
copyPages();
copyUsers();

View File

@ -0,0 +1,48 @@
const sqlite = require("better-sqlite3");
const showdown = require("showdown");
const db = new sqlite('./the_big_db.db', { verbose: console.log });
const converter = new showdown.Converter();
const createPages = db.prepare(`
create table if not exists pages (
id integer primary key,
title varchar(255),
description text,
html text
)
`);
const createUsers = db.prepare(`
create table if not exists users (
name varchar(64) primary key,
password varchar(128)
)
`);
function initDb() {
console.log("creating page table")
console.log(createPages.run());
console.log("finding pages that havent been rendered");
rows = db.prepare(`select * from pages where html is null`).all();
console.log(rows);
const insertPage = db.prepare(`replace into pages (id, title, description, html) values (:id, :title, :description, :html)`);
rows.forEach((pageData) => {
const {id, title, description, html} = pageData;
console.log(`rendering page number ${id}`)
console.log(`${id}. ${title}: ${description} ( ${html} )`);
const renderedPage = converter.makeHtml(description);
insertPage.run({...pageData, html: renderedPage});
});
console.log("creating user table");
console.log(createUsers.run());
}
initDb();

View File

@ -0,0 +1,4 @@
const sqlite = require("better-sqlite3");
const db = new sqlite('./the_big_db.db', { verbose: console.log });
console.log(db.prepare("select name from sqlite_master where type='table'").all());

View File

@ -4,12 +4,12 @@ const { circular } = require('graphology-layout');
function graphFromList(allTheStuff) {
const graph = new graphology.Graph();
for (const {id, html} of allTheStuff) {
for (const [id, html] of allTheStuff) {
graph.addNode(id);
}
for (const [id, html] of allTheStuff) {
for (const {id, html} of allTheStuff) {
const { document } = (new JSDOM(html)).window;
const links = document.querySelectorAll('a');
links.forEach((link) => {

818
server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,13 @@
"author": "",
"license": "ISC",
"dependencies": {
"@libsql/client": "^0.14.0",
"argon2": "^0.41.1",
"better-sqlite3": "^11.3.0",
"better-sqlite3-session-store": "^0.1.0",
"client-sessions": "^0.8.0",
"express": "^4.21.0",
"express-session": "^1.18.0",
"graphology": "^0.25.4",
"graphology-layout": "^0.6.1",
"graphology-layout-force": "^0.2.4",

View File

@ -1,11 +1,28 @@
const express = require('express');
const session = require('express-session');
const sqlite = require('better-sqlite3');
const apiRoutes = require('./api.js');
const port = process.env.PORT || 3001; // Use the port provided by the host or default to 3000
const db = new sqlite('the_big_db.db', { verbose: console.log });
const SqliteStore = require('better-sqlite3-session-store')(session);
const app = express();
app.use(express.json());
app.use(session({
store: new SqliteStore({
client: db,
expired: {
clear: true,
intervalMs: 15*60*1000
}
}),
secret: "dno'tt check me into versino control",
resave: false
}));
app.use("/api", apiRoutes);
app.listen(port, () => {