Borders v2.0.0 - Pirate Stache

Now using handlebars instead of format! for page generation. Added
quick and dirty logging with SimpleLogger. `Board` objects now have a
`last_active` field.

Bit of a version gap, but whatever. This is a significant enough update
I think it deserves a change in the major version.

0 Warnings, 0 Errors

Handlebars eliminated the XSS vulnerability because it automatically
escapes text.

Added comments to src/main.rs

Moved some static HTML blobs into static/ rather than baking them into
the binary.

Now licensed under Zlib

libBoarders is now compiled separately as a library. It may have VERY
little application, but whatever.

Made banner 3D, and thus cooler. Replaced "Boarders" with the banner
used in the backend.
This commit is contained in:
saw 2021-07-14 22:30:53 -04:00
parent f10be383f9
commit 2404b04e76
23 changed files with 567 additions and 509 deletions

View File

@ -1,7 +1,9 @@
[package] [package]
name = "board_server" name = "board_server"
version = "1.0.1" version = "2.0.0"
edition = "2018" edition = "2018"
license = "Zlib"
repository = "https://git.solow.xyz/cgit.cgi/Boarders/"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -13,4 +15,19 @@ serde = "1.0.125"
serde_json = "1.0" serde_json = "1.0"
bcrypt-bsd = "0.1.3" bcrypt-bsd = "0.1.3"
rand = "0.8.3" rand = "0.8.3"
console = "0.14.1"
colored = "2.0.0" colored = "2.0.0"
tokio = {version = "1.7.1", features=["net", "rt-multi-thread", "sync", "macros"]}
futures = "0.3.15"
handlebars = "4.0.1"
maplit = "1.0.2"
simple_logger = "1.11.0"
[lib]
name = "boarders"
path = "src/lib/mod.rs"
test = false
bench = false
doc = false
harness = false
edition = "2018"

View File

@ -61,7 +61,7 @@ pub async fn new(
} }
}, form.content.clone()); }, form.content.clone());
println!("new message"); println!("new message");
a.add_message(msg.clone()); a.clone().add_message(msg.clone());
Ok(HttpResponse::Created().body(serde_json::to_string(&msg).unwrap())) Ok(HttpResponse::Created().body(serde_json::to_string(&msg).unwrap()))
}, },
None => Err(HttpResponse::BadRequest().body("Bad Request: Board does not exist")) None => Err(HttpResponse::BadRequest().body("Bad Request: Board does not exist"))

View File

@ -0,0 +1,3 @@
fn main() {
println!("Client");
}

View File

@ -1,14 +1,21 @@
use actix_web::{ use actix_web::{
get, post, web, get, post, web,
HttpResponse, Responder, HttpResponse,
cookie::Cookie cookie::Cookie
}; };
use actix_files::NamedFile;
use crate::api::types::*; use crate::api::types::*;
use crate::lib::users::{User, Account}; use crate::lib::users::{User, Account};
use crate::ThreadData;
use handlebars::to_json;
use maplit::btreemap;
#[post("/account/auth")] #[post("/account/auth")]
pub async fn auth_html( pub async fn auth_html(
tdata: web::Data<crate::ThreadData<'_>>,
data: web::Data<AppState>, data: web::Data<AppState>,
form: web::Form<CryptoUserForm> form: web::Form<CryptoUserForm>
) -> HttpResponse { ) -> HttpResponse {
@ -17,22 +24,10 @@ pub async fn auth_html(
match u.verify(form.password.clone()) { match u.verify(form.password.clone()) {
Ok(a) => if a { Ok(a) => if a {
let mut response = HttpResponse::Ok() let mut response = HttpResponse::Ok()
.body(format!(r#" .body(tdata.handlebars.render("auth", &btreemap!("user" => u.clone())).unwrap());
<html><head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
<link rel="stylesheet" type="text/css" href="/static/index.css">
</head><body>
<div id="title" class="bar">
<h1>Hello, {0}</h1>
</div><div id="index">
<p>
Logged in as {0}, return to <a href="/boards/">index</a>
</p>
</body></html></div></body></html>
"#, u.username));
response.add_cookie( response.add_cookie(
&Cookie::build("auth", format!("{}&{}", u.user_id, u.secret)) &Cookie::build("auth", format!("{}&{}", u.user_id, u.secret))
.domain("boards.solow.xyz") .domain("localhost")
//.domain("localhost") //.domain("localhost")
.path("/") .path("/")
.same_site(actix_web::cookie::SameSite::Strict) .same_site(actix_web::cookie::SameSite::Strict)
@ -51,36 +46,15 @@ Logged in as {0}, return to <a href="/boards/">index</a>
} }
#[get("/account/login/")] #[get("/account/login/")]
pub async fn login() -> impl Responder { pub async fn login() -> actix_web::Result<NamedFile> {
HttpResponse::Ok().body(r#" Ok(NamedFile::open("static/account_login.html")?)
<html><head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
<link rel="stylesheet" type="text/css" href="/static/index.css">
</head><body>
<div id="title" class="bar">
<h1>Login</h1>
</div><div id="index">
<div class="auth">
<form action="/account/auth" method="post">
<div>
<label for="username">Username: </label>
<input type="text" id="username" name="username" required>
</div>
<div>
<label for="password">Password: </label>
<input type="password" id="password" name="password" required>
</div>
<input type="submit" value="Login">
</form>
</div>
</body></html></div></body></html>
"#)
} }
#[post("/account/new")] #[post("/account/new")]
pub async fn sign_up_result( pub async fn sign_up_result(
data: web::Data<AppState>, data: web::Data<AppState>,
form: web::Form<CryptoUserForm> form: web::Form<CryptoUserForm>,
tdata: web::Data<ThreadData<'_>>
) -> HttpResponse { ) -> HttpResponse {
println!("new user"); println!("new user");
let mut um = data.user_manager.lock().unwrap(); let mut um = data.user_manager.lock().unwrap();
@ -93,44 +67,12 @@ pub async fn sign_up_result(
} }
}; };
um.add_user(user.clone()); um.add_user(user.clone());
HttpResponse::Created().body(format!(r#" HttpResponse::Created().body(tdata.handlebars.render("new_user_redirect", &btreemap! {
<html><head> "user" => to_json(user.username)
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css"> }).unwrap())
<link rel="stylesheet" type="text/css" href="/static/index.css">
</head><body>
<div id="title" class="bar">
<h1>Hello, {0}</h1>
</div><div id="index">
<p>
Hooray! You can log into you new account <a href="/account/login/">here</a>
</p>
</body></html></div></body></html>
"#, user.username))
} }
#[get("/account/new/")] #[get("/account/new/")]
pub async fn sign_up() -> impl Responder { pub async fn sign_up() -> actix_web::Result<NamedFile> {
HttpResponse::Ok().body(r#" Ok(NamedFile::open("static/new_account.html")?)
<html><head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
<link rel="stylesheet" type="text/css" href="/static/index.css">
</head><body>
<div id="title" class="bar">
<h1>Sign Up</h1>
</div><div id="index">
<div class="auth">
<form action="/account/new" method="post">
<div>
<label for="username">Username: </label>
<input type="text" id="username" name="username" required>
</div>
<div>
<label for="password">Password: </label>
<input type="password" id="password" name="password" required>
</div>
<input type="submit" value="Sign Up">
</form>
</div>
</body></html></div></body></html>
"#)
} }

View File

@ -1,7 +1,6 @@
use std::{ use std::{
time, path::PathBuf,
time::Duration, time::SystemTime
path::PathBuf
}; };
use actix_web::{ use actix_web::{
@ -14,59 +13,60 @@ use crate::{
boards::Board, boards::Board,
users::{UserCookie, User} users::{UserCookie, User}
}, },
api::types::* api::types::*,
ThreadData
}; };
use handlebars::to_json;
use maplit::btreemap;
use super::TimeDerive;
#[get("/boards/")] #[get("/boards/")]
pub async fn list_boards( pub async fn list_boards(
req: HttpRequest,
data: web::Data<AppState>, data: web::Data<AppState>,
tdata: web::Data<ThreadData<'_>>
) -> impl Responder { ) -> impl Responder {
let now = time::SystemTime::now().duration_since(time::UNIX_EPOCH).unwrap(); let cookie = req.cookie("auth");
let mut ab = false; let user = if let Some(c) = cookie {
if let Ok(v) = UserCookie::parse(c.value()) {
data.user_manager.lock().unwrap().get_user_by_id(v.id)
} else {
None
}
} else {
None
};
let boards = (*data.boards.lock().unwrap()).clone(); let boards = (*data.boards.lock().unwrap()).clone();
HttpResponse::Ok().body(format!( let mut boards_json = to_json(&boards);
r#" let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap();
<html><head> let boards_json = boards_json.as_array_mut().unwrap().iter_mut().zip(boards).map(|x| -> &serde_json::Value {
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css"> x.0.as_object_mut().unwrap().insert(
<link rel="stylesheet" type="text/css" href="/static/index.css"> "time_meta".to_string(),
</head><body> if let Some(t) = x.1.last_active {
<div id="title" class="bar"> to_json(std::time::Duration::from_secs(t).derive_time_since(now).map(|d| (format!("{:.2}", d.0), d.1)))
<div> } else {
<h1>Board Index</h1> to_json::<Option<u8>>(None)
</div> }
<div id="login-status"> );
<p><a href="/account/login/">Login</a>/<a href="/account/new/">Sign Up</a></p> //println!("{:?}", x.0);
</div> x.0
</div><div id="index"> }).collect::<Vec<&serde_json::Value>>();
<a href="/boards/new/" style="margin-bottom: 0.5em;">New Board [+]</a> HttpResponse::Ok().body(tdata.handlebars.render("board_index", &btreemap!(
<table style="width: 100%;" cellpadding=8px> "boards" => to_json(boards_json),
<tr><th>Name</th><th>Description</th><th>Last Active</th></tr> "user" => to_json(user)
{} )).unwrap())
</table></body></html></div></body></html>
"#,
boards.iter().map(|b| {
ab = !ab;
format!(
"<tr class=\"{}\"><td><a href=\"/board/{}/\">{}</a></td><td>{}</td><td>{}</td></tr>",
if ab { "a" } else { "b" }, b.id, b.title, b.desc,
if let Some(a) = b.messages.borrow().last() {
format!("{:.2} minutes", now.checked_sub(Duration::from_secs(a.timestamp)).unwrap().as_secs()/60)
} else {
String::from("Never")
}
)
}).collect::<String>()
))
} }
#[get("/board/{board_id}/")] #[get("/board/{board_id}/")]
pub async fn get_board( pub async fn get_board(
req: HttpRequest, req: HttpRequest,
tdata: web::Data<ThreadData<'_>>,
data: web::Data<AppState>, data: web::Data<AppState>,
web::Path(board_id): web::Path<u32> web::Path(board_id): web::Path<u32>
) -> HttpResponse { ) -> HttpResponse {
let mut ab = false;
let now = time::SystemTime::now().duration_since(time::UNIX_EPOCH).unwrap();
let boards = data.boards.lock().unwrap(); let boards = data.boards.lock().unwrap();
let cookie = req.cookie("auth"); let cookie = req.cookie("auth");
let uid = if cookie.is_some() { let uid = if cookie.is_some() {
@ -75,58 +75,15 @@ pub async fn get_board(
UserCookie::parse("null") UserCookie::parse("null")
}; };
if let Some(a) = Board::search(boards.iter(), board_id) { if let Some(a) = Board::search(boards.iter(), board_id) {
HttpResponse::Ok().body(format!( let data = btreemap!(
r#" "messages" => to_json(&a.messages),
<html><head> "user" => match uid {
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css"> Ok(a) => to_json(data.user_manager.lock().unwrap().get_user_by_id(a.id)),
<link rel="stylesheet" type="text/css" href="/static/index.css"> Err(_) => to_json("null")
</head> },
<body> "board" => to_json(a)
<div id="title" class="bar"> );
<div><h1> HttpResponse::Ok().body(tdata.handlebars.render("board", &data).unwrap())
{} | <a href="/boards/"><- Back to index</a>
</h1></div>
<div id="description">
{}
</div>
<div id="login-status">
<p><a href="/account/login/">Login</a>/<a href="/account/new/">Sign Up</a></p>
</div>
</div>
<div id="index">
<table style="width: 100%;" cellpadding=8px>
{}
</table>
<form action="/boards/post" method="post">
<label for="content">Content:</label><br/>
<input type="hidden" id="board_id" name="board_id" value="{}">
<input type="hidden" id="user_id" name="user_id" value="{}">
<textarea name="content" id="content" rows=12 cols=50 placeholder="Enter message here..."></textarea>
<br/><input type="submit" value="Post Message" style="margin: 0.5em;">
</form>
</div></body></html>
"#,
a.title,
a.desc,
a.messages.borrow().iter().map(|x| {
ab = !ab;
format!(
"<tr class=\"message {}\"><td>{}</td><td>{}</td><td>{}</td></tr>",
if ab { "a" } else { "b" },
{
let author = x.author.clone().into_inner();
author.nick.clone().or_else(|| Some(author.username.clone())).unwrap()
},
x.text.replace('\n', "<br>"),
format!("{:.2} min.", now.checked_sub(Duration::from_secs(x.timestamp)).unwrap().as_secs()/60)//x.timestamp
)
}).collect::<String>(),
board_id,
match uid {
Ok(a) => a.id.to_string(),
Err(_e) => "null".to_string()
}
))
} else { } else {
HttpResponse::NotFound().body("That board could not be found") HttpResponse::NotFound().body("That board could not be found")
} }
@ -150,11 +107,12 @@ pub async fn board_post(
} }
}; };
let mut mf = data.msg_factory.lock().unwrap(); let mut mf = data.msg_factory.lock().unwrap();
let b = data.boards.lock().unwrap(); let mut b = data.boards.lock().unwrap();
let board = b.iter().find(|x| (x.id == form.board_id)); let mut board = b.iter_mut().find(|x| (x.id == form.board_id));
let um = data.user_manager.lock().unwrap(); let um = data.user_manager.lock().unwrap();
match board { match board {
Some(a) => { Some(ref mut a) => {
//let a: &mut Board = board.unwrap();
let msg = mf.create_message({ let msg = mf.create_message({
let user = um.get_user_by_id(form.user_id).ok_or_else(|| { let user = um.get_user_by_id(form.user_id).ok_or_else(|| {
HttpResponse::NotFound().body("Not Found: User does not exist") HttpResponse::NotFound().body("Not Found: User does not exist")
@ -166,6 +124,8 @@ pub async fn board_post(
} }
}, form.content.clone()); }, form.content.clone());
println!("new message"); println!("new message");
//println!("board: {{ id: {}, name: {} }}{:?}", a.id, a.title, msg);
//let a = a.clone().add_message(msg);
a.add_message(msg); a.add_message(msg);
Ok(HttpResponse::SeeOther().header("location", format!("/board/{}/", a.id)).finish()) Ok(HttpResponse::SeeOther().header("location", format!("/board/{}/", a.id)).finish())
}, },
@ -198,29 +158,7 @@ pub async fn new_board(
return HttpResponse::BadRequest().body("Bad Request: User does not exist"); return HttpResponse::BadRequest().body("Bad Request: User does not exist");
} }
HttpResponse::Ok().body(r#" HttpResponse::Ok().body(include_str!("../../static/new_board.html"))
<html><head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
<link rel="stylesheet" type="text/css" href="/static/index.css">
</head><body>
<div id="title" class="bar">
<h1>New Board</h1>
</div><div id="index">
<div class="auth">
<form action="/boards/new" method="post">
<div>
<label for="name">Board Title: </label><br>
<input type="text" id="name" name="name" required>
</div>
<div>
<label for="description">Board Description: </label>
<input type="text" id="description" name="description" required>
</div>
<input type="submit" value="Create Board">
</form>
</div>
</body></html></div></body></html>
"#)
} }
#[post("/boards/new")] #[post("/boards/new")]

View File

@ -1,158 +0,0 @@
body {
margin: unset !important;
padding: unset;
max-width: unset;
}
.bar {
padding: 0.5em;
background-color: #111111;
z-index: 1;
display: flex;
flex-direction: row;
}
.bar > div {
flex-grow: 1;
align-self: center;
}
.bar > div > h1 {
font-size: xx-large;
margin: unset;
}
.bar > div > p {
margin-left: auto;
margin-right: 0;
margin-right: 2em;
}
.bar > h1 {
width: fit-content;
height: fit-content;
}
.bar > h1 > a {
font-size: 0.5em;
vertical-align: middle;
}
#login-status {
display: contents;
}
#description {
color: #777777;
}
#title {
width: 100%;
height: 3em;
position: inherit;
top: 0;
left: 0;
margin-bottom: 1rem;
padding-left: 1em;
}
#title > h1 {
font-size: 2.5em !important;
}
#nav {
width: 3em;
position: fixed;
right: 0;
top: 0;
margin-left: 3rem;
height: 100%;
}
.arrow {
width: 2.5rem;
background-color: #07a6ea;
color: white;
padding: 0.25rem;
border-radius: 0.5em;
}
.orange-arrow {
width: 2.5rem;
color: orange;
padding: 0.25rem;
border-radius: 0.5em;
}
.down {
transform: rotate(180deg);
-webkit-transform:rotate(180deg);
-moz-transform: rotate(180deg);
-ms-transform: rotate(180deg);
-o-transform: rotate(180deg);
}
#navbar > img.arrow.down {
unset: top;
bottom: 0;
margin-bottom: 0.5em;
}
#navbar > img.arrow {
position: relative;
}
.message {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
.message.right {
}
.message.left {
}
.vertical-center {
margin: 0;
position: absolute;
top: 50%;
-ms-transform: translateY(-50%);
transform: translateY(-50%);
}
table {
font-size: 1em;
}
table, th, td {
border-collapse: collapse;
}
th {
text-align: left;
margin-bottom: 1em;
}
tr.a {
background-color: #3A3A3A;
}
tr.b {
background-color: inherit;
}
.auth {
margin: auto;
width: 20em;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
border: 2px solid white;
border-radius: 5px;
background-color: #3A3A3A;
}
#index {
padding: 3em;
padding-top: 0;
}

View File

@ -1,2 +1,22 @@
pub mod accounts; pub mod accounts;
pub mod boards; pub mod boards;
use std::time::Duration;
trait TimeDerive {
fn derive_time_since(&self, time: Duration) -> Option<(f64, String)>;
}
impl TimeDerive for Duration {
fn derive_time_since(&self, time: Duration) -> Option<(f64, String)> {
time.checked_sub(*self).map(|a| {
match a.as_secs() {
t if t > 604800 => (t as f64/60.0/60.0/24.0/7.0, "weeks".to_string()),
t if t > 86400 => (t as f64/60.0/60.0/24.0, "days".to_string()),
t if t > 3600 => (t as f64/60.0/60.0, "hours".to_string()),
t if t > 120 => (t as f64/60.0, "mins".to_string()),
t => (t as f64, "secs".to_string())
}
})
}
}

View File

@ -1,17 +0,0 @@
<div id="nav" class="bar">
<img class="arrow up" src="https://static.solow.xyz/boards/arrow.svg">
<img class="arrow down" style="bottom: 0; position: absolute; right: 0; margin-right: 9; margin-bottom: 28;" src="https://static.solow.xyz/boards/arrow.svg">
<div class="vertical-center">
<!--<img class="orange-arrow right" src="https://static.solow.xyz/boards/arrow.svg">-->
<a href="../">
Next<br>
Page
</a>
<hr>
<a href="../">
Prev<br>
Page
</a>
<!--<img class="orange-arrow left" src="https://static.solow.xyz/boards/arrow.svg">-->
</div>
</div>

View File

@ -13,8 +13,6 @@ use super::{
messages::Message messages::Message
}; };
//use colored::*;
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Board { pub struct Board {
pub title: String, pub title: String,
@ -23,15 +21,22 @@ pub struct Board {
pub creation: u64, pub creation: u64,
pub id: u32, pub id: u32,
pub path: String, pub path: String,
pub owner: u32 pub owner: u32,
pub last_active: Option<u64>
} }
impl Board { impl Board {
pub fn open<P>(path: P) -> Result<Board> where P: AsRef<Path> { pub fn open<P>(path: P) -> Result<Board> where P: AsRef<Path> {
match File::open(&path) { let mut b: std::result::Result<Board, std::io::Error> = match File::open(&path) {
Ok(a) => Ok(serde_json::from_reader(a).unwrap()), Ok(a) => Ok(serde_json::from_reader(a).unwrap()),
Err(e) => Err(e) Err(e) => Err(e)
};
if let Ok(ref mut b) = b {
if b.last_active.is_none() {
b.last_active = b.messages.borrow().last().map(|x| x.timestamp);
}
} }
b
} }
pub fn search<'a, I>( pub fn search<'a, I>(
@ -44,8 +49,10 @@ impl Board {
iter.find(|x| x.id == id) iter.find(|x| x.id == id)
} }
pub fn add_message(&self, msg: Message) { pub fn add_message(&mut self, msg: Message) -> &Board {
self.last_active = Some(msg.timestamp);
self.messages.borrow_mut().push(msg); self.messages.borrow_mut().push(msg);
self
} }
} }
@ -89,21 +96,10 @@ impl BoardFactory {
creation: SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(), creation: SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(),
id: self.assign_id(), id: self.assign_id(),
path: path.to_str().unwrap().to_string(), path: path.to_str().unwrap().to_string(),
owner: creator owner: creator,
last_active: None
}; };
Ok(b) Ok(b)
/*
println!("board path: {:?}", path);
fs::create_dir_all(&path).unwrap();
path.push("board.json");
Board::open(&path).or_else(|_e| -> Result<Board> {
if !path.exists() {
let mut file = File::create(&path).unwrap();
file.write_all(serde_json::to_string(&b).unwrap().as_bytes())?;
}
Board::open(path.parent().unwrap())
})
*/
} }
} }

View File

@ -58,11 +58,7 @@ impl FactoryFactory {
pub fn build(&self) -> (BoardFactory, MessageFactory, UserManager) { pub fn build(&self) -> (BoardFactory, MessageFactory, UserManager) {
(BoardFactory::new(self.data_dir.clone()), (BoardFactory::new(self.data_dir.clone()),
MessageFactory { MessageFactory::new(self.data_dir.clone()),
next_id: 0,
last_write: None,
data_dir: self.data_dir.clone()
},
UserManager::new(self.data_dir.clone())) UserManager::new(self.data_dir.clone()))
} }
} }

View File

@ -5,6 +5,7 @@ use std::{
ReadDir ReadDir
}, },
path::Path, path::Path,
error::Error
}; };
use actix_web::{ use actix_web::{
@ -17,8 +18,7 @@ use lib::{
*, *,
Archiveable, Archiveable,
boards::{ boards::{
Board, Board, BoardFactory
BoardFactory
}, },
users::UserManager, users::UserManager,
messages::MessageFactory messages::MessageFactory
@ -33,62 +33,34 @@ use html::{
boards::* boards::*
}; };
/* use tokio::{
struct PageBuilder { sync::{oneshot, watch},
body: &'static str, net::{
style: Option<String>, UnixStream, UnixListener,
table_labels: Option<String> },
runtime::Runtime,
select
};
use handlebars::Handlebars;
use console::style;
use maplit::btreemap;
pub struct ThreadData<'a> {
handlebars: Handlebars<'a>
} }
impl PageBuilder { use simple_logger::SimpleLogger;
fn new(text: &'static str) -> PageBuilder {
PageBuilder {
body: text,
style: None,
table_labels: None
}
}
fn build<S>(self, style: S, table_labels: S) -> String where S: AsRef<str> {
String::from(self.body)
.replacen("{}", style.as_ref(), 1)
.replacen("{}", table_labels.as_ref(), 1)
}
}
*/
#[get("/")] #[get("/")]
async fn index() -> impl Responder { async fn index(
HttpResponse::Ok().body(format!(r#" tdata: web::Data<ThreadData<'_>>
<html><head> ) -> impl Responder {
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css"> HttpResponse::Ok().body(
</head> tdata.handlebars.render("welcome", &btreemap!(
<body> "version" => env!("CARGO_PKG_VERSION").to_string()
<h1>Boarders v{}</h1> )).unwrap()
<p> )
<h2>What is it?</h2>
Boarders is an easily deployable message board written in Rust.
It is extremely efficient on both the frontend and backend.
On the frontent all pages are static and (at this time) have no JS, only CSS/HTML.
The backend uses the 5th fastest web framework, Actix, according to <a href="https://www.techempower.com/benchmarks/">techempower.com</a>.
Which ensures that response times will always be snappy.
</p>
<p>
<hr>
<h2>Rules</h2>
<ol>
<li>No NSFW<br>
At least until NSFW channels are implemented</li>
<li>Don't spam boards or user accounts<br>
It's just anoying, and if it gets too bad I may have to hard reset everything<br>
Don't ruin the fun for everyone</li>
<li>Have Fun!<br>Pretty much anything goes, but if people don't like you, that's your fault</li>
</ol>
</p>
<h3 style="text-align: center;">------<a href="/boards/">[Get Started]</a>------</h3>
</body>
</html>
"#, env!("CARGO_PKG_VERSION")))
} }
#[get("/debug/")] #[get("/debug/")]
@ -100,29 +72,40 @@ async fn debug(
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
/* quick and dirty logging */
SimpleLogger::new().init().unwrap();
println!("Initializing..."); println!("Initializing...");
/*
* TODO: config files
* TODO: allow host to specify the data dir via a config file
* why? why not? maybe you want multiple servers running on
* the same machine
* TODO: allow host to specify config file via cmd line (overrides default name)
*/
let data_dir = Path::new("data/"); let data_dir = Path::new("data/");
let mut pwd = env::current_dir().unwrap(); let mut pwd = env::current_dir().unwrap();
let board_files: ReadDir = match fs::read_dir({ /* open/create the directory where board data is placed */
let board_files: ReadDir = fs::read_dir({
pwd.push(data_dir); pwd.push(data_dir);
pwd.push("boards/"); pwd.push("boards/");
&pwd &pwd
}) { }).unwrap_or({
Ok(a) => a, fs::create_dir_all(&pwd).unwrap();
Err(_e) => { fs::read_dir(pwd).unwrap()
fs::create_dir_all(&pwd).unwrap(); });
fs::read_dir(pwd).unwrap()
}
};
let mut boards: Vec<Board> = Vec::new(); let mut boards: Vec<Board> = Vec::new();
/*
* Iterate over all the files inside the board data dir and
* open/create them. Will panic! if there is an error opening
* a board
*/
for file in board_files.map(|x| x.unwrap()) { for file in board_files.map(|x| x.unwrap()) {
let mut path = file.path().to_path_buf(); if file.file_type().unwrap().is_dir() {
//let meta = fs::metadata(&path).unwrap(); let mut path = file.path().to_path_buf();
let ftype = file.file_type().unwrap();
if ftype.is_dir() {
path.push("board.json"); path.push("board.json");
println!("loading board: {:?}", path); println!("loading board: {:?}", path);
boards.push( boards.push(
@ -133,7 +116,9 @@ async fn main() -> std::io::Result<()> {
} }
} }
/* sort boards by id so they display correctly on the board index */
boards.sort_by(|a,b| a.id.cmp(&b.id)); boards.sort_by(|a,b| a.id.cmp(&b.id));
println!("{}", style(format!("Found and loaded {} boards!", boards.len())).green());
let mut board_data = std::path::PathBuf::new(); let mut board_data = std::path::PathBuf::new();
board_data.push(&data_dir); board_data.push(&data_dir);
@ -142,6 +127,7 @@ async fn main() -> std::io::Result<()> {
board_data.push("boards/"); board_data.push("boards/");
bf.set_data_dir(board_data); bf.set_data_dir(board_data);
/* Initialize all the the shared data crap. copy/pasting code time!! */
let web_data = web::Data::new(AppState { let web_data = web::Data::new(AppState {
boards: Mutex::new(boards.clone()), boards: Mutex::new(boards.clone()),
board_factory: Mutex::new(BoardFactory::load_state({ board_factory: Mutex::new(BoardFactory::load_state({
@ -164,39 +150,65 @@ async fn main() -> std::io::Result<()> {
}).unwrap_or(um)) }).unwrap_or(um))
}); });
let wb = web_data.clone(); let wb = web_data.clone();
println!("{}", style("Loaded/Created application data files!").green());
println!("Finished initialization"); println!("{}", style("Finished initialization").green().bold());
println!("Starting server..."); println!("Starting server...");
let server = HttpServer::new(move || { let server = HttpServer::new(move || {
App::new() App::new()
.app_data(web_data.clone()) .data(ThreadData {
.service(index) handlebars: {
.service(debug) let mut h = Handlebars::new();
.service(list_boards) /*
.service(login) * TODO: allow user to specify wether or not templates should be
.service(auth_html) * loaded dynamically or statically, probably via environment
.service(sign_up) * variable at compile time
.service(sign_up_result) */
.service(new_board) /* register handlebars partials here */
.service(new_board_result) h.register_partial("login_status", include_str!("res/snippets/login_status.hbs")).unwrap();
.service(get_board) h.register_partial("board_bar", include_str!("res/snippets/board_bar.hbs")).unwrap();
.service(board_post) /* register handlebars templates here */
.service( h.register_template_string("board_index", include_str!("res/board_index.hbs")).unwrap();
web::scope("/api") h.register_template_string("welcome", include_str!("res/welcome.hbs")).unwrap();
.service(api::boards::new) h.register_template_string("auth", include_str!("res/auth.hbs")).unwrap();
.service(api::boards::list) h.register_template_string("board", include_str!("res/board.hbs")).unwrap();
.service(api::messages::new) h
.service(api::users::new) }
.service(api::users::list) }).app_data(web_data.clone())
.service(api::users::get) .service(index)
.service(api::users::auth)) .service(debug)
.service(list_boards)
.service(login)
.service(auth_html)
.service(sign_up)
.service(sign_up_result)
.service(new_board)
.service(new_board_result)
.service(get_board)
.service(board_post)
.service(
/* add all api services under the /api/ scope */
web::scope("/api")
.service(api::boards::new)
.service(api::boards::list)
.service(api::messages::new)
.service(api::users::new)
.service(api::users::list)
.service(api::users::get)
.service(api::users::auth))
/* serve all static files inside ../static/ */
.service(actix_files::Files::new("/static/", "./static/").show_files_listing()) .service(actix_files::Files::new("/static/", "./static/").show_files_listing())
}).bind("127.0.0.1:8080")? }).bind("127.0.0.1:8080")?.run();
.run();
println!("Started server"); println!("Started server");
println!("{}v{}", include_str!("res/banner"), env!("CARGO_PKG_VERSION")); println!("{}v{}", include_str!("res/banner"), env!("CARGO_PKG_VERSION"));
/*
* Program will block here until it is Ctrl-C 'd on the command line
* TODO: Block until cmd line exit OR client shutdown command
*/
let res = server.await; let res = server.await;
println!("\nExiting..."); println!("\nExiting...");
/* /*
* Note to self: handle all errors as much as possible * Note to self: handle all errors as much as possible
@ -214,28 +226,35 @@ async fn main() -> std::io::Result<()> {
} }
wb.board_factory wb.board_factory
.lock() .lock()
.unwrap() .unwrap()
.save_state("factory.json") .save_state("factory.json")
.unwrap_or_else( .unwrap_or_else(
|x| { println!("Could not save user factory state: {}", x) } |x| { println!("Could not save user factory state: {}", x) }
); );
wb.msg_factory wb.msg_factory
.lock() .lock()
.unwrap() .unwrap()
.save_state("message_factory.json") .save_state("message_factory.json")
.unwrap_or_else( .unwrap_or_else(
|x| { println!("Could not save message factory state: {}", x) } |x| { println!("Could not save message factory state: {}", x) }
); );
wb.user_manager wb.user_manager
.lock() .lock()
.unwrap() .unwrap()
.save_state("users.json") .save_state("users.json")
.unwrap_or_else( .unwrap_or_else(
|x| { println!("Could not save user data: {}", x) } |x| { println!("Could not save user data: {}", x) }
); );
/* send shutdown signal to all unix socket threads */
println!("Shutting down client threads...");
if s_tx.send(AppPhase::Shutdown).is_err() {
println!("{}", style("Could not update the app phase to Shutdown").red().bold());
}
rt.shutdown_timeout(std::time::Duration::from_secs(10));
println!("Done!"); println!("Done!");
res res
} }

19
src/res/auth.hbs Normal file
View File

@ -0,0 +1,19 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
<link rel="stylesheet" type="text/css" href="/static/index.css">
</head>
<body>
<div id="title" class="bar">
{{#if user.nick}}
<h1>Hello, {{user.nick}}</h1>
{{else}}
<h1>Hello, {{user.nickname}}</h1>
{{/if}}
</div><div id="index">
<p>
Logged in as {{user.username}}, return to <a href="/boards/">index</a>
</p>
</div>
</body>
</html>

View File

@ -1,5 +1,6 @@
____ __ ____ __
/ __ ) ____ ____ _ _____ ____/ /___ _____ _____ / __ )\ ____ ____ _ _____ ____/ /\__ _____ _____
/ __ |/ __ \ / __ `// ___// __ // _ \ / ___// ___/ / __ | / __ \ / __ `/\/ ___// __ // _ \ / ___// ___/\
/ /_/ // /_/ // /_/ // / / /_/ // __// / (__ ) / /_/ / / /_/ // /_/ / / / _// /_/ // __/\/ / _/(__ )\/
/_____/ \____/ \__,_//_/ \__,_/ \___//_/ /____/ /_____/ /\____/ \__,_/ /_/ / \__,_/ \___/ /_/ / /____/\)
\_____\/ \___\/ \___\/\_\/ \___\/ \__\/\_\/ \____\/

42
src/res/board.hbs Normal file
View File

@ -0,0 +1,42 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
<link rel="stylesheet" type="text/css" href="/static/index.css">
</head>
<body>
{{> board_bar board=board}}
<div id="index">
<table style="width: 100%;" cellpadding=8px>
<!--messages-->
{{#each messages as | msg |}}
<tr style="border-bottom: 1px solid #555" class="message">
<td>
{{#if msg.author.nick}}
<div style="display: flex; flex-direction: column; align-items: center;">
{{msg.author.nick}}<br>
<div style="color: #777777;">{{author.username}}</div>
</div>
{{else}}
{{msg.author.username}}
{{/if}}
</td>
<td>
{{msg.text}}
</td>
<td>
{{msg.timestamp}}
</td>
</tr>
{{/each}}
</table>
<form action="/boards/post" method="post">
<label for="content">Content:</label><br/>
<input type="hidden" id="board_id" name="board_id" value="{{board.id}}">
<input type="hidden" id="user_id" name="user_id" value="{{user.user_id}}">
<textarea name="content" id="content" rows=12 cols=50 placeholder="Enter message here..."></textarea>
<br/>
<input type="submit" value="Post Message" style="margin: 0.5em;">
</form>
</div>
</body>
</html>

31
src/res/board_index.hbs Normal file
View File

@ -0,0 +1,31 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
<link rel="stylesheet" type="text/css" href="/static/index.css">
</head>
<body>
<div id="title" class="bar">
<div>
<h1>Board Index</h1>
</div>
{{> login_status}}
</div>
<div id="index">
<a href="/boards/new/" style="margin-bottom: 0.5em;">New Board [+]</a>
<table style="width: 100%;" cellpadding=8px>
<tr><th>Name</th><th>Description</th><th>Last Active</th></tr>
{{#each boards}}
<tr class="board"><td><a href="/board/{{id}}/">{{title}}</a></td><td>{{desc}}</td>
<td>
{{#if time_meta}}
{{time_meta.0}} {{time_meta.1}}
{{else}}
Never
{{/if}}
</td>
</tr>
{{/each}}
</table>
</div>
</body>
</html>

View File

@ -0,0 +1,15 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
<link rel="stylesheet" type="text/css" href="/static/index.css">
</head>
<body>
<div id="title" class="bar">
<h1>Hello, {{name}}</h1>
</div>
<div id="index">
<p>
Hooray! You can log into you new account <a href="/account/login/">here</a>
</p>
</body>
</html>

View File

@ -0,0 +1,11 @@
<div id="title" class="bar">
<div>
<h1>
{{board.title}} | <a href="/boards/">&lt;- Back to index</a>
</h1>
</div>
<div id="description">
{{board.desc}}
</div>
{{> login_status}}
</div>

View File

@ -0,0 +1,17 @@
<div id="login-status">
<p>
{{#if user}}
<a href="/account/dashboard/">
{{#with user}}
{{#if nick}}
Welcome, {{nick}}
{{else}}
Welcome, {{username}}
{{/if}}
{{/with}}
</a>
{{else}}
<a href="/account/login/">Login</a>/<a href="/account/new/">Sign Up</a>
{{/if}}
</p>
</div>

50
src/res/welcome.hbs Normal file
View File

@ -0,0 +1,50 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
</head>
<body>
<h1>
<div style="display: flex; align-items: center; flex-direction: column;">
<pre style="font-size: 1.5rem; background-color: unset; overflow-x: unset; margin: unset; padding: unset;">
<b>
____ __
/ __ )\ ____ ____ _ _____ ____/ /\__ _____ _____
/ __ | / __ \ / __ `/\/ ___// __ // _ \ / ___// ___/\
/ /_/ / / /_/ // /_/ / / / _// /_/ // __/\/ / _/(__ )\/
/_____/ /\____/ \__,_/ /_/ / \__,_/ \___/ /_/ / /____/\)
\_____\/ \___\/ \___\/\_\/ \___\/ \__\/\_\/ \____\/
</b>
</pre>
<div style="font-size: 2rem;">
v{{version}}
</div>
</div>
</h1>
<p>
<h2>What is it?</h2>
Boarders is an easily deployable message board written in Rust.
It is extremely efficient on both the frontend and backend.
On the frontent all pages are static and (at this time) have no JS, only CSS/HTML.
The backend uses the 5th fastest web framework, Actix, according to
<a href="https://www.techempower.com/benchmarks/">techempower.com</a>.
Which ensures that response times will always be snappy.
</p>
<p>
<hr>
<h2>Rules</h2>
<ol>
<li>No NSFW<br>
At least until NSFW channels are implemented</li>
<li>
Don't spam boards or user accounts<br>
It's just anoying, and if it gets too bad I may have to hard reset everything<br>
Don't ruin the fun for everyone
</li>
<li>Have Fun!<br>
Pretty much anything goes, but if people don't like you, that's your fault
</li>
</ol>
</p>
<h3 style="text-align: center;">------<a href="/boards/">[Get Started]</a>------</h3>
</body>
</html>

25
static/account_login.html Normal file
View File

@ -0,0 +1,25 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
<link rel="stylesheet" type="text/css" href="/static/index.css">
</head>
<body>
<div id="title" class="bar">
<h1>Login</h1>
</div>
<div id="index">
<div class="auth">
<form action="/account/auth" method="post">
<div>
<label for="username">Username: </label>
<input type="text" id="username" name="username" required>
</div>
<div>
<label for="password">Password: </label>
<input type="password" id="password" name="password" required>
</div>
<input type="submit" value="Login">
</form>
</div>
</body>
</html>

View File

@ -105,6 +105,22 @@ body {
.message { .message {
margin-top: 0.5em; margin-top: 0.5em;
margin-bottom: 0.5em; margin-bottom: 0.5em;
border-bottom: 1px solid #555;
}
@keyframes msg-hover {
from {
background-color: inherit;
}
to {
background-color: #5a5a5a
}
}
tr.message:hover {
animation-duration: 0.2s;
animation-name: msg-hover;
background-color: #5a5a5a
} }
.message.right { .message.right {
@ -156,3 +172,26 @@ tr.b {
padding: 3em; padding: 3em;
padding-top: 0; padding-top: 0;
} }
@keyframes board-hover {
from {
background-color: inherit;
}
to {
background-color: #444;
}
}
tr.board {
border-bottom: 1px solid #555;
}
tr.board:nth-child(1n+1) {
font-weight: lighter;
}
tr.board:hover {
animation-duration: 0.25s;
animation-name: board-hover;
background-color: #444;
}

24
static/new_account.html Normal file
View File

@ -0,0 +1,24 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
<link rel="stylesheet" type="text/css" href="/static/index.css">
</head>
<body>
<div id="title" class="bar">
<h1>Sign Up</h1>
</div><div id="index">
<div class="auth">
<form action="/account/new" method="post">
<div>
<label for="username">Username: </label>
<input type="text" id="username" name="username" required>
</div>
<div>
<label for="password">Password: </label>
<input type="password" id="password" name="password" required>
</div>
<input type="submit" value="Sign Up">
</form>
</div>
</body>
</html>

28
static/new_board.html Normal file
View File

@ -0,0 +1,28 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
<link rel="stylesheet" type="text/css" href="/static/index.css">
</head>
<body>
<div id="title" class="bar">
<h1>New Board</h1>
</div>
<div id="index">
<div class="auth">
<form action="/boards/new" method="post">
<div>
<label for="name">Board Title: </label><br>
<input type="text" id="name" name="name" required>
</div>
<div>
<label for="description">Board Description: </label>
<input type="text" id="description" name="description" required>
</div>
<input type="submit" value="Create Board">
</form>
</div>
</body>
</html>
</div>
</body>
</html>