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:
parent
f10be383f9
commit
2404b04e76
19
Cargo.toml
19
Cargo.toml
@ -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"
|
||||||
|
|||||||
@ -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"))
|
||||||
|
|||||||
3
src/bin/boarders_client.rs
Normal file
3
src/bin/boarders_client.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
println!("Client");
|
||||||
|
}
|
||||||
@ -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>
|
|
||||||
"#)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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")]
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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>
|
|
||||||
@ -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())
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
235
src/main.rs
235
src/main.rs
@ -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
19
src/res/auth.hbs
Normal 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>
|
||||||
@ -1,5 +1,6 @@
|
|||||||
____ __
|
____ __
|
||||||
/ __ ) ____ ____ _ _____ ____/ /___ _____ _____
|
/ __ )\ ____ ____ _ _____ ____/ /\__ _____ _____
|
||||||
/ __ |/ __ \ / __ `// ___// __ // _ \ / ___// ___/
|
/ __ | / __ \ / __ `/\/ ___// __ // _ \ / ___// ___/\
|
||||||
/ /_/ // /_/ // /_/ // / / /_/ // __// / (__ )
|
/ /_/ / / /_/ // /_/ / / / _// /_/ // __/\/ / _/(__ )\/
|
||||||
/_____/ \____/ \__,_//_/ \__,_/ \___//_/ /____/
|
/_____/ /\____/ \__,_/ /_/ / \__,_/ \___/ /_/ / /____/\)
|
||||||
|
\_____\/ \___\/ \___\/\_\/ \___\/ \__\/\_\/ \____\/
|
||||||
|
|||||||
42
src/res/board.hbs
Normal file
42
src/res/board.hbs
Normal 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
31
src/res/board_index.hbs
Normal 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>
|
||||||
15
src/res/new_account_redirect.hbs
Normal file
15
src/res/new_account_redirect.hbs
Normal 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>
|
||||||
11
src/res/snippets/board_bar.hbs
Normal file
11
src/res/snippets/board_bar.hbs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<div id="title" class="bar">
|
||||||
|
<div>
|
||||||
|
<h1>
|
||||||
|
{{board.title}} | <a href="/boards/"><- Back to index</a>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div id="description">
|
||||||
|
{{board.desc}}
|
||||||
|
</div>
|
||||||
|
{{> login_status}}
|
||||||
|
</div>
|
||||||
17
src/res/snippets/login_status.hbs
Normal file
17
src/res/snippets/login_status.hbs
Normal 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
50
src/res/welcome.hbs
Normal 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
25
static/account_login.html
Normal 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>
|
||||||
@ -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
24
static/new_account.html
Normal 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
28
static/new_board.html
Normal 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>
|
||||||
Loading…
x
Reference in New Issue
Block a user