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