This commit is contained in:
Tyler Murphy 2023-02-12 14:11:50 -05:00
parent d85dd163e3
commit 3d71da4909
29 changed files with 1339 additions and 657 deletions

View file

@ -5,17 +5,16 @@
<link rel="stylesheet" href="/css/main.css"> <link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/header.css"> <link rel="stylesheet" href="/css/header.css">
<link rel="stylesheet" href="/css/admin.css"> <link rel="stylesheet" href="/css/admin.css">
<script src="/js/admin.js" type="module"></script>
<title>XSSBook - Admin Panel</title> <title>XSSBook - Admin Panel</title>
</head> </head>
<body> <body>
<script src="/js/api.js"></script>
<script src="/js/admin.js"></script>
<div id="header"> <div id="header">
<span class="logo"><a href="/">xssbook</a></span> <span class="logo"><a href="/">xssbook</a></span>
<span class="gtext desc" style="margin-left: 6em; font-size: 2em; color: #606770">Admin Panel</span> <span class="gtext desc" style="margin-left: 6em; font-size: 2em; color: #606770">Admin Panel</span>
</div> </div>
<div id="login" class="hidden"> <div id="login" class="hidden">
<form autocomplete="off" onsubmit="auth(event)"> <form autocomplete="off" onsubmit="window.auth(event)">
<input autocomplete="new-password" type="password" name="adminpassword" id="adminpassword" placeholder="Login Secret"> <input autocomplete="new-password" type="password" name="adminpassword" id="adminpassword" placeholder="Login Secret">
</form> </form>
</div> </div>
@ -23,10 +22,12 @@
<div id="queryinput"> <div id="queryinput">
<input type="text" name="query" id="query" placeholder="SQL Query"> <input type="text" name="query" id="query" placeholder="SQL Query">
<div id="buttons"> <div id="buttons">
<button class="submit" onclick="submit()">Submit</button> <button class="submit" onclick="window.submit()">Submit</button>
<button class="view" onclick="posts()">View Posts</button> <button class="view" onclick="window.posts()">View Posts</button>
<button class="view" onclick="users()">View Users</button> <button class="view" onclick="window.users()">View Users</button>
<button class="view" onclick="sessions()">View Sessions</button> <button class="view" onclick="window.sessions()">View Sessions</button>
<button class="view" onclick="window.comments()">View Comments</button>
<button class="view" onclick="window.likes()">View Likes</button>
</div> </div>
</div> </div>
<table id="table"></table> <table id="table"></table>

View file

@ -2,16 +2,12 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="stylesheet" href="/css/header.css">
<link rel="stylesheet" href="/css/main.css"> <link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/header.css">
<link rel="stylesheet" href="/css/home.css"> <link rel="stylesheet" href="/css/home.css">
<title>XSSBook - Home</title> <title>XSSBook - Home</title>
<script type="module" src="/js/home.js"></script>
</head> </head>
<body> <body>
<script src="/js/main.js"></script>
<script src="/js/header.js"></script>
<script src="/js/api.js"></script>
<script src="/js/home.js"></script>
<script>init()</script>
</body> </body>
</html> </html>

View file

@ -1,3 +1,5 @@
import { adminauth, admincheck, admincomments, adminposts, adminquery, adminsessions, adminusers, adminlikes } from './api.js'
async function auth(event) { async function auth(event) {
event.preventDefault(); event.preventDefault();
const text = event.target.elements.adminpassword.value; const text = event.target.elements.adminpassword.value;
@ -10,12 +12,14 @@ async function auth(event) {
} }
return false; return false;
} }
window.auth = auth
async function submit() { async function submit() {
let text = document.getElementById("query").value let text = document.getElementById("query").value
let response = await adminquery(text) let response = await adminquery(text)
alert(response.msg) alert(response.msg)
} }
window.submit = submit
async function posts() { async function posts() {
let response = await adminposts(); let response = await adminposts();
@ -26,6 +30,7 @@ async function posts() {
let table = document.getElementById("table") let table = document.getElementById("table")
table.innerHTML = response.msg table.innerHTML = response.msg
} }
window.posts = posts
async function users() { async function users() {
let response = await adminusers(); let response = await adminusers();
@ -36,6 +41,7 @@ async function users() {
let table = document.getElementById("table") let table = document.getElementById("table")
table.innerHTML = response.msg table.innerHTML = response.msg
} }
window.users = users
async function sessions() { async function sessions() {
let response = await adminsessions(); let response = await adminsessions();
@ -46,6 +52,29 @@ async function sessions() {
let table = document.getElementById("table") let table = document.getElementById("table")
table.innerHTML = response.msg table.innerHTML = response.msg
} }
window.sessions = sessions
async function comments() {
let response = await admincomments();
if (response.status !== 200) {
alert(response.msg)
return
}
let table = document.getElementById("table")
table.innerHTML = response.msg
}
window.comments = comments
async function likes() {
let response = await adminlikes();
if (response.status !== 200) {
alert(response.msg)
return
}
let table = document.getElementById("table")
table.innerHTML = response.msg
}
window.likes = likes
async function load() { async function load() {
let check = await admincheck(); let check = await admincheck();

View file

@ -43,78 +43,90 @@ const request = async (url, body, method) => {
} }
} }
const login = async (email, password) => { export const login = async (email, password) => {
return await request('/auth/login', {email, password}) return await request('/auth/login', {email, password})
} }
const register = async (firstname, lastname, email, password, gender, day, month, year) => { export const register = async (firstname, lastname, email, password, gender, day, month, year) => {
return await request('/auth/register', {firstname, lastname, email, password, gender, day, month, year}) return await request('/auth/register', {firstname, lastname, email, password, gender, day, month, year})
} }
const logout = async () => { export const logout = async () => {
return await request('/auth/logout', {}) return await request('/auth/logout', {})
} }
const loadpostspage = async (page) => { export const loadpostspage = async (page) => {
return await request('/posts/page', {page}) return await request('/posts/page', {page})
} }
const loadusersposts = async (user_id, page) => { export const loadcommentspage = async (page, post_id) => {
return await request('/posts/comments', {page, post_id})
}
export const loadusersposts = async (user_id, page) => {
return await request('/posts/user', {user_id, page}) return await request('/posts/user', {user_id, page})
} }
const loadusers = async (ids) => { export const loadusers = async (ids) => {
return await request('/users/load', {ids}) return await request('/users/load', {ids})
} }
const loaduserspage = async (page) => { export const loaduserspage = async (page) => {
return await request('/users/page', {page}) return await request('/users/page', {page})
} }
const loadself = async () => { export const loadself = async () => {
return await request("/users/self", {}) return await request("/users/self", {})
} }
const postcomment = async (post_id, content) => { export const postcomment = async (post_id, content) => {
return await request('/posts/comment', {post_id, content}, 'PATCH') return await request('/posts/comment', {post_id, content}, 'PATCH')
} }
const postlike = async (post_id, state) => { export const postlike = async (post_id, state) => {
return await request('/posts/like', {post_id, state}, 'PATCH') return await request('/posts/like', {post_id, state}, 'PATCH')
} }
const createpost = async (content) => { export const createpost = async (content) => {
return await request('/posts/create', {content}) return await request('/posts/create', {content})
} }
const adminauth = async (secret) => { export const adminauth = async (secret) => {
return await request('/admin/auth', {secret}) return await request('/admin/auth', {secret})
} }
const admincheck = async () => { export const admincheck = async () => {
return await request('/admin/check', {}) return await request('/admin/check', {})
} }
const adminquery = async (query) => { export const adminquery = async (query) => {
return await request('/admin/query', {query}) return await request('/admin/query', {query})
} }
const adminposts = async () => { export const adminposts = async () => {
return await request('/admin/posts', {}) return await request('/admin/posts', {})
} }
const adminusers = async () => { export const adminusers = async () => {
return await request('/admin/users', {}) return await request('/admin/users', {})
} }
const adminsessions = async () => { export const adminsessions = async () => {
return await request('/admin/sessions', {}) return await request('/admin/sessions', {})
} }
const updateavatar = async (file) => { export const admincomments = async () => {
return await request('/admin/comments', {})
}
export const adminlikes = async () => {
return await request('/admin/likes', {})
}
export const updateavatar = async (file) => {
return await fileRequest('/users/avatar', file, 'PUT') return await fileRequest('/users/avatar', file, 'PUT')
} }
const updatebanner = async (file) => { export const updatebanner = async (file) => {
return await fileRequest('/users/banner', file, 'PUT') return await fileRequest('/users/banner', file, 'PUT')
} }

249
public/js/components.js Normal file
View file

@ -0,0 +1,249 @@
import { div, a, pfp, span, i, parse, parseDate, p, form, input, svg, path, parseMonth } from './main.js'
import { postlike, postcomment, loadcommentspage } from './api.js';
window.parse = parse;
export function header(home, people, user_id) {
return [
div({id: 'header'},
span({class: 'logo'},
a({href: '/'},
parse('xssbook')
)
),
div({class: 'buttons'},
a({id: 'home', class: home ? 'selected' : '', href: 'home'},
svg({viewBox: '0 0 28 28', fill: 'currentColor', height: '28', width: '28'},
path({d: "M25.825 12.29C25.824 12.289 25.823 12.288 25.821 12.286L15.027 2.937C14.752 2.675 14.392 2.527 13.989 2.521 13.608 2.527 13.248 2.675 13.001 2.912L2.175 12.29C1.756 12.658 1.629 13.245 1.868 13.759 2.079 14.215 2.567 14.479 3.069 14.479L5 14.479 5 23.729C5 24.695 5.784 25.479 6.75 25.479L11 25.479C11.552 25.479 12 25.031 12 24.479L12 18.309C12 18.126 12.148 17.979 12.33 17.979L15.67 17.979C15.852 17.979 16 18.126 16 18.309L16 24.479C16 25.031 16.448 25.479 17 25.479L21.25 25.479C22.217 25.479 23 24.695 23 23.729L23 14.479 24.931 14.479C25.433 14.479 25.921 14.215 26.132 13.759 26.371 13.245 26.244 12.658 25.825 12.29"})
)
),
a({id: 'people', class: people ? 'selected' : '', href: 'people'},
svg({viewBox: '0 0 28 28', fill: 'currentColor', height: '28', width: '28'},
path({d: "M10.5 4.5c-2.272 0-2.75 1.768-2.75 3.25C7.75 9.542 8.983 11 10.5 11s2.75-1.458 2.75-3.25c0-1.482-.478-3.25-2.75-3.25zm0 8c-2.344 0-4.25-2.131-4.25-4.75C6.25 4.776 7.839 3 10.5 3s4.25 1.776 4.25 4.75c0 2.619-1.906 4.75-4.25 4.75zm9.5-6c-1.41 0-2.125.841-2.125 2.5 0 1.378.953 2.5 2.125 2.5 1.172 0 2.125-1.122 2.125-2.5 0-1.659-.715-2.5-2.125-2.5zm0 6.5c-1.999 0-3.625-1.794-3.625-4 0-2.467 1.389-4 3.625-4 2.236 0 3.625 1.533 3.625 4 0 2.206-1.626 4-3.625 4zm4.622 8a.887.887 0 00.878-.894c0-2.54-2.043-4.606-4.555-4.606h-1.86c-.643 0-1.265.148-1.844.413a6.226 6.226 0 011.76 4.336V21h5.621zm-7.122.562v-1.313a4.755 4.755 0 00-4.749-4.749H8.25A4.755 4.755 0 003.5 20.249v1.313c0 .518.421.938.937.938h12.125c.517 0 .938-.42.938-.938zM20.945 14C24.285 14 27 16.739 27 20.106a2.388 2.388 0 01-2.378 2.394h-5.81a2.44 2.44 0 01-2.25 1.5H4.437A2.44 2.44 0 012 21.562v-1.313A6.256 6.256 0 018.25 14h4.501a6.2 6.2 0 013.218.902A5.932 5.932 0 0119.084 14h1.861z"})
)
)
),
a({class: 'pfp', id: 'profile', href: 'profile'},
user_id === undefined ? parse('') : pfp(user_id)
)
),
div({class: 'spacer'})
]
}
export function parsePost(post, users, self) {
let content = post.content
let date = post.date
let likes = post.likes
let author = users[post.user_id]
let comments = []
for (const comment of post.comments) {
comments.push(parseComment(comment, users))
}
let liked = post.liked;
var page = 0
return (
div({class: 'post', postid: post.post_id},
div({class: 'postheader'},
a({class: 'pfp', href: `/profile?id=${author.user_id}`},
pfp(author.user_id)
),
div({class: 'postname'},
span({class: 'bold'},
parse(author.firstname + ' ' + author.lastname)
),
span({class: 'gtext mtext'},
parse(parseDate(new Date(date)))
)
)
),
p({class: 'mtext', style: "color: var(--text);"},
parse(content.replace(/\n/g,'<br>'))
),
span({class: 'gtext mtext likes'},
parse(`${likes} Likes`)
),
div({class: 'fullline nb'}),
div({class: 'postbuttons'},
span({class: 'likeclicky', onclick: async (event) => {
var post = event.target;
while(post.parentElement) {
post = post.parentElement
if (post.getAttribute('postid')) {
break;
}
}
let likes = post.getElementsByClassName('likes')[0]
let post_id = parseInt(post.getAttribute('postid'))
let like_text = likes.textContent;
let like_count = parseInt(like_text.substring(0, like_text.indexOf(" Likes")))
const response = await postlike(post_id, !liked)
if (response.status !== 200) { return }
liked = !liked
let el = post.getElementsByClassName('liketoggle')
if (liked) {
el[0].classList.add('blue')
el[1].classList.add('blue')
like_count++
} else {
el[0].classList.remove('blue')
el[1].classList.remove('blue')
like_count--
}
likes.textContent = like_count + " Likes"
}},
i({class: `liketoggle icons like ${liked ? 'blue' : ''}`}),
span({class: `liketoggle bold ${liked ? 'blue' : ''}`},
parse('Like')
)
),
span({onclick: () => this.parentElement.parentElement.getElementsByClassName('newcomment')[0].focus()},
i({class: 'icons comm'}),
span({class: 'bold'},
parse('Comment')
)
)
),
div({class: 'fullline nb', style: 'margin-top: 0'}),
div({class: 'comments'},
div({class: 'comment commentsubmit', style: 'margin-top: 0'}),
...comments,
comments.length > 0 ?
div({id: 'load', class: 'load', style: 'justify-content: inherit; margin-left: 3.5em; font-size: .9em; margin-bottom: -.5em;'},
a({class: 'blod gtext', onclick: async (event) => {
page++;
const response = await loadcommentspage(page, post.post_id)
if (response.status != 200) { return };
let comments = response.json
for (const comment of comments) {
event.target.parentElement.parentElement.insertBefore(
parseComment(comment, users),
event.target.parentElement
)
}
if (comments.length < 5) {
event.target.parentElement.remove()
}
}},
parse('Load more comments')
)
)
: parse(''),
div({class: 'comment commentsubmit'},
a({class: 'pfp', href: 'profile'},
pfp(self.user_id)
),
form({onsubmit: async (event) => {
event.preventDefault()
let text = event.target.elements.text.value.trim();
if (text.length < 1) {
return
}
let post = event.target.parentElement.parentElement.parentElement
let post_id = parseInt(post.getAttribute('postid'))
const response = await postcomment(post_id, text)
if (response.status != 200) { return };
let comment = {
user_id: self.user_id,
content: text,
date: Date.now()
}
let comments = post.getElementsByClassName('comments')[0]
let load = comments.getElementsByClassName('load')[0];
if (load == undefined) {
load = comments.lastChild
}
comments.insertBefore(
parseComment(comment, users),
load
)
event.target.elements.text.value = ''
}},
input({type: 'text', name: 'text', placeholder: 'Write a comment', id: 'newcomment', class: 'newcomment'})
)
)
)
)
)
}
export function parseComment(comment, users) {
let author = users[comment.user_id]
return (
div({class: 'comment'},
a({class: 'pfp'},
pfp(comment.user_id)
),
span({},
span({class: 'bold mtext'},
parse(author.firstname + ' ' + author.lastname),
span({class: 'gtext mtext', style: 'margin-left: 1em'},
parse(parseDate(new Date(comment.date)))
)
),
p({class: 'mtext'},
parse(comment.content)
)
)
)
)
}
export function parseUser(user) {
return (
a({class: 'person', href: `/profile?id=${user.user_id}`},
div({class: 'profile'},
pfp(user.user_id)
),
div({class: 'info'},
span({class: 'bold ltext'},
parse(user.firstname + ' ' + user.lastname)
),
span({class: 'gtext'},
parse('Joined ' + parseDate(new Date(user.date)))
),
span({class: 'gtext'},
parse('Gender :' + user.gender)
),
span({class: 'gtext'},
parse('Birthday: ' + parseMonth(user.month) + ' ' + user.day + ', ' + user.year)
),
span({class: 'gtext', style: 'margin-bottom: -100px'},
parse('User ID: ' + user.user_id)
)
)
)
)
}

View file

@ -1,24 +0,0 @@
function header(home, people, user_id) {
const html = `
<div id="header">
<span class="logo"><a href="/">xssbook</a></span>
<div class="buttons">
<a id="home" ${home ? 'class="selected"' : ''} href="home">
<svg viewBox="0 0 28 28" fill="currentColor" height="28" width="28">
<path d="M25.825 12.29C25.824 12.289 25.823 12.288 25.821 12.286L15.027 2.937C14.752 2.675 14.392 2.527 13.989 2.521 13.608 2.527 13.248 2.675 13.001 2.912L2.175 12.29C1.756 12.658 1.629 13.245 1.868 13.759 2.079 14.215 2.567 14.479 3.069 14.479L5 14.479 5 23.729C5 24.695 5.784 25.479 6.75 25.479L11 25.479C11.552 25.479 12 25.031 12 24.479L12 18.309C12 18.126 12.148 17.979 12.33 17.979L15.67 17.979C15.852 17.979 16 18.126 16 18.309L16 24.479C16 25.031 16.448 25.479 17 25.479L21.25 25.479C22.217 25.479 23 24.695 23 23.729L23 14.479 24.931 14.479C25.433 14.479 25.921 14.215 26.132 13.759 26.371 13.245 26.244 12.658 25.825 12.29"></path>
</svg>
</a>
<a id="people" ${people ? 'class="selected"' : ''} href="people">
<svg viewBox="0 0 28 28" fill="currentColor" height="28" width="28">
<path d="M10.5 4.5c-2.272 0-2.75 1.768-2.75 3.25C7.75 9.542 8.983 11 10.5 11s2.75-1.458 2.75-3.25c0-1.482-.478-3.25-2.75-3.25zm0 8c-2.344 0-4.25-2.131-4.25-4.75C6.25 4.776 7.839 3 10.5 3s4.25 1.776 4.25 4.75c0 2.619-1.906 4.75-4.25 4.75zm9.5-6c-1.41 0-2.125.841-2.125 2.5 0 1.378.953 2.5 2.125 2.5 1.172 0 2.125-1.122 2.125-2.5 0-1.659-.715-2.5-2.125-2.5zm0 6.5c-1.999 0-3.625-1.794-3.625-4 0-2.467 1.389-4 3.625-4 2.236 0 3.625 1.533 3.625 4 0 2.206-1.626 4-3.625 4zm4.622 8a.887.887 0 00.878-.894c0-2.54-2.043-4.606-4.555-4.606h-1.86c-.643 0-1.265.148-1.844.413a6.226 6.226 0 011.76 4.336V21h5.621zm-7.122.562v-1.313a4.755 4.755 0 00-4.749-4.749H8.25A4.755 4.755 0 003.5 20.249v1.313c0 .518.421.938.937.938h12.125c.517 0 .938-.42.938-.938zM20.945 14C24.285 14 27 16.739 27 20.106a2.388 2.388 0 01-2.378 2.394h-5.81a2.44 2.44 0 01-2.25 1.5H4.437A2.44 2.44 0 012 21.562v-1.313A6.256 6.256 0 018.25 14h4.501a6.2 6.2 0 013.218.902A5.932 5.932 0 0119.084 14h1.861z"></path>
</svg>
</a>
</div>
<a class="pfp" id="profile" hreF="profile">
${user_id === undefined ? '' : pfp(user_id)}
</a>
</div>
<div class="spacer"></div>
`
append(html)
}

View file

@ -1,124 +1,6 @@
function parseComment(comment) { import { div, pfp, p, parse, button, body, a, textarea, span, crawl } from './main.js'
let author = data.users[comment[0]] import { loadself, loadpostspage, createpost, loadusers } from './api.js'
if (author === undefined) { import { parsePost, header } from './components.js'
author = {}
}
const html = `
<div class="comment">
<a class="pfp">
${pfp(author.user_id)}
</a>
<span>
<span class="bold mtext">${author.firstname + ' ' + author.lastname}</span>
<p class="mtext">${comment[1]}</p>
</span>
</div>
`
return html
}
function parsePost(post) {
let author = data.users[post.user_id]
if (author === undefined) {
author = {}
}
const html = `
<div class="post" postid=${post.post_id}>
<div class="postheader">
<a class="pfp" href=/profile?id=${author.user_id}>
${pfp(author.user_id)}
</a>
<div class="postname">
<span class="bold">${author.firstname + ' ' + author.lastname}</span>
<span class="gtext mtext">${parseDate(new Date(post.date))}</span>
</div>
</div>
<p class="mtext" style="color: var(--text);">
${post.content.replace(/\n/g,'<br>')}
</p>
<span class="gtext mtext likes">
${post.likes.length} Likes
</span>
<div class="fullline nb"></div>
<div class="postbuttons">
<span class="likeclicky" onclick="like(this)">
<i class="liketoggle icons like ${post.likes.includes(data.user.user_id) ? 'blue' : ''}"></i>
<span class="liketoggle bold ${post.likes.includes(data.user.user_id) ? 'bltext' : ''}">Like</span>
</span>
<span onclick="this.parentElement.parentElement.getElementsByClassName('newcomment')[0].focus()">
<i class="icons comm"></i>
<span class="bold">Comment</span>
</span>
</div>
<div class="comments">
<div class="fullline" style="margin-top: 0"></div>
${post.comments.map(parseComment).join('')}
<div class="comment commentsubmit">
<a class="pfp" href="profile">
${pfp(data.user.user_id)}
</a>
<form onsubmit="comment(event)">
<input type="text" name="text" placeholder="Write a comment..." id="newcomment" class="newcomment">
</form>
</div>
</div>
</div>
`
return html
}
function getPost(post_id) {
for (let i = 0; i < data.posts.length; i++) {
if (data.posts[i].post_id === post_id) {
return i
}
}
return -1
}
async function like(span) {
const container = span.parentElement.parentElement;
const id = parseInt(container.getAttribute('postid'))
const post = data.posts[getPost(id)]
const index = post.likes.indexOf(data.user.user_id)
const current = index !== -1
const response = await postlike(id, !current)
if (response.status != 200) return;
if (current) {
post.likes.splice(index, 1)
} else {
post.likes.push(data.user.user_id)
}
const buttons = container
.getElementsByClassName("postbuttons")[0]
.getElementsByClassName("likeclicky")[0]
.getElementsByClassName("liketoggle")
if (current) {
buttons[0].classList.remove("blue")
buttons[1].classList.remove("bltext")
} else {
buttons[0].classList.add("blue")
buttons[1].classList.add("bltext")
}
container.getElementsByClassName("likes")[0].innerHTML = post.likes.length + " Likes"
}
async function comment(event) {
event.preventDefault();
const text = event.target.elements.text.value.trim();
if (text.length < 1) return;
const container = event.target.parentElement.parentElement.parentElement;
const post_id = parseInt(container.getAttribute('postid'))
var index = getPost(post_id);
if (index === -1) return;
const response = await postcomment(post_id, text)
if (response.status != 200) return;
event.target.elements.text.value = '';
let new_comment = [data.user.user_id, text]
data.posts[index].comments.push(new_comment)
let comments = container.getElementsByClassName("comments")[0]
prepend(parseComment(new_comment), comments, comments.getElementsByClassName("commentsubmit")[0])
}
async function post() { async function post() {
const text = document.getElementById("text").value.trim() const text = document.getElementById("text").value.trim()
@ -133,7 +15,7 @@ async function post() {
error.innerHTML = ''; error.innerHTML = '';
let post = { let post = {
post_id: response.json.post_id, post_id: response.json.post_id,
user_id: data.user.user_id, user_id: data.self.user_id,
date: Date.now(), date: Date.now(),
content: text, content: text,
likes: [], likes: [],
@ -145,113 +27,127 @@ async function post() {
document.getElementById('popup').classList.add('hidden') document.getElementById('popup').classList.add('hidden')
} }
async function loadMore() {
const posts = await load()
data.posts.push(... posts)
const posts_block = document.getElementById("posts")
for (p of posts) {
append(parsePost(p), posts_block)
}
}
function render() { function render() {
const html = `
<div id="create">
<div class="create">
<a class="pfp" href="profile">
${pfp(data.user.user_id)}
</a>
<button class="pfp">
<p class="gtext" onclick="document.getElementById('popup').classList.remove('hidden')">
What's on your mind, ${data.user.firstname}?
</p>
</button>
</div>
</div>
<div id="posts">
${data.posts.map(p => parsePost(p)).join('')}
</div>
`
append(html) let new_body =
body({},
...header(true, false, data.self.user_id),
div({id: 'create'},
div({class: 'create'},
a({class: 'pfp', href: 'profile'},
pfp(data.self.user_id)
),
button({class: 'pfp'},
p({class: 'gtext', onclick: () => document.getElementById('popup').classList.remove('hidden')},
parse(`What' on your mind, ${data.self.firstname}`)
)
)
)
),
div({id: 'posts'},
...data.posts.map(p => parsePost(p, data.users, data.self))
),
div({id: 'popup', class: 'hidden'},
div({class: 'createpost'},
div({class: 'close', onclick: () => document.getElementById('popup').classList.add('hidden')}),
span({class: 'ltext ctext bold'},
parse('Create post')
),
div({class: 'fullline'}),
div({class: 'postheader'},
a({class: 'pfp', style: 'cursor: auto'},
pfp(data.self.user_id)
),
div({class: 'postname'},
span({class: 'bold'},
parse(data.self.firstname + ' ' + data.self.lastname)
),
span({class: 'gtext mtext'},
parse('Now')
)
)
),
textarea({type: 'text', name: 'text', id: 'text', placeholder: `What's on your mind, ${data.self.firstname}?`}),
span({class: 'error ctext', style: 'padding-bottom: 15px; margin-top: -30px'}),
button({class: 'primary', onclick: post},
parse('Post')
)
)
),
div({id: 'load'},
a({class: 'blod gtext', onclick: async () => {
const popup = ` const posts = await load()
<div id="popup" class="hidden"> data.posts.push(... posts)
<div class="createpost">
<div class="close" onclick="document.getElementById('popup').classList.add('hidden')"></div>
<span class="ltext ctext bold">Create post</span>
<div class="fullline"></div>
<div class="postheader">
<a class="pfp" style="cursor: auto">
${pfp(data.user.user_id)}
</a>
<div class="postname">
<span class="bold">${data.user.firstname + ' ' + data.user.lastname}</span>
<span class="gtext mtext">Now</span>
</div>
</div>
<textarea type="text" name="text" id="text" placeholder="What's on your mind, ${data.user.firstname}?"></textarea>
<span class="error ctext" style="padding-bottom: 15px; margin-top: -30px;"></span>
<button class="primary" onclick="post(this)">Post</button>
</div>
</div>
`
append(popup) const el = document.getElementById("posts")
const load = ` for (const post of posts) {
<div id="load"> el.appendChild(
<a class="bold gtext" onclick="loadMore()">Load more posts</a> parsePost(post, data.users, data.self)
</div> )
` }
}},
parse('Load more posts')
)
)
)
document.body.replaceWith(new_body)
append(load)
} }
var page = 0 var page = 0
const data = { const data = {
user: {}, self: {},
users: {}, users: {},
posts: [] posts: []
} }
async function load() { async function load() {
const posts = (await loadpostspage(page)).json const posts = (await loadpostspage(page)).json
if (posts.length === 0) { if (posts.length === 0) {
page = -1 document.getElementById('load').remove()
remove('load')
return [] return []
} else { } else {
page++ page++
} }
const batch = []
for (const post of posts) { const batch = Array.from(new Set(crawl('user_id', posts))).filter(id => data.users[id] == undefined)
for(const comment of post.comments) {
if (data.users[comment[0]] !== undefined) continue if (batch.length != 0) {
if (batch.includes(comment[0])) continue const users = (await loadusers(batch)).json
batch.push(comment[0]) for (const user of users) {
data.users[user.user_id] = user
} }
if (data.users[post.user_id] !== undefined) continue
if (batch.includes(post.user_id)) continue
batch.push(post.user_id)
}
const users = batch.length == 0 ? [] : (await loadusers(batch)).json
for (const user of users) {
data.users[user.user_id] = user
} }
return posts return posts
} }
async function init() { async function init() {
let request = (await loadself()); let request = (await loadself());
if (request.status === 429) { if (request.status === 429) {
header(true, false) let new_body =
body({},
...header(true, false)
)
document.body.replaceWith(new_body)
throw new Error("Rate limited"); throw new Error("Rate limited");
} }
data.user = request.json
header(true, false, data.user.user_id) data.self = request.json
data.users[data.user.user_id] = data.user data.users[data.self.user_id] = data.self
const posts = await load() const posts = await load()
data.posts.push(... posts) data.posts.push(... posts)
render() render()
} }
init()

View file

@ -1,3 +1,5 @@
import { login, register } from './api.js'
async function onlogin() { async function onlogin() {
const email = document.getElementById('email').value const email = document.getElementById('email').value
const password = document.getElementById('pass').value const password = document.getElementById('pass').value
@ -10,6 +12,8 @@ async function onlogin() {
} }
} }
window.onlogin = onlogin
async function onregister() { async function onregister() {
const first = document.getElementById('firstname').value const first = document.getElementById('firstname').value
const last = document.getElementById('lastname').value const last = document.getElementById('lastname').value
@ -27,3 +31,5 @@ async function onregister() {
location.href = '/home' location.href = '/home'
} }
} }
window.onregister = onregister

View file

@ -1,43 +1,108 @@
function prepend(html, container, before) { function createElement(name, attrs, ...children) {
if (container === undefined) { const el = document.createElement(name);
container = document.body
for (const attr in attrs) {
if(attr.startsWith("on")) {
el[attr] = attrs[attr];
} else {
el.setAttribute(attr, attrs[attr])
}
} }
if (before === undefined) {
before = container.firstChild for (const child of children) {
if (child == null) {
continue
}
el.appendChild(child)
} }
console.log(html, container, before)
var range = document.createRange() return el
range.setStart(container, 0);
container.insertBefore(
range.createContextualFragment(html),
before
)
} }
function append(html, container) { export function createElementNS(name, attrs, ...children) {
if (container === undefined) { var svgns = "http://www.w3.org/2000/svg";
container = document.body var el = document.createElementNS(svgns, name);
for (const attr in attrs) {
if(attr.startsWith("on")) {
el[attr] = attrs[attr];
} else {
el.setAttribute(attr, attrs[attr])
}
} }
var range = document.createRange()
range.setStart(container, 0);
container.appendChild(
range.createContextualFragment(html)
)
}
function remove(id) { for (const child of children) {
const old = document.getElementById(id) if (child == null) {
if (old !== null) { continue
old.remove() }
el.appendChild(child)
} }
return el
} }
function pfp(id) { export function p(attrs, ...children) {
return `<img src="/image/avatar?user_id=${id}">` return createElement("p", attrs, ...children)
} }
function banner(id) { export function span(attrs, ...children) {
return `<img src="/image/banner?user_id=${id}" onerror="this.remove()" >` return createElement("span", attrs, ...children)
}
export function div(attrs, ...children) {
return createElement("div", attrs, ...children)
}
export function a(attrs, ...children) {
return createElement("a", attrs, ...children)
}
export function i(attrs, ...children) {
return createElement("i", attrs, ...children)
}
export function form(attrs, ...children) {
return createElement("form", attrs, ...children)
}
export function img(attrs, ...children) {
return createElement("img", attrs, ...children)
}
export function input(attrs, ...children) {
return createElement("input", attrs, ...children)
}
export function button(attrs, ...children) {
return createElement("button", attrs, ...children)
}
export function path(attrs, ...children) {
return createElementNS("path", attrs, ...children)
}
export function svg(attrs, ...children) {
return createElementNS("svg", attrs, ...children)
}
export function body(attrs, ...children) {
return createElement("body", attrs, ...children)
}
export function textarea(attrs, ...children) {
return createElement("textarea", attrs, ...children)
}
export function parse(html) {
return document.createRange().createContextualFragment(html);
}
export function pfp(id) {
return img({src: `/image/avatar?user_id=${id}`})
}
export function banner(id) {
return img({src: `/image/banner?user_id=${id}`, onerror: () => {this.remove()}})
} }
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
@ -46,7 +111,7 @@ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
const letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', const letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']; 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
function parseMonth(month) { export function parseMonth(month) {
if (month > -1 && month < 26) { if (month > -1 && month < 26) {
return months[month] return months[month]
} else { } else {
@ -57,6 +122,18 @@ function parseMonth(month) {
} }
} }
function parseDate(date) { export function parseDate(date) {
return parseMonth(date.getUTCMonth()) + ' ' + date.getUTCDate() + ', ' + date.getUTCFullYear() + ' ' + date.toLocaleTimeString(); return parseMonth(date.getUTCMonth()) + ' ' + date.getUTCDate() + ', ' + date.getUTCFullYear() + ' ' + date.toLocaleTimeString();
} }
export function crawl(key, object) {
let data = []
for (const k in object) {
if (typeof object[k] === 'object') {
data.push(...crawl(key, object[k]))
} else if (k == key) {
data.push(object[k])
}
}
return data
}

View file

@ -1,64 +1,60 @@
function parseUser(user) { import { div, body, a, parse } from './main.js'
const html = ` import { loadself, loaduserspage } from './api.js'
<a class="person" href="/profile?id=${user.user_id}"> import { header, parseUser } from './components.js'
<div class="profile">
${pfp(user.user_id)}
</div>
<div class="info">
<span class="bold ltext">${user.firstname + ' ' + user.lastname}</span>
<span class="gtext">Joined ${parseDate(new Date(user.date))}</span>
<span class="gtext">Gender: ${user.gender}</span>
<span class="gtext">Birthday: ${parseMonth(user.month) + ' ' + user.day + ', ' + user.year}</span>
<span class="gtext" style="margin-bottom: -100px;">User ID: ${user.user_id}</span>
</div>
</a>
`
return html
}
function render() { function render() {
const html = `
<div id="users">
${data.users.map(u => parseUser(u)).join('')}
</div>
`
append(html) let new_body =
body({},
...header(false, true, data.self.user_id),
div({id: 'users'},
...data.users.map(u => parseUser(u))
),
div({id: 'load'},
a({class: 'bold gtext', onclick: async () => {
let users = await load()
const load = ` let el = document.getElementById("users")
<div id="load"> for (const user of users) {
<a class="bold gtext" onclick="loadMore()">Load more users</a> el.appendChild(parseUser(user))
</div> }
` }},
parse("Load more users")
)
)
)
append(load) document.body.replaceWith(new_body)
} }
var page = 0 var page = 0
var data = { const data = {
users: [] users: [],
} self: {}
async function loadMore() {
let users = await load()
const users_block = document.getElementById("users")
for (user of users) {
append(parseUser(user), users_block)
}
} }
async function load() { async function load() {
let request = await loadself() let request = await loadself()
if (request.status === 429) { if (request.status === 429) {
header(false, true) let new_body =
body({},
...header(false, true)
)
document.body.replaceWith(new_body)
throw new Error("Rate limited"); throw new Error("Rate limited");
} }
const self = request.json const self = request.json
header(false, true, self.user_id) header(false, true, self.user_id)
const users = (await loaduserspage(page)).json const users = (await loaduserspage(page)).json
if (users.length === 0) { if (users.length === 0) {
page = -1 document.getElementById('load').remove()
remove('load') return []
} else { } else {
page++ page++
} }
@ -66,8 +62,24 @@ async function load() {
} }
async function init() { async function init() {
let users = await load()
let request = (await loadself());
if (request.status === 429) {
let new_body =
body({},
...header(true, false, data.self.user_id)
)
document.body.replaceWith(new_body)
throw new Error("Rate limited");
}
data.self = request.json
const users = await load()
data.users.push(... users) data.users.push(... users)
render() render()
} }

View file

@ -1,18 +1,36 @@
import { div, pfp, banner, parse, button, body, a, span, crawl, parseDate, parseMonth } from './main.js'
import { loadself, loadusers, loadusersposts, updateavatar, updatebanner, logout } from './api.js'
import { parsePost, header } from './components.js'
function swap(value) { function swap(value) {
let postsb = document.getElementById("profilepostbutton"); let postsb = document.getElementById("profilepostbutton");
let aboutb = document.getElementById("profileaboutbutton"); let aboutb = document.getElementById("profileaboutbutton");
let posts = document.getElementById("posts"); let posts = document.getElementById("posts");
let about = document.getElementById("about"); let about = document.getElementById("about");
let load = document.getElementsByClassName("loadp")[0];
if (value) { if (value) {
postsb.classList.add("selected") postsb.classList.add("selected")
aboutb.classList.remove("selected") aboutb.classList.remove("selected")
about.classList.add("hidden") about.classList.add("hidden")
posts.classList.remove("hidden") posts.classList.remove("hidden")
if (load) {
load.classList.remove("hidden")
}
} else { } else {
postsb.classList.remove("selected") postsb.classList.remove("selected")
aboutb.classList.add("selected") aboutb.classList.add("selected")
about.classList.remove("hidden") about.classList.remove("hidden")
posts.classList.add("hidden") posts.classList.add("hidden")
if (load) {
load.classList.add("hidden")
}
} }
} }
@ -50,129 +68,155 @@ function changeimage(fn) {
} }
function render() { function render() {
const html = `
<div id="top">
<div id="banner">
<div class="bg">
${banner(data.user.user_id)}
</div>
${ isself ? `<div class="changebanner" onclick="changeimage(updatebanner)"></div>` : '' }
</div>
<div id="info">
<div class="face">
${pfp(data.user.user_id)}
${ isself ? `<div class="changeavatar" onclick="changeimage(updateavatar)"></div>` : '' }
</div>
<div class="infodata">
<span class="bold ltext">${data.user.firstname + ' ' + data.user.lastname}</span>
<span class="gtext">Joined ${parseDate(new Date(data.user.date))}</span>
</div>
</div>
<div class="fullline" style="width: 80em; margin-bottom: 0; z-index: 0;"></div>
<div class="profilebuttons">
<button id="profilepostbutton" class="${posts ? 'selected' : ''}" onclick="swap(true)">
Posts
</button>
<button id="profileaboutbutton" class="${posts ? '' : 'selected'}" onclick="swap(false)">
About
</button>
<div style="flex: 20"></div>
${ isself ? `<button class="logout" onclick="logout_button()">Logout</button>` : ''}
</div>
</div>
`
append(html) let new_body =
body({},
...header(false, false, data.self.user_id),
div({id: 'top'},
div({id: 'banner'},
div({class: 'bg'},
banner(data.user.user_id)
),
isself ? div({class: 'changebanner', onclick: () => changeimage(updatebanner)}) : parse(''),
),
div({id: 'info'},
div({class: 'face'},
pfp(data.user.user_id),
isself ? div({class: 'changeavatar', onclick: () => changeimage(updateavatar)}) : parse(''),
),
div({class: 'infodata'},
span({class: 'bold ltext'},
parse(data.user.firstname + ' ' + data.user.lastname)
),
span({class: 'gtext'},
parse('Joined ' + parseDate(new Date(data.user.date)))
)
)
),
div({class: 'fullline', style: 'width: 80em; margin-bottom: 0; z-index: 0;'}),
div({class: 'profilebuttons'},
button({id: 'profilepostbutton', class: 'selected', onclick: () => swap(true)},
parse('Posts')
),
button({id: 'profileaboutbutton', onclick: () => swap(false)},
parse('About')
),
div({style: 'flex: 20'}),
isself ? button({class: 'logout', onclick: async () => {
const response = await logout()
if (response.status != 200) return;
location.href = '/login'
}},
parse('Logout')
)
: parse('')
)
),
div({id: 'posts'},
...data.posts.map(p => parsePost(p, data.users, data.self))
),
div({id: 'load'},
a({class: 'loadp bold gtext', onclick: async () => {
const postsh = ` const posts = await load()
<div id="posts" class="${posts ? '' : 'hidden'}"> data.posts.push(... posts)
${data.posts.map(p => parsePost(p)).join('')}
</div>
<div id="load">
<a class="bold gtext" onclick="loadMore()">Load more posts</a>
</div>
`
if (data.posts.length > 0) { const el = document.getElementById("posts")
append(postsh)
for (const post of posts) {
el.appendChild(
parsePost(post, data.users, data.self)
)
}
}},
parse('Load more posts')
)
),
div({id: 'about', class: 'post hidden'},
span({class: 'bold ltext'},
parse('About')
),
div({class: 'data'},
span({class: 'gtext bold'},
parse('Name: ' + data.user.firstname + ' ' + data.user.lastname)
),
span({class: 'gtext bold'},
parse('Email: ' + data.user.email)
),
span({class: 'gtext bold'},
parse('Gender ' + data.user.gender)
),
span({class: 'gtext bold'},
parse('Birthday: ' + parseMonth(data.user.month) + ' ' + data.user.day + ', ' + data.user.year)
),
span({class: 'gtext bold'},
parse('User ID: ' + data.user_id)
)
)
),
div({id: 'popup', class: 'hidden'},
div({class: 'createpost'},
div({class: 'close', onclick: () => document.getElementById('popup').classList.add('hidden')}),
span({class: 'ltext ctext bold'},
parse('Uploading')
),
div({class: 'fullline'}),
div({class: 'fullwidth'},
div({class: 'loading'},
div({}),
div({}),
div({}),
div({})
)
),
span({class: 'message ctext', style: 'padding-top: 10px'})
)
)
)
document.body.replaceWith(new_body)
if (data.posts.length < 10) {
document.getElementById('load').remove()
} }
const about = `
<div id="about" class="post ${posts ? 'hidden' : ''}">
<span class="bold ltext">About</span>
<div class="data">
<span class="gtext bold">Name: ${data.user.firstname + ' ' + data.user.lastname}</span>
<span class="gtext bold">Email: ${data.user.email}</span>
<span class="gtext bold">Gender: ${data.user.gender}</span>
<span class="gtext bold">Birthday: ${parseMonth(data.user.month) + ' ' + data.user.day + ', ' + data.user.year}</span>
<span class="gtext bold">User ID: ${data.user.user_id}</span>
</div>
</div>
`
append(about)
const popup = `
<div id="popup" class="hidden">
<div class="createpost">
<div class="close" onclick="document.getElementById('popup').classList.add('hidden')"></div>
<span class="ltext ctext bold">Uploading</span>
<div class="fullline"></div>
<div class="fullwidth"><div class="loading"><div></div><div></div><div></div><div></div></div></div>
<span class="message ctext" style="padding-top: 10px"></span>
</div>
</div>
`
append(popup)
} }
async function logout_button() {
const response = await logout()
if (response.status != 200) return;
location.href = '/login'
}
async function loadMore() {
const posts = await load()
data.posts.push(... posts)
const posts_block = document.getElementById("posts")
for (p of posts) {
append(parsePost(p), posts_block)
}
}
var posts = true
var isself = false var isself = false
var page = 0 var page = 0
var id const data = {
self: {},
user: {},
users: {},
posts: []
}
async function load(id) {
if (id === undefined) {
id = data.user.user_id
}
async function load() {
const posts = (await loadusersposts(id, page)).json const posts = (await loadusersposts(id, page)).json
if (posts.length === 0) {
page = -1 if (posts.length < 1) {
remove('load') document.getElementsByClassName('loadp')[0].remove()
return []
} else { } else {
page++ page++
} }
let batch = []
const batch = Array.from(new Set(crawl('user_id', posts))).filter(id => data.users[id] == undefined)
if (!isself) { if (!isself) {
batch.push(id) batch.push(id)
} }
for (const post of posts) {
for(const comment of post.comments) { if (batch.length != 0) {
if (data.users[comment[0]] !== undefined) continue const users = await loadusers(batch).json
if (batch.includes(comment[0])) continue for (const user of users) {
batch.push(comment[0]) data.users[user.user_id] = user
} }
if (data.users[post.user_id] !== undefined) continue
if (batch.includes(post.user_id)) continue
batch.push(post.user_id)
}
const users = batch.length == 0 ? [] : (await loadusers(batch)).json
for (const user of users) {
data.users[user.user_id] = user
} }
return posts return posts
} }
@ -180,21 +224,26 @@ async function load() {
async function init() { async function init() {
let request = await loadself() let request = await loadself()
if (request.status === 429) { if (request.status === 429) {
header(false, false) let new_body =
body({},
...header(false, false)
)
document.body.replaceWith(new_body)
throw new Error("Rate limited"); throw new Error("Rate limited");
} }
data.self = request.json; data.self = request.json;
data.users[data.self.user_id] = data.self data.users[data.self.user_id] = data.self
header(false, false, data.self.user_id)
var params = {}; var params = {};
for (const [key, value] of new URLSearchParams(location.search)) { for (const [key, value] of new URLSearchParams(location.search)) {
params[key] = value params[key] = value
} }
let id;
if (params.id !== undefined && !isNaN(params.id)) { if (params.id !== undefined && !isNaN(params.id)) {
id = parseInt(params.id); id = parseInt(params.id);
} else { } else {
@ -203,9 +252,11 @@ async function init() {
isself = id === data.self.user_id isself = id === data.self.user_id
const posts = await load() const posts = await load(id)
data.posts.push(... posts) data.posts.push(... posts)
data.user = data.users[id] data.user = data.users[id]
render() render()
} }

View file

@ -4,8 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="stylesheet" href="/css/main.css"> <link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/login.css"> <link rel="stylesheet" href="/css/login.css">
<script src="/js/api.js"></script> <script src="/js/login.js" type="module"></script>
<script src="/js/login.js"></script>
<title>XSSBook - Login</title> <title>XSSBook - Login</title>
</head> </head>
<body> <body>
@ -18,7 +17,7 @@
<input type="text" name="email" id="email" placeholder="Email" autofocus="1"> <input type="text" name="email" id="email" placeholder="Email" autofocus="1">
<input type="password" name="pass" id="pass" placeholder="Password"> <input type="password" name="pass" id="pass" placeholder="Password">
<span class="error ctext"></span> <span class="error ctext"></span>
<button class="primary login-button bold" value="1" name="login" type="submit" id="login" onclick="onlogin()">Log In</button> <button class="primary login-button bold" value="1" name="login" type="submit" id="login" onclick="window.onlogin()">Log In</button>
<a class="btext ctext">Forgot Password?</a> <a class="btext ctext">Forgot Password?</a>
<div class="line"></div> <div class="line"></div>
<button class="success newacc" onclick="document.getElementById('popup').classList.remove('hidden')">Create new account</button> <button class="success newacc" onclick="document.getElementById('popup').classList.remove('hidden')">Create new account</button>
@ -159,7 +158,7 @@
<span class="error ctext" style="padding-bottom: 10px;"></span> <span class="error ctext" style="padding-bottom: 10px;"></span>
<span class="label stext">By clicking Sign Up, you agree to have your password stored in plain text and have any javascript run on your pc at any time.</span> <span class="label stext">By clicking Sign Up, you agree to have your password stored in plain text and have any javascript run on your pc at any time.</span>
<span class="label stext">XSSBook is not responsible for any ones loss of finances, mental state, relationships, or life when using this site.</span> <span class="label stext">XSSBook is not responsible for any ones loss of finances, mental state, relationships, or life when using this site.</span>
<button class="success signacc" onclick="onregister()">Sign Up</button> <button class="success signacc" onclick="window.onregister()">Sign Up</button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -3,13 +3,11 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="stylesheet" href="/css/main.css"> <link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/people.css">
<link rel="stylesheet" href="/css/header.css"> <link rel="stylesheet" href="/css/header.css">
<link rel="stylesheet" href="/css/people.css">
<script src="/js/people.js" type="module"></script>
<title>XSSBook - People</title> <title>XSSBook - People</title>
</head> </head>
<body> <body>
<script src="/js/main.js"></script>
<script src="/js/header.js"></script>
<script src="/js/api.js"></script>
<script src="/js/people.js"></script>
</body> </body>
</html>

View file

@ -6,12 +6,9 @@
<link rel="stylesheet" href="/css/header.css"> <link rel="stylesheet" href="/css/header.css">
<link rel="stylesheet" href="/css/profile.css"> <link rel="stylesheet" href="/css/profile.css">
<link rel="stylesheet" href="/css/home.css"> <link rel="stylesheet" href="/css/home.css">
<script src="/js/profile.js" type="module"></script>
<title>XSSBook - Profile</title> <title>XSSBook - Profile</title>
</head> </head>
<body> <body>
<script src="/js/main.js"></script>
<script src="/js/header.js"></script>
<script src="/js/api.js"></script>
<script src="/js/home.js"></script>
<script src="/js/profile.js"></script>
</body> </body>
</html>

View file

@ -6,7 +6,10 @@ use tower_cookies::{Cookie, Cookies};
use crate::{ use crate::{
database, database,
public::{admin, docs::{EndpointDocumentation, EndpointMethod}}, public::{
admin,
docs::{EndpointDocumentation, EndpointMethod},
},
types::{ types::{
extract::{AdminUser, Check, CheckResult, Json}, extract::{AdminUser, Check, CheckResult, Json},
http::ResponseCode, http::ResponseCode,
@ -17,14 +20,16 @@ pub const ADMIN_AUTH: EndpointDocumentation = EndpointDocumentation {
uri: "/api/admin/auth", uri: "/api/admin/auth",
method: EndpointMethod::Post, method: EndpointMethod::Post,
description: "Authenticates on the admin panel", description: "Authenticates on the admin panel",
body: Some(r#" body: Some(
r#"
{ {
"secret" : "admin" "secret" : "admin"
} }
"#), "#,
),
responses: &[ responses: &[
(200, "Successfully executed SQL query"), (200, "Successfully executed SQL query"),
(400, " Successfully authed, admin cookie returned") (400, " Successfully authed, admin cookie returned"),
], ],
cookie: None, cookie: None,
}; };
@ -60,16 +65,18 @@ pub const ADMIN_QUERY: EndpointDocumentation = EndpointDocumentation {
uri: "/api/admin/query", uri: "/api/admin/query",
method: EndpointMethod::Post, method: EndpointMethod::Post,
description: "Run a SQL query on the database", description: "Run a SQL query on the database",
body: Some(r#" body: Some(
r#"
{ {
"query" : "DROP TABLE users;" "query" : "DROP TABLE users;"
} }
"#), "#,
),
responses: &[ responses: &[
(200, "Successfully executed SQL query"), (200, "Successfully executed SQL query"),
(400, "Body does not match parameters"), (400, "Body does not match parameters"),
(401, "Unauthorized"), (401, "Unauthorized"),
(500, "SQL query ran into an error") (500, "SQL query ran into an error"),
], ],
cookie: Some("admin"), cookie: Some("admin"),
}; };
@ -102,7 +109,7 @@ pub const ADMIN_POSTS: EndpointDocumentation = EndpointDocumentation {
responses: &[ responses: &[
(200, "Returns sql table in <span>text/html</span>"), (200, "Returns sql table in <span>text/html</span>"),
(401, "Unauthorized"), (401, "Unauthorized"),
(500, "Failed to fetch data") (500, "Failed to fetch data"),
], ],
cookie: Some("admin"), cookie: Some("admin"),
}; };
@ -119,7 +126,7 @@ pub const ADMIN_USERS: EndpointDocumentation = EndpointDocumentation {
responses: &[ responses: &[
(200, "Returns sql table in <span>text/html</span>"), (200, "Returns sql table in <span>text/html</span>"),
(401, "Unauthorized"), (401, "Unauthorized"),
(500, "Failed to fetch data") (500, "Failed to fetch data"),
], ],
cookie: Some("admin"), cookie: Some("admin"),
}; };
@ -136,7 +143,7 @@ pub const ADMIN_SESSIONS: EndpointDocumentation = EndpointDocumentation {
responses: &[ responses: &[
(200, "Returns sql table in <span>text/html</span>"), (200, "Returns sql table in <span>text/html</span>"),
(401, "Unauthorized"), (401, "Unauthorized"),
(500, "Failed to fetch data") (500, "Failed to fetch data"),
], ],
cookie: Some("admin"), cookie: Some("admin"),
}; };
@ -145,6 +152,40 @@ async fn sessions(_: AdminUser) -> Response {
admin::generate_sessions() admin::generate_sessions()
} }
pub const ADMIN_COMMENTS: EndpointDocumentation = EndpointDocumentation {
uri: "/api/admin/comments",
method: EndpointMethod::Post,
description: "Returns the entire comments table",
body: None,
responses: &[
(200, "Returns sql table in <span>text/html</span>"),
(401, "Unauthorized"),
(500, "Failed to fetch data"),
],
cookie: Some("admin"),
};
async fn comments(_: AdminUser) -> Response {
admin::generate_comments()
}
pub const ADMIN_LIKES: EndpointDocumentation = EndpointDocumentation {
uri: "/api/admin/likes",
method: EndpointMethod::Post,
description: "Returns the entire likes table",
body: None,
responses: &[
(200, "Returns sql table in <span>text/html</span>"),
(401, "Unauthorized"),
(500, "Failed to fetch data"),
],
cookie: Some("admin"),
};
async fn likes(_: AdminUser) -> Response {
admin::generate_likes()
}
async fn check(check: Option<AdminUser>) -> Response { async fn check(check: Option<AdminUser>) -> Response {
if check.is_none() { if check.is_none() {
ResponseCode::Success.text("false") ResponseCode::Success.text("false")
@ -160,5 +201,7 @@ pub fn router() -> Router {
.route("/posts", post(posts)) .route("/posts", post(posts))
.route("/users", post(users)) .route("/users", post(users))
.route("/sessions", post(sessions)) .route("/sessions", post(sessions))
.route("/comments", post(comments))
.route("/likes", post(likes))
.route("/check", post(check)) .route("/check", post(check))
} }

View file

@ -3,18 +3,22 @@ use serde::Deserialize;
use time::{Duration, OffsetDateTime}; use time::{Duration, OffsetDateTime};
use tower_cookies::{Cookie, Cookies}; use tower_cookies::{Cookie, Cookies};
use crate::{types::{ use crate::{
extract::{AuthorizedUser, Check, CheckResult, Json, Log}, public::docs::{EndpointDocumentation, EndpointMethod},
http::ResponseCode, types::{
session::Session, extract::{AuthorizedUser, Check, CheckResult, Json, Log},
user::User, http::ResponseCode,
}, public::docs::{EndpointDocumentation, EndpointMethod}}; session::Session,
user::User,
},
};
pub const AUTH_REGISTER: EndpointDocumentation = EndpointDocumentation { pub const AUTH_REGISTER: EndpointDocumentation = EndpointDocumentation {
uri: "/api/auth/register", uri: "/api/auth/register",
method: EndpointMethod::Post, method: EndpointMethod::Post,
description: "Registeres a new account", description: "Registeres a new account",
body: Some(r#" body: Some(
r#"
{ {
"firstname": "[Object]", "firstname": "[Object]",
"lastname": "object]", "lastname": "object]",
@ -25,7 +29,8 @@ pub const AUTH_REGISTER: EndpointDocumentation = EndpointDocumentation {
"month": 1, "month": 1,
"year": 1970 "year": 1970
} }
"#), "#,
),
responses: &[ responses: &[
(201, "Successfully registered new user"), (201, "Successfully registered new user"),
(400, "Body does not match parameters"), (400, "Body does not match parameters"),
@ -123,15 +128,20 @@ pub const AUTH_LOGIN: EndpointDocumentation = EndpointDocumentation {
uri: "/api/auth/login", uri: "/api/auth/login",
method: EndpointMethod::Post, method: EndpointMethod::Post,
description: "Logs into an existing account", description: "Logs into an existing account",
body: Some(r#" body: Some(
r#"
{ {
"email": "object@object.object", "email": "object@object.object",
"password": "i love js" "password": "i love js"
} }
"#), "#,
),
responses: &[ responses: &[
(200, "Successfully logged in, auth cookie is returned"), (200, "Successfully logged in, auth cookie is returned"),
(400, "Body does not match parameters, or invalid email password combination"), (
400,
"Body does not match parameters, or invalid email password combination",
),
], ],
cookie: None, cookie: None,
}; };
@ -184,7 +194,7 @@ pub const AUTH_LOGOUT: EndpointDocumentation = EndpointDocumentation {
responses: &[ responses: &[
(200, "Successfully logged out"), (200, "Successfully logged out"),
(401, "Unauthorized"), (401, "Unauthorized"),
(500, "Failed to log out user") (500, "Failed to log out user"),
], ],
cookie: None, cookie: None,
}; };

View file

@ -5,26 +5,33 @@ use axum::{
}; };
use serde::Deserialize; use serde::Deserialize;
use crate::{types::{ use crate::{
extract::{AuthorizedUser, Check, CheckResult, Json}, public::docs::{EndpointDocumentation, EndpointMethod},
http::ResponseCode, types::{
post::Post, comment::Comment,
}, public::docs::{EndpointDocumentation, EndpointMethod}}; extract::{AuthorizedUser, Check, CheckResult, Json},
http::ResponseCode,
like::Like,
post::Post,
},
};
pub const POSTS_CREATE: EndpointDocumentation = EndpointDocumentation { pub const POSTS_CREATE: EndpointDocumentation = EndpointDocumentation {
uri: "/api/posts/create", uri: "/api/posts/create",
method: EndpointMethod::Post, method: EndpointMethod::Post,
description: "Creates a new post", description: "Creates a new post",
body: Some(r#" body: Some(
r#"
{ {
"content" : "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." "content" : "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
} }
"#), "#,
),
responses: &[ responses: &[
(201, "Successfully created post"), (201, "Successfully created post"),
(400, "Body does not match parameters"), (400, "Body does not match parameters"),
(401, "Unauthorized"), (401, "Unauthorized"),
(500, "Failed to create post") (500, "Failed to create post"),
], ],
cookie: Some("auth"), cookie: Some("auth"),
}; };
@ -65,16 +72,18 @@ pub const POSTS_PAGE: EndpointDocumentation = EndpointDocumentation {
uri: "/api/posts/page", uri: "/api/posts/page",
method: EndpointMethod::Post, method: EndpointMethod::Post,
description: "Load a section of posts from newest to oldest", description: "Load a section of posts from newest to oldest",
body: Some(r#" body: Some(
r#"
{ {
"page": 0 "page": 0
} }
"#), "#,
),
responses: &[ responses: &[
(200, "Returns posts in <span>application/json<span>"), (200, "Returns posts in <span>application/json<span>"),
(400, "Body does not match parameters"), (400, "Body does not match parameters"),
(401, "Unauthorized"), (401, "Unauthorized"),
(500, "Failed to fetch posts") (500, "Failed to fetch posts"),
], ],
cookie: Some("auth"), cookie: Some("auth"),
}; };
@ -105,21 +114,71 @@ async fn page(
ResponseCode::Success.json(&json) ResponseCode::Success.json(&json)
} }
pub const COMMENTS_PAGE: EndpointDocumentation = EndpointDocumentation {
uri: "/api/posts/comments",
method: EndpointMethod::Post,
description: "Load a section of comments from newest to oldest",
body: Some(
r#"
{
"page": 1,
"post_id": 13
}
"#,
),
responses: &[
(200, "Returns comments in <span>application/json<span>"),
(400, "Body does not match parameters"),
(401, "Unauthorized"),
(500, "Failed to fetch comments"),
],
cookie: Some("auth"),
};
#[derive(Deserialize)]
struct CommentsPageRequest {
page: u64,
post_id: u64
}
impl Check for CommentsPageRequest {
fn check(&self) -> CheckResult {
Ok(())
}
}
async fn comments(
AuthorizedUser(_user): AuthorizedUser,
Json(body): Json<CommentsPageRequest>,
) -> Response {
let Ok(comments) = Comment::from_comment_page(body.page, body.post_id) else {
return ResponseCode::InternalServerError.text("Failed to fetch comments")
};
let Ok(json) = serde_json::to_string(&comments) else {
return ResponseCode::InternalServerError.text("Failed to fetch comments")
};
ResponseCode::Success.json(&json)
}
pub const POSTS_USER: EndpointDocumentation = EndpointDocumentation { pub const POSTS_USER: EndpointDocumentation = EndpointDocumentation {
uri: "/api/posts/user", uri: "/api/posts/user",
method: EndpointMethod::Post, method: EndpointMethod::Post,
description: "Load a section of posts from newest to oldest from a specific user", description: "Load a section of posts from newest to oldest from a specific user",
body: Some(r#" body: Some(
r#"
{ {
"user_id": 3, "user_id": 3,
"page": 0 "page": 0
} }
"#), "#,
),
responses: &[ responses: &[
(200, "Returns posts in <span>application/json<span>"), (200, "Returns posts in <span>application/json<span>"),
(400, "Body does not match parameters"), (400, "Body does not match parameters"),
(401, "Unauthorized"), (401, "Unauthorized"),
(500, "Failed to fetch posts") (500, "Failed to fetch posts"),
], ],
cookie: Some("auth"), cookie: Some("auth"),
}; };
@ -155,17 +214,19 @@ pub const POSTS_COMMENT: EndpointDocumentation = EndpointDocumentation {
uri: "/api/posts/comment", uri: "/api/posts/comment",
method: EndpointMethod::Patch, method: EndpointMethod::Patch,
description: "Add a comment to a post", description: "Add a comment to a post",
body: Some(r#" body: Some(
r#"
{ {
"content": "This is a very cool comment", "content": "This is a very cool comment",
"post_id": 0 "post_id": 0
} }
"#), "#,
),
responses: &[ responses: &[
(200, "Successfully added comment"), (200, "Successfully added comment"),
(400, "Body does not match parameters"), (400, "Body does not match parameters"),
(401, "Unauthorized"), (401, "Unauthorized"),
(500, "Failed to add comment") (500, "Failed to add comment"),
], ],
cookie: Some("auth"), cookie: Some("auth"),
}; };
@ -192,11 +253,7 @@ async fn comment(
AuthorizedUser(user): AuthorizedUser, AuthorizedUser(user): AuthorizedUser,
Json(body): Json<PostCommentRequest>, Json(body): Json<PostCommentRequest>,
) -> Response { ) -> Response {
let Ok(mut post) = Post::from_post_id(body.post_id) else { if let Err(err) = Comment::new(user.user_id, body.post_id, &body.content) {
return ResponseCode::InternalServerError.text("Failed to add comment")
};
if let Err(err) = post.comment(user.user_id, body.content) {
return err; return err;
} }
@ -207,17 +264,19 @@ pub const POSTS_LIKE: EndpointDocumentation = EndpointDocumentation {
uri: "/api/posts/like", uri: "/api/posts/like",
method: EndpointMethod::Patch, method: EndpointMethod::Patch,
description: "Set like status on a post", description: "Set like status on a post",
body: Some(r#" body: Some(
r#"
{ {
"post_id" : 0, "post_id" : 0,
"status" : true "status" : true
} }
"#), "#,
),
responses: &[ responses: &[
(200, "Successfully set like status"), (200, "Successfully set like status"),
(400, "Body does not match parameters"), (400, "Body does not match parameters"),
(401, "Unauthorized"), (401, "Unauthorized"),
(500, "Failed to set like status") (500, "Failed to set like status"),
], ],
cookie: Some("auth"), cookie: Some("auth"),
}; };
@ -235,11 +294,11 @@ impl Check for PostLikeRequest {
} }
async fn like(AuthorizedUser(user): AuthorizedUser, Json(body): Json<PostLikeRequest>) -> Response { async fn like(AuthorizedUser(user): AuthorizedUser, Json(body): Json<PostLikeRequest>) -> Response {
let Ok(mut post) = Post::from_post_id(body.post_id) else { if body.state {
return ResponseCode::InternalServerError.text("Failed to fetch posts") if let Err(err) = Like::add_liked(user.user_id, body.post_id) {
}; return err;
}
if let Err(err) = post.like(user.user_id, body.state) { } else if let Err(err) = Like::remove_liked(user.user_id, body.post_id) {
return err; return err;
} }
@ -250,6 +309,7 @@ pub fn router() -> Router {
Router::new() Router::new()
.route("/create", post(create)) .route("/create", post(create))
.route("/page", post(page)) .route("/page", post(page))
.route("/comments", post(comments))
.route("/user", post(user)) .route("/user", post(user))
.route("/comment", patch(comment)) .route("/comment", patch(comment))
.route("/like", patch(like)) .route("/like", patch(like))

View file

@ -1,8 +1,11 @@
use crate::{types::{ use crate::{
extract::{AuthorizedUser, Check, CheckResult, Json, Png}, public::docs::{EndpointDocumentation, EndpointMethod},
http::ResponseCode, types::{
user::User, extract::{AuthorizedUser, Check, CheckResult, Json, Png},
}, public::docs::{EndpointDocumentation, EndpointMethod}}; http::ResponseCode,
user::User,
},
};
use axum::{ use axum::{
response::Response, response::Response,
routing::{post, put}, routing::{post, put},
@ -14,16 +17,18 @@ pub const USERS_LOAD: EndpointDocumentation = EndpointDocumentation {
uri: "/api/users/load", uri: "/api/users/load",
method: EndpointMethod::Post, method: EndpointMethod::Post,
description: "Loads a requested set of users", description: "Loads a requested set of users",
body: Some(r#" body: Some(
r#"
{ {
"ids": [0, 3, 7] "ids": [0, 3, 7]
} }
"#), "#,
),
responses: &[ responses: &[
(200, "Returns users in <span>application/json</span>"), (200, "Returns users in <span>application/json</span>"),
(400, "Body does not match parameters"), (400, "Body does not match parameters"),
(401, "Unauthorized"), (401, "Unauthorized"),
(500, "Failed to fetch users") (500, "Failed to fetch users"),
], ],
cookie: Some("auth"), cookie: Some("auth"),
}; };
@ -55,17 +60,19 @@ pub const USERS_PAGE: EndpointDocumentation = EndpointDocumentation {
uri: "/api/users/page", uri: "/api/users/page",
method: EndpointMethod::Post, method: EndpointMethod::Post,
description: "Load a section of users from newest to oldest", description: "Load a section of users from newest to oldest",
body: Some(r#" body: Some(
r#"
{ {
"user_id": 3, "user_id": 3,
"page": 0 "page": 0
} }
"#), "#,
),
responses: &[ responses: &[
(200, "Returns users in <span>application/json</span>"), (200, "Returns users in <span>application/json</span>"),
(400, "Body does not match parameters"), (400, "Body does not match parameters"),
(401, "Unauthorized"), (401, "Unauthorized"),
(500, "Failed to fetch users") (500, "Failed to fetch users"),
], ],
cookie: Some("auth"), cookie: Some("auth"),
}; };
@ -104,7 +111,7 @@ pub const USERS_SELF: EndpointDocumentation = EndpointDocumentation {
responses: &[ responses: &[
(200, "Successfully executed SQL query"), (200, "Successfully executed SQL query"),
(401, "Unauthorized"), (401, "Unauthorized"),
(500, "Failed to fetch user") (500, "Failed to fetch user"),
], ],
cookie: Some("auth"), cookie: Some("auth"),
}; };
@ -126,7 +133,7 @@ pub const USERS_AVATAR: EndpointDocumentation = EndpointDocumentation {
(200, "Successfully updated avatar"), (200, "Successfully updated avatar"),
(400, "Invalid PNG or disallowed size"), (400, "Invalid PNG or disallowed size"),
(401, "Unauthorized"), (401, "Unauthorized"),
(500, "Failed to update avatar") (500, "Failed to update avatar"),
], ],
cookie: Some("auth"), cookie: Some("auth"),
}; };
@ -150,7 +157,7 @@ pub const USERS_BANNER: EndpointDocumentation = EndpointDocumentation {
(200, "Successfully updated banner"), (200, "Successfully updated banner"),
(400, "Invalid PNG or disallowed size"), (400, "Invalid PNG or disallowed size"),
(401, "Unauthorized"), (401, "Unauthorized"),
(500, "Failed to update banner") (500, "Failed to update banner"),
], ],
cookie: Some("auth"), cookie: Some("auth"),
}; };

91
src/database/comments.rs Normal file
View file

@ -0,0 +1,91 @@
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use rusqlite::Row;
use tracing::instrument;
use crate::{database, types::comment::Comment};
pub fn init() -> Result<(), rusqlite::Error> {
let sql = "
CREATE TABLE IF NOT EXISTS comments (
comment_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
post_id INTEGER NOT NULL,
date INTEGER NOT NULL,
content VARCHAR(255) NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(user_id),
FOREIGN KEY(post_id) REFERENCES posts(post_id)
);
";
let conn = database::connect()?;
conn.execute(sql, ())?;
let sql2 = "CREATE INDEX IF NOT EXISTS post_ids on comments (post_id);";
conn.execute(sql2, ())?;
Ok(())
}
fn comment_from_row(row: &Row) -> Result<Comment, rusqlite::Error> {
let comment_id = row.get(0)?;
let user_id = row.get(1)?;
let post_id = row.get(2)?;
let date = row.get(3)?;
let content = row.get(4)?;
Ok(Comment {
comment_id,
user_id,
post_id,
date,
content,
})
}
#[instrument()]
pub fn get_comments_page(page: u64, post_id: u64) -> Result<Vec<Comment>, rusqlite::Error> {
tracing::trace!("Retrieving comments page");
let page_size = 5;
let conn = database::connect()?;
let mut stmt = conn.prepare(
"SELECT * FROM comments WHERE post_id = ? ORDER BY comment_id ASC LIMIT ? OFFSET ?",
)?;
let row = stmt.query_map([post_id, page_size, page_size * page], |row| {
let row = comment_from_row(row)?;
Ok(row)
})?;
Ok(row.into_iter().flatten().collect())
}
#[instrument()]
pub fn get_all_comments() -> Result<Vec<Comment>, rusqlite::Error> {
tracing::trace!("Retrieving comments page");
let conn = database::connect()?;
let mut stmt = conn.prepare("SELECT * FROM comments ORDER BY comment_id DESC")?;
let row = stmt.query_map([], |row| {
let row = comment_from_row(row)?;
Ok(row)
})?;
Ok(row.into_iter().flatten().collect())
}
#[instrument()]
pub fn add_comment(user_id: u64, post_id: u64, content: &str) -> Result<Comment, rusqlite::Error> {
tracing::trace!("Adding comment");
let date = u64::try_from(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_millis(),
)
.unwrap_or(0);
let conn = database::connect()?;
let mut stmt = conn.prepare(
"INSERT INTO comments (user_id, post_id, date, content) VALUES(?,?,?,?) RETURNING *;",
)?;
let post = stmt.query_row((user_id, post_id, date, content), |row| {
let row = comment_from_row(row)?;
Ok(row)
})?;
Ok(post)
}

77
src/database/likes.rs Normal file
View file

@ -0,0 +1,77 @@
use rusqlite::OptionalExtension;
use tracing::instrument;
use crate::{database, types::like::Like};
pub fn init() -> Result<(), rusqlite::Error> {
let sql = "
CREATE TABLE IF NOT EXISTS likes (
user_id INTEGER NOT NULL,
post_id INTEGER NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(user_id),
FOREIGN KEY(post_id) REFERENCES posts(post_id),
PRIMARY KEY (user_id, post_id)
);
";
let conn = database::connect()?;
conn.execute(sql, ())?;
Ok(())
}
#[instrument()]
pub fn get_like_count(post_id: u64) -> Result<Option<u64>, rusqlite::Error> {
tracing::trace!("Retrieving like count");
let conn = database::connect()?;
let mut stmt = conn.prepare("SELECT COUNT(post_id) FROM likes WHERE post_id = ?")?;
let row = stmt
.query_row([post_id], |row| {
let row = row.get(0)?;
Ok(row)
})
.optional()?;
Ok(row)
}
#[instrument()]
pub fn get_liked(user_id: u64, post_id: u64) -> Result<bool, rusqlite::Error> {
tracing::trace!("Retrieving if liked");
let conn = database::connect()?;
let mut stmt = conn.prepare("SELECT * FROM likes WHERE user_id = ? AND post_id = ?")?;
let liked = stmt.query_row([user_id, post_id], |_| {
Ok(())
}).optional()?;
Ok(liked.is_some())
}
#[instrument()]
pub fn add_liked(user_id: u64, post_id: u64) -> Result<bool, rusqlite::Error> {
tracing::trace!("Adding like");
let conn = database::connect()?;
let mut stmt = conn.prepare("INSERT OR REPLACE INTO likes (user_id, post_id) VALUES (?,?)")?;
let changes = stmt.execute([user_id, post_id])?;
Ok(changes == 1)
}
#[instrument()]
pub fn remove_liked(user_id: u64, post_id: u64) -> Result<bool, rusqlite::Error> {
tracing::trace!("Removing like");
let conn = database::connect()?;
let mut stmt = conn.prepare("DELETE FROM likes WHERE user_id = ? AND post_id = ?;")?;
let changes = stmt.execute((user_id, post_id))?;
Ok(changes == 1)
}
#[instrument()]
pub fn get_all_likes() -> Result<Vec<Like>, rusqlite::Error> {
tracing::trace!("Retrieving comments page");
let conn = database::connect()?;
let mut stmt = conn.prepare("SELECT * FROM likes")?;
let row = stmt.query_map([], |row| {
let like = Like {
user_id: row.get(0)?,
post_id: row.get(1)?
};
Ok(like)
})?;
Ok(row.into_iter().flatten().collect())
}

View file

@ -1,5 +1,7 @@
use tracing::instrument; use tracing::instrument;
pub mod comments;
pub mod likes;
pub mod posts; pub mod posts;
pub mod sessions; pub mod sessions;
pub mod users; pub mod users;
@ -12,6 +14,8 @@ pub fn init() -> Result<(), rusqlite::Error> {
users::init()?; users::init()?;
posts::init()?; posts::init()?;
sessions::init()?; sessions::init()?;
likes::init()?;
comments::init()?;
Ok(()) Ok(())
} }

View file

@ -1,4 +1,3 @@
use std::collections::HashSet;
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::time::{Duration, SystemTime, UNIX_EPOCH};
use rusqlite::{OptionalExtension, Row}; use rusqlite::{OptionalExtension, Row};
@ -7,14 +6,14 @@ use tracing::instrument;
use crate::database; use crate::database;
use crate::types::post::Post; use crate::types::post::Post;
use super::{comments, likes};
pub fn init() -> Result<(), rusqlite::Error> { pub fn init() -> Result<(), rusqlite::Error> {
let sql = " let sql = "
CREATE TABLE IF NOT EXISTS posts ( CREATE TABLE IF NOT EXISTS posts (
post_id INTEGER PRIMARY KEY AUTOINCREMENT, post_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
content VARCHAR(500) NOT NULL, content VARCHAR(500) NOT NULL,
likes TEXT NOT NULL,
comments TEXT NOT NULL,
date INTEGER NOT NULL, date INTEGER NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(user_id) FOREIGN KEY(user_id) REFERENCES users(user_id)
); );
@ -28,25 +27,20 @@ fn post_from_row(row: &Row) -> Result<Post, rusqlite::Error> {
let post_id = row.get(0)?; let post_id = row.get(0)?;
let user_id = row.get(1)?; let user_id = row.get(1)?;
let content = row.get(2)?; let content = row.get(2)?;
let likes_json: String = row.get(3)?; let date = row.get(3)?;
let comments_json: String = row.get(4)?;
let date = row.get(5)?;
let Ok(likes) = serde_json::from_str(&likes_json) else { let comments = comments::get_comments_page(0, post_id).unwrap_or_else(|_| Vec::new());
return Err(rusqlite::Error::InvalidQuery) let likes = likes::get_like_count(post_id).unwrap_or(None).unwrap_or(0);
}; let liked = likes::get_liked(user_id, post_id).unwrap_or(false);
let Ok(comments) = serde_json::from_str(&comments_json) else {
return Err(rusqlite::Error::InvalidQuery)
};
Ok(Post { Ok(Post {
post_id, post_id,
user_id, user_id,
content, content,
likes,
comments,
date, date,
likes,
liked,
comments,
}) })
} }
@ -106,14 +100,6 @@ pub fn get_users_post_page(user_id: u64, page: u64) -> Result<Vec<Post>, rusqlit
#[instrument()] #[instrument()]
pub fn add_post(user_id: u64, content: &str) -> Result<Post, rusqlite::Error> { pub fn add_post(user_id: u64, content: &str) -> Result<Post, rusqlite::Error> {
tracing::trace!("Adding post"); tracing::trace!("Adding post");
let likes: HashSet<u64> = HashSet::new();
let comments: Vec<(u64, String)> = Vec::new();
let Ok(likes_json) = serde_json::to_string(&likes) else {
return Err(rusqlite::Error::InvalidQuery)
};
let Ok(comments_json) = serde_json::to_string(&comments) else {
return Err(rusqlite::Error::InvalidQuery)
};
let date = u64::try_from( let date = u64::try_from(
SystemTime::now() SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
@ -122,29 +108,11 @@ pub fn add_post(user_id: u64, content: &str) -> Result<Post, rusqlite::Error> {
) )
.unwrap_or(0); .unwrap_or(0);
let conn = database::connect()?; let conn = database::connect()?;
let mut stmt = conn.prepare("INSERT INTO posts (user_id, content, likes, comments, date) VALUES(?,?,?,?,?) RETURNING *;")?; let mut stmt =
let post = stmt.query_row((user_id, content, likes_json, comments_json, date), |row| { conn.prepare("INSERT INTO posts (user_id, content, date) VALUES(?,?,?) RETURNING *;")?;
let post = stmt.query_row((user_id, content, date), |row| {
let row = post_from_row(row)?; let row = post_from_row(row)?;
Ok(row) Ok(row)
})?; })?;
Ok(post) Ok(post)
} }
#[instrument()]
pub fn update_post(
post_id: u64,
likes: &HashSet<u64>,
comments: &Vec<(u64, String)>,
) -> Result<(), rusqlite::Error> {
tracing::trace!("Updating post");
let Ok(likes_json) = serde_json::to_string(&likes) else {
return Err(rusqlite::Error::InvalidQuery)
};
let Ok(comments_json) = serde_json::to_string(&comments) else {
return Err(rusqlite::Error::InvalidQuery)
};
let conn = database::connect()?;
let sql = "UPDATE posts SET likes = ?, comments = ? WHERE post_id = ?";
conn.execute(sql, (likes_json, comments_json, post_id))?;
Ok(())
}

View file

@ -21,6 +21,13 @@ pub fn init() -> Result<(), rusqlite::Error> {
"; ";
let conn = database::connect()?; let conn = database::connect()?;
conn.execute(sql, ())?; conn.execute(sql, ())?;
let sql2 = "CREATE UNIQUE INDEX IF NOT EXISTS emails on users (email);";
conn.execute(sql2, ())?;
let sql3 = "CREATE UNIQUE INDEX IF NOT EXISTS passwords on users (password);";
conn.execute(sql3, ())?;
Ok(()) Ok(())
} }

View file

@ -4,8 +4,8 @@ use rand::{distributions::Alphanumeric, Rng};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::{ use crate::{
console::{self, sanatize}, console::sanatize,
types::{http::ResponseCode, post::Post, session::Session, user::User}, types::{http::ResponseCode, post::Post, session::Session, user::User, comment::Comment, like::Like},
}; };
lazy_static! { lazy_static! {
@ -79,24 +79,17 @@ pub fn generate_posts() -> Response {
<th>Post ID</th> <th>Post ID</th>
<th>User ID</th> <th>User ID</th>
<th>Content</th> <th>Content</th>
<th>Likes</th>
<th>Comments</th>
<th>Date</th> <th>Date</th>
</tr> </tr>
"# "#
.to_string(); .to_string();
for post in posts { for post in posts {
let Ok(likes) = serde_json::to_string(&post.likes) else { continue };
let Ok(comments) = serde_json::to_string(&post.comments) else { continue };
html.push_str(&format!( html.push_str(&format!(
"<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>", "<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>",
post.post_id, post.post_id,
post.user_id, post.user_id,
sanatize(&post.content), sanatize(&post.content),
console::beautify(&likes),
console::beautify(&comments),
post.date post.date
)); ));
} }
@ -127,3 +120,54 @@ pub fn generate_sessions() -> Response {
ResponseCode::Success.text(&html) ResponseCode::Success.text(&html)
} }
pub fn generate_comments() -> Response {
let comments = match Comment::reterieve_all() {
Ok(comments) => comments,
Err(err) => return err,
};
let mut html = r#"
<tr>
<th>Comment ID</th>
<th>User ID</th>
<th>Post ID</th>
<th>Content</th>
<th>Date</th>
</tr>
"#
.to_string();
for comment in comments {
html.push_str(&format!(
"<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>",
comment.comment_id, comment.user_id, comment.post_id, sanatize(&comment.content), comment.date
));
}
ResponseCode::Success.text(&html)
}
pub fn generate_likes() -> Response {
let likes = match Like::reterieve_all() {
Ok(likes) => likes,
Err(err) => return err,
};
let mut html = r#"
<tr>
<th>User ID</th>
<th>Post ID</th>
</tr>
"#
.to_string();
for like in likes {
html.push_str(&format!(
"<tr><td>{}</td><td>{}</td></tr>",
like.user_id, like.post_id
));
}
ResponseCode::Success.text(&html)
}

View file

@ -2,7 +2,10 @@ use axum::response::Response;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::{api::{admin, users, posts, auth}, types::http::ResponseCode}; use crate::{
api::{admin, auth, posts, users},
types::http::ResponseCode,
};
use super::console::beautify; use super::console::beautify;
@ -40,10 +43,10 @@ fn generate_body(body: Option<&'static str>) -> String {
return String::new() return String::new()
}; };
let html = r#" let html = r#"
<h2>Body</h2> <h2>Body</h2>
<div class="body"> <div class="body">
$body $body
</div> </div>
"# "#
.to_string(); .to_string();
let body = body.trim(); let body = body.trim();
@ -60,20 +63,20 @@ fn generate_body(body: Option<&'static str>) -> String {
fn generate_responses(responses: &[(u16, &'static str)]) -> String { fn generate_responses(responses: &[(u16, &'static str)]) -> String {
let mut html = r#" let mut html = r#"
<h2>Responses</h2> <h2>Responses</h2>
$responses $responses
"# "#
.to_string(); .to_string();
for response in responses { for response in responses {
let res = format!( let res = format!(
r#" r#"
<div> <div>
<span class="ptype">{}</span> <span class="ptype">{}</span>
<span class="pdesc">{}</span> <span class="pdesc">{}</span>
</div> </div>
$responses $responses
"#, "#,
response.0, response.1 response.0, response.1
); );
html = html.replace("$responses", &res); html = html.replace("$responses", &res);
@ -93,18 +96,18 @@ fn generate_cookie(cookie: Option<&'static str>) -> String {
fn generate_endpoint(doc: &EndpointDocumentation) -> String { fn generate_endpoint(doc: &EndpointDocumentation) -> String {
let html = r#" let html = r#"
<div> <div>
<div class="endpoint"> <div class="endpoint">
<span class="method $method_class">$method</span> <span class="method $method_class">$method</span>
<span class="uri">$uri</span> <span class="uri">$uri</span>
<span class="desc">$description</span> <span class="desc">$description</span>
$cookie $cookie
</div>
<div class="info">
$body
$responses
</div>
</div> </div>
<div class="info">
$body
$responses
</div>
</div>
"#; "#;
html.replace("$method_class", &doc.method.to_string().to_lowercase()) html.replace("$method_class", &doc.method.to_string().to_lowercase())
@ -123,6 +126,7 @@ pub async fn init() {
auth::AUTH_LOGOUT, auth::AUTH_LOGOUT,
posts::POSTS_CREATE, posts::POSTS_CREATE,
posts::POSTS_PAGE, posts::POSTS_PAGE,
posts::COMMENTS_PAGE,
posts::POSTS_USER, posts::POSTS_USER,
posts::POSTS_COMMENT, posts::POSTS_COMMENT,
posts::POSTS_LIKE, posts::POSTS_LIKE,
@ -136,6 +140,8 @@ pub async fn init() {
admin::ADMIN_POSTS, admin::ADMIN_POSTS,
admin::ADMIN_USERS, admin::ADMIN_USERS,
admin::ADMIN_SESSIONS, admin::ADMIN_SESSIONS,
admin::ADMIN_COMMENTS,
admin::ADMIN_LIKES
]; ];
let mut endpoints = ENDPOINTS.lock().await; let mut endpoints = ENDPOINTS.lock().await;
for doc in docs { for doc in docs {
@ -154,27 +160,27 @@ pub async fn generate() -> Response {
let html = format!( let html = format!(
r#" r#"
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="stylesheet" href="/css/main.css"> <link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/header.css"> <link rel="stylesheet" href="/css/header.css">
<link rel="stylesheet" href="/css/console.css"> <link rel="stylesheet" href="/css/console.css">
<link rel="stylesheet" href="/css/api.css"> <link rel="stylesheet" href="/css/api.css">
<title>XSSBook - API Documentation</title> <title>XSSBook - API Documentation</title>
</head> </head>
<body> <body>
<div id="header"> <div id="header">
<span class="logo"><a href="/">xssbook</a></span> <span class="logo"><a href="/">xssbook</a></span>
<span class="gtext desc" style="margin-left: 6em; font-size: 2em; color: #606770">API Documentation</span> <span class="gtext desc" style="margin-left: 6em; font-size: 2em; color: #606770">API Documentation</span>
</div> </div>
<div id="docs"> <div id="docs">
{data} {data}
</div> </div>
</body> </body>
</html> </html>
"# "#
); );
ResponseCode::Success.html(&html) ResponseCode::Success.html(&html)

44
src/types/comment.rs Normal file
View file

@ -0,0 +1,44 @@
use serde::Serialize;
use tracing::instrument;
use crate::{
database::{self, comments},
types::http::{ResponseCode, Result},
};
#[derive(Serialize)]
pub struct Comment {
pub comment_id: u64,
pub user_id: u64,
pub post_id: u64,
pub date: u64,
pub content: String,
}
impl Comment {
#[instrument()]
pub fn new(user_id: u64, post_id: u64, content: &str) -> Result<Self> {
let Ok(comment) = comments::add_comment(user_id, post_id, content) else {
tracing::error!("Failed to create comment");
return Err(ResponseCode::InternalServerError.text("Failed to create post"))
};
Ok(comment)
}
#[instrument()]
pub fn from_comment_page(page: u64, post_id: u64) -> Result<Vec<Self>> {
let Ok(posts) = database::comments::get_comments_page(page, post_id) else {
return Err(ResponseCode::BadRequest.text("Failed to fetch comments"))
};
Ok(posts)
}
#[instrument()]
pub fn reterieve_all() -> Result<Vec<Self>> {
let Ok(posts) = database::comments::get_all_comments() else {
return Err(ResponseCode::InternalServerError.text("Failed to fetch comments"))
};
Ok(posts)
}
}

48
src/types/like.rs Normal file
View file

@ -0,0 +1,48 @@
use serde::Serialize;
use tracing::instrument;
use crate::database;
use crate::types::http::{ResponseCode, Result};
#[derive(Serialize)]
pub struct Like {
pub user_id: u64,
pub post_id: u64
}
impl Like {
#[instrument()]
pub fn add_liked(user_id: u64, post_id: u64) -> Result<()> {
let Ok(liked) = database::likes::add_liked(user_id, post_id) else {
return Err(ResponseCode::BadRequest.text("Failed to add like status"))
};
if !liked {
return Err(ResponseCode::InternalServerError.text("Failed to add like status"));
}
Ok(())
}
#[instrument()]
pub fn remove_liked(user_id: u64, post_id: u64) -> Result<()> {
let Ok(liked) = database::likes::remove_liked(user_id, post_id) else {
return Err(ResponseCode::BadRequest.text("Failed to remove like status"))
};
if !liked {
return Err(ResponseCode::InternalServerError.text("Failed to remove like status"));
}
Ok(())
}
#[instrument()]
pub fn reterieve_all() -> Result<Vec<Self>> {
let Ok(likes) = database::likes::get_all_likes() else {
return Err(ResponseCode::InternalServerError.text("Failed to fetch likes"))
};
Ok(likes)
}
}

View file

@ -1,5 +1,7 @@
pub mod comment;
pub mod extract; pub mod extract;
pub mod http; pub mod http;
pub mod like;
pub mod post; pub mod post;
pub mod session; pub mod session;
pub mod user; pub mod user;

View file

@ -1,19 +1,21 @@
use core::fmt; use core::fmt;
use serde::Serialize; use serde::Serialize;
use std::collections::HashSet;
use tracing::instrument; use tracing::instrument;
use crate::database; use crate::database;
use crate::types::http::{ResponseCode, Result}; use crate::types::http::{ResponseCode, Result};
use super::comment::Comment;
#[derive(Serialize)] #[derive(Serialize)]
pub struct Post { pub struct Post {
pub post_id: u64, pub post_id: u64,
pub user_id: u64, pub user_id: u64,
pub content: String, pub content: String,
pub likes: HashSet<u64>,
pub comments: Vec<(u64, String)>,
pub date: u64, pub date: u64,
pub likes: u64,
pub liked: bool,
pub comments: Vec<Comment>,
} }
impl fmt::Debug for Post { impl fmt::Debug for Post {
@ -67,34 +69,4 @@ impl Post {
Ok(post) Ok(post)
} }
#[instrument()]
pub fn comment(&mut self, user_id: u64, content: String) -> Result<()> {
self.comments.push((user_id, content));
if database::posts::update_post(self.post_id, &self.likes, &self.comments).is_err() {
tracing::error!("Failed to comment on post");
return Err(ResponseCode::InternalServerError.text("Failed to comment on post"));
}
Ok(())
}
#[instrument()]
pub fn like(&mut self, user_id: u64, state: bool) -> Result<()> {
if state {
self.likes.insert(user_id);
} else {
self.likes.remove(&user_id);
}
if database::posts::update_post(self.post_id, &self.likes, &self.comments).is_err() {
tracing::error!("Failed to change like state on post");
return Err(
ResponseCode::InternalServerError.text("Failed to change like state on post")
);
}
Ok(())
}
} }