diff --git a/client/src/App.css b/client/src/App.css index 2962eebb..6c035b17 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -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; } \ No newline at end of file diff --git a/client/src/App.jsx b/client/src/App.jsx index 1fc43b6d..94cd0831 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -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 ( - - - }/> - }/> - }/> - }/> - }/> - - + + + + }/> + }/> + }/> + }/> + }/> + }/> + + + ); } diff --git a/client/src/AuthProvider.jsx b/client/src/AuthProvider.jsx new file mode 100644 index 00000000..91ec04ee --- /dev/null +++ b/client/src/AuthProvider.jsx @@ -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 ( + + {children} + + ); +} + +export function useLoggedIn() { + return useContext(LogInContext); +} + +export default AuthProvider; \ No newline at end of file diff --git a/client/src/apiTools.jsx b/client/src/apiTools.jsx index 38bd9d55..914520f1 100644 --- a/client/src/apiTools.jsx +++ b/client/src/apiTools.jsx @@ -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 postLogOut() { + return shoofetch(`${apiUrl}/logout`, { + method: 'POST' + }).then((res) => { + LogInStatusUpdateEscapeTool.setLoggedIn(false); + return res; }) } -export async function logOut() { - return shoofetch(`${apiUrl}/logout`, { method: 'POST' }); +export async function fetchProfile() { + return shoofetch(`${apiUrl}/user`, {method: 'GET'}); } \ No newline at end of file diff --git a/client/src/clientStuff.jsx b/client/src/clientStuff.jsx index fc54f60f..8bed4e7a 100644 --- a/client/src/clientStuff.jsx +++ b/client/src/clientStuff.jsx @@ -1,7 +1,7 @@ -// New hook to make links use the react-router `navigate` method instead of default browser behavior. - -import { useNavigate } from 'react-router-dom' +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. export function useFixLinks() { // spread this return value on elements in order to make them navigate const navigate = useNavigate(); diff --git a/client/src/landing/Landing.css b/client/src/landing/Landing.css index e63efdd9..5107a302 100644 --- a/client/src/landing/Landing.css +++ b/client/src/landing/Landing.css @@ -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; +} + diff --git a/client/src/landing/Landing.jsx b/client/src/landing/Landing.jsx index fcb5fa42..0c04558d 100644 --- a/client/src/landing/Landing.jsx +++ b/client/src/landing/Landing.jsx @@ -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 ( -
+

Welcome to the forest.


@@ -37,10 +42,20 @@ function Landing() {
-
-
-
-
+ { loggedIn + ? + <> +
+
+
+ + : + <> +
+
+
+ + }
diff --git a/client/src/login/LogIn.jsx b/client/src/login/LogIn.jsx index 0c3e24c5..b7c9530d 100644 --- a/client/src/login/LogIn.jsx +++ b/client/src/login/LogIn.jsx @@ -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 ( -
- - - -
+
+
+ + + +
+
); } diff --git a/client/src/login/Profile.jsx b/client/src/login/Profile.jsx new file mode 100644 index 00000000..c6ef958b --- /dev/null +++ b/client/src/login/Profile.jsx @@ -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 ( +
+
+

the page {isPending ? "is" : "isn't"} pending

+

there {isError ? "is" : "isn't"} an error

+

error is {error?.message}

+
{})}> + + + + +
+
+
+ ); +} + +export default Profile; \ No newline at end of file diff --git a/client/src/login/Register.jsx b/client/src/login/Register.jsx index b977bd61..232e941b 100644 --- a/client/src/login/Register.jsx +++ b/client/src/login/Register.jsx @@ -13,24 +13,26 @@ function Register() { }; return ( -
- - - - -
+
+
+ + + + +
+
); } diff --git a/client/src/page/PageEdit.jsx b/client/src/page/PageEdit.jsx index e7f23b58..08784587 100644 --- a/client/src/page/PageEdit.jsx +++ b/client/src/page/PageEdit.jsx @@ -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 = " "; - if (!page_text) page_text = " "; + let {id, title, description, html} = fetchQuery.data || {}; + if (!title) title = " "; + if (!description) description = " "; 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 ( -
+

+ 🌳 {pagenumber}.  + dangerouslySetInnerHTML={{__html: ready ? title : "..." }} />


@@ -71,7 +71,7 @@ function PageView() {
+              dangerouslySetInnerHTML={{__html: description}} />
           
+
diff --git a/client/src/page/PageWithSidebar.jsx b/client/src/page/PageWithSidebar.jsx new file mode 100644 index 00000000..e69de29b diff --git a/client/src/page/Pages.css b/client/src/page/Pages.css index 0d212f67..9c2120e2 100644 --- a/client/src/page/Pages.css +++ b/client/src/page/Pages.css @@ -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; } diff --git a/server/api.js b/server/api.js deleted file mode 100644 index d7a53f17..00000000 --- a/server/api.js +++ /dev/null @@ -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; \ No newline at end of file diff --git a/server/authStuff.js b/server/authStuff.js new file mode 100644 index 00000000..3b211ddd --- /dev/null +++ b/server/authStuff.js @@ -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 }; \ No newline at end of file diff --git a/server/routes/api.js b/server/routes/api.js new file mode 100644 index 00000000..c2d317ba --- /dev/null +++ b/server/routes/api.js @@ -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; \ No newline at end of file diff --git a/server/routes/pages.js b/server/routes/pages.js new file mode 100644 index 00000000..49f98095 --- /dev/null +++ b/server/routes/pages.js @@ -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; \ No newline at end of file diff --git a/server/routes/users.js b/server/routes/users.js new file mode 100644 index 00000000..d6504799 --- /dev/null +++ b/server/routes/users.js @@ -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; \ No newline at end of file diff --git a/server/server.js b/server/server.js index 7d585247..8870b07b 100644 --- a/server/server.js +++ b/server/server.js @@ -1,14 +1,16 @@ const express = require('express'); +const app = express(); + const session = require('express-session'); -const sqlite = require('better-sqlite3'); -const apiRoutes = require('./api.js'); +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({