From 7f1a57d1a6a42485b5baeb4af8630baa2de8623c Mon Sep 17 00:00:00 2001 From: Tyler Murphy Date: Sun, 22 Jan 2023 14:41:39 -0500 Subject: [PATCH] finish --- index.js | 136 ++++++++++++++++++++++- package-lock.json | 54 +++++---- package.json | 1 + public/404.html | 4 +- public/css/console.css | 60 ++++++++++ public/css/home.css | 5 + public/css/main.css | 10 ++ public/css/people.css | 41 +++++++ public/css/profile.css | 118 ++++++++++++++++++++ public/home.html | 2 + public/js/api.js | 57 ++++++++++ public/js/header.js | 2 +- public/js/home.js | 144 ++++++++++++------------ public/js/login.js | 29 +++++ public/js/people.js | 48 +++++++- public/js/profile.js | 88 +++++++++++++++ public/login.html | 38 ++++--- public/people.html | 3 +- public/profile.html | 26 ++--- src/api.js | 221 ++++++++++++++++++++++++++++++++++++- src/database.js | 244 +++++++++++++++++++++++++++++++++++++++-- 21 files changed, 1176 insertions(+), 155 deletions(-) create mode 100644 public/css/console.css create mode 100644 public/js/api.js create mode 100644 public/js/login.js create mode 100644 public/js/profile.js diff --git a/index.js b/index.js index 1564775..c35ccbb 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,10 @@ const express = require('express') const app = express() const port = 8080 +app.set('trust proxy', true) + +const database = require('./src/database.js') + const rateLimiter = require('express-rate-limit') const limiter = (min, count) => { return rateLimiter({ @@ -21,11 +25,55 @@ app.use(cookieParser()) app.use(express.json()); app.use(express.static('public')) +app.use((req, res, next) => { + const public = { ... req.body } + if (public.password !== undefined) { + public.password = '********' + } + console.log(req.ip, req.method, req.path, public) + update(req.ip, req.method, req.path, public) + next() +}) + +app.get('/', (req, res) => { + const cookies = req.cookies; + if (cookies === undefined || cookies.auth === undefined) { + res.redirect('/login') + return + } + const user = database.auth(req.cookies.auth) + if (user === undefined) { + res.redirect('/login') + return + } + res.redirect('/home') +}) + app.get('/login', (req, res) => { - res.sendFile('login.html', { root: './public' }) + const cookies = req.cookies; + if (cookies === undefined || cookies.auth === undefined) { + res.sendFile('login.html', { root: './public' }) + return + } + const user = database.auth(req.cookies.auth) + if (user === undefined) { + res.sendFile('login.html', { root: './public' }) + return + } + res.redirect('/home') }) app.get('/home', (req, res) => { + const cookies = req.cookies; + if (cookies === undefined || cookies.auth === undefined) { + res.redirect('/login') + return + } + const user = database.auth(req.cookies.auth) + if (user === undefined) { + res.redirect('/login') + return + } res.sendFile('home.html', { root: './public' }) }) @@ -37,17 +85,95 @@ app.get('/profile', (req, res) => { res.sendFile('profile.html', { root: './public' }) }) -app.use('/api', limiter(1,60)) -app.use('/api/register', limiter(60, 5)) -app.use('/api/login', limiter(10, 5)) - const api = require('./src/api.js') app.use('/api', api); +const connections = [] +app.get('/console', (req, res) => { + res.write(` + + + + + + XSSBook - Console + + + `) + connections.push(res) +}) + +function color(method) { + switch(method) { + case 'GET': + return '4ae04a' + case 'POST': + return 'b946db' + case 'PUT': + return 'ff9705' + case 'PATCH': + return `42caff` + case 'DELETE': + return `ff4a4a` + case 'HEAD': + return '424cff' + case 'OPTIONS': + return 'ff9757' + } +} + +function highlight(json) { + if (typeof json != 'string') { + json = JSON.stringify(json, undefined, 2); + } + json = json.replace(/&/g, '&').replace(//g, '>'); + return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { + var cls = 'number'; + if (/^"/.test(match)) { + if (/:$/.test(match)) { + cls = 'key'; + } else { + cls = 'string'; + } + } else if (/true|false/.test(match)) { + cls = 'boolean'; + } else if (/null/.test(match)) { + cls = 'null'; + } + return '' + match + ''; + }); +} + +async function update(ip, method, path, json) { + connections.forEach(con => { + con.write(` +
+ ${ip} + ${method} + ${path} + ${highlight(json)} +
+ `) + }) +} + +// app.use('/api', limiter(1,60)) +// app.use('/api/register', limiter(60, 5)) +// app.use('/api/login', limiter(10, 5)) + app.use((req, res, next) => { res.status(404).sendFile('404.html', { root: './public' }) }) +app.use((err, req, res, next) => { + if (err instanceof SyntaxError && err.status === 400 && 'body' in err) { + res.status(400).send({msg: 'Invalid json body'}) + return + } + console.error(err) + res.status(500).send({msg: 'Internal server error'}) +}) + app.listen(port, () => { console.log(`App listening on port http://127.0.0.1:${port}`) }) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1044105..3eea334 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "better-sqlite3": "^8.0.1", "cookie-parser": "^1.4.6", + "crypto": "^1.0.1", "dotenv": "^16.0.3", "express": "^4.18.2", "express-rate-limit": "^6.7.0" @@ -171,9 +172,9 @@ } }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", "engines": { "node": ">= 0.6" } @@ -190,19 +191,17 @@ "node": ">= 0.8.0" } }, - "node_modules/cookie-parser/node_modules/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in." + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -360,6 +359,14 @@ "express": "^4 || ^5" } }, + "node_modules/express/node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1145,9 +1152,9 @@ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" }, "cookie-parser": { "version": "1.4.6", @@ -1156,13 +1163,6 @@ "requires": { "cookie": "0.4.1", "cookie-signature": "1.0.6" - }, - "dependencies": { - "cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" - } } }, "cookie-signature": { @@ -1170,6 +1170,11 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==" + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1280,6 +1285,13 @@ "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" + }, + "dependencies": { + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + } } }, "express-rate-limit": { diff --git a/package.json b/package.json index c185b82..ef46f50 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "better-sqlite3": "^8.0.1", "cookie-parser": "^1.4.6", + "crypto": "^1.0.1", "dotenv": "^16.0.3", "express": "^4.18.2", "express-rate-limit": "^6.7.0" diff --git a/public/404.html b/public/404.html index 7aedb82..49f0d5d 100644 --- a/public/404.html +++ b/public/404.html @@ -5,11 +5,11 @@ - XSSBook - Home + XSSBook - Not Found
diff --git a/public/css/console.css b/public/css/console.css new file mode 100644 index 0000000..bc07969 --- /dev/null +++ b/public/css/console.css @@ -0,0 +1,60 @@ +body { + margin: 0; + padding: 0; + background-color: #181818; + display: flex; + flex-direction: column-reverse; +} + +@font-face { + font-family: sfpro; + src: url("../fonts/sfpro.otf") format("opentype"); +} + +div { + background-color: #282828; + font-family: sfpro; + margin: 15px; + margin-bottom: 0px; + border-radius: 5px; + padding: 10px; + width: calc(100% - 50px) +} + +span { + display: inline-block; + padding: 0; + margin: 0; + color: #ffffff; + font-family: sfpro; + margin-right: 10px; +} + +.json span { + display: inline; + margin: 0; +} + +.key { + color: white; +} + +.value { + color: white; +} + +.boolean { + color: aqua; +} + +.null { + color: blue; +} + +.number { + color: yellow; +} + +.string { + color: #4ae04a +} \ No newline at end of file diff --git a/public/css/home.css b/public/css/home.css index de0c2d0..33d72c0 100644 --- a/public/css/home.css +++ b/public/css/home.css @@ -148,6 +148,11 @@ body { margin-bottom: 10px; } +.comment p, .post p { + word-break: break-all; + white-space: normal; +} + .comment>span { display: flex; flex-direction: column; diff --git a/public/css/main.css b/public/css/main.css index 9d2fe1d..a75a941 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -42,6 +42,14 @@ body { color: #1778f2 } +.error { + font-family: sfpro; + color: #f02849; + padding-top: 10px; + margin-bottom: -10px; + font-size: 15px; +} + .gtext { font-family: sfpro; color: #606770 @@ -77,6 +85,7 @@ body { } a { + color: inherit; text-decoration: none; cursor: pointer; } @@ -263,6 +272,7 @@ footer { .hidden { visibility: hidden; pointer-events: none; + display: none !important; } .pfp, .pfp img { diff --git a/public/css/people.css b/public/css/people.css index 70db81f..b8cf025 100644 --- a/public/css/people.css +++ b/public/css/people.css @@ -1,3 +1,44 @@ body { background-color: #f0f2f5; +} + +#users { + display: flex; + flex-direction: column; + align-items: center; +} + +.person { + width: 30em; + height: fit-content; + background-color: white; + border-radius: 10px; + box-shadow: 0 2px 4px rgba(0, 0, 0, .05); + margin-bottom: 1.5em; + display: flex; + flex-direction: row; +} + +.profile, .profile img { + border-radius: 10px 0px 0px 10px; + width: 10em; + height: 10em; + padding: 0; + display: block; + background-color: #e4e6e8; + flex-shrink: 0; +} + +.info { + margin: 20px; + display: flex; + flex-direction: column; +} + +.info span { + width: 280px; + margin-bottom: 5px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; } \ No newline at end of file diff --git a/public/css/profile.css b/public/css/profile.css index 70db81f..4c5ae10 100644 --- a/public/css/profile.css +++ b/public/css/profile.css @@ -1,3 +1,121 @@ body { background-color: #f0f2f5; +} + +.spacer { + margin-bottom: 3.5em; +} + +#top { + background-color: white; + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + box-shadow: 0 2px 4px rgba(0, 0, 0, .05); +} + +#banner { + background-image: linear-gradient(#949494, white, white); + height: 30em; + width: 100%; + display: flex; + justify-content: center; +} + +#banner div, #banner img { + width: 80em; + height: inherit; + background-color: #e4e6e8; + border-radius: 0px 0px 20px 20px; +} + +#info { + width: 80em; + display: flex; + flex-direction: row; +} + +.face { + background-color: #e4e6e8; + height: 12em; + width: 12em; + border-radius: 7em; + border: solid 5px white; + margin-top: -2em; + margin-left: 2em; + margin-right: 2em; +} + +.infodata { + margin-top: 2em; + display: flex; + flex-direction: column; +} + +.infodata span { + margin-bottom: .5em; +} + +.profilebuttons { + width: 80em; + height: 3em; + display: flex; + align-items: center; +} + +.profilebuttons button { + all: unset; + font-family: sfprobold; + padding: 0px 50px; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + color: #606770; + cursor: pointer; +} + +.profilebuttons button:hover { + background-color: #dddfe2; +} + +.selected { + color: #1778f2 !important; + border-bottom: 3px solid #1778f2 !important; +} + +#about { + margin-top: 2em; + align-self: center; + padding: 0; + display: flex; + flex-direction: row; +} + +#posts { + margin-top: 2em; +} + +#about .ltext { + border-right: 2px solid #dadde1; + padding: 10px; + padding-right: 3em; +} + +#about .data { + display: flex; + flex-direction: column; + padding: 10px; + padding-left: 20px; + padding-top: 15px; +} + +#about .data span { + margin-bottom: 10px; + width: 28em; + margin-bottom: 5px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; } \ No newline at end of file diff --git a/public/home.html b/public/home.html index 6035fc4..cc780ac 100644 --- a/public/home.html +++ b/public/home.html @@ -10,6 +10,8 @@ + + \ No newline at end of file diff --git a/public/js/api.js b/public/js/api.js new file mode 100644 index 0000000..371ecf3 --- /dev/null +++ b/public/js/api.js @@ -0,0 +1,57 @@ +const endpoint = 'https://xssbook.com/api' + +const request = async (url, body, method) => { + if (method === undefined) method = 'POST' + const response = await fetch(endpoint + url, { + method, + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json' + } + }); + if (response.status == 401) { + location.href = 'login' + } + const json = await response.json() + return { status: response.status, msg: json.msg, json } +} + +const login = async (email, password) => { + return await request('/auth/login', {email, password}) +} + +const register = async (first, last, email, password, gender, month, day, year) => { + return await request('/auth/register', {first, last, email, password, gender, month, day, year}) +} + +const loadposts = async (page) => { + return await request('/posts/load', {page}) +} + +const loadusersposts = async (id) => { + return await request('/posts/user', {id}) +} + +const loadusers = async (ids) => { + return await request('/users/load', {ids}) +} + +const loadallusers = async () => { + return await request('/users/all', {}) +} + +const loadself = async () => { + return await request("/auth/self", {}) +} + +const postcomment = async (id, content) => { + return await request('/posts/comment', {id, content}, 'PUT') +} + +const postlike = async (id, state) => { + return await request('/posts/like', {id, state}, 'PUT') +} + +const createpost = async (content) => { + return await request('/posts/create', {content}) +} \ No newline at end of file diff --git a/public/js/header.js b/public/js/header.js index 24643d6..8fe03e5 100644 --- a/public/js/header.js +++ b/public/js/header.js @@ -1,7 +1,7 @@ function header(home, people) { const html = `
diff --git a/public/people.html b/public/people.html index a46487d..f0aa514 100644 --- a/public/people.html +++ b/public/people.html @@ -3,12 +3,13 @@ - + XSSBook - People + \ No newline at end of file diff --git a/public/profile.html b/public/profile.html index 5f7a902..0274327 100644 --- a/public/profile.html +++ b/public/profile.html @@ -3,27 +3,15 @@ - + + XSSBook - Profile - + + + + + \ No newline at end of file diff --git a/src/api.js b/src/api.js index 6b5e594..8091a3c 100644 --- a/src/api.js +++ b/src/api.js @@ -2,13 +2,230 @@ const express = require('express') const router = express.Router() const database = require('./database.js') +const check = (test, type) => { + return text === undefined || text === null || typeof test !== type +} + +const text = (test, min, max) => { + return check(test, 'string') || test.length > max || test.length < min +} + router.get('/', (req, res) => { res.status(200).send( {msg: 'xssbook api'} ) }) -router.post('/', (req, res) => { - res.status(200).send( {msg: 'xssbook api'} ) +router.post('/auth/register', (req, res) => { + const first = req.body.first; + if (text(first, 1, 20)) { + res.status(400).send( {msg: 'Invalid first name'} ); return; + } + const last = req.body.last; + if (text(last, 1, 20)) { + res.status(400).send( {msg: 'Invalid last name'} ); return; + } + const email = req.body.email; + if (text(email, 1, 50)) { + res.status(400).send( {msg: 'Invalid email'} ); return; + } + const password = req.body.password; + if (text(password, 1, 50)) { + res.status(400).send( {msg: 'Invalid password'} ); return; + } + const gender = req.body.gender; + if (text(gender, 1, 100)) { + res.status(400).send( {msg: 'Invalid gender'} ); return; + } + const month = req.body.month; + if (text(month, 1, 10)) { + res.status(400).send( {msg: 'Invalid month'} ); return; + } + const day = req.body.day; + if (check(day, 'number')) { + res.status(400).send( {msg: 'Invalid day'} ); return; + } + const year = req.body.year; + if (check(year, 'number')) { + res.status(400).send( {msg: 'Invalid year'} ); return; + } + let exists = database.getUserByEmail(email); + if (exists !== undefined) { + res.status(400).send( {msg: 'Email is already in use'} ); return; + } + exists = database.getUserByPassword(password); + if (exists !== undefined) { + res.status(400).send( {msg: `Password is already in use by ${exists.email}`} ); return; + } + const key = database.register(first, last, email, password, gender, month, day, year); + if (key === undefined) { + res.status(500).send( {msg: 'Failed to register user'} ); return; + } + res.status(200).cookie('auth', key).send({msg: 'Successfully registered new user'}) }) +router.post('/auth/login', (req, res) => { + const email = req.body.email + if (check(email, 'string')) { + res.status(400).send( {msg: 'Invalid email'} ); return; + } + const password = req.body.password + if (check(password, 'string')) { + res.status(400).send( {msg: 'Invalid password'} ); return; + } + const key = database.login(email, password) + if (key === undefined) { + res.status(400).send( {msg: 'Invalid login combination'} ); return; + } + res.status(200).cookie('auth', key).send({msg: 'Successfully logged in'}) +}) + +router.post('/auth/self', (req, res) => { + const cookies = req.cookies; + if (cookies === undefined || cookies.auth === undefined) { + res.status(401).send({msg: 'Unauthorized'}); return; + } + const user = database.auth(req.cookies.auth) + if (user === undefined) { + res.status(401).send({msg: 'Unauthorized'}); return; + } + delete user.password + res.status(200).send(user) +}) + +router.post('/posts/create', (req, res) => { + const content = req.body.content + if (text(content, 1, 420)) { + res.status(400).send({msg: 'Invalid content'}); return; + } + const cookies = req.cookies; + if (cookies === undefined || cookies.auth === undefined) { + res.status(401).send({msg: 'Unauthorized'}); return; + } + const user = database.auth(req.cookies.auth) + if (user === undefined) { + res.status(401).send({msg: 'Unauthorized'}); return; + } + const success = database.addPost(user.id, content) + if (!success) { + res.status(500).send({msg: 'Failed to create post'}) + } + res.status(200).send({msg: 'Successfully created post'}) +}) + +router.post('/posts/load', (req, res) => { + const page = req.body.page + if (check(page, 'number') || page < 0) { + res.status(400).send({msg: 'Invalid page'}); return; + } + const cookies = req.cookies; + if (cookies === undefined || cookies.auth === undefined) { + res.status(401).send({msg: 'Unauthorized'}); return; + } + const user = database.auth(req.cookies.auth) + if (user === undefined) { + res.status(401).send({msg: 'Unauthorized'}); return; + } + const data = database.getPosts(page) + res.status(200).send(data) +}) + +router.post('/posts/user', (req, res) => { + const id = req.body.id + if (check(id, 'number')) { + res.status(400).send({msg: 'Invalid user id'}); return; + } + const cookies = req.cookies; + if (cookies === undefined || cookies.auth === undefined) { + res.status(401).send({msg: 'Unauthorized'}); return; + } + const user = database.auth(req.cookies.auth) + if (user === undefined) { + res.status(401).send({msg: 'Unauthorized'}); return; + } + const data = database.getUsersPosts(id) + res.status(200).send(data) +}) + +router.put('/posts/comment', (req, res) => { + const content = req.body.content + if (text(content, 1, 200)) { + res.status(400).send({msg: 'Invalid comment content'}); return; + } + const id = req.body.id + if (check(id, 'number')) { + res.status(400).send({msg: 'Invalid post id'}); return; + } + const cookies = req.cookies; + if (cookies === undefined || cookies.auth === undefined) { + res.status(401).send({msg: 'Unauthorized'}); return; + } + const user = database.auth(req.cookies.auth) + if (user === undefined) { + res.status(401).send({msg: 'Unauthorized'}); return; + } + const success = database.comment(id, user.id, content) + if (!success) { + res.status(500).send({msg: 'Failed to add comment to post'}); return; + } + res.status(200).send({msg: 'Successfully posted comment'}) +}) + +router.put('/posts/like', (req, res) => { + const state = req.body.state + if (check(state, 'boolean')) { + res.status(400).send({msg: 'Invalid like state'}); return; + } + const id = req.body.id + if (check(id, 'number')) { + res.status(400).send({msg: 'Invalid post id'}); return; + } + const cookies = req.cookies; + if (cookies === undefined || cookies.auth === undefined) { + res.status(401).send({msg: 'Unauthorized'}); return; + } + const user = database.auth(req.cookies.auth) + if (user === undefined) { + res.status(401).send({msg: 'Unauthorized'}); return; + } + const success = database.like(id, user.id, state) + if (!success) { + res.status(500).send({msg: 'Failed to change like state on post'}); return; + } + res.status(200).send({msg: 'Successfully changed like state on post'}) +}) + +router.post('/users/load', (req, res) => { + const ids = req.body.ids + if (!Array.isArray(ids)) { + res.status(400).send({msg: 'Invalid ids'}); return; + } + for (const id of ids) { + if (typeof id !== 'number') { + res.status(400).send({msg: 'Invalid ids'}); return; + } + } + const cookies = req.cookies; + if (cookies === undefined || cookies.auth === undefined) { + res.status(401).send({msg: 'Unauthorized'}); return; + } + const user = database.auth(req.cookies.auth) + if (user === undefined) { + res.status(401).send({msg: 'Unauthorized'}); return; + } + const data = database.getUsers(ids) + res.status(200).send(data) +}) + +router.post('/users/all', (req, res) => { + const cookies = req.cookies; + if (cookies === undefined || cookies.auth === undefined) { + res.status(401).send({msg: 'Unauthorized'}); return; + } + const user = database.auth(req.cookies.auth) + if (user === undefined) { + res.status(401).send({msg: 'Unauthorized'}); return; + } + const data = database.getAllUsers() + res.status(200).send(data) +}) module.exports = router; \ No newline at end of file diff --git a/src/database.js b/src/database.js index 6168f4c..12964b7 100644 --- a/src/database.js +++ b/src/database.js @@ -1,5 +1,7 @@ const Database = require('better-sqlite3') const db = createDatabase() +const crypto = require('crypto') +const token = () => { return crypto.randomBytes(32).toString('hex') } function createDatabase() { try { @@ -20,7 +22,7 @@ function createTables(db) { last VARCHAR(20) NOT NULL, email VARCHAR(50) NOT NULL, password VARCHAR(50) NOT NULL, - gender VARCHAR(20) NOT NULL, + gender VARCHAR(100) NOT NULL, date INTEGER NOT NULL, month VARCHAR(10) NOT NULL, day INTEGER NOT NULL, @@ -52,9 +54,73 @@ function createTables(db) { `); } +function register(first, last, email, password, gender, month, day, year) { + try { + var key = undefined; + db.transaction(() => { + if (!addUser(first, last, email, password, gender, Date.now(), month, day, year)) { + throw new Error('Failed to register user'); + } + const user = getUserByEmail(email); + if (user === undefined) { + throw new Error('Failed to register user'); + } + key = token() + if (!setSession(user.id, key)) { + throw new Error('Failed to register user'); + } + })() + return key + } catch (err) { + return undefined + } +} + +function login(email, pass) { + try { + var key = undefined + db.transaction(() => { + const user = getUserByEmail(email); + if (user === undefined) { + throw new Error('Failed to login user') + } + if (user.password !== pass) { + throw new Error("Failed to login user") + } + key = token() + if (!setSession(user.id, key)) { + throw new Error('Failed to login user'); + } + })() + return key + } catch (err) { + return undefined + } +} + +function auth(token) { + try { + var user = undefined; + db.transaction(() => { + const session = getSession(token); + if (session === undefined) { + throw new Error('Failed to auth user') + } + const u = getUserById(session.user); + if (u === undefined) { + throw new Error('Failed to auth user') + } + user = u; + })() + return user + } catch (err) { + return undefined + } +} + function addUser(first, last, email, password, gender, date, month, day, year) { try { - const stmt = db.prepare('INSERT OR REPLACE INTO users (first, last, email, password, gender, date, month, day, year) VALUES (@first, @last, @email, @password, @gender, @date, @month, @day, @year);') + const stmt = db.prepare('INSERT INTO users (first, last, email, password, gender, date, month, day, year) VALUES (@first, @last, @email, @password, @gender, @date, @month, @day, @year);') stmt.run({first, last, email, password, gender, date, month, day, year}) return true } catch (err) { @@ -63,7 +129,7 @@ function addUser(first, last, email, password, gender, date, month, day, year) { } } -function getUser(id) { +function getUserById(id) { try { const stmt = db.prepare('SELECT * FROM users WHERE id = @id;') const info = stmt.get({id}) @@ -75,6 +141,30 @@ function getUser(id) { } } +function getUserByEmail(email) { + try { + const stmt = db.prepare('SELECT * FROM users WHERE email = @email;') + const info = stmt.get({email}) + if (info === undefined) return undefined + return info + } catch (err) { + console.log(err) + return undefined + } +} + +function getUserByPassword(password) { + try { + const stmt = db.prepare('SELECT * FROM users WHERE password = @password;') + const info = stmt.get({password}) + if (info === undefined) return undefined + return info + } catch (err) { + console.log(err) + return undefined + } +} + function getUsers(ids) { try { const stmt = db.prepare('SELECT * FROM users WHERE id = @id;') @@ -90,14 +180,28 @@ function getUsers(ids) { return people } catch (err) { console.log(err) - return undefined + return [] } } -function addPost(user, content, likes, comments, date) { +function getAllUsers() { try { - const stmt = db.prepare('INSERT OR REPLACE INTO posts (user, content, likes, comments, date) VALUES (@user, @content, @likes, @comments, @date);') - const info = stmt.run({user, content, likes, comments, date}) + const stmt = db.prepare('SELECT * FROM users;') + const info = stmt.all({}) + if (info === undefined) { + return [] + } + return info + } catch (err) { + console.log(err) + return [] + } +} + +function addPost(user, content) { + try { + const stmt = db.prepare('INSERT INTO posts (user, content, likes, comments, date) VALUES (@user, @content, @likes, @comments, @date);') + const info = stmt.run({user, content, likes: "[]", comments: "[]", date: Date.now()}) return info.changes === 1 } catch (err) { console.log(err) @@ -106,10 +210,112 @@ function addPost(user, content, likes, comments, date) { } function getPosts(page) { - const stmt = db.prepare('SELECT * FROM posts ORDER BY id DESC LIMIT @limit OFFSET @offset;') - const count = 20 - const info = stmt.all({limit: count, offset: page * count}); - console.log(info) + try { + const stmt = db.prepare('SELECT * FROM posts ORDER BY id DESC LIMIT @limit OFFSET @offset;') + const count = 20 + const info = stmt.all({limit: count, offset: page * count}); + if (info === undefined || info === {}) { + return [] + } + for (const post of info) { + post.likes = JSON.parse(post.likes) + post.comments = JSON.parse(post.comments) + } + return info + } catch (err) { + console.log(err) + return [] + } +} + +function getUsersPosts(user) { + try { + const stmt = db.prepare('SELECT * FROM posts WHERE user = @user ORDER BY id DESC;') + const info = stmt.all({user}); + if (info === undefined || info === {}) { + return [] + } + for (const post of info) { + post.likes = JSON.parse(post.likes) + post.comments = JSON.parse(post.comments) + } + return info + } catch (err) { + console.log(err) + return [] + } +} + +function getPost(id) { + try { + const stmt = db.prepare('SELECT * FROM posts WHERE id = @id;') + const info = stmt.get({id}) + if (info === undefined) return undefined + info.likes = JSON.parse(info.likes) + info.comments = JSON.parse(info.comments) + return info + } catch (err) { + console.log(err) + return undefined + } +} + +function updatePost(id, likes, comments) { + try { + const stmt = db.prepare('UPDATE posts SET likes = @likes, comments = @comments WHERE id = @id') + const info = stmt.run({likes, comments, id}) + return info.changes === 1 + } catch (err) { + console.log(err) + return false + } +} + +function comment(id, user, content) { + try { + db.transaction(() => { + const post = getPost(id) + if (post === undefined) { + throw new Error('Unable to add comment') + } + post.comments.push({user, content}) + const success = updatePost(post.id, JSON.stringify(post.likes), JSON.stringify(post.comments)) + if (!success) { + throw new Error('Unable to add comment') + } + })() + return true + } catch (err) { + return false + } +} + +function like(id, user, state) { + try { + db.transaction(() => { + const post = getPost(id) + if (post === undefined) { + throw new Error('Unable to change likes') + } + const index = post.likes.indexOf(user) + if ( (index === -1 && !state) || (index !== -1 && state)) { + // Nothing to change, already set! + return + } + if (index === -1) { + post.likes.push(user) + } else { + post.likes.splice(index, 1) + } + const success = updatePost(post.id, JSON.stringify(post.likes), JSON.stringify(post.comments)) + if (!success) { + throw new Error('Unable to change likes') + } + })() + return true + } catch (err) { + return false + } } function setSession(user, token) { @@ -121,4 +327,18 @@ function setSession(user, token) { console.log(err) return false } -} \ No newline at end of file +} + +function getSession(token) { + try { + const stmt = db.prepare('SELECT * FROM sessions WHERE token = @token;') + const info = stmt.get({token}) + if (info === undefined) return undefined + return info + } catch (err) { + console.log(err) + return undefined + } +} + +module.exports = { addUser, getUser: getUserById, getUserByEmail, getUserByPassword, getUsers, addPost, getPosts, setSession, getSession, register, login, auth, comment, updatePost, getPost, like, getAllUsers, getUsersPosts } \ No newline at end of file