Compare commits

..

No commits in common. "2571ce57564f2a193933ffae43fad9c56d49afa4" and "e44d120ba72bc7332c21f60f2c3f9c8eb9c5cb93" have entirely different histories.

44 changed files with 876 additions and 1227 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
/target
# old data directory
/boards
# new data directory, specified in default config file
/data
Cargo.lock

16
Cargo.toml Normal file
View File

@ -0,0 +1,16 @@
[package]
name = "board_server"
version = "1.0.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web = "3.3.2"
actix-files = "0.5.0"
toml = "0.5"
serde = "1.0.125"
serde_json = "1.0"
bcrypt-bsd = "0.1.3"
rand = "0.8.3"
colored = "2.0.0"

View File

@ -61,7 +61,7 @@ pub async fn new(
}
}, form.content.clone());
println!("new message");
a.clone().add_message(msg.clone());
a.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"))

136
src/html/accounts.rs Normal file
View File

@ -0,0 +1,136 @@
use actix_web::{
get, post, web,
HttpResponse, Responder,
cookie::Cookie
};
use crate::api::types::*;
use crate::lib::users::{User, Account};
#[post("/account/auth")]
pub async fn auth_html(
data: web::Data<AppState>,
form: web::Form<CryptoUserForm>
) -> HttpResponse {
let user = data.user_manager.lock().unwrap().get_user_by_uname(form.username.clone());
if let Some(u) = user {
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));
response.add_cookie(
&Cookie::build("auth", format!("{}&{}", u.user_id, u.secret))
.domain("boards.solow.xyz")
//.domain("localhost")
.path("/")
.same_site(actix_web::cookie::SameSite::Strict)
//.secure(true)
.finish()
).unwrap();
response
} else {
HttpResponse::Forbidden().body("Forbidden: Could not authenticate user")
}
Err(e) => HttpResponse::BadRequest().body(format!("Bad Request: {}", e))
}
} else {
HttpResponse::BadRequest().body("Bad Request: User does not exist")
}
}
#[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>
"#)
}
#[post("/account/new")]
pub async fn sign_up_result(
data: web::Data<AppState>,
form: web::Form<CryptoUserForm>
) -> HttpResponse {
println!("new user");
let mut um = data.user_manager.lock().unwrap();
let user = {
if let Some(_a) = um.get_user_by_uname(form.username.clone()) {
return HttpResponse::Conflict().body("Conflict: user with that username already exists");
} else {
let new_id = um.assign_id();
User::new(new_id, &form.username).encrypt(form.password.clone())
}
};
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))
}
#[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>
"#)
}

View File

@ -1,6 +1,7 @@
use std::{
path::PathBuf,
time::SystemTime
time,
time::Duration,
path::PathBuf
};
use actix_web::{
@ -13,61 +14,59 @@ use crate::{
boards::Board,
users::{UserCookie, User}
},
api::types::*,
ThreadData
api::types::*
};
use handlebars::to_json;
use maplit::btreemap;
use super::TimeDerive;
use chrono::{DateTime, Utc};
use ammonia::clean;
use pulldown_cmark::{Parser, Options, html::push_html};
#[get("/boards/")]
pub async fn list_boards(
req: HttpRequest,
data: web::Data<AppState>,
tdata: web::Data<ThreadData<'_>>
) -> impl Responder {
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 now = time::SystemTime::now().duration_since(time::UNIX_EPOCH).unwrap();
let mut ab = false;
let boards = (*data.boards.lock().unwrap()).clone();
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)))
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>
<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 {
to_json::<Option<u8>>(None)
String::from("Never")
}
);
//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())
)
}).collect::<String>()
))
}
#[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() {
@ -76,38 +75,49 @@ pub async fn get_board(
UserCookie::parse("null")
};
if let Some(a) = Board::search(boards.iter(), board_id) {
let data = btreemap!(
"messages" => {
let mut msg_json = to_json(&a.messages);
let mut op = Options::empty();
op.insert(Options::ENABLE_TABLES);
let msg_json = msg_json.as_array_mut().unwrap().iter_mut().zip(a.messages.borrow().iter()).map(|x| {
let parse = Parser::new_ext(&x.1.text, op);
let o = x.0.as_object_mut().unwrap();
o.insert(
"time_meta".to_string(),
to_json(DateTime::<Utc>::from(x.1.timestamp).to_rfc2822().trim_matches(|c| c=='0' || c=='+' || c==' '))
);
o.insert(
"render_text".to_string(),
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>
{} | <a href="/boards/"><- Back to index</a>
</h1></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.messages.borrow().iter().map(|x| {
ab = !ab;
format!(
"<tr class=\"message {}\"><td>{}</td><td>{}</td><td>{}</td></tr>",
if ab { "a" } else { "b" },
{
let mut dirty_html = String::new();
push_html(&mut dirty_html, parse);
to_json(dirty_html)
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()
}
);
x.0
}).collect::<Vec<&mut serde_json::Value>>();
to_json(msg_json)
},
"user" => match uid {
Ok(a) => to_json(data.user_manager.lock().unwrap().get_user_by_id(a.id)),
Err(_) => to_json(None::<Option<u8>>)
},
"board" => to_json(a)
);
HttpResponse::Ok().body(tdata.handlebars.render("board", &data).unwrap())
))
} else {
HttpResponse::NotFound().body("That board could not be found")
}
@ -127,15 +137,15 @@ pub async fn board_post(
Ok(a) => a,
Err(e) => {
println!("{}", e);
return Err(HttpResponse::BadRequest().body("Bad Request: Could not parse cookie data.\n Not logged in, or bad cookie."));
return Err(HttpResponse::BadRequest().body("Bad Request: Could not parse cookie data"));
}
};
let mut mf = data.msg_factory.lock().unwrap();
let mut b = data.boards.lock().unwrap();
let mut board = b.iter_mut().find(|x| (x.id == form.board_id));
let b = data.boards.lock().unwrap();
let board = b.iter().find(|x| (x.id == form.board_id));
let um = data.user_manager.lock().unwrap();
match board {
Some(ref mut a) => {
Some(a) => {
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")
@ -145,7 +155,8 @@ pub async fn board_post(
} else {
return Err(HttpResponse::Unauthorized().body("Unauthorized: Bad login attempt, invalid auth token"));
}
}, clean(&form.content));
}, form.content.clone());
println!("new message");
a.add_message(msg);
Ok(HttpResponse::SeeOther().header("location", format!("/board/{}/", a.id)).finish())
},
@ -178,7 +189,29 @@ pub async fn new_board(
return HttpResponse::BadRequest().body("Bad Request: User does not exist");
}
HttpResponse::Ok().body(include_str!("../../static/new_board.html"))
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>
"#)
}
#[post("/boards/new")]

154
src/html/index.css Normal file
View File

@ -0,0 +1,154 @@
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: xxx-large;
margin: unset;
width: fit-content;
height: fit-content;
}
.bar > div > p {
width: fit-content;
height: fit-content;
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;
}
#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;
}

2
src/html/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod accounts;
pub mod boards;

View File

@ -13,6 +13,8 @@ use super::{
messages::Message
};
//use colored::*;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Board {
pub title: String,
@ -21,23 +23,16 @@ pub struct Board {
pub creation: u64,
pub id: u32,
pub path: String,
pub owner: u32,
pub last_active: Option<u64>
pub owner: u32
}
impl Board {
pub fn open<P>(path: P) -> Result<Board> where P: AsRef<Path> {
let mut b: std::result::Result<Board, _> = File::open(&path).map(
|a| serde_json::from_reader(a).unwrap()
);
/* backwards compatability */
if let Ok(ref mut b) = b {
if b.last_active.is_none() {
b.last_active = b.messages.borrow().last().map(|x| x.as_secs());
match File::open(&path) {
Ok(a) => Ok(serde_json::from_reader(a).unwrap()),
Err(e) => Err(e)
}
}
b
}
pub fn search<'a, I>(
mut iter: I,
@ -49,10 +44,8 @@ impl Board {
iter.find(|x| x.id == id)
}
pub fn add_message(&mut self, msg: Message) -> &Board {
self.last_active = Some(msg.as_secs());
pub fn add_message(&self, msg: Message) {
self.messages.borrow_mut().push(msg);
self
}
}
@ -96,10 +89,21 @@ 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,
last_active: None
owner: creator
};
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

@ -14,16 +14,10 @@ use super::{
pub struct Message {
pub text: String,
pub id: u32,
pub timestamp: SystemTime,
pub timestamp: u64,
pub author: RefCell<User>
}
impl Message {
pub fn as_secs(&self) -> u64 {
self.timestamp.duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct MessageFactory {
pub next_id: u32,
@ -42,10 +36,11 @@ impl MessageFactory {
pub fn create_message<S>(&mut self, user: User, content: S) -> Message where S: AsRef<str> {
self.next_id += 1;
let time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
Message {
text: String::from(content.as_ref()),
id: self.next_id-1,
timestamp: SystemTime::now(),
timestamp: time,
author: RefCell::new(user)
}
}

View File

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

241
src/main.rs Normal file
View File

@ -0,0 +1,241 @@
use std::{
env, fs,
sync::Mutex,
fs::{
ReadDir
},
path::Path,
};
use actix_web::{
get, web,
App, HttpResponse, HttpServer, Responder,
};
mod lib;
use lib::{
*,
Archiveable,
boards::{
Board,
BoardFactory
},
users::UserManager,
messages::MessageFactory
};
mod api;
use api::types::AppState;
mod html;
use html::{
accounts::*,
boards::*
};
/*
struct PageBuilder {
body: &'static str,
style: Option<String>,
table_labels: Option<String>
}
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)
}
}
*/
#[get("/")]
async fn index() -> impl Responder {
HttpResponse::Ok().body(r#"
<html><head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
</head>
<body>
<h1>Boarders v1.0.0</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>
"#)
}
#[get("/debug/")]
async fn debug(
data: web::Data<AppState>
) -> impl Responder {
HttpResponse::Ok().body(serde_json::to_string_pretty(data.as_ref()).unwrap())
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
println!("Initializing...");
let data_dir = Path::new("data/");
let mut pwd = env::current_dir().unwrap();
let board_files: ReadDir = match 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()
}
};
let mut boards: Vec<Board> = Vec::new();
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() {
path.push("board.json");
println!("loading board: {:?}", path);
boards.push(
Board::open(&path).unwrap_or_else(|e| {
panic!("Could not open board {:?}\n{}", path, e)
})
);
}
}
boards.sort_by(|a,b| a.id.cmp(&b.id));
let mut board_data = std::path::PathBuf::new();
board_data.push(&data_dir);
let ff = FactoryFactory::new(data_dir);
let (mut bf, mf, um) = ff.build();
board_data.push("boards/");
bf.set_data_dir(board_data);
let web_data = web::Data::new(AppState {
boards: Mutex::new(boards.clone()),
board_factory: Mutex::new(BoardFactory::load_state({
println!("Loading board factory file...");
let mut dd = data_dir.to_path_buf();
dd.push("boards/factory.json");
dd
}).unwrap_or(bf)),
msg_factory: Mutex::new(MessageFactory::load_state({
println!("Loading message factory file...");
let mut dd = data_dir.to_path_buf();
dd.push("message_factory.json");
dd
}).unwrap_or(mf)),
user_manager: Mutex::new(UserManager::load_state({
println!("Loading user manager file...");
let mut dd = data_dir.to_path_buf();
dd.push("users.json");
dd
}).unwrap_or(um))
});
let wb = web_data.clone();
println!("Finished initialization");
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))
.service(actix_files::Files::new("/static/", "./static/").show_files_listing())
}).bind("127.0.0.1:8080")?
.run();
println!("Started server");
println!("{}", include_str!("res/banner"));
let res = server.await;
println!("\nExiting...");
/*
* Note to self: handle all errors as much as possible
* Use unwrap() as little as possible
* If anything goes wrong while saving data data WILL get lost
*/
println!("Saving application state...");
for b in wb.boards.lock().unwrap().iter() {
fs::create_dir_all(&b.path).unwrap_or_else(
|x| { println!("Could not create directory {:?}: {}", b.path, x) }
);
b.save_state("board.json").unwrap_or_else(
|x| { println!("Could not save board {:?}: {}", b.title, x) }
);
}
wb.board_factory
.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) }
);
wb.user_manager
.lock()
.unwrap()
.save_state("users.json")
.unwrap_or_else(
|x| { println!("Could not save user data: {}", x) }
);
println!("Done!");
res
}

5
src/res/banner Normal file
View File

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

154
static/index.css Normal file
View File

@ -0,0 +1,154 @@
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: xxx-large;
margin: unset;
width: fit-content;
height: fit-content;
}
.bar > div > p {
width: fit-content;
height: fit-content;
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;
}
#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;
}

17
static/navbar.html Normal file
View File

@ -0,0 +1,17 @@
<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>

6
v1/.gitignore vendored
View File

@ -1,6 +0,0 @@
target
data
Cargo.lock

View File

@ -1,37 +0,0 @@
[package]
name = "board_server"
version = "2.2.1"
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
[dependencies]
actix-web = "3.3.2"
actix-files = "0.5.0"
toml = "0.5"
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"
log = "0.4.14"
simple_logger = "1.11.0"
chrono = "0.4.19"
ammonia = "3.1.2"
pulldown-cmark = "0.8.0"
[lib]
name = "boarders"
path = "src/lib/mod.rs"
test = false
bench = false
doc = false
harness = false
edition = "2018"

View File

@ -1,8 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
for board in ./data/boards/*/ ; do
FILE="$board/board.json"
sed -E 's/"timestamp"\:([0-9]+)/"timestamp":{"secs_since_epoch":\1,"nanos_since_epoch":0}/g' $FILE > "${FILE}.new"
mv "${FILE}.new" $FILE
done

View File

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

View File

@ -1,78 +0,0 @@
use actix_web::{
get, post, web,
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 {
let user = data.user_manager.lock().unwrap().get_user_by_uname(form.username.clone());
if let Some(u) = user {
match u.verify(form.password.clone()) {
Ok(a) => if a {
let mut response = HttpResponse::Ok()
.body(tdata.handlebars.render("auth", &btreemap!("user" => u.clone())).unwrap());
response.add_cookie(
&Cookie::build("auth", format!("{}&{}", u.user_id, u.secret))
.domain("localhost")
//.domain("localhost")
.path("/")
.same_site(actix_web::cookie::SameSite::Strict)
//.secure(true)
.finish()
).unwrap();
response
} else {
HttpResponse::Forbidden().body("Forbidden: Could not authenticate user")
}
Err(e) => HttpResponse::BadRequest().body(format!("Bad Request: {}", e))
}
} else {
HttpResponse::BadRequest().body("Bad Request: User does not exist")
}
}
#[get("/account/login/")]
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>,
tdata: web::Data<ThreadData<'_>>
) -> HttpResponse {
println!("new user");
let mut um = data.user_manager.lock().unwrap();
let user = {
if let Some(_a) = um.get_user_by_uname(form.username.clone()) {
return HttpResponse::Conflict().body("Conflict: user with that username already exists");
} else {
let new_id = um.assign_id();
User::new(new_id, &form.username).encrypt(form.password.clone())
}
};
um.add_user(user.clone());
HttpResponse::Created().body(tdata.handlebars.render("new_user_redirect", &btreemap! {
"user" => to_json(user.username)
}).unwrap())
}
#[get("/account/new/")]
pub async fn sign_up() -> actix_web::Result<NamedFile> {
Ok(NamedFile::open("static/new_account.html")?)
}

View File

@ -1,22 +0,0 @@
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,480 +0,0 @@
use std::{
env,
error::Error,
fs,
fs::ReadDir,
io,
io::{Read, Write},
path::Path,
process::Command,
sync::Mutex,
};
use actix_web::{get, web, App, HttpResponse, HttpServer, Responder};
mod lib;
use lib::{
boards::{Board, BoardFactory},
messages::MessageFactory,
users::UserManager,
Archiveable, *,
};
mod api;
use api::types::AppState;
mod html;
use html::{accounts::*, boards::*};
use tokio::{
net::{UnixListener, UnixStream},
runtime::Runtime,
select,
sync::{oneshot, watch},
};
use console::style;
use handlebars::{handlebars_helper, Handlebars};
use maplit::btreemap;
use log::LevelFilter;
use simple_logger::SimpleLogger;
use serde::{Deserialize, Serialize};
pub struct ThreadData<'a> {
handlebars: Handlebars<'a>,
}
#[get("/")]
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/")]
async fn debug(data: web::Data<AppState>) -> impl Responder {
HttpResponse::Ok().body(serde_json::to_string_pretty(data.as_ref()).unwrap())
}
async fn socket_loop(socket: &UnixStream) -> Result<(), Box<dyn Error>> {
loop {
let mut data = vec![0; 1024];
socket.readable().await?;
let mut command = String::new();
loop {
match socket.try_read(&mut data) {
Ok(0) => break,
Ok(_n) => {
command.push_str(String::from_utf8(data.clone()).unwrap().trim());
println!("command: {}", command);
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
break;
}
Err(e) => {
return Err(e.into());
}
}
}
socket.writable().await?;
match socket.try_write(&command.into_bytes()) {
Ok(_n) => {}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
continue;
}
Err(e) => {
return Err(e.into());
}
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct AppInfo<'s> {
version: &'s str,
}
impl<'s> AppInfo<'s> {
fn get_version(&self) -> Option<[u8; 3]> {
let mut a: [u8; 3] = [0; 3];
let mut ver = self.version.split('.');
a[0] = ver.next()?.parse().ok()?;
a[1] = ver.next()?.parse().ok()?;
a[2] = ver.next()?.parse().ok()?;
Some(a)
}
}
static mut APP_INFO: AppInfo = AppInfo {
version: env!("CARGO_PKG_VERSION"),
};
#[actix_web::main]
async fn main() -> io::Result<()> {
let args: Vec<String> = env::args().collect();
let mut args = args.iter();
args.next().unwrap();
for arg in args {
match arg.as_str() {
"-v" | "--version" => {
println!("Boarders v{}", unsafe { APP_INFO.version });
std::process::exit(0);
}
_ => {
println!("Unknown argument \"{}\"", arg);
}
}
}
/* quick and dirty logging */
SimpleLogger::new()
.with_module_level("mio::poll", LevelFilter::Off)
.with_module_level("actix_server::worker", LevelFilter::Debug)
.with_module_level("handlebars", LevelFilter::Info)
.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();
/* Getting app version and checking for backwards compatability issues */
let mut app_info_file = data_dir.to_path_buf();
app_info_file.push("app.json");
let mut app_info_file = fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(app_info_file)
.unwrap();
let mut app_info_str = String::new();
app_info_file.read_to_string(&mut app_info_str).unwrap();
let cur_app_info: AppInfo = match serde_json::from_str(&app_info_str) {
Ok(a) => a,
Err(_) => {
println!("Malformed app.json file, backing up then creating new");
let s = unsafe { serde_json::to_string(&APP_INFO).unwrap() };
app_info_file.write_all(s.as_bytes()).unwrap();
unsafe { APP_INFO.clone() }
}
};
let ver = cur_app_info.get_version().unwrap();
match ver[0] {
x if x <= 2 => match ver[1] {
y if y <= 2 => match ver[2] {
0 => {
if Command::new("./compat/2.2.0.sh").output().is_err() {
println!("Error, could not complete compatability upgrade");
} else {
println!("Fixed compat issues for v2.2.0");
}
}
_ => {
println!("Invalid version number?")
}
},
_ => {
println!("Invalid version number?")
}
},
_ => {
println!("Invalid version number?")
}
}
/* open/create the directory where board data is placed */
let board_files: ReadDir = fs::read_dir({
pwd.push(data_dir);
pwd.push("boards/");
&pwd
})
.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()) {
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(
Board::open(&path)
.unwrap_or_else(|e| panic!("Could not open board {:?}\n{}", path, e)),
);
}
}
/* 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);
let ff = FactoryFactory::new(data_dir);
let (mut bf, mf, um) = ff.build();
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({
println!("Loading board factory file...");
let mut dd = data_dir.to_path_buf();
dd.push("boards/factory.json");
dd
})
.unwrap_or(bf),
),
msg_factory: Mutex::new(
MessageFactory::load_state({
println!("Loading message factory file...");
let mut dd = data_dir.to_path_buf();
dd.push("message_factory.json");
dd
})
.unwrap_or(mf),
),
user_manager: Mutex::new(
UserManager::load_state({
println!("Loading user manager file...");
let mut dd = data_dir.to_path_buf();
dd.push("users.json");
dd
})
.unwrap_or(um),
),
});
let wb = web_data.clone();
println!(
"{}",
style("Loaded/Created application data files!").green()
);
println!("Connecting to Unix socket...");
let (tx0, rx0) = oneshot::channel::<bool>();
/* NOTE: More phases will probably exist in the future */
enum AppPhase {
Normal,
Shutdown,
}
let (s_tx, mut s_rx) = watch::channel::<AppPhase>(AppPhase::Normal);
let rt = Runtime::new().unwrap();
let _guard = rt.enter();
/* spawn the thread that manages the unix socket */
tokio::spawn(async move {
let p = Path::new("./boarders.sock");
if p.exists() {
std::fs::remove_file(p).unwrap();
}
let socket = UnixListener::bind("./boarders.sock").unwrap_or_else(|_| {
panic!("{}", style("Could not connect to Unix socket").red());
});
println!("{}", style("Listening on Unix socket!").green());
if tx0.send(true).is_err() {
panic!(
"{}",
style("[FATAL] Thread reciever was dropped! (should never happen)")
.red()
.bold()
);
}
/*
* HACK: clone the reciever a bunch of times to get around moves
* not sure if there's a better way
*/
let s_rx_0 = s_rx.clone();
let spawn_t = tokio::spawn(async move {
let s_rx = s_rx_0.clone();
loop {
let mut s_rx = s_rx.clone();
/* accept socket then wait for exit, or shutdown signal */
let (conn, _addr) = socket.accept().await.unwrap();
tokio::spawn(async move {
select! {
_socket = socket_loop(&conn) => 0,
_state = s_rx.changed() => 1
}
});
}
});
/*
* FIXME: use enums when I feel like it (not urgent)
*/
match select! {
_st = spawn_t => 0,
state = s_rx.changed() => if state.is_ok() {
match *s_rx.borrow() {
AppPhase::Shutdown => 1,
_ => 0
}
} else {
0
}
} {
0 => println!("Client disconnected"),
1 => println!("Shut down client thread(s)"),
_ => println!("Unexpected value from client thread"),
}
});
/* why do I want a special message for that? Honestly I don't know... */
rx0.await.unwrap_or_else(|_| {
panic!(
"{}",
style("[FATAL] Thread transmitter was dropped! (should never happen)")
.red()
.bold()
)
});
println!("{}", style("Finished initialization").green().bold());
println!("Starting server...");
let server =
HttpServer::new(move || {
App::new()
.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 */
handlebars_helper!(msg_helper: |s: str| format!("helped: {}", s.to_string()));
h.register_helper("msg", Box::new(msg_helper));
h.register_partial("head", include_str!("res/snippets/head.hbs")).unwrap();
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();
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
* Use unwrap() as little as possible
* If anything goes wrong while saving data data WILL get lost
*/
println!("Saving application state...");
for b in wb.boards.lock().unwrap().iter() {
fs::create_dir_all(&b.path)
.unwrap_or_else(|x| println!("Could not create directory {:?}: {}", b.path, x));
b.save_state("board.json")
.unwrap_or_else(|x| println!("Could not save board {:?}: {}", b.title, x));
}
wb.board_factory
.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));
wb.user_manager
.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
}

View File

@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="en">
{{> 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,6 +0,0 @@
____ __
/ __ )\ ____ ____ _ _____ ____/ /\__ _____ _____
/ __ | / __ \ / __ `/\/ ___// __ // _ \ / ___// ___/\
/ /_/ / / /_/ // /_/ / / / _// /_/ // __/\/ / _/(__ )\/
/_____/ /\____/ \__,_/ /_/ / \__,_/ \___/ /_/ / /____/\)
\_____\/ \___\/ \___\/\_\/ \___\/ \__\/\_\/ \____\/

View File

@ -1,36 +0,0 @@
<!DOCTYPE html>
<html lang="en">
{{> head}}
<body style="overflow-x: hidden;">
{{> board_bar board=board user=user}}
<div id="index">
<!--messages-->
{{#each messages as | msg |}}
<div style="border-bottom: 1px solid #555" class="message">
{{#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}}
<b>{{msg.author.username}}</b>
{{/if}}
<div class="message-content">
<pre>{{{msg.render_text}}}</pre>
</div>
<div class="message-time">
{{msg.time_meta}}
</div>
</div>
{{/each}}
</div>
<form action="/boards/post" method="post" style="display: flex; align-items: center; flex-direction: column;">
<label for="content">Write a message:</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>
</body>
</html>

View File

@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="en">
{{> head}}
<body style="overflow-x: hidden;">
<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>
{{#if time_meta}}
<td>{{time_meta.0}} {{time_meta.1}}</td>
{{else}}
<td>Never</td>
{{/if}}
</tr>
{{/each}}
</table>
</div>
</body>
</html>

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
{{> 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

@ -1,11 +0,0 @@
<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 user=user}}
</div>

View File

@ -1,9 +0,0 @@
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon"/>
{{#if nolocal}}
{{else}}
<link rel="stylesheet" type="text/css" href="/static/index.css">
{{/if}}
</head>

View File

@ -1,17 +0,0 @@
<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>

View File

@ -1,49 +0,0 @@
<!DOCTYPE html>
<html lang="en">
{{> head nolocal=1}}
<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>

View File

@ -1,25 +0,0 @@
<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

@ -1,210 +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;
}
.message {
margin-top: 0.5em;
margin-bottom: 0.5em;
border-bottom: 1px solid #555;
display: flex;
flex-direction: inherit;
justify-content: flex-start;
}
.message > b {
padding-left: 1em;
}
.message > * {
padding: 0.2em;
}
.message:hover {
background-color: #3a3a3a;
animation-duration: 0.1s;
animation-name: msg-hover;
}
@keyframes msg-hover {
from {
background-color: inherit;
}
to {
background-color: #3a3a3a;
}
}
.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;
}
.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;
display: flex;
flex-direction: column;
justify-content: space-between;
}
@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;
}
.message-content {
flex-grow: 1;
/*max-width: 90%; */
}
.message-content > pre {
margin-top: 0.1rem;
margin-bottom: 0.1rem;
}
pre {
white-space: pre-wrap; /* Since CSS 2.1 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
}
pre > h1 {
font-size: 2rem;
}
pre > h2 {
font-size: 1.75rem;
}
pre > h3 {
font-size: 1.5rem;
}
pre > h4 {
font-size: 1.25rem;
}
pre > h5 {
font-size: 1rem;
}
pre > *:last-child {
margin-bottom: unset;
}
code {
background-color: #222;
padding: 0.5rem;
border-radius: 0.25rem;
}
.message-time {
font-size: 0.75rem;
color: #555
}

View File

@ -1,24 +0,0 @@
<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>

View File

@ -1,28 +0,0 @@
<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>

1
v2/.gitignore vendored
View File

@ -1 +0,0 @@
boarders.v2

View File

@ -1,3 +0,0 @@
module boarders.v2
go 1.22.0

View File

@ -1,9 +0,0 @@
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello Boarders!")
}