i did things

This commit is contained in:
Tyler Murphy 2023-01-26 17:29:16 -05:00
commit 88209d8823
42 changed files with 3603 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
xssbook.db

1063
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

17
Cargo.toml Normal file
View file

@ -0,0 +1,17 @@
[package]
name = "xssbook"
version = "0.0.1"
edition = "2021"
[dependencies]
tokio = { version = "1.23.0", features = ["full"] }
axum = { version = "0.6.4", features = ["headers"] }
tower-http = { version = "0.3.5", features = ["fs"] }
tower-cookies = "0.8.0"
tower = "0.4.13"
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0"
rusqlite = { version = "0.28.0", features = ["bundled"] }
rand = "0.8.5"
time = "0.3.17"
bytes = "1.3.0"

18
public/404.html Normal file
View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/404.css">
<link rel="stylesheet" href="/css/header.css">
<title>XSSBook - Not Found</title>
</head>
<body>
<div id="header">
<span class="logo"><a href="/">xssbook</a></span>
</div>
<div class="error">
<span class="logo">404</span>
<span class="gtext desc">Page not found.</span>
</div>
</body>

20
public/css/404.css Normal file
View file

@ -0,0 +1,20 @@
body {
background-color: #f0f2f5;
}
.error {
display: flex;
justify-content: center;
align-items: center;
width: 100vw;
height: 100vh;
flex-direction: column;
}
.error .logo {
font-size: 100px;
}
.desc {
font-size: 40px;
}

60
public/css/console.css Normal file
View file

@ -0,0 +1,60 @@
body {
margin: 0;
padding: 0;
background-color: #181818;
display: flex;
flex-direction: column-reverse;
}
@font-face {
font-family: sfpro;
src: url("../fonts/sfpro.otf") format("opentype");
}
div {
background-color: #282828;
font-family: sfpro;
margin: 15px;
margin-bottom: 0px;
border-radius: 5px;
padding: 10px;
width: calc(100% - 50px)
}
span {
display: inline-block;
padding: 0;
margin: 0;
color: #ffffff;
font-family: sfpro;
margin-right: 10px;
}
.json span {
display: inline;
margin: 0;
}
.key {
color: white;
}
.value {
color: white;
}
.boolean {
color: aqua;
}
.null {
color: blue;
}
.number {
color: yellow;
}
.string {
color: #4ae04a
}

55
public/css/header.css Normal file
View file

@ -0,0 +1,55 @@
#header {
height: 3.5em;
background-color: white;
position: fixed;
width: 100vw;
box-shadow: 0 2px 4px rgba(0, 0, 0, .05), 0 8px 16px rgba(0, 0, 0, .05);
display: flex;
align-items: center;
justify-content: space-between;
z-index: 5;
}
.spacer {
margin-bottom: 5em;
}
#header .logo {
position: absolute;
font-size: 2.5em;
padding-left: .5em;
padding-top: .2em;
}
#header .buttons {
flex: 1;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
#header .buttons a {
padding: 0px 50px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #606770;
}
#header .buttons a:hover {
background-color: #dddfe2;
}
.selected {
color: #1778f2 !important;
border-bottom: 3px solid #1778f2;
}
#header .pfp, #header .pfp img {
position: absolute;
right: 1em;
top: .5em;
}

182
public/css/home.css Normal file

File diff suppressed because one or more lines are too long

47
public/css/login.css Normal file
View file

@ -0,0 +1,47 @@
.login {
background-color: #f0f2f5;
display: flex;
justify-content: center;
align-content: center;
flex-direction: row;
padding: 8em 0em;
}
.prompt {
display: flex;
position: relative;
flex-direction: column;
background-color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, .1), 0 8px 16px rgba(0, 0, 0, .1);
border-radius: 8px;
width: 396px;
padding: 10px
}
.show {
display: flex;
flex-direction: column;
justify-content: center;
}
.login-button {
margin-bottom: 10px;
font-size: 20px;
}
.newacc {
margin: 10px 70px;
margin-bottom: 20px;
}
.signacc {
margin: 10px 70px;
margin-bottom: 0;
}
@media (max-aspect-ratio: 2/3) {
.login {
flex-direction: column;
align-items: center;
}
}

307
public/css/main.css Normal file
View file

@ -0,0 +1,307 @@
body {
background-color: white;
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
@font-face {
font-family: facebook;
src: url("../fonts/facebook.otf") format("opentype");
}
@font-face {
font-family: sfpro;
src: url("../fonts/sfpro.otf") format("opentype");
}
@font-face {
font-family: sfprobold;
src: url("../fonts/sfprobold.otf") format("opentype");
}
.logo {
color: #1778f2;
font-size: 3.5em;
font-family: facebook;
}
.text {
font-family: sfpro;
font-size: 28px;
font-weight: normal;
line-height: 32px;
width: 500px;
}
.btext {
font-family: sfpro;
color: #1778f2
}
.error {
font-family: sfpro;
color: #f02849;
padding-top: 10px;
margin-bottom: -10px;
font-size: 15px;
}
.gtext {
font-family: sfpro;
color: #606770
}
.label {
font-family: sfpro;
color: #606770;
font-size: 15px;
padding-top: 10px;
padding-left: 10px;
}
.stext {
font-family: sfpro;
font-size: 10px;
}
.mtext {
font-family: sfpro;
font-size: 15px;
}
.ltext {
font-family: sfpro;
font-size: 22px;
}
.ctext {
display: block;
font-family: sfpro;
text-align: center;
}
a {
color: inherit;
text-decoration: none;
cursor: pointer;
}
p {
padding: 0;
margin: 0;
}
span {
padding: 0;
margin: 0;
}
footer {
bottom: 0;
height: 400px;
background-color: white;
}
input {
flex: 1;
font-family: sfpro;
background-color: white;
padding: 10px;
margin: 10px;
margin-bottom: 0;
border-radius: 5px;
border: 1px solid #dddfe2;
color: #1d2129;
font-size: 18px;
}
.radiomenu {
display: flex;
flex-wrap: wrap;
}
.radiomenu span {
display: inline-block;
position: relative;
font-family: sfpro;
background-color: white;
margin: 10px;
margin-bottom: 0;
border-radius: 5px;
border: 1px solid #dddfe2;
color: #1d2129;
font-size: 15px;
flex: 1 0 auto;
}
.radiomenu span label {
padding: 10px;
display: block;
box-sizing: border-box;
width: auto;
color: #1d2129;
}
[type="radio"] {
height: 40px;
margin: 0;
position: absolute;
right: 10px;
top: 0;
text-align: left;
}
select {
all: unset;
flex: 1;
font-family: sfpro;
background-color: white;
padding: 10px;
margin: 10px;
margin-bottom: 0;
border-radius: 5px;
border: 1px solid #dddfe2;
color: #1d2129;
font-size: 15px;
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYAgMAAACdGdVrAAAADFBMVEVMaXEFBQUFBQUFBQXG+MOgAAAAA3RSTlMAn3BcqiM3AAAAOUlEQVQIW53BsQ2AMAwAMJelV6C80qPYw4k9JmskbqA2px5uNIlcoxF7FmbFxuhckA2iwNzgev3wAR4FDUQbc/qhAAAAAElFTkSuQmCC");
background-position: right 10px center;
background-repeat: no-repeat;
background-size: 15px;
}
input:focus {
border: 1px solid #1778f2;
}
.primary {
all: unset;
font-family: sfpro;
background-color: #1778f2;
color: white;
padding: 10px;
margin: 20px;
border-radius: 5px;
padding-bottom: 15px;
text-align: center;
cursor: pointer;
}
.success {
all: unset;
font-family: sfpro;
background-color: #42b72a;
color: white;
padding: 10px;
margin-left: 10px;
margin-right: 10px;
border-radius: 5px;
text-align: center;
cursor: pointer;
}
.bold {
font-family: sfprobold;
}
.line {
width: calc(100% - 40px);
margin-left: 20px;
margin-right: 20px;
border-bottom: 1px solid #dadde1;
margin-bottom: 10px;
margin-top: 10px;
}
.fullline {
width: calc(100%);
border-bottom: 1px solid #dadde1;
margin-bottom: 10px;
margin-top: 10px;
}
footer {
text-align: center;
font-family: sfpro;
padding-top: 30px;
padding-bottom: 30px;
font-size: 13px;
color: #737373;
}
#popup {
position: absolute;
width: 100vw;
height: 100vh;
background-color: rgba(255, 255, 255, .8);
margin: 0;
padding: 0;
top: 0;
display: flex;
justify-content: center;
align-items: center;
}
.row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.row input {
width: 50%
}
.close {
position: absolute;
z-index: 2;
width: 20px;
height: 20px;
right: 12px;
top: 12px;
cursor: pointer;
background-size: 20px;
background-position: right;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYAgMAAACdGdVrAAAADFBMVEVgZ3FHcExgZ3FgZ3Fd28LEAAAAA3RSTlOfAHBcPEovAAAASklEQVQIW2MIBQMG7FRmaGgmUG5PaKgdkNovGvIPSPkfDf4IpII/+h8FUiF/7EVB2u3/gE3x/4hMQQShSqAaoNqhhkGNhlqE2y0A1E85Y0JErBoAAAAASUVORK5CYII=');
}
.hidden {
visibility: hidden;
pointer-events: none;
display: none !important;
}
.pfp, .pfp img {
display: block;
width: 2.5em;
height: 2.5em;
border-radius: 3em;
background-color: #e4e6e8;
flex-shrink: 0;
}
.nb {
margin-bottom: 0;
}
form {
all: unset;
border-radius: 10px;
margin-left: 10px;
width: 100%;
}
#load {
width: 100%;
display: flex;
justify-content: center;
padding-bottom: 20px;
}
#load a:hover {
border-bottom: #606770 1px solid;
}

44
public/css/people.css Normal file
View file

@ -0,0 +1,44 @@
body {
background-color: #f0f2f5;
}
#users {
display: flex;
flex-direction: column;
align-items: center;
}
.person {
width: 30em;
height: fit-content;
background-color: white;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, .05);
margin-bottom: 1.5em;
display: flex;
flex-direction: row;
}
.profile, .profile img {
border-radius: 10px 0px 0px 10px;
width: 10em;
height: 10em;
padding: 0;
display: block;
background-color: #e4e6e8;
flex-shrink: 0;
}
.info {
margin: 20px;
display: flex;
flex-direction: column;
}
.info span {
width: 280px;
margin-bottom: 5px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}

121
public/css/profile.css Normal file
View file

@ -0,0 +1,121 @@
body {
background-color: #f0f2f5;
}
.spacer {
margin-bottom: 3.5em;
}
#top {
background-color: white;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
box-shadow: 0 2px 4px rgba(0, 0, 0, .05);
}
#banner {
background-image: linear-gradient(#949494, white, white);
height: 30em;
width: 100%;
display: flex;
justify-content: center;
}
#banner div, #banner img {
width: 80em;
height: inherit;
background-color: #e4e6e8;
border-radius: 0px 0px 20px 20px;
}
#info {
width: 80em;
display: flex;
flex-direction: row;
}
.face {
background-color: #e4e6e8;
height: 12em;
width: 12em;
border-radius: 7em;
border: solid 5px white;
margin-top: -2em;
margin-left: 2em;
margin-right: 2em;
}
.infodata {
margin-top: 2em;
display: flex;
flex-direction: column;
}
.infodata span {
margin-bottom: .5em;
}
.profilebuttons {
width: 80em;
height: 3em;
display: flex;
align-items: center;
}
.profilebuttons button {
all: unset;
font-family: sfprobold;
padding: 0px 50px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #606770;
cursor: pointer;
}
.profilebuttons button:hover {
background-color: #dddfe2;
}
.selected {
color: #1778f2 !important;
border-bottom: 3px solid #1778f2 !important;
}
#about {
margin-top: 2em;
align-self: center;
padding: 0;
display: flex;
flex-direction: row;
}
#posts {
margin-top: 2em;
}
#about .ltext {
border-right: 2px solid #dadde1;
padding: 10px;
padding-right: 3em;
}
#about .data {
display: flex;
flex-direction: column;
padding: 10px;
padding-left: 20px;
padding-top: 15px;
}
#about .data span {
margin-bottom: 10px;
width: 28em;
margin-bottom: 5px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}

BIN
public/fonts/facebook.otf Executable file

Binary file not shown.

BIN
public/fonts/sfpro.otf Normal file

Binary file not shown.

BIN
public/fonts/sfprobold.otf Normal file

Binary file not shown.

17
public/home.html Normal file
View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="/css/header.css">
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/home.css">
<title>XSSBook - Home</title>
</head>
<body>
<script src="/js/main.js"></script>
<script src="/js/header.js"></script>
<script src="/js/api.js"></script>
<script src="/js/home.js"></script>
<script>init()</script>
</body>
</html>

63
public/js/api.js Normal file
View file

@ -0,0 +1,63 @@
const endpoint = '/api'
const request = async (url, body, method) => {
if (method === undefined) method = 'POST'
const response = await fetch(endpoint + url, {
method,
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json'
}
});
if (response.status == 401) {
location.href = 'login'
}
const contentType = response.headers.get("content-type");
if (contentType && contentType.indexOf("application/json") !== -1) {
const json = await response.json()
return { status: response.status, msg: json.msg, json }
} else {
const msg = await response.text();
return { status: response.status, msg }
}
}
const login = async (email, password) => {
return await request('/auth/login', {email, password})
}
const register = async (firstname, lastname, email, password, gender, day, month, year) => {
return await request('/auth/register', {firstname, lastname, email, password, gender, day, month, year})
}
const loadpostspage = async (page) => {
return await request('/posts/page', {page})
}
const loadusersposts = async (user_id) => {
return await request('/posts/user', {user_id})
}
const loadusers = async (ids) => {
return await request('/users/load', {ids})
}
const loaduserspage = async (page) => {
return await request('/users/page', {page})
}
const loadself = async () => {
return await request("/users/self", {})
}
const postcomment = async (post_id, content) => {
return await request('/posts/comment', {post_id, content}, 'PATCH')
}
const postlike = async (post_id, state) => {
return await request('/posts/like', {post_id, state}, 'PATCH')
}
const createpost = async (content) => {
return await request('/posts/create', {content})
}

25
public/js/header.js Normal file
View file

@ -0,0 +1,25 @@
function header(home, people) {
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">
</a>
</div>
<div class="spacer"></div>
`
add(html, 'header')
}

233
public/js/home.js Normal file
View file

@ -0,0 +1,233 @@
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
function parseDate(date) {
return months[date.getUTCMonth()] + ' ' + date.getUTCDate() + ', ' + date.getUTCFullYear() + ' ' + date.toLocaleTimeString();
}
function parseComment(comment) {
const author = data.users[comment[0]]
if (author === undefined) {
author = {}
}
const html = `
<div class="comment">
<a class="pfp">
</a>
<span>
<span class="bold mtext">${author.firstname + ' ' + author.lastname}</span>
<p class="mtext">${comment[1]}</p>
</span>
</div>
`
return html
}
function parsePost(post) {
console.log(post.likes)
const 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">
</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">
${post.content.replace(/\n/g,'<br>')}
</p>
<span class="gtext mtext">
${Object.keys(post.likes).map(k => post.likes[k]).filter(v => v !== false).length} Likes
</span>
<div class="fullline nb"></div>
<div class="postbuttons">
<span onclick="like(this)">
<i class="icons like ${post.likes.includes(data.user.user_id) ? 'blue' : ''}"></i>
<span class="bold ${post.likes.includes(data.user.user_id) ? 'blue' : ''}">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 id="comments">
<div class="fullline" style="margin-top: 0"></div>
${post.comments.map(parseComment).join('')}
<div class="comment">
<a class="pfp" href="profile">
</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 id = parseInt(span.parentElement.parentElement.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)
}
render()
}
async function comment(event) {
event.preventDefault();
const text = event.target.elements.text.value.trim();
if (text.length < 1) return;
const id = parseInt(event.target.parentElement.parentElement.parentElement.getAttribute('postid'))
var index = getPost(id);
if (index === -1) return;
const response = await postcomment(id, text)
if (response.status != 200) return;
event.target.elements.text.value = '';
data.posts[index].comments.push([data.user.user_id, text])
render()
}
async function post() {
const text = document.getElementById("text").value.trim()
const error = document.getElementsByClassName('error')[0]
if (text.length < 1) return;
const response = await createpost(text);
if (response.status != 201) {
error.innerHTML = response.msg
return;
}
error.innerHTML = '';
data.posts.unshift({
post_id: response.msg,
user_id: data.user.user_id,
date: Date.now(),
content: text,
likes: [],
comments: []
})
render()
}
function render() {
const html = `
<div id="posts">
<div class="create">
<a class="pfp" href="profile">
</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>
${data.posts.map(p => parsePost(p)).join('')}
</div>
`
add(html, 'posts')
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">
</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>
`
add(popup, 'popup')
const load = `
<div id="load">
<a class="bold gtext" onclick="load()">Load more posts</a>
</div>
`
if (page !== -1) {
add(load, 'load')
} else {
remove('load')
}
}
var page = 0
const data = {
user: {},
users: {},
posts: []
}
async function load() {
const posts = (await loadpostspage(page)).json
if (posts.length === 0) {
page = -1
} else {
page++
}
data.posts.push(... posts)
const batch = []
for (const post of posts) {
for(const comment of post.comments) {
if (data.users[comment[0]] !== undefined) continue
if (batch.includes(comment[0])) continue
batch.push(comment[0])
}
if (data.users[post.user_id] !== undefined) continue
if (batch.includes(post.user_id)) continue
batch.push(post.user_id)
}
const users = (await loadusers(batch)).json
for (const id in users) {
data.users[id] = users[id]
}
render()
}
async function init() {
header(true, false)
data.user = (await loadself()).json
data.users[data.user.user_id] = data.user
load()
}

29
public/js/login.js Normal file
View file

@ -0,0 +1,29 @@
async function onlogin() {
const email = document.getElementById('email').value
const password = document.getElementById('pass').value
const response = await login(email, password)
if (response.status !== 200) {
const error = document.getElementsByClassName('error')[0]
error.innerHTML = response.msg
} else {
location.href = '/home'
}
}
async function onregister() {
const first = document.getElementById('firstname').value
const last = document.getElementById('lastname').value
const email = document.getElementById('newemail').value
const pass = document.getElementById('newpass').value
const month = document.getElementById('month').value
const day = document.getElementById('day').value
const year = document.getElementById('year').value
const gender = document.querySelector('input[name="gender"]:checked').value
const response = await register(first, last, email, pass, gender, parseInt(day), parseInt(month), parseInt(year))
if (response.status !== 200) {
const error = document.getElementsByClassName('error')[1]
error.innerHTML = response.msg
} else {
location.href = '/home'
}
}

22
public/js/main.js Normal file
View file

@ -0,0 +1,22 @@
var range;
function add(html, id) {
const old = document.getElementById(id)
if (old !== null) {
old.remove()
}
if (range === undefined) {
var range = document.createRange()
range.setStart(document.body, 0)
}
document.body.appendChild(
range.createContextualFragment(html)
)
}
function remove(id) {
const old = document.getElementById(id)
if (old !== null) {
old.remove()
}
}

65
public/js/people.js Normal file
View file

@ -0,0 +1,65 @@
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
function parseDate(date) {
return months[date.getUTCMonth()] + ' ' + date.getUTCDate() + ', ' + date.getUTCFullYear() + ' ' + date.toLocaleTimeString();
}
function parseUser(user) {
const html = `
<a class="person" href="/profile?id=${user.id}">
<div class="profile">
</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: ${months[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() {
const html = `
<div id="users">
${data.users.map(u => parseUser(u)).join('')}
</div>
`
add(html, 'users')
const load = `
<div id="load">
<a class="bold gtext" onclick="load()">Load more users</a>
</div>
`
if (page !== -1) {
add(load, 'load')
} else {
remove('load')
}
}
var page = 0
var data = {
users: []
}
async function load() {
const users = (await loaduserspage(page)).json
if (users.length === 0) {
page = -1
} else {
page++
}
data.users.push(... users)
render()
}
header(false, true)
load()

88
public/js/profile.js Normal file
View file

@ -0,0 +1,88 @@
function render() {
const html = `
<div id="top">
<div id="banner">
<div>
</div>
</div>
<div id="info">
<div class="face">
</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;"></div>
<div class="profilebuttons">
<button class="${posts ? 'selected' : ''}" onclick="posts = true; render()">
Posts
</button>
<button class="${posts ? '' : 'selected'}" onclick="posts = false; render()">
About
</button>
</div>
</div>
`
add(html, 'top')
const postsh = `
<div id="posts" class="${posts ? '' : 'hidden'}">
${data.posts.map(p => parsePost(p)).join('')}
</div>
`
add(postsh, 'posts')
const about = `
<div id="about" class="post ${posts ? 'hidden' : ''}">
<span class="bold ltext">About</span>
<div class="data">
<span class="gtext bold">Name: ${data.user.firstname + ' ' + data.user.lastname}</span>
<span class="gtext bold">Email: ${data.user.email}</span>
<span class="gtext bold">Gender: ${data.user.gender}</span>
<span class="gtext bold">Birthday: ${months[data.user.month] + ' ' + data.user.day + ', ' + data.user.year}</span>
<span class="gtext bold">User ID: ${data.user.user_id}</span>
</div>
</div>
`
add(about, 'about')
}
var posts = true
async function load() {
header(false, false)
var params = {};
for (const [key, value] of new URLSearchParams(location.search)) {
params[key] = value
}
const id = params.id !== undefined && !isNaN(params.id) ? parseInt(params.id) : (await loadself()).json.user_id
const posts = (await loadusersposts(id)).json
data.posts.push(... posts)
const batch = [id]
for (const post of posts) {
for(const comment of post.comments) {
if (data.users[comment[0]] !== undefined) continue
if (batch.includes(comment[0])) continue
batch.push(comment[0])
}
if (data.users[post.user_id] !== undefined) continue
if (batch.includes(post.user_id)) continue
batch.push(post.user_id)
}
const users = (await loadusers(batch)).json
for (const user of users) {
data.users[user.user_id] = user
}
data.user = data.users[id]
render()
}
load()

170
public/login.html Normal file
View file

@ -0,0 +1,170 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/login.css">
<script src="/js/api.js"></script>
<script src="/js/login.js"></script>
<title>XSSBook - Login</title>
</head>
<body>
<div class="login">
<div class="show">
<span class="logo">xssbook</span>
<p class="text">Connect with javascript and the world around you on XSSBook.</p>
</div>
<div class="prompt">
<input type="text" name="email" id="email" placeholder="Email" autofocus="1">
<input type="password" name="pass" id="pass" placeholder="Password">
<span class="error ctext"></span>
<button class="primary login-button bold" value="1" name="login" type="submit" id="login" onclick="onlogin()">Log In</button>
<a class="btext ctext">Forgot Password?</a>
<div class="line"></div>
<button class="success newacc" onclick="document.getElementById('popup').classList.remove('hidden')">Create new account</button>
</div>
<div id="popup" class="hidden">
<div class="prompt">
<div class="close" onclick="document.getElementById('popup').classList.add('hidden')"></div>
<span class="text bold">Sign Up</span>
<span class="gtext">It's quick and easy.</span>
<div class="fullline"></div>
<div class="row">
<input type="text" name="firstname" id="firstname" placeholder="First Name" autofocus="1">
<input type="text" name="lastname" id="lastname" placeholder="Last Name">
</div>
<input type="text" name="newemail" id="newemail" placeholder="Email" autofocus="1">
<input type="password" name="newpass" id="newpass" placeholder="Password">
<span class="label">Birthday</span>
<div class="row">
<select name="month" id="month" title="Month">
<option value="1" selected="1">Jan</option>
<option value="2">Feb</option>
<option value="3">Mar</option>
<option value="4">Apr</option>
<option value="5">May</option>
<option value="6">Jun</option>
<option value="7">Jul</option>
<option value="8">Aug</option>
<option value="9">Sep</option>
<option value="10">Oct</option>
<option value="11">Nov</option>
<option value="12">Dec</option>
</select>
<select name="day" id="day" title="Day">
<option value="1" selected="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
<option value="9">9</option>
<option value="10">10</option>
<option value="11">11</option>
<option value="12">12</option>
<option value="13">13</option>
<option value="14">14</option>
<option value="15">15</option>
<option value="16">16</option>
<option value="17">17</option>
<option value="18">18</option>
<option value="19">19</option>
<option value="20">20</option>
<option value="21">21</option>
<option value="22">22</option>
<option value="23">23</option>
<option value="24">24</option>
<option value="25">25</option>
<option value="26">26</option>
<option value="27">27</option>
<option value="28">28</option>
<option value="29">29</option>
<option value="30">30</option>
<option value="31">31</option>
</select>
<select name="year" id="year" title="Year">
<option value="2023" selected="1">2023</option>
<option value="2022">2022</option>
<option value="2021">2021</option>
<option value="2020">2020</option>
<option value="2019">2019</option>
<option value="2018">2018</option>
<option value="2017">2017</option>
<option value="2016">2016</option>
<option value="2015">2015</option>
<option value="2014">2014</option>
<option value="2013">2013</option>
<option value="2012">2012</option>
<option value="2011">2011</option>
<option value="2010">2010</option>
<option value="2009">2009</option>
<option value="2008">2008</option>
<option value="2007">2007</option>
<option value="2006">2006</option>
<option value="2005">2005</option>
<option value="2004">2004</option>
<option value="2003">2003</option>
<option value="2002">2002</option>
<option value="2001">2001</option>
<option value="2000">2000</option>
<option value="1999">1999</option>
<option value="1998">1998</option>
<option value="1997">1997</option>
<option value="1996">1996</option>
<option value="1995">1995</option>
<option value="1994">1994</option>
<option value="1993">1993</option>
<option value="1992">1992</option>
<option value="1991">1991</option>
<option value="1990">1990</option>
<option value="1989">1989</option>
<option value="1988">1988</option>
<option value="1987">1987</option>
<option value="1986">1986</option>
<option value="1985">1985</option>
<option value="1984">1984</option>
<option value="1983">1983</option>
<option value="1982">1982</option>
<option value="1981">1981</option>
<option value="1980">1980</option>
<option value="1979">1979</option>
<option value="1978">1978</option>
<option value="1977">1977</option>
<option value="1976">1976</option>
<option value="1975">1975</option>
<option value="1974">1974</option>
<option value="1973">1973</option>
<option value="1972">1972</option>
<option value="1971">1971</option>
<option value="1970">1970</option>
</select>
</div>
<span class="label">Gender</span>
<div class="radiomenu" data-type="radio" data-name="gender_wrapper">
<span>
<label class="gtext" for="female">Female</label>
<input id="female" type="radio" name="gender" value="Female" checked="true">
</span>
<span>
<label class="gtext" for="male">Male</label>
<input id="male" type="radio" name="gender" value="Male">
</span>
<span>
<label class="gtext" for="lettuce">Lettuce</label>
<input id="lettuce" type="radio" name="gender" value="Lettuce">
</span>
</div>
<span class="error ctext" style="padding-bottom: 10px;"></span>
<span class="label stext">By clicking Sign Up, you agree to have your password stored in plain text and have any javascript run on your pc at any time.</span>
<span class="label stext">XSSBook is not responsible for any ones loss of finances, mental state, relationships, or life when using this site.</span>
<button class="success signacc" onclick="onregister()">Sign Up</button>
</div>
</div>
</div>
<footer>
Metashit © 2023 | This website does not care about you
</footer>
</body>
</html>

15
public/people.html Normal file
View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/people.css">
<link rel="stylesheet" href="/css/header.css">
<title>XSSBook - People</title>
</head>
<body>
<script src="/js/main.js"></script>
<script src="/js/header.js"></script>
<script src="/js/api.js"></script>
<script src="/js/people.js"></script>
</body>

17
public/profile.html Normal file
View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/header.css">
<link rel="stylesheet" href="/css/profile.css">
<link rel="stylesheet" href="/css/home.css">
<title>XSSBook - Profile</title>
</head>
<body>
<script src="/js/main.js"></script>
<script src="/js/header.js"></script>
<script src="/js/api.js"></script>
<script src="/js/home.js"></script>
<script src="/js/profile.js"></script>
</body>

98
src/api/auth.rs Normal file
View file

@ -0,0 +1,98 @@
use axum::{Router, routing::post, response::Response};
use serde::Deserialize;
use time::{OffsetDateTime, Duration};
use tower_cookies::{Cookies, Cookie};
use crate::types::{user::User, response::ResponseCode, session::Session, extract::{Json, AuthorizedUser}};
#[derive(Deserialize)]
struct RegistrationRequet {
firstname: String,
lastname: String,
email: String,
password: String,
gender: String,
day: u8,
month: u8,
year: u32
}
async fn register(cookies: Cookies, Json(body): Json<RegistrationRequet>) -> Response {
let user = match User::new(body.firstname, body.lastname, body.email, body.password, body.gender, body.day, body.month, body.year) {
Ok(user) => user,
Err(err) => return err
};
let session = match Session::new(user.user_id) {
Ok(session) => session,
Err(err) => return err
};
let mut now = OffsetDateTime::now_utc();
now += Duration::weeks(52);
let mut cookie = Cookie::new("auth", session.token);
cookie.set_secure(false);
cookie.set_http_only(false);
cookie.set_expires(now);
cookie.set_path("/");
cookies.add(cookie);
ResponseCode::Created.msg("Successfully created new user")
}
#[derive(Deserialize)]
struct LoginRequest {
email: String,
password: String,
}
async fn login(cookies: Cookies, Json(body): Json<LoginRequest>) -> Response {
let Ok(user) = User::from_email(&body.email) else {
return ResponseCode::BadRequest.msg("Email is not registered")
};
if user.password != body.password {
return ResponseCode::BadRequest.msg("Password is not correct")
}
let session = match Session::new(user.user_id) {
Ok(session) => session,
Err(err) => return err
};
let mut now = OffsetDateTime::now_utc();
now += Duration::weeks(52);
let mut cookie = Cookie::new("auth", session.token);
cookie.set_secure(false);
cookie.set_http_only(false);
cookie.set_expires(now);
cookie.set_path("/");
cookies.add(cookie);
ResponseCode::Success.msg("Successfully logged in")
}
async fn logout(cookies: Cookies, AuthorizedUser(user): AuthorizedUser) -> Response {
cookies.remove(Cookie::new("auth", ""));
if let Err(err) = Session::delete(user.user_id) {
return err
}
ResponseCode::Success.msg("Successfully logged out")
}
pub fn router() -> Router {
Router::new()
.route("/register", post(register))
.route("/login", post(login))
.route("/logout", post(logout))
}

4
src/api/mod.rs Normal file
View file

@ -0,0 +1,4 @@
pub mod auth;
pub mod pages;
pub mod posts;
pub mod users;

58
src/api/pages.rs Normal file
View file

@ -0,0 +1,58 @@
use axum::{Router, response::{Response, Redirect, IntoResponse}, routing::get};
use crate::types::{extract::AuthorizedUser, response::ResponseCode};
async fn root(user: Option<AuthorizedUser>) -> Response {
println!("{}", user.is_some());
if user.is_some() {
return Redirect::to("/home").into_response()
} else {
return Redirect::to("/login").into_response()
}
}
async fn login(user: Option<AuthorizedUser>) -> Response {
if user.is_some() {
return Redirect::to("/home").into_response()
} else {
return ResponseCode::Success.file("/login.html").await.unwrap()
}
}
async fn home(user: Option<AuthorizedUser>) -> Response {
if user.is_none() {
return Redirect::to("/login").into_response()
} else {
return ResponseCode::Success.file("/home.html").await.unwrap()
}
}
async fn people(user: Option<AuthorizedUser>) -> Response {
if user.is_none() {
return Redirect::to("/login").into_response()
} else {
return ResponseCode::Success.file("/people.html").await.unwrap()
}
}
async fn profile(user: Option<AuthorizedUser>) -> Response {
if user.is_none() {
return Redirect::to("/login").into_response()
} else {
return ResponseCode::Success.file("/profile.html").await.unwrap()
}
}
async fn wordpress() -> Response {
ResponseCode::ImATeapot.msg("Hello i am a teapot owo")
}
pub fn router() -> Router {
Router::new()
.route("/", get(root))
.route("/login", get(login))
.route("/home", get(home))
.route("/people", get(people))
.route("/profile", get(profile))
.route("/wp-admin", get(wordpress))
}

102
src/api/posts.rs Normal file
View file

@ -0,0 +1,102 @@
use axum::{response::Response, Router, routing::{post, patch}};
use serde::Deserialize;
use crate::types::{extract::{AuthorizedUser, Json}, post::Post, response::ResponseCode};
#[derive(Deserialize)]
struct PostCreateRequest {
content: String
}
async fn create(AuthorizedUser(user): AuthorizedUser, Json(body): Json<PostCreateRequest>) -> Response {
let Ok(_post) = Post::new(user.user_id, body.content) else {
return ResponseCode::InternalServerError.msg("Failed to create post")
};
ResponseCode::Created.msg("Successfully created new post")
}
#[derive(Deserialize)]
struct PostPageRequest {
page: u64
}
async fn page(AuthorizedUser(_user): AuthorizedUser, Json(body): Json<PostPageRequest>) -> Response {
let Ok(posts) = Post::from_post_page(body.page) else {
return ResponseCode::InternalServerError.msg("Failed to fetch posts")
};
let Ok(json) = serde_json::to_string(&posts) else {
return ResponseCode::InternalServerError.msg("Failed to fetch posts")
};
ResponseCode::Success.json(&json)
}
#[derive(Deserialize)]
struct UsersPostsRequest {
user_id: u64
}
async fn user(AuthorizedUser(_user): AuthorizedUser, Json(body): Json<UsersPostsRequest>) -> Response {
let Ok(posts) = Post::from_user_id(body.user_id) else {
return ResponseCode::InternalServerError.msg("Failed to fetch posts")
};
let Ok(json) = serde_json::to_string(&posts) else {
return ResponseCode::InternalServerError.msg("Failed to fetch posts")
};
ResponseCode::Success.json(&json)
}
#[derive(Deserialize)]
struct PostCommentRequest {
content: String,
post_id: u64
}
async fn comment(AuthorizedUser(user): AuthorizedUser, Json(body): Json<PostCommentRequest>) -> Response {
let Ok(mut post) = Post::from_post_id(body.post_id) else {
return ResponseCode::InternalServerError.msg("Failed to fetch posts")
};
if let Err(err) = post.comment(user.user_id, body.content) {
return err;
}
ResponseCode::Success.msg("Successfully commented on post")
}
#[derive(Deserialize)]
struct PostLikeRequest {
state: bool,
post_id: u64
}
async fn like(AuthorizedUser(user): AuthorizedUser, Json(body): Json<PostLikeRequest>) -> Response {
let Ok(mut post) = Post::from_post_id(body.post_id) else {
return ResponseCode::InternalServerError.msg("Failed to fetch posts")
};
if let Err(err) = post.like(user.user_id, body.state) {
return err;
}
ResponseCode::Success.msg("Successfully changed like status on post")
}
pub fn router() -> Router {
Router::new()
.route("/create", post(create))
.route("/page", post(page))
.route("/user", post(user))
.route("/comment", patch(comment))
.route("/like", patch(like))
}

52
src/api/users.rs Normal file
View file

@ -0,0 +1,52 @@
use axum::{Router, response::Response, routing::post};
use serde::Deserialize;
use crate::types::{extract::{AuthorizedUser, Json}, response::ResponseCode, user::User};
#[derive(Deserialize)]
struct UserLoadRequest {
ids: Vec<u64>
}
async fn load_batch(AuthorizedUser(_user): AuthorizedUser, Json(body): Json<UserLoadRequest>) -> Response {
let users = User::from_user_ids(body.ids);
let Ok(json) = serde_json::to_string(&users) else {
return ResponseCode::InternalServerError.msg("Failed to fetch users")
};
ResponseCode::Success.json(&json)
}
#[derive(Deserialize)]
struct UserPageReqiest {
page: u64
}
async fn load_page(AuthorizedUser(_user): AuthorizedUser, Json(body): Json<UserPageReqiest>) -> Response {
let Ok(users) = User::from_user_page(body.page) else {
return ResponseCode::InternalServerError.msg("Failed to fetch users")
};
let Ok(json) = serde_json::to_string(&users) else {
return ResponseCode::InternalServerError.msg("Failed to fetch users")
};
ResponseCode::Success.json(&json)
}
async fn load_self(AuthorizedUser(user): AuthorizedUser) -> Response {
let Ok(json) = serde_json::to_string(&user) else {
return ResponseCode::InternalServerError.msg("Failed to fetch user")
};
ResponseCode::Success.json(&json)
}
pub fn router() -> Router {
Router::new()
.route("/load", post(load_batch))
.route("/self", post(load_self))
.route("/page", post(load_page))
}

16
src/database/mod.rs Normal file
View file

@ -0,0 +1,16 @@
use rusqlite::Result;
pub mod posts;
pub mod users;
pub mod sessions;
pub fn connect() -> Result<rusqlite::Connection, rusqlite::Error> {
return rusqlite::Connection::open("xssbook.db");
}
pub fn init() -> Result<()> {
users::init()?;
posts::init()?;
sessions::init()?;
Ok(())
}

94
src/database/posts.rs Normal file
View file

@ -0,0 +1,94 @@
use std::collections::HashSet;
use std::time::{SystemTime, UNIX_EPOCH};
use rusqlite::{OptionalExtension, Row};
use crate::types::post::Post;
use crate::database;
pub fn init() -> Result<(), rusqlite::Error> {
let sql = "
CREATE TABLE IF NOT EXISTS posts (
post_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
content TEXT NOT NULL,
likes TEXT NOT NULL,
comments TEXT NOT NULL,
date INTEGER NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(user_id)
);
";
let conn = database::connect()?;
conn.execute(sql, ())?;
Ok(())
}
fn post_from_row(row: &Row) -> Result<Post, rusqlite::Error> {
let post_id = row.get(0)?;
let user_id = row.get(1)?;
let content = row.get(2)?;
let likes_json: String = row.get(3)?;
let comments_json: String = row.get(4)?;
let date = row.get(5)?;
let Ok(likes) = serde_json::from_str(&likes_json) else {
return Err(rusqlite::Error::InvalidQuery)
};
let Ok(comments) = serde_json::from_str(&comments_json) else {
return Err(rusqlite::Error::InvalidQuery)
};
Ok(Post{post_id, user_id, content, likes, comments, date})
}
pub fn get_post(post_id: u64) -> Result<Option<Post>, rusqlite::Error> {
let conn = database::connect()?;
let mut stmt = conn.prepare("SELECT * FROM posts WHERE post_id = ?")?;
let row = stmt.query_row([post_id], |row| Ok(post_from_row(row)?)).optional()?;
Ok(row)
}
pub fn get_post_page(page: u64) -> Result<Vec<Post>, rusqlite::Error> {
let page_size = 10;
let conn = database::connect()?;
let mut stmt = conn.prepare("SELECT * FROM posts ORDER BY post_id DESC LIMIT ? OFFSET ?")?;
let row = stmt.query_map([page_size, page_size * page], |row| Ok(post_from_row(row)?))?;
Ok(row.into_iter().flatten().collect())
}
pub fn get_users_posts(user_id: u64) -> Result<Vec<Post>, rusqlite::Error> {
let conn = database::connect()?;
let mut stmt = conn.prepare("SELECT * FROM posts WHERE user_id = ? ORDER BY post_id DESC")?;
let row = stmt.query_map([user_id], |row| Ok(post_from_row(row)?))?;
Ok(row.into_iter().flatten().collect())
}
pub fn add_post(user_id: u64, content: &str) -> Result<Post, rusqlite::Error> {
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 = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64;
let conn = database::connect()?;
let mut stmt = conn.prepare("INSERT INTO posts (user_id, content, likes, comments, date) VALUES(?,?,?,?,?) RETURNING *;")?;
let post = stmt.query_row((user_id, content, likes_json, comments_json, date), |row| Ok(post_from_row(row)?))?;
Ok(post)
}
pub fn update_post(post_id: u64, likes: &HashSet<u64>, comments: &Vec<(u64, String)>) -> Result<(), rusqlite::Error> {
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(())
}

42
src/database/sessions.rs Normal file
View file

@ -0,0 +1,42 @@
use rusqlite::OptionalExtension;
use crate::{database, types::session::Session};
pub fn init() -> Result<(), rusqlite::Error> {
let sql = "
CREATE TABLE IF NOT EXISTS sessions (
user_id INTEGER PRIMARY KEY NOT NULL,
token TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(user_id)
);
";
let conn = database::connect()?;
conn.execute(sql, ())?;
Ok(())
}
pub fn get_session(token: &str) -> Result<Option<Session>, rusqlite::Error> {
let conn = database::connect()?;
let mut stmt = conn.prepare("SELECT * FROM sessions WHERE token = ?")?;
let row = stmt.query_row([token], |row| {
Ok(Session {
user_id: row.get(0)?,
token: row.get(1)?,
})
}).optional()?;
Ok(row)
}
pub fn set_session(user_id: u64, token: &str) -> Result<(), Box<dyn std::error::Error>> {
let conn = database::connect()?;
let sql = "INSERT OR REPLACE INTO sessions (user_id, token) VALUES (?, ?);";
conn.execute(sql, (user_id, token))?;
Ok(())
}
pub fn delete_session(user_id: u64) -> Result<(), Box<dyn std::error::Error>> {
let conn = database::connect()?;
let sql = "DELETE FROM sessions WHERE user_id = ?;";
conn.execute(sql, [user_id])?;
Ok(())
}

79
src/database/users.rs Normal file
View file

@ -0,0 +1,79 @@
use std::time::{SystemTime, UNIX_EPOCH};
use rusqlite::{OptionalExtension, Row};
use crate::{database, types::user::User};
pub fn init() -> Result<(), rusqlite::Error> {
let sql = "
CREATE TABLE IF NOT EXISTS users (
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
firstname VARCHAR(20) NOT NULL,
lastname VARCHAR(20) NOT NULL,
email VARCHAR(50) NOT NULL,
password VARCHAR(50) NOT NULL,
gender VARCHAR(100) NOT NULL,
date BIGINT NOT NULL,
day TINYINT NOT NULL,
month TINYINT NOT NULL,
year INTEGER NOT NULL
);
";
let conn = database::connect()?;
conn.execute(sql, ())?;
Ok(())
}
fn user_from_row(row: &Row, hide_password: bool) -> Result<User, rusqlite::Error> {
let user_id = row.get(0)?;
let firstname = row.get(1)?;
let lastname = row.get(2)?;
let email = row.get(3)?;
let password = row.get(4)?;
let gender = row.get(5)?;
let date = row.get(6)?;
let day = row.get(7)?;
let month = row.get(8)?;
let year = row.get(9)?;
let password = if hide_password { "".to_string() } else { password };
Ok(User{user_id, firstname, lastname, email, password, gender,date, day, month, year})
}
pub fn get_user_by_id(user_id: u64, hide_password: bool) -> Result<Option<User>, rusqlite::Error> {
let conn = database::connect()?;
let mut stmt = conn.prepare("SELECT * FROM users WHERE user_id = ?")?;
let row = stmt.query_row([user_id], |row| Ok(user_from_row(row, hide_password)?)).optional()?;
Ok(row)
}
pub fn get_user_by_email(email: &str, hide_password: bool) -> Result<Option<User>, rusqlite::Error> {
let conn = database::connect()?;
let mut stmt = conn.prepare("SELECT * FROM users WHERE email = ?")?;
let row = stmt.query_row([email], |row| Ok(user_from_row(row, hide_password)?)).optional()?;
Ok(row)
}
pub fn get_user_by_password(password: &str, hide_password: bool) -> Result<Option<User>, rusqlite::Error> {
let conn = database::connect()?;
let mut stmt = conn.prepare("SELECT * FROM users WHERE password = ?")?;
let row = stmt.query_row([password], |row| Ok(user_from_row(row, hide_password)?)).optional()?;
Ok(row)
}
pub fn get_user_page(page: u64, hide_password: bool) -> Result<Vec<User>, rusqlite::Error> {
let page_size = 5;
let conn = database::connect()?;
let mut stmt = conn.prepare("SELECT * FROM users ORDER BY user_id DESC LIMIT ? OFFSET ?")?;
let row = stmt.query_map([page_size, page_size * page], |row| Ok(user_from_row(row, hide_password)?))?;
Ok(row.into_iter().flatten().collect())
}
pub fn add_user(firstname: &str, lastname: &str, email: &str, password: &str, gender: &str, day: u8, month: u8, year: u32) -> Result<User, rusqlite::Error> {
let date = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64;
let conn = database::connect()?;
let mut stmt = conn.prepare("INSERT INTO users (firstname, lastname, email, password, gender, date, day, month, year) VALUES(?,?,?,?,?,?,?,?,?) RETURNING *;")?;
let user = stmt.query_row((firstname, lastname, email, password, gender, date, day, month, year), |row| Ok(user_from_row(row, false)?))?;
Ok(user)
}

48
src/main.rs Normal file
View file

@ -0,0 +1,48 @@
use std::net::SocketAddr;
use axum::{Router, response::Response, http::Request, middleware::{Next, self}};
use tower_cookies::CookieManagerLayer;
use types::response::ResponseCode;
use crate::api::{pages, auth, users, posts};
mod api;
mod database;
mod types;
async fn serve<B>(req: Request<B>, next: Next<B>) -> Response {
let Ok(file) = ResponseCode::Success.file(&req.uri().to_string()).await else {
return next.run(req).await
};
file
}
async fn not_found() -> Response {
match ResponseCode::NotFound.file("/404.html").await {
Ok(file) => file,
Err(err) => err
}
}
#[tokio::main]
async fn main() {
database::init().unwrap();
let app = Router::new()
.fallback(not_found)
.layer(middleware::from_fn(serve))
.nest("/", pages::router())
.nest("/api/auth", auth::router())
.nest("/api/users", users::router())
.nest("/api/posts", posts::router())
.layer(CookieManagerLayer::new());
let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
println!("Listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}

65
src/types/extract.rs Normal file
View file

@ -0,0 +1,65 @@
use std::io::Read;
use axum::{extract::{FromRequestParts, FromRequest}, async_trait, response::Response, http::{request::Parts, Request}, TypedHeader, headers::Cookie, body::HttpBody, BoxError};
use bytes::Bytes;
use serde::de::DeserializeOwned;
use crate::types::{user::User, response::{ResponseCode, Result}, session::Session};
pub struct AuthorizedUser(pub User);
#[async_trait]
impl<S> FromRequestParts<S> for AuthorizedUser where S: Send + Sync {
type Rejection = Response;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self> {
let Ok(Some(cookies)) = Option::<TypedHeader<Cookie>>::from_request_parts(parts, state).await else {
return Err(ResponseCode::Forbidden.msg("No cookies provided"))
};
let Some(token) = cookies.get("auth") else {
return Err(ResponseCode::Forbidden.msg("No auth token provided"))
};
let Ok(session) = Session::from_token(&token) else {
return Err(ResponseCode::Unauthorized.msg("Auth token invalid"))
};
let Ok(user) = User::from_user_id(session.user_id, true) else {
return Err(ResponseCode::InternalServerError.msg("Valid token but no valid user"))
};
Ok(AuthorizedUser(user))
}
}
pub struct Json<T>(pub T);
#[async_trait]
impl<T, S, B> FromRequest<S, B> for Json<T> where
T: DeserializeOwned,
B: HttpBody + Send + 'static,
B::Data: Send,
B::Error: Into<BoxError>,
S: Send + Sync,
{
type Rejection = Response;
async fn from_request(req: Request<B>, state: &S) -> Result<Self> {
let Ok(bytes) = Bytes::from_request(req, state).await else {
return Err(ResponseCode::InternalServerError.msg("Failed to read request body"));
};
let Ok(string) = String::from_utf8(bytes.bytes().flatten().collect()) else {
return Err(ResponseCode::BadRequest.msg("Invalid utf8 body"))
};
let Ok(value) = serde_json::from_str(&string) else {
return Err(ResponseCode::BadRequest.msg("Invalid request body"))
};
Ok(Json(value))
}
}

5
src/types/mod.rs Normal file
View file

@ -0,0 +1,5 @@
pub mod user;
pub mod post;
pub mod session;
pub mod extract;
pub mod response;

83
src/types/post.rs Normal file
View file

@ -0,0 +1,83 @@
use std::collections::HashSet;
use serde::Serialize;
use crate::database;
use crate::types::response::{Result, ResponseCode};
#[derive(Serialize)]
pub struct Post {
pub post_id: u64,
pub user_id: u64,
pub content: String,
pub likes: HashSet<u64>,
pub comments: Vec<(u64, String)>,
pub date: u64
}
impl Post {
pub fn from_post_id(post_id: u64) -> Result<Self> {
let Ok(Some(post)) = database::posts::get_post(post_id) else {
return Err(ResponseCode::BadRequest.msg("Post does not exist"))
};
Ok(post)
}
// pub fn from_post_ids(post_ids: Vec<u64>) -> Vec<Self> {
// post_ids.iter().map(|id| {
// let Ok(post) = Post::from_post_id(*id) else {
// return None;
// };
// Some(post)
// }).flatten().collect()
// }
pub fn from_post_page(page: u64) -> Result<Vec<Self>> {
let Ok(posts) = database::posts::get_post_page(page) else {
return Err(ResponseCode::BadRequest.msg("Failed to fetch posts"))
};
Ok(posts)
}
pub fn from_user_id(user_id: u64) -> Result<Vec<Self>> {
let Ok(posts) = database::posts::get_users_posts(user_id) else {
return Err(ResponseCode::BadRequest.msg("Failed to fetch posts"))
};
Ok(posts)
}
pub fn new(user_id: u64, content: String) -> Result<Self> {
let Ok(post) = database::posts::add_post(user_id, &content) else {
return Err(ResponseCode::InternalServerError.msg("Failed to create post"))
};
Ok(post)
}
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() {
return Err(ResponseCode::InternalServerError.msg("Failed to comment on post"))
}
Ok(())
}
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() {
return Err(ResponseCode::InternalServerError.msg("Failed to comment on post"))
}
Ok(())
}
}

60
src/types/response.rs Normal file
View file

@ -0,0 +1,60 @@
use axum::{response::{IntoResponse, Response}, http::{StatusCode, Request, HeaderValue}, body::Body, headers::HeaderName};
use tower::ServiceExt;
use tower_http::services::ServeFile;
#[derive(Debug)]
pub enum ResponseCode {
Success,
Created,
BadRequest,
Unauthorized,
Forbidden,
NotFound,
ImATeapot,
InternalServerError
}
impl ResponseCode {
pub fn code(self) -> StatusCode {
match self {
Self::Success => StatusCode::OK,
Self::Created => StatusCode::CREATED,
Self::BadRequest => StatusCode::BAD_REQUEST,
Self::Unauthorized => StatusCode::UNAUTHORIZED,
Self::Forbidden => StatusCode::FORBIDDEN,
Self::NotFound => StatusCode::NOT_FOUND,
Self::ImATeapot => StatusCode::IM_A_TEAPOT,
Self::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR
}
}
pub fn msg(self, msg: &str) -> Response {
(self.code(), msg.to_owned()).into_response()
}
pub fn json(self, json: &str) -> Response {
let mut res = (self.code(), json.to_owned()).into_response();
res.headers_mut().insert(
HeaderName::from_static("content-type"), HeaderValue::from_static("application/json"),
);
res
}
pub async fn file(self, path: &str) -> Result<Response> {
if path.chars().position(|c| c == '.' ).is_none() {
return Err(ResponseCode::BadRequest.msg("Folders cannot be served"));
}
let path = format!("public{}", path);
let svc = ServeFile::new(path);
let Ok(mut res) = svc.oneshot(Request::new(Body::empty())).await else {
return Err(ResponseCode::InternalServerError.msg("Error wile fetching file"));
};
if res.status() != StatusCode::OK {
return Err(ResponseCode::NotFound.msg("File not found"));
}
*res.status_mut() = self.code();
Ok(res.into_response())
}
}
pub type Result<T> = std::result::Result<T, Response>;

38
src/types/session.rs Normal file
View file

@ -0,0 +1,38 @@
use rand::{distributions::Alphanumeric, Rng};
use serde::Serialize;
use crate::database;
use crate::types::response::{Result, ResponseCode};
#[derive(Serialize)]
pub struct Session {
pub user_id: u64,
pub token: String
}
impl Session {
pub fn from_token(token: &str) -> Result<Self> {
let Ok(Some(session)) = database::sessions::get_session(token) else {
return Err(ResponseCode::BadRequest.msg("Invalid auth token"));
};
Ok(session)
}
pub fn new(user_id: u64) -> Result<Self> {
let token: String = rand::thread_rng().sample_iter(&Alphanumeric).take(32).map(char::from).collect();
match database::sessions::set_session(user_id, &token) {
Err(_) => return Err(ResponseCode::BadRequest.msg("Failed to create session")),
Ok(_) => return Ok(Session {user_id, token})
};
}
pub fn delete(user_id: u64) -> Result<()> {
if let Err(_) = database::sessions::delete_session(user_id) {
return Err(ResponseCode::InternalServerError.msg("Failed to logout"));
};
Ok(())
}
}

79
src/types/user.rs Normal file
View file

@ -0,0 +1,79 @@
use serde::{Serialize, Deserialize};
use crate::database;
use crate::types::response::{Result, ResponseCode};
#[derive(Serialize, Deserialize, Debug)]
pub struct User {
pub user_id: u64,
pub firstname: String,
pub lastname: String,
pub email: String,
pub password: String,
pub gender: String,
pub date: u64,
pub day: u8,
pub month: u8,
pub year: u32,
}
impl User {
pub fn from_user_id(user_id: u64, hide_password: bool) -> Result<Self> {
let Ok(Some(user)) = database::users::get_user_by_id(user_id, hide_password) else {
return Err(ResponseCode::BadRequest.msg("User does not exist"))
};
Ok(user)
}
pub fn from_user_ids(user_ids: Vec<u64>) -> Vec<Self> {
user_ids.iter().map(|user_id| {
let Ok(Some(user)) = database::users::get_user_by_id(*user_id, true) else {
return None;
};
Some(user)
}).flatten().collect()
}
pub fn from_user_page(page: u64) -> Result<Vec<Self>> {
let Ok(users) = database::users::get_user_page(page, true) else {
return Err(ResponseCode::BadRequest.msg("Failed to fetch users"))
};
Ok(users)
}
pub fn from_email(email: &str) -> Result<Self> {
let Ok(Some(user)) = database::users::get_user_by_email(email, false) else {
return Err(ResponseCode::BadRequest.msg("User does not exist"))
};
Ok(user)
}
pub fn from_password(password: &str) -> Result<Self> {
let Ok(Some(user)) = database::users::get_user_by_password(password, true) else {
return Err(ResponseCode::BadRequest.msg("User does not exist"))
};
Ok(user)
}
pub fn new(firstname: String, lastname: String, email: String, password: String, gender: String, day: u8, month: u8, year: u32) -> Result<Self> {
if let Ok(_) = User::from_email(&email) {
return Err(ResponseCode::BadRequest.msg(&format!("Email is already in use by {}", &email)))
}
if let Ok(user) = User::from_password(&password) {
return Err(ResponseCode::BadRequest.msg(&format!("Password is already in use by {}", user.email)))
}
let Ok(user) = database::users::add_user(&firstname, &lastname, &email, &password, &gender, day, month, year) else {
return Err(ResponseCode::InternalServerError.msg("Failed to create new uesr"))
};
Ok(user)
}
}