refactor
This commit is contained in:
parent
d85dd163e3
commit
3d71da4909
29 changed files with 1339 additions and 657 deletions
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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();
|
||||||
|
|
|
@ -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
249
public/js/components.js
Normal 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)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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() {
|
function render() {
|
||||||
|
|
||||||
|
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()
|
const posts = await load()
|
||||||
data.posts.push(... posts)
|
data.posts.push(... posts)
|
||||||
const posts_block = document.getElementById("posts")
|
|
||||||
for (p of posts) {
|
const el = document.getElementById("posts")
|
||||||
append(parsePost(p), posts_block)
|
|
||||||
|
for (const post of posts) {
|
||||||
|
el.appendChild(
|
||||||
|
parsePost(post, data.users, data.self)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}},
|
||||||
|
parse('Load more posts')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
function render() {
|
document.body.replaceWith(new_body)
|
||||||
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)
|
|
||||||
|
|
||||||
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
|
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])
|
|
||||||
}
|
|
||||||
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) {
|
for (const user of users) {
|
||||||
data.users[user.user_id] = user
|
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()
|
|
@ -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
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
console.log(html, container, before)
|
|
||||||
var range = document.createRange()
|
for (const child of children) {
|
||||||
range.setStart(container, 0);
|
if (child == null) {
|
||||||
container.insertBefore(
|
continue
|
||||||
range.createContextualFragment(html),
|
}
|
||||||
before
|
el.appendChild(child)
|
||||||
)
|
}
|
||||||
|
|
||||||
|
return el
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
|
||||||
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) {
|
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
|
||||||
|
}
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,151 +68,182 @@ 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({},
|
||||||
const postsh = `
|
...header(false, false, data.self.user_id),
|
||||||
<div id="posts" class="${posts ? '' : 'hidden'}">
|
div({id: 'top'},
|
||||||
${data.posts.map(p => parsePost(p)).join('')}
|
div({id: 'banner'},
|
||||||
</div>
|
div({class: 'bg'},
|
||||||
<div id="load">
|
banner(data.user.user_id)
|
||||||
<a class="bold gtext" onclick="loadMore()">Load more posts</a>
|
),
|
||||||
</div>
|
isself ? div({class: 'changebanner', onclick: () => changeimage(updatebanner)}) : parse(''),
|
||||||
`
|
),
|
||||||
|
div({id: 'info'},
|
||||||
if (data.posts.length > 0) {
|
div({class: 'face'},
|
||||||
append(postsh)
|
pfp(data.user.user_id),
|
||||||
}
|
isself ? div({class: 'changeavatar', onclick: () => changeimage(updateavatar)}) : parse(''),
|
||||||
|
),
|
||||||
const about = `
|
div({class: 'infodata'},
|
||||||
<div id="about" class="post ${posts ? 'hidden' : ''}">
|
span({class: 'bold ltext'},
|
||||||
<span class="bold ltext">About</span>
|
parse(data.user.firstname + ' ' + data.user.lastname)
|
||||||
<div class="data">
|
),
|
||||||
<span class="gtext bold">Name: ${data.user.firstname + ' ' + data.user.lastname}</span>
|
span({class: 'gtext'},
|
||||||
<span class="gtext bold">Email: ${data.user.email}</span>
|
parse('Joined ' + parseDate(new Date(data.user.date)))
|
||||||
<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({class: 'fullline', style: 'width: 80em; margin-bottom: 0; z-index: 0;'}),
|
||||||
</div>
|
div({class: 'profilebuttons'},
|
||||||
`
|
button({id: 'profilepostbutton', class: 'selected', onclick: () => swap(true)},
|
||||||
|
parse('Posts')
|
||||||
append(about)
|
),
|
||||||
|
button({id: 'profileaboutbutton', onclick: () => swap(false)},
|
||||||
const popup = `
|
parse('About')
|
||||||
<div id="popup" class="hidden">
|
),
|
||||||
<div class="createpost">
|
div({style: 'flex: 20'}),
|
||||||
<div class="close" onclick="document.getElementById('popup').classList.add('hidden')"></div>
|
isself ? button({class: 'logout', onclick: async () => {
|
||||||
<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()
|
const response = await logout()
|
||||||
if (response.status != 200) return;
|
if (response.status != 200) return;
|
||||||
location.href = '/login'
|
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 () => {
|
||||||
|
|
||||||
async function loadMore() {
|
|
||||||
const posts = await load()
|
const posts = await load()
|
||||||
data.posts.push(... posts)
|
data.posts.push(... posts)
|
||||||
const posts_block = document.getElementById("posts")
|
|
||||||
for (p of posts) {
|
const el = document.getElementById("posts")
|
||||||
append(parsePost(p), posts_block)
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
batch.push(comment[0])
|
|
||||||
}
|
|
||||||
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) {
|
for (const user of users) {
|
||||||
data.users[user.user_id] = user
|
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(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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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::{
|
||||||
|
public::docs::{EndpointDocumentation, EndpointMethod},
|
||||||
|
types::{
|
||||||
extract::{AuthorizedUser, Check, CheckResult, Json, Log},
|
extract::{AuthorizedUser, Check, CheckResult, Json, Log},
|
||||||
http::ResponseCode,
|
http::ResponseCode,
|
||||||
session::Session,
|
session::Session,
|
||||||
user::User,
|
user::User,
|
||||||
}, public::docs::{EndpointDocumentation, EndpointMethod}};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
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,
|
||||||
};
|
};
|
||||||
|
|
114
src/api/posts.rs
114
src/api/posts.rs
|
@ -5,26 +5,33 @@ use axum::{
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{types::{
|
use crate::{
|
||||||
|
public::docs::{EndpointDocumentation, EndpointMethod},
|
||||||
|
types::{
|
||||||
|
comment::Comment,
|
||||||
extract::{AuthorizedUser, Check, CheckResult, Json},
|
extract::{AuthorizedUser, Check, CheckResult, Json},
|
||||||
http::ResponseCode,
|
http::ResponseCode,
|
||||||
|
like::Like,
|
||||||
post::Post,
|
post::Post,
|
||||||
}, public::docs::{EndpointDocumentation, EndpointMethod}};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
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))
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
use crate::{types::{
|
use crate::{
|
||||||
|
public::docs::{EndpointDocumentation, EndpointMethod},
|
||||||
|
types::{
|
||||||
extract::{AuthorizedUser, Check, CheckResult, Json, Png},
|
extract::{AuthorizedUser, Check, CheckResult, Json, Png},
|
||||||
http::ResponseCode,
|
http::ResponseCode,
|
||||||
user::User,
|
user::User,
|
||||||
}, public::docs::{EndpointDocumentation, EndpointMethod}};
|
},
|
||||||
|
};
|
||||||
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
91
src/database/comments.rs
Normal 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
77
src/database/likes.rs
Normal 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())
|
||||||
|
}
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(())
|
|
||||||
}
|
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
|
44
src/types/comment.rs
Normal file
44
src/types/comment.rs
Normal 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
48
src/types/like.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue