Compare commits
No commits in common. "2571ce57564f2a193933ffae43fad9c56d49afa4" and "e44d120ba72bc7332c21f60f2c3f9c8eb9c5cb93" have entirely different histories.
2571ce5756
...
e44d120ba7
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal 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
16
Cargo.toml
Normal 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"
|
@ -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
136
src/html/accounts.rs
Normal 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>
|
||||
"#)
|
||||
}
|
@ -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)))
|
||||
} else {
|
||||
to_json::<Option<u8>>(None)
|
||||
}
|
||||
);
|
||||
//println!("{:?}", x.0);
|
||||
x.0
|
||||
}).collect::<Vec<&serde_json::Value>>();
|
||||
HttpResponse::Ok().body(tdata.handlebars.render("board_index", &btreemap!(
|
||||
"boards" => to_json(boards_json),
|
||||
"user" => to_json(user)
|
||||
)).unwrap())
|
||||
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 {
|
||||
String::from("Never")
|
||||
}
|
||||
)
|
||||
}).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(),
|
||||
{
|
||||
let mut dirty_html = String::new();
|
||||
push_html(&mut dirty_html, parse);
|
||||
to_json(dirty_html)
|
||||
}
|
||||
);
|
||||
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())
|
||||
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 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 {
|
||||
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
154
src/html/index.css
Normal 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
2
src/html/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod accounts;
|
||||
pub mod boards;
|
@ -13,6 +13,8 @@ use super::{
|
||||
messages::Message
|
||||
};
|
||||
|
||||
//use colored::*;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Board {
|
||||
pub title: String,
|
||||
@ -21,22 +23,15 @@ 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>(
|
||||
@ -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())
|
||||
})
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
241
src/main.rs
Normal 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
5
src/res/banner
Normal file
@ -0,0 +1,5 @@
|
||||
__ __
|
||||
/ /_ ____ ____ _ _____ ____/ / _____ _____
|
||||
/ __ \ / __ \ / __ `// ___// __ / / ___// ___/
|
||||
/ /_/ // /_/ // /_/ // / / /_/ /_ / / (__ )
|
||||
/_.___/ \____/ \__,_//_/ \__,_/(_)/_/ /____/
|
154
static/index.css
Normal file
154
static/index.css
Normal 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
17
static/navbar.html
Normal 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
6
v1/.gitignore
vendored
@ -1,6 +0,0 @@
|
||||
|
||||
target
|
||||
|
||||
data
|
||||
|
||||
Cargo.lock
|
@ -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"
|
@ -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
|
@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
println!("Client");
|
||||
}
|
@ -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")?)
|
||||
}
|
@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
480
v1/src/main.rs
480
v1/src/main.rs
@ -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
|
||||
}
|
@ -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>
|
@ -1,6 +0,0 @@
|
||||
____ __
|
||||
/ __ )\ ____ ____ _ _____ ____/ /\__ _____ _____
|
||||
/ __ | / __ \ / __ `/\/ ___// __ // _ \ / ___// ___/\
|
||||
/ /_/ / / /_/ // /_/ / / / _// /_/ // __/\/ / _/(__ )\/
|
||||
/_____/ /\____/ \__,_/ /_/ / \__,_/ \___/ /_/ / /____/\)
|
||||
\_____\/ \___\/ \___\/\_\/ \___\/ \__\/\_\/ \____\/
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -1,11 +0,0 @@
|
||||
<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 user=user}}
|
||||
</div>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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
|
||||
}
|
@ -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>
|
@ -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
1
v2/.gitignore
vendored
@ -1 +0,0 @@
|
||||
boarders.v2
|
@ -1,9 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("Hello Boarders!")
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user