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/header.css">
<link rel="stylesheet" href="/css/admin.css">
<script src="/js/admin.js" type="module"></script>
<title>XSSBook - Admin Panel</title>
</head>
<body>
<script src="/js/api.js"></script>
<script src="/js/admin.js"></script>
<div id="header">
<span class="logo"><a href="/">xssbook</a></span>
<span class="gtext desc" style="margin-left: 6em; font-size: 2em; color: #606770">Admin Panel</span>
</div>
<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">
</form>
</div>
@ -23,10 +22,12 @@
<div id="queryinput">
<input type="text" name="query" id="query" placeholder="SQL Query">
<div id="buttons">
<button class="submit" onclick="submit()">Submit</button>
<button class="view" onclick="posts()">View Posts</button>
<button class="view" onclick="users()">View Users</button>
<button class="view" onclick="sessions()">View Sessions</button>
<button class="submit" onclick="window.submit()">Submit</button>
<button class="view" onclick="window.posts()">View Posts</button>
<button class="view" onclick="window.users()">View Users</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>
<table id="table"></table>

View file

@ -2,16 +2,12 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="/css/header.css">
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/header.css">
<link rel="stylesheet" href="/css/home.css">
<title>XSSBook - Home</title>
<script type="module" src="/js/home.js"></script>
</head>
<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>
</html>

View file

@ -1,3 +1,5 @@
import { adminauth, admincheck, admincomments, adminposts, adminquery, adminsessions, adminusers, adminlikes } from './api.js'
async function auth(event) {
event.preventDefault();
const text = event.target.elements.adminpassword.value;
@ -10,12 +12,14 @@ async function auth(event) {
}
return false;
}
window.auth = auth
async function submit() {
let text = document.getElementById("query").value
let response = await adminquery(text)
alert(response.msg)
}
window.submit = submit
async function posts() {
let response = await adminposts();
@ -26,6 +30,7 @@ async function posts() {
let table = document.getElementById("table")
table.innerHTML = response.msg
}
window.posts = posts
async function users() {
let response = await adminusers();
@ -36,6 +41,7 @@ async function users() {
let table = document.getElementById("table")
table.innerHTML = response.msg
}
window.users = users
async function sessions() {
let response = await adminsessions();
@ -46,6 +52,29 @@ async function sessions() {
let table = document.getElementById("table")
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() {
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})
}
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})
}
const logout = async () => {
export const logout = async () => {
return await request('/auth/logout', {})
}
const loadpostspage = async (page) => {
export const loadpostspage = async (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})
}
const loadusers = async (ids) => {
export const loadusers = async (ids) => {
return await request('/users/load', {ids})
}
const loaduserspage = async (page) => {
export const loaduserspage = async (page) => {
return await request('/users/page', {page})
}
const loadself = async () => {
export const loadself = async () => {
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')
}
const postlike = async (post_id, state) => {
export const postlike = async (post_id, state) => {
return await request('/posts/like', {post_id, state}, 'PATCH')
}
const createpost = async (content) => {
export const createpost = async (content) => {
return await request('/posts/create', {content})
}
const adminauth = async (secret) => {
export const adminauth = async (secret) => {
return await request('/admin/auth', {secret})
}
const admincheck = async () => {
export const admincheck = async () => {
return await request('/admin/check', {})
}
const adminquery = async (query) => {
export const adminquery = async (query) => {
return await request('/admin/query', {query})
}
const adminposts = async () => {
export const adminposts = async () => {
return await request('/admin/posts', {})
}
const adminusers = async () => {
export const adminusers = async () => {
return await request('/admin/users', {})
}
const adminsessions = async () => {
export const adminsessions = async () => {
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')
}
const updatebanner = async (file) => {
export const updatebanner = async (file) => {
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) {
let author = data.users[comment[0]]
if (author === undefined) {
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])
}
import { div, pfp, p, parse, button, body, a, textarea, span, crawl } from './main.js'
import { loadself, loadpostspage, createpost, loadusers } from './api.js'
import { parsePost, header } from './components.js'
async function post() {
const text = document.getElementById("text").value.trim()
@ -133,7 +15,7 @@ async function post() {
error.innerHTML = '';
let post = {
post_id: response.json.post_id,
user_id: data.user.user_id,
user_id: data.self.user_id,
date: Date.now(),
content: text,
likes: [],
@ -145,113 +27,127 @@ async function post() {
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() {
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>
`
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 posts = await load()
data.posts.push(... posts)
const el = document.getElementById("posts")
for (const post of posts) {
el.appendChild(
parsePost(post, data.users, data.self)
)
}
}},
parse('Load more posts')
)
)
)
append(html)
document.body.replaceWith(new_body)
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">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 load = `
<div id="load">
<a class="bold gtext" onclick="loadMore()">Load more posts</a>
</div>
`
append(load)
}
var page = 0
const data = {
user: {},
self: {},
users: {},
posts: []
}
async function load() {
const posts = (await loadpostspage(page)).json
if (posts.length === 0) {
page = -1
remove('load')
document.getElementById('load').remove()
return []
} else {
page++
}
const batch = []
for (const post of posts) {
for(const comment of post.comments) {
if (data.users[comment[0]] !== undefined) continue
if (batch.includes(comment[0])) continue
batch.push(comment[0])
const batch = Array.from(new Set(crawl('user_id', posts))).filter(id => data.users[id] == undefined)
if (batch.length != 0) {
const users = (await loadusers(batch)).json
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
}
async function init() {
let request = (await loadself());
if (request.status === 429) {
header(true, false)
let new_body =
body({},
...header(true, false)
)
document.body.replaceWith(new_body)
throw new Error("Rate limited");
}
data.user = request.json
header(true, false, data.user.user_id)
data.users[data.user.user_id] = data.user
data.self = request.json
data.users[data.self.user_id] = data.self
const posts = await load()
data.posts.push(... posts)
render()
}
}
init()

View file

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

View file

@ -1,43 +1,108 @@
function prepend(html, container, before) {
if (container === undefined) {
container = document.body
function createElement(name, attrs, ...children) {
const el = document.createElement(name);
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()
range.setStart(container, 0);
container.insertBefore(
range.createContextualFragment(html),
before
)
return el
}
function append(html, container) {
if (container === undefined) {
container = document.body
export function createElementNS(name, attrs, ...children) {
var svgns = "http://www.w3.org/2000/svg";
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) {
const old = document.getElementById(id)
if (old !== null) {
old.remove()
for (const child of children) {
if (child == null) {
continue
}
el.appendChild(child)
}
return el
}
function pfp(id) {
return `<img src="/image/avatar?user_id=${id}">`
export function p(attrs, ...children) {
return createElement("p", attrs, ...children)
}
function banner(id) {
return `<img src="/image/banner?user_id=${id}" onerror="this.remove()" >`
export function span(attrs, ...children) {
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',
@ -46,7 +111,7 @@ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
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'];
function parseMonth(month) {
export function parseMonth(month) {
if (month > -1 && month < 26) {
return months[month]
} 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();
}
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) {
const html = `
<a class="person" href="/profile?id=${user.user_id}">
<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
}
import { div, body, a, parse } from './main.js'
import { loadself, loaduserspage } from './api.js'
import { header, parseUser } from './components.js'
function render() {
const html = `
<div id="users">
${data.users.map(u => parseUser(u)).join('')}
</div>
`
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()
append(html)
let el = document.getElementById("users")
for (const user of users) {
el.appendChild(parseUser(user))
}
}},
parse("Load more users")
)
)
)
const load = `
<div id="load">
<a class="bold gtext" onclick="loadMore()">Load more users</a>
</div>
`
append(load)
document.body.replaceWith(new_body)
}
var page = 0
var data = {
users: []
}
async function loadMore() {
let users = await load()
const users_block = document.getElementById("users")
for (user of users) {
append(parseUser(user), users_block)
}
const data = {
users: [],
self: {}
}
async function load() {
let request = await loadself()
if (request.status === 429) {
header(false, true)
let new_body =
body({},
...header(false, true)
)
document.body.replaceWith(new_body)
throw new Error("Rate limited");
}
const self = request.json
header(false, true, self.user_id)
const users = (await loaduserspage(page)).json
if (users.length === 0) {
page = -1
remove('load')
document.getElementById('load').remove()
return []
} else {
page++
}
@ -66,8 +62,24 @@ async function load() {
}
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)
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) {
let postsb = document.getElementById("profilepostbutton");
let aboutb = document.getElementById("profileaboutbutton");
let posts = document.getElementById("posts");
let about = document.getElementById("about");
let load = document.getElementsByClassName("loadp")[0];
if (value) {
postsb.classList.add("selected")
aboutb.classList.remove("selected")
about.classList.add("hidden")
posts.classList.remove("hidden")
if (load) {
load.classList.remove("hidden")
}
} else {
postsb.classList.remove("selected")
aboutb.classList.add("selected")
about.classList.remove("hidden")
posts.classList.add("hidden")
if (load) {
load.classList.add("hidden")
}
}
}
@ -50,129 +68,155 @@ function changeimage(fn) {
}
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 posts = await load()
data.posts.push(... posts)
const postsh = `
<div id="posts" class="${posts ? '' : 'hidden'}">
${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) {
append(postsh)
const el = document.getElementById("posts")
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 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
if (posts.length === 0) {
page = -1
remove('load')
if (posts.length < 1) {
document.getElementsByClassName('loadp')[0].remove()
return []
} else {
page++
}
let batch = []
const batch = Array.from(new Set(crawl('user_id', posts))).filter(id => data.users[id] == undefined)
if (!isself) {
batch.push(id)
}
for (const post of posts) {
for(const comment of post.comments) {
if (data.users[comment[0]] !== undefined) continue
if (batch.includes(comment[0])) continue
batch.push(comment[0])
if (batch.length != 0) {
const users = await loadusers(batch).json
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
}
@ -180,21 +224,26 @@ async function load() {
async function init() {
let request = await loadself()
if (request.status === 429) {
header(false, false)
let new_body =
body({},
...header(false, false)
)
document.body.replaceWith(new_body)
throw new Error("Rate limited");
}
data.self = request.json;
data.users[data.self.user_id] = data.self
header(false, false, data.self.user_id)
var params = {};
for (const [key, value] of new URLSearchParams(location.search)) {
params[key] = value
}
let id;
if (params.id !== undefined && !isNaN(params.id)) {
id = parseInt(params.id);
} else {
@ -203,9 +252,11 @@ async function init() {
isself = id === data.self.user_id
const posts = await load()
const posts = await load(id)
data.posts.push(... posts)
data.user = data.users[id]
render()
}

View file

@ -4,8 +4,7 @@
<meta charset="UTF-8">
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/login.css">
<script src="/js/api.js"></script>
<script src="/js/login.js"></script>
<script src="/js/login.js" type="module"></script>
<title>XSSBook - Login</title>
</head>
<body>
@ -18,7 +17,7 @@
<input type="text" name="email" id="email" placeholder="Email" autofocus="1">
<input type="password" name="pass" id="pass" placeholder="Password">
<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>
<div class="line"></div>
<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="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>
<button class="success signacc" onclick="onregister()">Sign Up</button>
<button class="success signacc" onclick="window.onregister()">Sign Up</button>
</div>
</div>
</div>

View file

@ -3,13 +3,11 @@
<head>
<meta charset="UTF-8">
<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/people.css">
<script src="/js/people.js" type="module"></script>
<title>XSSBook - People</title>
</head>
<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/profile.css">
<link rel="stylesheet" href="/css/home.css">
<script src="/js/profile.js" type="module"></script>
<title>XSSBook - Profile</title>
</head>
<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::{
database,
public::{admin, docs::{EndpointDocumentation, EndpointMethod}},
public::{
admin,
docs::{EndpointDocumentation, EndpointMethod},
},
types::{
extract::{AdminUser, Check, CheckResult, Json},
http::ResponseCode,
@ -17,14 +20,16 @@ pub const ADMIN_AUTH: EndpointDocumentation = EndpointDocumentation {
uri: "/api/admin/auth",
method: EndpointMethod::Post,
description: "Authenticates on the admin panel",
body: Some(r#"
body: Some(
r#"
{
"secret" : "admin"
}
"#),
"#,
),
responses: &[
(200, "Successfully executed SQL query"),
(400, " Successfully authed, admin cookie returned")
(400, " Successfully authed, admin cookie returned"),
],
cookie: None,
};
@ -60,16 +65,18 @@ pub const ADMIN_QUERY: EndpointDocumentation = EndpointDocumentation {
uri: "/api/admin/query",
method: EndpointMethod::Post,
description: "Run a SQL query on the database",
body: Some(r#"
body: Some(
r#"
{
"query" : "DROP TABLE users;"
}
"#),
"#,
),
responses: &[
(200, "Successfully executed SQL query"),
(400, "Body does not match parameters"),
(401, "Unauthorized"),
(500, "SQL query ran into an error")
(500, "SQL query ran into an error"),
],
cookie: Some("admin"),
};
@ -102,7 +109,7 @@ pub const ADMIN_POSTS: EndpointDocumentation = EndpointDocumentation {
responses: &[
(200, "Returns sql table in <span>text/html</span>"),
(401, "Unauthorized"),
(500, "Failed to fetch data")
(500, "Failed to fetch data"),
],
cookie: Some("admin"),
};
@ -119,7 +126,7 @@ pub const ADMIN_USERS: EndpointDocumentation = EndpointDocumentation {
responses: &[
(200, "Returns sql table in <span>text/html</span>"),
(401, "Unauthorized"),
(500, "Failed to fetch data")
(500, "Failed to fetch data"),
],
cookie: Some("admin"),
};
@ -136,7 +143,7 @@ pub const ADMIN_SESSIONS: EndpointDocumentation = EndpointDocumentation {
responses: &[
(200, "Returns sql table in <span>text/html</span>"),
(401, "Unauthorized"),
(500, "Failed to fetch data")
(500, "Failed to fetch data"),
],
cookie: Some("admin"),
};
@ -145,6 +152,40 @@ async fn sessions(_: AdminUser) -> Response {
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 {
if check.is_none() {
ResponseCode::Success.text("false")
@ -160,5 +201,7 @@ pub fn router() -> Router {
.route("/posts", post(posts))
.route("/users", post(users))
.route("/sessions", post(sessions))
.route("/comments", post(comments))
.route("/likes", post(likes))
.route("/check", post(check))
}

View file

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

View file

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

View file

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

View file

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

View file

@ -4,8 +4,8 @@ use rand::{distributions::Alphanumeric, Rng};
use tokio::sync::Mutex;
use crate::{
console::{self, sanatize},
types::{http::ResponseCode, post::Post, session::Session, user::User},
console::sanatize,
types::{http::ResponseCode, post::Post, session::Session, user::User, comment::Comment, like::Like},
};
lazy_static! {
@ -79,24 +79,17 @@ pub fn generate_posts() -> Response {
<th>Post ID</th>
<th>User ID</th>
<th>Content</th>
<th>Likes</th>
<th>Comments</th>
<th>Date</th>
</tr>
"#
.to_string();
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!(
"<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.user_id,
sanatize(&post.content),
console::beautify(&likes),
console::beautify(&comments),
post.date
));
}
@ -127,3 +120,54 @@ pub fn generate_sessions() -> Response {
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 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;
@ -40,10 +43,10 @@ fn generate_body(body: Option<&'static str>) -> String {
return String::new()
};
let html = r#"
<h2>Body</h2>
<div class="body">
$body
</div>
<h2>Body</h2>
<div class="body">
$body
</div>
"#
.to_string();
let body = body.trim();
@ -60,20 +63,20 @@ fn generate_body(body: Option<&'static str>) -> String {
fn generate_responses(responses: &[(u16, &'static str)]) -> String {
let mut html = r#"
<h2>Responses</h2>
$responses
<h2>Responses</h2>
$responses
"#
.to_string();
for response in responses {
let res = format!(
r#"
<div>
<span class="ptype">{}</span>
<span class="pdesc">{}</span>
</div>
$responses
"#,
<div>
<span class="ptype">{}</span>
<span class="pdesc">{}</span>
</div>
$responses
"#,
response.0, response.1
);
html = html.replace("$responses", &res);
@ -93,18 +96,18 @@ fn generate_cookie(cookie: Option<&'static str>) -> String {
fn generate_endpoint(doc: &EndpointDocumentation) -> String {
let html = r#"
<div>
<div class="endpoint">
<span class="method $method_class">$method</span>
<span class="uri">$uri</span>
<span class="desc">$description</span>
$cookie
</div>
<div class="info">
$body
$responses
</div>
<div>
<div class="endpoint">
<span class="method $method_class">$method</span>
<span class="uri">$uri</span>
<span class="desc">$description</span>
$cookie
</div>
<div class="info">
$body
$responses
</div>
</div>
"#;
html.replace("$method_class", &doc.method.to_string().to_lowercase())
@ -123,6 +126,7 @@ pub async fn init() {
auth::AUTH_LOGOUT,
posts::POSTS_CREATE,
posts::POSTS_PAGE,
posts::COMMENTS_PAGE,
posts::POSTS_USER,
posts::POSTS_COMMENT,
posts::POSTS_LIKE,
@ -136,6 +140,8 @@ pub async fn init() {
admin::ADMIN_POSTS,
admin::ADMIN_USERS,
admin::ADMIN_SESSIONS,
admin::ADMIN_COMMENTS,
admin::ADMIN_LIKES
];
let mut endpoints = ENDPOINTS.lock().await;
for doc in docs {
@ -154,27 +160,27 @@ pub async fn generate() -> Response {
let html = format!(
r#"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/header.css">
<link rel="stylesheet" href="/css/console.css">
<link rel="stylesheet" href="/css/api.css">
<title>XSSBook - API Documentation</title>
</head>
<body>
<div id="header">
<span class="logo"><a href="/">xssbook</a></span>
<span class="gtext desc" style="margin-left: 6em; font-size: 2em; color: #606770">API Documentation</span>
</div>
<div id="docs">
{data}
</div>
</body>
</html>
"#
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/header.css">
<link rel="stylesheet" href="/css/console.css">
<link rel="stylesheet" href="/css/api.css">
<title>XSSBook - API Documentation</title>
</head>
<body>
<div id="header">
<span class="logo"><a href="/">xssbook</a></span>
<span class="gtext desc" style="margin-left: 6em; font-size: 2em; color: #606770">API Documentation</span>
</div>
<div id="docs">
{data}
</div>
</body>
</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 http;
pub mod like;
pub mod post;
pub mod session;
pub mod user;

View file

@ -1,19 +1,21 @@
use core::fmt;
use serde::Serialize;
use std::collections::HashSet;
use tracing::instrument;
use crate::database;
use crate::types::http::{ResponseCode, Result};
use super::comment::Comment;
#[derive(Serialize)]
pub struct Post {
pub post_id: u64,
pub user_id: u64,
pub content: String,
pub likes: HashSet<u64>,
pub comments: Vec<(u64, String)>,
pub date: u64,
pub likes: u64,
pub liked: bool,
pub comments: Vec<Comment>,
}
impl fmt::Debug for Post {
@ -67,34 +69,4 @@ impl 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(())
}
}