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]
name = "board_server"
version = "1.0.1"
version = "2.0.0"
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
@ -13,4 +15,19 @@ serde = "1.0.125"
serde_json = "1.0"
bcrypt-bsd = "0.1.3"
rand = "0.8.3"
console = "0.14.1"
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());
println!("new message");
a.add_message(msg.clone());
a.clone().add_message(msg.clone());
Ok(HttpResponse::Created().body(serde_json::to_string(&msg).unwrap()))
},
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::{
get, post, web,
HttpResponse, Responder,
HttpResponse,
cookie::Cookie
};
use actix_files::NamedFile;
use crate::api::types::*;
use crate::lib::users::{User, Account};
use crate::ThreadData;
use handlebars::to_json;
use maplit::btreemap;
#[post("/account/auth")]
pub async fn auth_html(
tdata: web::Data<crate::ThreadData<'_>>,
data: web::Data<AppState>,
form: web::Form<CryptoUserForm>
) -> HttpResponse {
@ -17,22 +24,10 @@ pub async fn auth_html(
match u.verify(form.password.clone()) {
Ok(a) => if a {
let mut response = HttpResponse::Ok()
.body(format!(r#"
<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));
.body(tdata.handlebars.render("auth", &btreemap!("user" => u.clone())).unwrap());
response.add_cookie(
&Cookie::build("auth", format!("{}&{}", u.user_id, u.secret))
.domain("boards.solow.xyz")
.domain("localhost")
//.domain("localhost")
.path("/")
.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/")]
pub async fn login() -> impl Responder {
HttpResponse::Ok().body(r#"
<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>
"#)
pub async fn login() -> actix_web::Result<NamedFile> {
Ok(NamedFile::open("static/account_login.html")?)
}
#[post("/account/new")]
pub async fn sign_up_result(
data: web::Data<AppState>,
form: web::Form<CryptoUserForm>
form: web::Form<CryptoUserForm>,
tdata: web::Data<ThreadData<'_>>
) -> HttpResponse {
println!("new user");
let mut um = data.user_manager.lock().unwrap();
@ -93,44 +67,12 @@ pub async fn sign_up_result(
}
};
um.add_user(user.clone());
HttpResponse::Created().body(format!(r#"
<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>
Hooray! You can log into you new account <a href="/account/login/">here</a>
</p>
</body></html></div></body></html>
"#, user.username))
HttpResponse::Created().body(tdata.handlebars.render("new_user_redirect", &btreemap! {
"user" => to_json(user.username)
}).unwrap())
}
#[get("/account/new/")]
pub async fn sign_up() -> impl Responder {
HttpResponse::Ok().body(r#"
<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>
"#)
pub async fn sign_up() -> actix_web::Result<NamedFile> {
Ok(NamedFile::open("static/new_account.html")?)
}

View File

@ -1,7 +1,6 @@
use std::{
time,
time::Duration,
path::PathBuf
path::PathBuf,
time::SystemTime
};
use actix_web::{
@ -14,59 +13,60 @@ use crate::{
boards::Board,
users::{UserCookie, User}
},
api::types::*
api::types::*,
ThreadData
};
use handlebars::to_json;
use maplit::btreemap;
use super::TimeDerive;
#[get("/boards/")]
pub async fn list_boards(
req: HttpRequest,
data: web::Data<AppState>,
tdata: web::Data<ThreadData<'_>>
) -> impl Responder {
let now = time::SystemTime::now().duration_since(time::UNIX_EPOCH).unwrap();
let mut ab = false;
let cookie = req.cookie("auth");
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();
HttpResponse::Ok().body(format!(
r#"
<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>
<div id="login-status">
<p><a href="/account/login/">Login</a>/<a href="/account/new/">Sign Up</a></p>
</div>
</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>
{}
</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>()
))
let mut boards_json = to_json(&boards);
let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap();
let boards_json = boards_json.as_array_mut().unwrap().iter_mut().zip(boards).map(|x| -> &serde_json::Value {
x.0.as_object_mut().unwrap().insert(
"time_meta".to_string(),
if let Some(t) = x.1.last_active {
to_json(std::time::Duration::from_secs(t).derive_time_since(now).map(|d| (format!("{:.2}", d.0), d.1)))
} else {
to_json::<Option<u8>>(None)
}
);
//println!("{:?}", x.0);
x.0
}).collect::<Vec<&serde_json::Value>>();
HttpResponse::Ok().body(tdata.handlebars.render("board_index", &btreemap!(
"boards" => to_json(boards_json),
"user" => to_json(user)
)).unwrap())
}
#[get("/board/{board_id}/")]
pub async fn get_board(
req: HttpRequest,
tdata: web::Data<ThreadData<'_>>,
data: web::Data<AppState>,
web::Path(board_id): web::Path<u32>
) -> HttpResponse {
let mut ab = false;
let now = time::SystemTime::now().duration_since(time::UNIX_EPOCH).unwrap();
let boards = data.boards.lock().unwrap();
let cookie = req.cookie("auth");
let uid = if cookie.is_some() {
@ -75,58 +75,15 @@ pub async fn get_board(
UserCookie::parse("null")
};
if let Some(a) = Board::search(boards.iter(), board_id) {
HttpResponse::Ok().body(format!(
r#"
<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>
{} | <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()
}
))
let data = btreemap!(
"messages" => to_json(&a.messages),
"user" => match uid {
Ok(a) => to_json(data.user_manager.lock().unwrap().get_user_by_id(a.id)),
Err(_) => to_json("null")
},
"board" => to_json(a)
);
HttpResponse::Ok().body(tdata.handlebars.render("board", &data).unwrap())
} else {
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 b = data.boards.lock().unwrap();
let board = b.iter().find(|x| (x.id == form.board_id));
let mut b = data.boards.lock().unwrap();
let mut board = b.iter_mut().find(|x| (x.id == form.board_id));
let um = data.user_manager.lock().unwrap();
match board {
Some(a) => {
Some(ref mut a) => {
//let a: &mut Board = board.unwrap();
let msg = mf.create_message({
let user = um.get_user_by_id(form.user_id).ok_or_else(|| {
HttpResponse::NotFound().body("Not Found: User does not exist")
@ -166,6 +124,8 @@ pub async fn board_post(
}
}, form.content.clone());
println!("new message");
//println!("board: {{ id: {}, name: {} }}{:?}", a.id, a.title, msg);
//let a = a.clone().add_message(msg);
a.add_message(msg);
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");
}
HttpResponse::Ok().body(r#"
<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>
"#)
HttpResponse::Ok().body(include_str!("../../static/new_board.html"))
}
#[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 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
};
//use colored::*;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Board {
pub title: String,
@ -23,15 +21,22 @@ pub struct Board {
pub creation: u64,
pub id: u32,
pub path: String,
pub owner: u32
pub owner: u32,
pub last_active: Option<u64>
}
impl Board {
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()),
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>(
@ -44,8 +49,10 @@ impl Board {
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
}
}
@ -89,21 +96,10 @@ impl BoardFactory {
creation: SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(),
id: self.assign_id(),
path: path.to_str().unwrap().to_string(),
owner: creator
owner: creator,
last_active: None
};
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) {
(BoardFactory::new(self.data_dir.clone()),
MessageFactory {
next_id: 0,
last_write: None,
data_dir: self.data_dir.clone()
},
MessageFactory::new(self.data_dir.clone()),
UserManager::new(self.data_dir.clone()))
}
}

View File

@ -5,6 +5,7 @@ use std::{
ReadDir
},
path::Path,
error::Error
};
use actix_web::{
@ -17,8 +18,7 @@ use lib::{
*,
Archiveable,
boards::{
Board,
BoardFactory
Board, BoardFactory
},
users::UserManager,
messages::MessageFactory
@ -33,62 +33,34 @@ use html::{
boards::*
};
/*
struct PageBuilder {
body: &'static str,
style: Option<String>,
table_labels: Option<String>
use tokio::{
sync::{oneshot, watch},
net::{
UnixStream, UnixListener,
},
runtime::Runtime,
select
};
use handlebars::Handlebars;
use console::style;
use maplit::btreemap;
pub struct ThreadData<'a> {
handlebars: Handlebars<'a>
}
impl PageBuilder {
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)
}
}
*/
use simple_logger::SimpleLogger;
#[get("/")]
async fn index() -> impl Responder {
HttpResponse::Ok().body(format!(r#"
<html><head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
</head>
<body>
<h1>Boarders v{}</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>
"#, env!("CARGO_PKG_VERSION")))
async fn index(
tdata: web::Data<ThreadData<'_>>
) -> impl Responder {
HttpResponse::Ok().body(
tdata.handlebars.render("welcome", &btreemap!(
"version" => env!("CARGO_PKG_VERSION").to_string()
)).unwrap()
)
}
#[get("/debug/")]
@ -100,29 +72,40 @@ async fn debug(
#[actix_web::main]
async fn main() -> std::io::Result<()> {
/* quick and dirty logging */
SimpleLogger::new().init().unwrap();
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 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("boards/");
&pwd
}) {
Ok(a) => a,
Err(_e) => {
fs::create_dir_all(&pwd).unwrap();
fs::read_dir(pwd).unwrap()
}
};
}).unwrap_or({
fs::create_dir_all(&pwd).unwrap();
fs::read_dir(pwd).unwrap()
});
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()) {
let mut path = file.path().to_path_buf();
//let meta = fs::metadata(&path).unwrap();
let ftype = file.file_type().unwrap();
if ftype.is_dir() {
if file.file_type().unwrap().is_dir() {
let mut path = file.path().to_path_buf();
path.push("board.json");
println!("loading board: {:?}", path);
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));
println!("{}", style(format!("Found and loaded {} boards!", boards.len())).green());
let mut board_data = std::path::PathBuf::new();
board_data.push(&data_dir);
@ -142,6 +127,7 @@ async fn main() -> std::io::Result<()> {
board_data.push("boards/");
bf.set_data_dir(board_data);
/* Initialize all the the shared data crap. copy/pasting code time!! */
let web_data = web::Data::new(AppState {
boards: Mutex::new(boards.clone()),
board_factory: Mutex::new(BoardFactory::load_state({
@ -164,39 +150,65 @@ async fn main() -> std::io::Result<()> {
}).unwrap_or(um))
});
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...");
let server = HttpServer::new(move || {
App::new()
.app_data(web_data.clone())
.service(index)
.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(
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))
.data(ThreadData {
handlebars: {
let mut h = Handlebars::new();
/*
* TODO: allow user to specify wether or not templates should be
* loaded dynamically or statically, probably via environment
* variable at compile time
*/
/* register handlebars partials here */
h.register_partial("login_status", include_str!("res/snippets/login_status.hbs")).unwrap();
h.register_partial("board_bar", include_str!("res/snippets/board_bar.hbs")).unwrap();
/* register handlebars templates here */
h.register_template_string("board_index", include_str!("res/board_index.hbs")).unwrap();
h.register_template_string("welcome", include_str!("res/welcome.hbs")).unwrap();
h.register_template_string("auth", include_str!("res/auth.hbs")).unwrap();
h.register_template_string("board", include_str!("res/board.hbs")).unwrap();
h
}
}).app_data(web_data.clone())
.service(index)
.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())
}).bind("127.0.0.1:8080")?
.run();
}).bind("127.0.0.1:8080")?.run();
println!("Started server");
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;
println!("\nExiting...");
/*
* Note to self: handle all errors as much as possible
@ -214,28 +226,35 @@ async fn main() -> std::io::Result<()> {
}
wb.board_factory
.lock()
.unwrap()
.save_state("factory.json")
.unwrap_or_else(
|x| { println!("Could not save user factory state: {}", x) }
);
.lock()
.unwrap()
.save_state("factory.json")
.unwrap_or_else(
|x| { println!("Could not save user factory state: {}", x) }
);
wb.msg_factory
.lock()
.unwrap()
.save_state("message_factory.json")
.unwrap_or_else(
|x| { println!("Could not save message factory state: {}", x) }
);
.lock()
.unwrap()
.save_state("message_factory.json")
.unwrap_or_else(
|x| { println!("Could not save message factory state: {}", x) }
);
wb.user_manager
.lock()
.unwrap()
.save_state("users.json")
.unwrap_or_else(
|x| { println!("Could not save user data: {}", x) }
);
.lock()
.unwrap()
.save_state("users.json")
.unwrap_or_else(
|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!");
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 {
margin-top: 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 {
@ -156,3 +172,26 @@ tr.b {
padding: 3em;
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>