Initial commit v1.0.0 release of Boarders
Version 1.0 release of Boarders. `overflow.tar.gz` is an archive of the project where I discovered a stack overflow due to a bug in Actix, or Rust itself, more investigation is needed. In the future `src/api/` needs to be rewritten to be more reusable so code isn't copy/pasted from there to `src/html/` for the frontend. In v1.1.0 page generation should be handled by a `PageBuilder` that is instantiated once for each thread. This is to dramatically reduce the number of HTML blobs scattered around in `src/html/`
This commit is contained in:
commit
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"
|
||||
BIN
overflow.tar.gz
Normal file
BIN
overflow.tar.gz
Normal file
Binary file not shown.
69
src/api/boards.rs
Normal file
69
src/api/boards.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use actix_web::{
|
||||
web, get, post,
|
||||
Responder, HttpResponse, HttpRequest, HttpMessage
|
||||
};
|
||||
|
||||
use crate::lib::{
|
||||
boards::*,
|
||||
users::UserCookie
|
||||
};
|
||||
use crate::api::types::{AppState, BoardForm};
|
||||
//use types::{AppState, BoardForm};
|
||||
|
||||
#[get("/boards/list")]
|
||||
pub async fn list(
|
||||
data: web::Data<AppState>
|
||||
) -> impl Responder {
|
||||
HttpResponse::Ok().body(serde_json::to_string_pretty(&data.boards.lock().unwrap().clone()).unwrap())
|
||||
}
|
||||
|
||||
#[post("/boards/new")]
|
||||
pub async fn new(
|
||||
req: HttpRequest,
|
||||
data: web::Data<AppState>,
|
||||
form: web::Form<BoardForm>
|
||||
) -> impl Responder {
|
||||
let cookie = req.cookie("auth");
|
||||
if cookie.is_none() {
|
||||
return HttpResponse::Unauthorized().body("Unauthorized: You are not logged in");
|
||||
}
|
||||
|
||||
let cookie = match UserCookie::parse(cookie.unwrap().value()) {
|
||||
Ok(a) => a,
|
||||
Err(_e) => { return HttpResponse::BadRequest().body("Bad Request: Could not parse user cookie") }
|
||||
};
|
||||
|
||||
let um = data.user_manager.lock().unwrap();
|
||||
|
||||
if let Some(u) = um.get_user_by_id(cookie.id) {
|
||||
if cookie.secret != u.secret {
|
||||
return HttpResponse::BadRequest().body("Bad Request: Could not authenticate user");
|
||||
}
|
||||
} else {
|
||||
return HttpResponse::BadRequest().body("Bad Request: User does not exist");
|
||||
}
|
||||
|
||||
let mut bf = data.board_factory.lock().unwrap();
|
||||
let mut path = PathBuf::from(
|
||||
format!(
|
||||
"{}/board.json", bf.next_id
|
||||
)
|
||||
);
|
||||
path.push(&form.name);
|
||||
if let Ok(_a) = Board::open(path) {
|
||||
return HttpResponse::Conflict().body("Conflict: Board already exists");
|
||||
}
|
||||
match bf.create_board(&form.name, &form.description, cookie.id) {
|
||||
Ok(a) => {
|
||||
if form.name.contains(' ') {
|
||||
return HttpResponse::BadRequest().body("Bad Request: Name cannot contain spaces");
|
||||
}
|
||||
println!("new board");
|
||||
data.boards.lock().unwrap().push(a.clone());
|
||||
HttpResponse::Created().body(serde_json::to_string(&a).unwrap())
|
||||
},
|
||||
Err(e) => HttpResponse::InternalServerError().body(format!("Internal Server Error: {}", e))
|
||||
}
|
||||
}
|
||||
69
src/api/messages.rs
Normal file
69
src/api/messages.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use actix_web::{
|
||||
web, post,
|
||||
HttpResponse, HttpRequest, HttpMessage//, Responder
|
||||
};
|
||||
|
||||
use super::types::{
|
||||
AppState, MessageForm
|
||||
};
|
||||
|
||||
use crate::lib::users::{
|
||||
User, UserCookie//, UserCookieParseError
|
||||
};
|
||||
|
||||
/*
|
||||
impl From<UserCookieParseError> for HttpResponse {
|
||||
fn from(ucpe: UserCookieParseError) -> HttpResponse {
|
||||
ucpe.into()
|
||||
}
|
||||
}
|
||||
|
||||
//@ line 40: let cookie = UserCookie::parse(cookie.unwrap().value())?;
|
||||
*/
|
||||
/*
|
||||
* For some reason the compiler lets me implicitly convert from UserCookieParseError
|
||||
* even though (as far as I know) have no real way of (implicitly) converting between the two
|
||||
*
|
||||
* Down the road this causes a core dump following a stack overflow.
|
||||
*/
|
||||
|
||||
#[post("/messages/new")]
|
||||
pub async fn new(
|
||||
req: HttpRequest,
|
||||
form: web::Form<MessageForm>,
|
||||
data: web::Data<AppState>
|
||||
) -> Result<HttpResponse, HttpResponse> {
|
||||
let cookie = req.cookie("auth");
|
||||
if cookie.is_none() {
|
||||
return Err(HttpResponse::Unauthorized().body("Unauthorized: You are not logged in"));
|
||||
}
|
||||
let cookie = match UserCookie::parse(cookie.unwrap().value()) {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
println!("{}", e);
|
||||
return Err(HttpResponse::BadRequest().body("Bad Request: Could not parse cookie data"));
|
||||
}
|
||||
};
|
||||
let mut mf = data.msg_factory.lock().unwrap();
|
||||
let b = data.boards.lock().unwrap();
|
||||
let board = b.iter().find(|x| (x.id == form.board_id));
|
||||
let um = data.user_manager.lock().unwrap();
|
||||
match board {
|
||||
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")
|
||||
})?;
|
||||
if user.secret == cookie.secret {
|
||||
User::from(user)
|
||||
} else {
|
||||
return Err(HttpResponse::Unauthorized().body("Unauthorized: Bad login attempt, invalid auth token"));
|
||||
}
|
||||
}, form.content.clone());
|
||||
println!("new message");
|
||||
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"))
|
||||
}
|
||||
}
|
||||
4
src/api/mod.rs
Normal file
4
src/api/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub mod boards;
|
||||
pub mod messages;
|
||||
pub mod types;
|
||||
pub mod users;
|
||||
55
src/api/types.rs
Normal file
55
src/api/types.rs
Normal file
@ -0,0 +1,55 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use serde::{
|
||||
Deserialize, Serialize
|
||||
};
|
||||
|
||||
use crate::lib::{
|
||||
boards::{BoardFactory, Board},
|
||||
messages::{MessageFactory},
|
||||
users::UserManager
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AppState {
|
||||
pub boards: Mutex<Vec<Board>>,
|
||||
pub board_factory: Mutex<BoardFactory>,
|
||||
pub msg_factory: Mutex<MessageFactory>,
|
||||
pub user_manager: Mutex<UserManager>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct BoardForm {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub author: Option<u32>
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PageForm {
|
||||
pub page: u16
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MessageForm {
|
||||
pub content: String,
|
||||
pub board_id: u32,
|
||||
pub user_id: u32
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UserForm {
|
||||
pub nick: Option<String>,
|
||||
pub id: Option<u32>
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CryptoUserForm {
|
||||
pub username: String,
|
||||
pub password: String
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct NickForm {
|
||||
pub nickname: String
|
||||
}
|
||||
114
src/api/users.rs
Normal file
114
src/api/users.rs
Normal file
@ -0,0 +1,114 @@
|
||||
//use std::fmt::Display;
|
||||
use crate::lib::users::{User, Account};
|
||||
|
||||
use super::types::{AppState, UserForm, CryptoUserForm};//, NickForm};
|
||||
|
||||
use actix_web::{
|
||||
web, get, post,
|
||||
http::{
|
||||
header::ContentType,
|
||||
Cookie,
|
||||
},
|
||||
HttpResponse//, HttpRequest, HttpMessage
|
||||
};
|
||||
|
||||
#[post("/users/new")]
|
||||
pub async fn new(
|
||||
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())
|
||||
}
|
||||
};
|
||||
//HttpResponse::Created().body("Created: Created user")
|
||||
um.add_user(user.clone());
|
||||
HttpResponse::Created().body(serde_json::to_string(&User::from(user)).unwrap())
|
||||
}
|
||||
|
||||
#[get("/users/list")]
|
||||
pub async fn list(
|
||||
data: web::Data<AppState>
|
||||
) -> HttpResponse {
|
||||
let v: Vec<User> = data.user_manager.lock().unwrap().users.clone().into_iter().map(User::from).collect();
|
||||
HttpResponse::Ok().set(ContentType::json()).body(
|
||||
serde_json::to_string(&v).unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/users/get")]
|
||||
pub async fn get(
|
||||
data: web::Data<AppState>,
|
||||
form: web::Form<UserForm>
|
||||
) -> HttpResponse {
|
||||
let um = data.user_manager.lock().unwrap();
|
||||
if let Some(id) = form.id {
|
||||
match um.get_user_by_id(id) {
|
||||
Some(a) => return HttpResponse::Found()
|
||||
.set(ContentType::json())
|
||||
.body(serde_json::to_string_pretty(&User::from(a)).unwrap()),
|
||||
None => return HttpResponse::NotFound().body("Not Found: That user could not be found")
|
||||
}
|
||||
} else if let Some(nick) = &form.nick {
|
||||
match um.get_user_by_nick(nick.to_string()) {
|
||||
Some(a) => return HttpResponse::Found()
|
||||
.set(ContentType::json())
|
||||
.body(serde_json::to_string_pretty(&User::from(a)).unwrap()),
|
||||
None => return HttpResponse::NotFound().body("Not Found: That user could not be found")
|
||||
}
|
||||
} else {
|
||||
HttpResponse::BadRequest().body("Bad Request: Bad form data")
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/users/auth")]
|
||||
pub async fn auth(
|
||||
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(serde_json::to_string(&User::from(u.clone())).unwrap());
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
#[post("/users/set_nick")]
|
||||
pub async fn set_nick(
|
||||
req: HttpRequest,
|
||||
data: web::Data<AppState>,
|
||||
form: web::Form<NickForm>
|
||||
) -> HttpResponse {
|
||||
if let Some(cookie) = req.cookie("auth") {
|
||||
HttpResponse::Ok().body("")
|
||||
} else {
|
||||
HttpResponse::Unauthorized().body("Unauthorized: You are not logged in")
|
||||
}
|
||||
}
|
||||
*/
|
||||
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>
|
||||
"#)
|
||||
}
|
||||
264
src/html/boards.rs
Normal file
264
src/html/boards.rs
Normal file
@ -0,0 +1,264 @@
|
||||
use std::{
|
||||
time,
|
||||
time::Duration,
|
||||
path::PathBuf
|
||||
};
|
||||
|
||||
use actix_web::{
|
||||
get, post, web,
|
||||
HttpResponse, Responder, HttpRequest, HttpMessage
|
||||
};
|
||||
|
||||
use crate::{
|
||||
lib::{
|
||||
boards::Board,
|
||||
users::{UserCookie, User}
|
||||
},
|
||||
api::types::*
|
||||
};
|
||||
|
||||
#[get("/boards/")]
|
||||
pub async fn list_boards(
|
||||
data: web::Data<AppState>,
|
||||
) -> impl Responder {
|
||||
let now = time::SystemTime::now().duration_since(time::UNIX_EPOCH).unwrap();
|
||||
let mut ab = false;
|
||||
let boards = (*data.boards.lock().unwrap()).clone();
|
||||
HttpResponse::Ok().body(format!(
|
||||
r#"
|
||||
<html><head>
|
||||
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/index.css">
|
||||
</head><body>
|
||||
<div id="title" class="bar">
|
||||
<div>
|
||||
<h1>Board Index</h1>
|
||||
</div>
|
||||
<div>
|
||||
<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,
|
||||
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() {
|
||||
UserCookie::parse(cookie.unwrap().value())
|
||||
} else {
|
||||
UserCookie::parse("null")
|
||||
};
|
||||
if let Some(a) = Board::search(boards.iter(), board_id) {
|
||||
HttpResponse::Ok().body(format!(
|
||||
r#"
|
||||
<html><head>
|
||||
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="title" class="bar"><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")
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/boards/post")]
|
||||
pub async fn board_post(
|
||||
req: HttpRequest,
|
||||
form: web::Form<MessageForm>,
|
||||
data: web::Data<AppState>
|
||||
) -> Result<HttpResponse, HttpResponse> {
|
||||
let cookie = req.cookie("auth");
|
||||
if cookie.is_none() {
|
||||
return Err(HttpResponse::Unauthorized().body("Unauthorized: You are not logged in"));
|
||||
}
|
||||
let cookie = match UserCookie::parse(cookie.unwrap().value()) {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
println!("{}", e);
|
||||
return Err(HttpResponse::BadRequest().body("Bad Request: Could not parse cookie data"));
|
||||
}
|
||||
};
|
||||
let mut mf = data.msg_factory.lock().unwrap();
|
||||
let b = data.boards.lock().unwrap();
|
||||
let board = b.iter().find(|x| (x.id == form.board_id));
|
||||
let um = data.user_manager.lock().unwrap();
|
||||
match board {
|
||||
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")
|
||||
})?;
|
||||
if user.secret == cookie.secret {
|
||||
User::from(user)
|
||||
} else {
|
||||
return Err(HttpResponse::Unauthorized().body("Unauthorized: Bad login attempt, invalid auth token"));
|
||||
}
|
||||
}, form.content.clone());
|
||||
println!("new message");
|
||||
a.add_message(msg);
|
||||
Ok(HttpResponse::SeeOther().header("location", format!("/board/{}/", a.id)).finish())
|
||||
},
|
||||
None => Err(HttpResponse::BadRequest().body("Bad Request: Board does not exist"))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/boards/new/")]
|
||||
pub async fn new_board(
|
||||
req: HttpRequest,
|
||||
data: web::Data<AppState>,
|
||||
) -> HttpResponse {
|
||||
let cookie = req.cookie("auth");
|
||||
if cookie.is_none() {
|
||||
return HttpResponse::Unauthorized().body("Unauthorized: You are not logged in");
|
||||
}
|
||||
|
||||
let cookie = match UserCookie::parse(cookie.unwrap().value()) {
|
||||
Ok(a) => a,
|
||||
Err(_e) => { return HttpResponse::BadRequest().body("Bad Request: Could not parse user cookie") }
|
||||
};
|
||||
|
||||
let um = data.user_manager.lock().unwrap();
|
||||
|
||||
if let Some(u) = um.get_user_by_id(cookie.id) {
|
||||
if cookie.secret != u.secret {
|
||||
return HttpResponse::BadRequest().body("Bad Request: Could not authenticate user");
|
||||
}
|
||||
} else {
|
||||
return HttpResponse::BadRequest().body("Bad Request: User does not exist");
|
||||
}
|
||||
|
||||
HttpResponse::Ok().body(r#"
|
||||
<html><head>
|
||||
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/index.css">
|
||||
</head><body>
|
||||
<div id="title" class="bar">
|
||||
<h1>New Board</h1>
|
||||
</div><div id="index">
|
||||
<div class="auth">
|
||||
<form action="/boards/new" method="post">
|
||||
<div>
|
||||
<label for="name">Board Title: </label><br>
|
||||
<input type="text" id="name" name="name" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="description">Board Description: </label>
|
||||
<input type="text" id="description" name="description" required>
|
||||
</div>
|
||||
<input type="submit" value="Create Board">
|
||||
</form>
|
||||
</div>
|
||||
</body></html></div></body></html>
|
||||
"#)
|
||||
}
|
||||
|
||||
#[post("/boards/new")]
|
||||
pub async fn new_board_result(
|
||||
req: HttpRequest,
|
||||
data: web::Data<AppState>,
|
||||
form: web::Form<BoardForm>
|
||||
) -> impl Responder {
|
||||
let cookie = req.cookie("auth");
|
||||
if cookie.is_none() {
|
||||
return HttpResponse::Unauthorized().body("Unauthorized: You are not logged in");
|
||||
}
|
||||
|
||||
let cookie = match UserCookie::parse(cookie.unwrap().value()) {
|
||||
Ok(a) => a,
|
||||
Err(_e) => { return HttpResponse::BadRequest().body("Bad Request: Could not parse user cookie") }
|
||||
};
|
||||
|
||||
let um = data.user_manager.lock().unwrap();
|
||||
|
||||
if let Some(u) = um.get_user_by_id(cookie.id) {
|
||||
if cookie.secret != u.secret {
|
||||
return HttpResponse::BadRequest().body("Bad Request: Could not authenticate user");
|
||||
}
|
||||
} else {
|
||||
return HttpResponse::BadRequest().body("Bad Request: User does not exist");
|
||||
}
|
||||
|
||||
let mut bf = data.board_factory.lock().unwrap();
|
||||
let mut path = PathBuf::from(
|
||||
format!(
|
||||
"{}/board.json", bf.next_id
|
||||
)
|
||||
);
|
||||
path.push(&form.name);
|
||||
if let Ok(_a) = Board::open(path) {
|
||||
return HttpResponse::Conflict().body("Conflict: Board already exists");
|
||||
}
|
||||
match bf.create_board(&form.name, &form.description, cookie.id) {
|
||||
Ok(a) => {
|
||||
if form.name.contains(' ') {
|
||||
return HttpResponse::BadRequest().body("Bad Request: Name cannot contain spaces");
|
||||
}
|
||||
println!("new board");
|
||||
data.boards.lock().unwrap().push(a.clone());
|
||||
HttpResponse::SeeOther().header("location", format!("/board/{}/", a.id)).finish()
|
||||
},
|
||||
Err(e) => HttpResponse::InternalServerError().body(format!("Internal Server Error: {}", e))
|
||||
}
|
||||
}
|
||||
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;
|
||||
17
src/html/navbar.html
Normal file
17
src/html/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>
|
||||
114
src/lib/boards.rs
Normal file
114
src/lib/boards.rs
Normal file
@ -0,0 +1,114 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
path::{Path, PathBuf},
|
||||
fs::File,
|
||||
time::SystemTime,
|
||||
io::Result,
|
||||
iter::Iterator
|
||||
};
|
||||
|
||||
use super::{
|
||||
Archiveable,
|
||||
messages::Message
|
||||
};
|
||||
|
||||
//use colored::*;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Board {
|
||||
pub title: String,
|
||||
pub desc: String,
|
||||
pub messages: RefCell<Vec<Message>>,
|
||||
pub creation: u64,
|
||||
pub id: u32,
|
||||
pub path: String,
|
||||
pub owner: u32
|
||||
}
|
||||
|
||||
impl Board {
|
||||
pub fn open<P>(path: P) -> Result<Board> where P: AsRef<Path> {
|
||||
match File::open(&path) {
|
||||
Ok(a) => Ok(serde_json::from_reader(a).unwrap()),
|
||||
Err(e) => Err(e)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn search<'a, I>(
|
||||
mut iter: I,
|
||||
id: u32
|
||||
) -> Option<&'a Board>
|
||||
where
|
||||
I: Iterator<Item = &'a Board>
|
||||
{
|
||||
iter.find(|x| x.id == id)
|
||||
}
|
||||
|
||||
pub fn add_message(&self, msg: Message) {
|
||||
self.messages.borrow_mut().push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
impl Archiveable<Self> for Board {
|
||||
fn get_root(&self) -> &Path {
|
||||
Path::new(&self.path)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct BoardFactory {
|
||||
pub next_id: u32,
|
||||
pub data_dir: String
|
||||
}
|
||||
|
||||
impl BoardFactory {
|
||||
pub fn new<P>(path: P) -> BoardFactory where P: AsRef<Path> {
|
||||
#[allow(clippy::redundant_field_names)]
|
||||
BoardFactory {
|
||||
next_id: 0,
|
||||
data_dir: path.as_ref().to_str().unwrap().into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assign_id(&mut self) -> u32 {
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
id
|
||||
}
|
||||
pub fn set_data_dir<P>(&mut self, path: P) where P: AsRef<Path> {
|
||||
self.data_dir = path.as_ref().to_str().unwrap().into();
|
||||
}
|
||||
pub fn create_board<S>(&mut self, name: S, descript: S, creator: u32) -> Result<Board> where S: AsRef<str> {
|
||||
let mut path: PathBuf = PathBuf::from(self.data_dir.clone());
|
||||
path.push(self.next_id.to_string());
|
||||
#[allow(clippy::redundant_field_names)]
|
||||
let b = Board {
|
||||
title: String::from(name.as_ref()),
|
||||
desc: String::from(descript.as_ref()),
|
||||
messages: RefCell::new(Vec::new()),
|
||||
creation: SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(),
|
||||
id: self.assign_id(),
|
||||
path: path.to_str().unwrap().to_string(),
|
||||
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())
|
||||
})
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
impl Archiveable<Self> for BoardFactory {
|
||||
fn get_root(&self) -> &Path {
|
||||
Path::new(&self.data_dir)
|
||||
}
|
||||
}
|
||||
53
src/lib/messages.rs
Normal file
53
src/lib/messages.rs
Normal file
@ -0,0 +1,53 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
time::SystemTime,
|
||||
path::Path
|
||||
};
|
||||
|
||||
use super::{
|
||||
Archiveable,
|
||||
users::User
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Message {
|
||||
pub text: String,
|
||||
pub id: u32,
|
||||
pub timestamp: u64,
|
||||
pub author: RefCell<User>
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct MessageFactory {
|
||||
pub next_id: u32,
|
||||
pub last_write: Option<u64>,
|
||||
pub data_dir: String
|
||||
}
|
||||
|
||||
impl MessageFactory {
|
||||
pub fn new<P>(path: P) -> MessageFactory where P: AsRef<Path> {
|
||||
MessageFactory {
|
||||
next_id: 0,
|
||||
last_write: None,
|
||||
data_dir: path.as_ref().to_str().unwrap().into()
|
||||
}
|
||||
}
|
||||
|
||||
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: time,
|
||||
author: RefCell::new(user)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Archiveable<Self> for MessageFactory {
|
||||
fn get_root(&self) -> &Path {
|
||||
Path::new(&self.data_dir)
|
||||
}
|
||||
}
|
||||
68
src/lib/mod.rs
Normal file
68
src/lib/mod.rs
Normal file
@ -0,0 +1,68 @@
|
||||
pub mod boards;
|
||||
pub mod messages;
|
||||
pub mod users;
|
||||
|
||||
use boards::BoardFactory;
|
||||
use messages::MessageFactory;
|
||||
use users::UserManager;
|
||||
|
||||
use std::{
|
||||
path::Path
|
||||
};
|
||||
|
||||
use serde::{de::{DeserializeOwned}, Serialize};
|
||||
|
||||
pub trait Archiveable<T>: Serialize where T: DeserializeOwned {
|
||||
fn get_json(&self) -> String where Self: Serialize {
|
||||
serde_json::to_string(self).unwrap()
|
||||
}
|
||||
|
||||
fn get_root(&self) -> &Path;
|
||||
|
||||
fn save_state<P>(
|
||||
&self, path: P
|
||||
) -> Result<(), std::io::Error> where
|
||||
Self: Serialize,
|
||||
P: AsRef<Path>
|
||||
{
|
||||
std::fs::write({
|
||||
let mut p = self.get_root().to_path_buf();
|
||||
p.push(path);
|
||||
p
|
||||
}, self.get_json())
|
||||
}
|
||||
|
||||
fn load_state<P>(
|
||||
path: P
|
||||
) -> Result<T, std::io::Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let t = std::fs::read_to_string(path)?;
|
||||
let a = serde_json::from_str::<T>(&t);
|
||||
Ok(a.unwrap())
|
||||
//Err(std::io::Error::new(std::io::ErrorKind::NotFound, "asd"))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FactoryFactory {
|
||||
data_dir: String
|
||||
}
|
||||
|
||||
impl FactoryFactory {
|
||||
pub fn new<P>(dir: P) -> FactoryFactory where P: AsRef<Path> {
|
||||
FactoryFactory {
|
||||
data_dir: dir.as_ref().to_str().unwrap().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build(&self) -> (BoardFactory, MessageFactory, UserManager) {
|
||||
(BoardFactory::new(self.data_dir.clone()),
|
||||
MessageFactory {
|
||||
next_id: 0,
|
||||
last_write: None,
|
||||
data_dir: self.data_dir.clone()
|
||||
},
|
||||
UserManager::new(self.data_dir.clone()))
|
||||
}
|
||||
}
|
||||
235
src/lib/users.rs
Normal file
235
src/lib/users.rs
Normal file
@ -0,0 +1,235 @@
|
||||
use std::{
|
||||
result::Result,
|
||||
iter,
|
||||
path::Path,
|
||||
fmt::Display
|
||||
};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use bcrypt_bsd::{gen_salt, hash, to_str};
|
||||
use rand::{self, Rng};
|
||||
use super::Archiveable;
|
||||
|
||||
pub trait Account {
|
||||
fn encrypt(&self, password: String) -> CryptoUser;
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct User {
|
||||
pub user_id: u32,
|
||||
pub nick: Option<String>,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn new<S>(id: u32, username: S) -> User where S: AsRef<str> {
|
||||
User {
|
||||
user_id: id,
|
||||
nick: None,
|
||||
username: username.as_ref().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Account for User {
|
||||
fn encrypt(&self, mut password: String) -> CryptoUser {
|
||||
let new_salt = gen_salt(12).unwrap();
|
||||
#[allow(unused_assignments)]
|
||||
CryptoUser {
|
||||
user_id: self.user_id,
|
||||
nick: self.nick.clone(),
|
||||
username: self.username.clone(),
|
||||
pwh: {
|
||||
let s = to_str(&hash(&password, &new_salt).unwrap()).unwrap().to_string();
|
||||
password = "\0".repeat(password.len());
|
||||
s
|
||||
},
|
||||
salt: to_str(&new_salt).unwrap().to_string(),
|
||||
secret: CryptoUser::gen_secret()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CryptoUser> for User {
|
||||
fn from(item: CryptoUser) -> Self {
|
||||
User {
|
||||
user_id: item.user_id,
|
||||
nick: item.nick.clone(),
|
||||
username: item.username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct CryptoUser {
|
||||
pub user_id: u32,
|
||||
nick: Option<String>,
|
||||
pub username: String,
|
||||
pwh: String,
|
||||
salt: String,
|
||||
pub secret: String
|
||||
}
|
||||
|
||||
impl CryptoUser {
|
||||
pub fn gen_secret() -> String {
|
||||
let mut rng = rand::thread_rng();
|
||||
let s: String = iter::repeat(())
|
||||
.map(|()| rng.sample(rand::distributions::Alphanumeric))
|
||||
.map(char::from)
|
||||
.take(32)
|
||||
.collect();
|
||||
s
|
||||
}
|
||||
|
||||
#[allow(unused_assignments)]
|
||||
pub fn verify(&self, mut password: String) -> Result<bool, bcrypt_bsd::CryptError> {
|
||||
let h = hash(
|
||||
&password,
|
||||
std::ffi::CString::new(self.salt.clone()).unwrap().as_bytes_with_nul()
|
||||
)?;
|
||||
let new_hash = to_str(&h).unwrap();
|
||||
password = "\0".repeat(password.len());
|
||||
Ok(new_hash == self.pwh)
|
||||
}
|
||||
}
|
||||
|
||||
impl Account for CryptoUser {
|
||||
fn encrypt(&self, mut password: String) -> CryptoUser {
|
||||
let new_salt = gen_salt(16).unwrap();
|
||||
#[allow(unused_assignments)]
|
||||
CryptoUser {
|
||||
user_id: self.user_id,
|
||||
nick: self.nick.clone(),
|
||||
username: self.username.clone(),
|
||||
pwh: {
|
||||
let s = to_str(&hash(&password, &new_salt).unwrap()).unwrap().to_string();
|
||||
password = "\0".repeat(password.len());
|
||||
s
|
||||
},
|
||||
salt: to_str(&new_salt).unwrap().to_string(),
|
||||
secret: self.secret.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct UserManager {
|
||||
pub next_id: u32,
|
||||
pub(in super) data_dir: String,
|
||||
pub users: Vec<CryptoUser>
|
||||
}
|
||||
|
||||
impl UserManager {
|
||||
pub fn new<P>(path: P) -> UserManager where P: AsRef<Path> {
|
||||
UserManager {
|
||||
next_id: 0,
|
||||
data_dir: path.as_ref().to_str().unwrap().to_string(),
|
||||
users: Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assign_id(&mut self) -> u32 {
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
id
|
||||
}
|
||||
|
||||
pub fn add_user(&mut self, user: CryptoUser) {
|
||||
self.users.push(user);
|
||||
}
|
||||
|
||||
pub fn get_user_by_id(&self, uid: u32) -> Option<CryptoUser> {
|
||||
self.users.clone().into_iter().find(|a| a.user_id == uid)
|
||||
}
|
||||
pub fn get_user_by_nick(&self, nick: String) -> Option<CryptoUser> {
|
||||
self.users.clone().into_iter().find(|a| {
|
||||
if let Some(an) = a.nick.clone() {
|
||||
an == nick
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
pub fn get_user_by_uname(&self, username: String) -> Option<CryptoUser> {
|
||||
self.users.clone().into_iter().find(|a| a.username == username)
|
||||
}
|
||||
}
|
||||
|
||||
impl Archiveable<Self> for UserManager {
|
||||
fn get_root(&self) -> &Path {
|
||||
Path::new(&self.data_dir)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UserCookie {
|
||||
pub id: u32,
|
||||
pub secret: String
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UserCookieParseError {
|
||||
msg: String
|
||||
}
|
||||
|
||||
impl UserCookieParseError {
|
||||
pub fn new<S>(s: S) -> UserCookieParseError where S: AsRef<str> {
|
||||
UserCookieParseError {
|
||||
msg: s.as_ref().to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for UserCookieParseError {
|
||||
fn from(s: &'static str) -> UserCookieParseError {
|
||||
UserCookieParseError::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::num::ParseIntError> for UserCookieParseError {
|
||||
fn from(err: std::num::ParseIntError) -> UserCookieParseError {
|
||||
UserCookieParseError::new(format!("{}", err))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UserCookieParseError> for String {
|
||||
fn from(s: UserCookieParseError) -> String {
|
||||
s.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for UserCookieParseError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl UserCookie {
|
||||
/*
|
||||
* Gross amounts of error handling because
|
||||
* we don't want a poisoned mutex if someone
|
||||
* decided to mess with cookie data
|
||||
*/
|
||||
pub fn parse<S>(s: S) -> Result<UserCookie, UserCookieParseError>
|
||||
where
|
||||
S: AsRef<str>
|
||||
{
|
||||
let s = s.as_ref().to_string();
|
||||
let mut ss = s.splitn(2, '&');
|
||||
Ok(UserCookie::new(
|
||||
ss.next().ok_or_else(|| UserCookieParseError::new("Not enough fields"))?.parse::<u32>()?,
|
||||
ss.next().ok_or_else(|| UserCookieParseError::new("Not enough fields"))?.to_string(),
|
||||
))
|
||||
}
|
||||
pub fn new<S>(i: u32, s: S) -> UserCookie where S: AsRef<str> {
|
||||
UserCookie {
|
||||
id: i,
|
||||
secret: s.as_ref().to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for UserCookie {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}&{}", self.id, self.secret)
|
||||
}
|
||||
}
|
||||
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>
|
||||
Loading…
x
Reference in New Issue
Block a user