split routes into separate files and added authentication

This commit is contained in:
Shoofle 2024-10-04 18:43:08 -04:00
parent 32913d582a
commit 5a7021b674
20 changed files with 402 additions and 232 deletions

View File

@ -15,4 +15,30 @@ a {
}
a:visited {
color: slategray;
}
.main-column {
width: 76ch;
margin-left: auto;
margin-right: auto;
padding-top: 3rem;
color: white;
}
header {
text-align: right;
width: 100%;
}
header hr {
width: 60%;
margin-inline-start: auto;
margin-inline-end: 0;
color: white;
}
.page-contents {
text-align: left;
background: rgba(10,66,30, 0.75);
margin: 1rem;
padding: 1rem;
border-radius: 1rem;
}

View File

@ -5,6 +5,8 @@ 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 Profile from '/src/login/Profile.jsx';
import AuthProvider from '/src/AuthProvider.jsx';
import './App.css';
@ -13,15 +15,18 @@ const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<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>
</BrowserRouter>
<AuthProvider>
<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/>}/>
<Route path="/profile" element={<Profile/>}/>
</Routes>
</BrowserRouter>
</AuthProvider>
</QueryClientProvider>
);
}

View File

@ -0,0 +1,29 @@
import { useState, createContext, useContext } from "react";
import { postLogIn, postLogOut } from './apiTools.jsx';
const LogInContext = createContext(false);
export const LogInStatusUpdateEscapeTool = {};
function AuthProvider({children}) {
let [loggedIn, setLoggedIn] = useState(localStorage.getItem("loggedIn") == "true");
function logOneWay (yeah) {
localStorage.setItem("loggedIn", yeah);
setLoggedIn(yeah);
}
LogInStatusUpdateEscapeTool.loggedIn = loggedIn;
LogInStatusUpdateEscapeTool.setLoggedIn = logOneWay;
return (
<LogInContext.Provider value={ loggedIn }>
{children}
</LogInContext.Provider>
);
}
export function useLoggedIn() {
return useContext(LogInContext);
}
export default AuthProvider;

View File

@ -1,4 +1,5 @@
import Graph from 'graphology';
import { LogInStatusUpdateEscapeTool } from './AuthProvider.jsx';
// This is wrapper functtions to do requests to the api, from the frontend.
@ -6,8 +7,14 @@ 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})
export const shoofetch = (url, config) => fetch(url, {...defaults, ...config})
.then(async (res) => {
if (res.status == 401) {
LogInStatusUpdateEscapeTool.setLoggedIn(false);
}
localStorage.setItem("session", res.session);
if (!res.ok) {
throw new Error(`got an error from the server: ${await res.text()}`);
}
@ -42,7 +49,7 @@ export async function fetchPage(id) {
}
export async function postPage({id, title, description}) {
return fetch(`${apiUrl}/page/${id}`, {
return shoofetch(`${apiUrl}/page/${id}`, {
method: 'POST',
body: JSON.stringify({id: id, title: title, description: description}),
...defaults
@ -75,13 +82,25 @@ export async function createAccount({name, password, nonce}) {
})
}
export async function logIn({name, password}) {
export async function postLogIn({name, password}) {
return shoofetch(`${apiUrl}/login`, {
method: 'POST',
body: JSON.stringify({name: name, password: password})
}).then((res) => {
LogInStatusUpdateEscapeTool.setLoggedIn(true);
return res;
})
}
export async function logOut() {
return shoofetch(`${apiUrl}/logout`, { method: 'POST' });
export async function postLogOut() {
return shoofetch(`${apiUrl}/logout`, {
method: 'POST'
}).then((res) => {
LogInStatusUpdateEscapeTool.setLoggedIn(false);
return res;
})
}
export async function fetchProfile() {
return shoofetch(`${apiUrl}/user`, {method: 'GET'});
}

View File

@ -1,7 +1,7 @@
import { useNavigate } from 'react-router-dom';
import { postLogOut } from './apiTools.jsx';
// New hook to make links use the react-router `navigate` method instead of default browser behavior.
import { useNavigate } from 'react-router-dom'
export function useFixLinks() {
// spread this return value on <a/> elements in order to make them navigate
const navigate = useNavigate();

View File

@ -1,21 +1,12 @@
.landing-page {
width: 76ch;
.landing-column {
width: 80%;
margin-left: auto;
margin-right: auto;
padding-top: 3rem;
color: white;
}
.landing-page header {
text-align: right;
width: 100%;
}
.landing-page header hr {
width: 60%;
margin-inline-start: auto;
margin-inline-end: 0;
color: white;
}
.landing-container {
display: grid;
width: 100%;
@ -29,3 +20,7 @@
background: rgba(10,66,30, 0.75);
}
ol li {
list-style-type: circle;
}

View File

@ -1,9 +1,12 @@
import { useNavigate, Link } from 'react-router-dom'
import { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { fetchPageList, postNewPage, logOut } from '../apiTools.jsx';
import { fetchPageList, postNewPage, postLogOut } from '../apiTools.jsx';
import { useFixLinks } from '../clientStuff.jsx';
import { useLoggedIn } from '../AuthProvider.jsx';
import PageList from './PageList.jsx';
import GraphRender from './GraphRender.jsx';
@ -23,8 +26,10 @@ function Landing() {
},
});
const loggedIn = useLoggedIn();
return (
<div className="landing-page">
<div className="landing-column">
<header>
<h1>Welcome to the forest.</h1>
<hr/>
@ -37,10 +42,20 @@ function Landing() {
<PageList />
</section>
<section className="landing-section">
<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/>
{ loggedIn
?
<>
<button onClick={makeNewPage.mutate}>Dig new room!</button><br/>
<button onClick={postLogOut}>Log Out</button><br/>
<button onClick={() => navigate('/profile')}>Profile</button><br/>
</>
:
<>
<button onClick={() => navigate('/register')}>Sign up</button><br/>
<button onClick={() => navigate('/login')}>Log in</button><br/>
<button onClick={() => navigate('/profile')}>Profile (but you're logged out, so it shouldn't work!)</button><br/>
</>
}
</section>
</div>
</div>

View File

@ -1,31 +1,31 @@
import { useForm } from "react-hook-form";
import { useNavigate } from 'react-router-dom';
import { logIn } from '../apiTools.jsx';
import { postLogIn } 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));
postLogIn(d).then( () => navigate('/') );
};
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>
<div className="main-column">
<form onSubmit={handleSubmit(onSubmit)}>
<label>
User name:
<input {...register("name")} />
<br/>
</label>
<label>
Password:
<input type="password" {...register("password")} />
<br/>
</label>
<button type="submit">Log right in, buckaroo!</button>
</form>
</div>
);
}

View File

@ -0,0 +1,52 @@
import { useEffect } from 'react';
import { useForm } from "react-hook-form";
import { useNavigate } from 'react-router-dom';
import { fetchProfile } from '../apiTools.jsx';
import { useLoggedIn } from '../AuthProvider.jsx';
import { useQuery } from '@tanstack/react-query';
function Profile() {
const { register, handleSubmit } = useForm();
const navigate = useNavigate();
const loggedIn = useLoggedIn();
const { isPending, isError, error, data } = useQuery({ // fetch the currrent values
queryKey: ['profile'],
queryFn: fetchProfile,
retry: 1
});
//useEffect(() => { if (!loggedIn) navigate('/'); }, [loggedIn]);
const { name, favoriteColor, leastFavoriteColor } = (data || {});
return (
<div className="main-column">
<section className="page-contents">
<p>the page {isPending ? "is" : "isn't"} pending</p>
<p>there {isError ? "is" : "isn't"} an error</p>
<p>error is {error?.message}</p>
<form onSubmit={handleSubmit(() => {})}>
<label>
User name:
<input {...register("name")} value={name}/>
<br/>
</label>
<label>
Favorite Color:
<input {...register("favoriteColor")} value={favoriteColor}/>
<br/>
</label>
<label>
Least Favorite Color:
<input {...register("leastFavoriteColor")} value={leastFavoriteColor}/>
<br/>
</label>
<button type="submit">Change those settings, bucko!!</button>
</form>
</section>
</div>
);
}
export default Profile;

View File

@ -13,24 +13,26 @@ function Register() {
};
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>
<div className="main-column">
<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>
</div>
);
}

View File

@ -4,7 +4,7 @@ import { apiUrl, fetchPage, postPage, deletePage } from '../apiTools.jsx';
import './Pages.css';
function PageView() {
function PageEdit() {
const queryClient = useQueryClient();
const navigate = useNavigate();
const { pagenumber } = useParams();
@ -32,10 +32,9 @@ function PageView() {
const ready = !(fetchQuery.error || fetchQuery.isPending);
let the_id, page_title, page_text, page_html;
if (ready) [the_id, page_title, page_text, page_html] = fetchQuery.data;
if (!page_title) page_title = "&nbsp;";
if (!page_text) page_text = "&nbsp;";
let {id, title, description, html} = fetchQuery.data || {};
if (!title) title = "&nbsp;";
if (!description) description = "&nbsp;";
function submitChanges(e) {
const newTitle = document.querySelector('span').innerHTML;
@ -45,7 +44,7 @@ function PageView() {
title: newTitle,
description: newText,
});
navigate(`/${pagenumber}`)
navigate(`/${pagenumber}`, {replace: true})
}
function submitDelete(e) {
@ -55,13 +54,14 @@ function PageView() {
}
return (
<div className="page-container">
<div className="main-column">
<header>
<h1>
<a href="/">🌳</a>
{pagenumber}.&nbsp;
<span
contentEditable="true"
dangerouslySetInnerHTML={{__html: ready ? page_title : "..." }} />
dangerouslySetInnerHTML={{__html: ready ? title : "..." }} />
</h1>
<hr/>
</header>
@ -71,7 +71,7 @@ function PageView() {
<div className="page-contents">
<pre
contentEditable="true"
dangerouslySetInnerHTML={{__html: page_text}} />
dangerouslySetInnerHTML={{__html: description}} />
</div>
<button
disabled={postMutation.isPending}
@ -94,4 +94,4 @@ function PageView() {
);
}
export default PageView;
export default PageEdit;

View File

@ -18,27 +18,27 @@ function PageView() {
const ready = !(error || isPending);
let the_id, page_title, page_text, page_html;
if (data) [the_id, page_title, page_text, page_html] = data;
let {id, title, description, html} = data || {};
return (
<div className="page-container">
<div className="main-column">
<div>
<header>
<h1>{pagenumber}. {ready ? (page_title || " ") : "..."}</h1>
<h1>
<a href="/" {...noLoad}>🌳</a>{pagenumber}. {ready ? (title || " ") : "..."}</h1>
<hr/>
</header>
<section>
{ ready ?
<div
className="page-contents"
dangerouslySetInnerHTML={{ __html: (page_html || " ") }}
dangerouslySetInnerHTML={{ __html: (html || " ") }}
{...noLoad}
/>
:
(isPending ? "Loading..." : JSON.stringify(error))
}
<button onClick={(e) => { e.preventDefault(); navigate(`/${pagenumber}/edit`)}}>Edit this page!</button>
<button onClick={(e) => { e.preventDefault(); navigate(`/${pagenumber}/edit`, {replace: true})}}>Edit this page!</button>
</section>
</div>
</div>

View File

View File

@ -1,22 +1,4 @@
.page-container {
width: 76ch;
margin-left: auto;
margin-right: auto;
padding-top: 3rem;
color: white;
}
.page-container header {
text-align: right;
width: 100%;
}
.page-container header hr {
width: 60%;
margin-inline-start: auto;
margin-inline-end: 0;
color: white;
}
.page-contents {
text-align: left;
background: rgba(17,66,0);
@ -25,7 +7,7 @@
border-radius: 1rem;
}
.page-contents pre {
pre {
width: 100%;
white-space: normal;
}

View File

@ -1,122 +0,0 @@
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) => {
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) => {
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) => {
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) => {
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) => {
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) => {
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', 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
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', 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;

10
server/authStuff.js Normal file
View File

@ -0,0 +1,10 @@
function loginRequired(req, res, next) {
console.log("checkinig on req.session for auhetnticaion: ", req.session);
if (!req.session.name) {
return res.status(401).json({"error": "need to be logged in for that bucko"});
}
next();
}
module.exports = { loginRequired };

25
server/routes/api.js Normal file
View File

@ -0,0 +1,25 @@
const { graphFromList } = require('../graphStuff.js');
const sqlite = require('better-sqlite3');
const db = new sqlite('the_big_db.db', { verbose: console.log });
const express = require('express');
const app = express.Router();
const page_routes = require('./pages.js');
app.use(page_routes);
const user_routes = require('./users.js');
app.use(user_routes);
app.get('/graph', (req, res) => {
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});
}
});
module.exports = app;

62
server/routes/pages.js Normal file
View File

@ -0,0 +1,62 @@
const express = require('express');
const app = express.Router();
const sqlite = require('better-sqlite3');
const db = new sqlite('../the_big_db.db', { verbose: console.log });
const { loginRequired } = require('../authStuff.js');
const showdown = require('showdown');
const converter = new showdown.Converter();
app.get('/pages', (req, res) => {
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', loginRequired, (req, res) => {
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) => {
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', loginRequired, (req, res) => {
try {
const html = converter.makeHtml(req.body.description);
const changes = db.prepare('replace into pages (id, title, description, 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', loginRequired, (req, res) => {
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});
}
});
module.exports = app;

68
server/routes/users.js Normal file
View File

@ -0,0 +1,68 @@
const express = require('express');
const app = express.Router();
const sqlite = require('better-sqlite3');
const db = new sqlite('../the_big_db.db', { verbose: console.log });
const argon2 = require('argon2');
const { loginRequired } = require('../authStuff.js');
// auth stuff
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
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', 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;
console.log('setting req.session.name! : ', req.session);
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"});
});
app.get('/user', loginRequired, (req, res) => {
res.status(200).json({
"name": req.session.name,
"favoriteColor": "red",
"leastFavoriteColor": "also red"
});
});
module.exports = app;

View File

@ -1,14 +1,16 @@
const express = require('express');
const session = require('express-session');
const sqlite = require('better-sqlite3');
const app = express();
const apiRoutes = require('./api.js');
const session = require('express-session');
const apiRoutes = require('./routes/api.js');
const port = process.env.PORT || 3001; // Use the port provided by the host or default to 3000
const sqlite = require('better-sqlite3');
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({