Compare commits
10 Commits
e44d120ba7
...
2571ce5756
Author | SHA1 | Date | |
---|---|---|---|
2571ce5756 | |||
![]() |
fa924285b0 | ||
![]() |
093b6cd868 | ||
![]() |
b92d1e6236 | ||
![]() |
1a3db7f676 | ||
![]() |
1535e037ca | ||
![]() |
b093903fbf | ||
![]() |
2404b04e76 | ||
![]() |
f10be383f9 | ||
![]() |
38a42fdbe3 |
9
.gitignore
vendored
9
.gitignore
vendored
@ -1,9 +0,0 @@
|
|||||||
/target
|
|
||||||
|
|
||||||
# old data directory
|
|
||||||
/boards
|
|
||||||
|
|
||||||
# new data directory, specified in default config file
|
|
||||||
/data
|
|
||||||
|
|
||||||
Cargo.lock
|
|
16
Cargo.toml
16
Cargo.toml
@ -1,16 +0,0 @@
|
|||||||
[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"
|
|
@ -1,136 +0,0 @@
|
|||||||
use actix_web::{
|
|
||||||
get, post, web,
|
|
||||||
HttpResponse, Responder,
|
|
||||||
cookie::Cookie
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::api::types::*;
|
|
||||||
use crate::lib::users::{User, Account};
|
|
||||||
|
|
||||||
#[post("/account/auth")]
|
|
||||||
pub async fn auth_html(
|
|
||||||
data: web::Data<AppState>,
|
|
||||||
form: web::Form<CryptoUserForm>
|
|
||||||
) -> HttpResponse {
|
|
||||||
let user = data.user_manager.lock().unwrap().get_user_by_uname(form.username.clone());
|
|
||||||
if let Some(u) = user {
|
|
||||||
match u.verify(form.password.clone()) {
|
|
||||||
Ok(a) => if a {
|
|
||||||
let mut response = HttpResponse::Ok()
|
|
||||||
.body(format!(r#"
|
|
||||||
<html><head>
|
|
||||||
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
|
|
||||||
<link rel="stylesheet" type="text/css" href="/static/index.css">
|
|
||||||
</head><body>
|
|
||||||
<div id="title" class="bar">
|
|
||||||
<h1>Hello, {0}</h1>
|
|
||||||
</div><div id="index">
|
|
||||||
<p>
|
|
||||||
Logged in as {0}, return to <a href="/boards/">index</a>
|
|
||||||
</p>
|
|
||||||
</body></html></div></body></html>
|
|
||||||
"#, u.username));
|
|
||||||
response.add_cookie(
|
|
||||||
&Cookie::build("auth", format!("{}&{}", u.user_id, u.secret))
|
|
||||||
.domain("boards.solow.xyz")
|
|
||||||
//.domain("localhost")
|
|
||||||
.path("/")
|
|
||||||
.same_site(actix_web::cookie::SameSite::Strict)
|
|
||||||
//.secure(true)
|
|
||||||
.finish()
|
|
||||||
).unwrap();
|
|
||||||
response
|
|
||||||
} else {
|
|
||||||
HttpResponse::Forbidden().body("Forbidden: Could not authenticate user")
|
|
||||||
}
|
|
||||||
Err(e) => HttpResponse::BadRequest().body(format!("Bad Request: {}", e))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
HttpResponse::BadRequest().body("Bad Request: User does not exist")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/account/login/")]
|
|
||||||
pub async fn login() -> impl Responder {
|
|
||||||
HttpResponse::Ok().body(r#"
|
|
||||||
<html><head>
|
|
||||||
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
|
|
||||||
<link rel="stylesheet" type="text/css" href="/static/index.css">
|
|
||||||
</head><body>
|
|
||||||
<div id="title" class="bar">
|
|
||||||
<h1>Login</h1>
|
|
||||||
</div><div id="index">
|
|
||||||
<div class="auth">
|
|
||||||
<form action="/account/auth" method="post">
|
|
||||||
<div>
|
|
||||||
<label for="username">Username: </label>
|
|
||||||
<input type="text" id="username" name="username" required>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="password">Password: </label>
|
|
||||||
<input type="password" id="password" name="password" required>
|
|
||||||
</div>
|
|
||||||
<input type="submit" value="Login">
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</body></html></div></body></html>
|
|
||||||
"#)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/account/new")]
|
|
||||||
pub async fn sign_up_result(
|
|
||||||
data: web::Data<AppState>,
|
|
||||||
form: web::Form<CryptoUserForm>
|
|
||||||
) -> HttpResponse {
|
|
||||||
println!("new user");
|
|
||||||
let mut um = data.user_manager.lock().unwrap();
|
|
||||||
let user = {
|
|
||||||
if let Some(_a) = um.get_user_by_uname(form.username.clone()) {
|
|
||||||
return HttpResponse::Conflict().body("Conflict: user with that username already exists");
|
|
||||||
} else {
|
|
||||||
let new_id = um.assign_id();
|
|
||||||
User::new(new_id, &form.username).encrypt(form.password.clone())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
um.add_user(user.clone());
|
|
||||||
HttpResponse::Created().body(format!(r#"
|
|
||||||
<html><head>
|
|
||||||
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
|
|
||||||
<link rel="stylesheet" type="text/css" href="/static/index.css">
|
|
||||||
</head><body>
|
|
||||||
<div id="title" class="bar">
|
|
||||||
<h1>Hello, {0}</h1>
|
|
||||||
</div><div id="index">
|
|
||||||
<p>
|
|
||||||
Hooray! You can log into you new account <a href="/account/login/">here</a>
|
|
||||||
</p>
|
|
||||||
</body></html></div></body></html>
|
|
||||||
"#, user.username))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/account/new/")]
|
|
||||||
pub async fn sign_up() -> impl Responder {
|
|
||||||
HttpResponse::Ok().body(r#"
|
|
||||||
<html><head>
|
|
||||||
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
|
|
||||||
<link rel="stylesheet" type="text/css" href="/static/index.css">
|
|
||||||
</head><body>
|
|
||||||
<div id="title" class="bar">
|
|
||||||
<h1>Sign Up</h1>
|
|
||||||
</div><div id="index">
|
|
||||||
<div class="auth">
|
|
||||||
<form action="/account/new" method="post">
|
|
||||||
<div>
|
|
||||||
<label for="username">Username: </label>
|
|
||||||
<input type="text" id="username" name="username" required>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="password">Password: </label>
|
|
||||||
<input type="password" id="password" name="password" required>
|
|
||||||
</div>
|
|
||||||
<input type="submit" value="Sign Up">
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</body></html></div></body></html>
|
|
||||||
"#)
|
|
||||||
}
|
|
@ -1,154 +0,0 @@
|
|||||||
body {
|
|
||||||
margin: unset !important;
|
|
||||||
padding: unset;
|
|
||||||
max-width: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar {
|
|
||||||
padding: 0.5em;
|
|
||||||
background-color: #111111;
|
|
||||||
z-index: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar > div {
|
|
||||||
flex-grow: 1;
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar > div > h1 {
|
|
||||||
font-size: 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;
|
|
||||||
}
|
|
@ -1,2 +0,0 @@
|
|||||||
pub mod accounts;
|
|
||||||
pub mod boards;
|
|
241
src/main.rs
241
src/main.rs
@ -1,241 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
__ __
|
|
||||||
/ /_ ____ ____ _ _____ ____/ / _____ _____
|
|
||||||
/ __ \ / __ \ / __ `// ___// __ / / ___// ___/
|
|
||||||
/ /_/ // /_/ // /_/ // / / /_/ /_ / / (__ )
|
|
||||||
/_.___/ \____/ \__,_//_/ \__,_/(_)/_/ /____/
|
|
154
static/index.css
154
static/index.css
@ -1,154 +0,0 @@
|
|||||||
body {
|
|
||||||
margin: unset !important;
|
|
||||||
padding: unset;
|
|
||||||
max-width: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar {
|
|
||||||
padding: 0.5em;
|
|
||||||
background-color: #111111;
|
|
||||||
z-index: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar > div {
|
|
||||||
flex-grow: 1;
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar > div > h1 {
|
|
||||||
font-size: 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;
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
<div id="nav" class="bar">
|
|
||||||
<img class="arrow up" src="https://static.solow.xyz/boards/arrow.svg">
|
|
||||||
<img class="arrow down" style="bottom: 0; position: absolute; right: 0; margin-right: 9; margin-bottom: 28;" src="https://static.solow.xyz/boards/arrow.svg">
|
|
||||||
<div class="vertical-center">
|
|
||||||
<!--<img class="orange-arrow right" src="https://static.solow.xyz/boards/arrow.svg">-->
|
|
||||||
<a href="../">
|
|
||||||
Next<br>
|
|
||||||
Page
|
|
||||||
</a>
|
|
||||||
<hr>
|
|
||||||
<a href="../">
|
|
||||||
Prev<br>
|
|
||||||
Page
|
|
||||||
</a>
|
|
||||||
<!--<img class="orange-arrow left" src="https://static.solow.xyz/boards/arrow.svg">-->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
6
v1/.gitignore
vendored
Normal file
6
v1/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
target
|
||||||
|
|
||||||
|
data
|
||||||
|
|
||||||
|
Cargo.lock
|
37
v1/Cargo.toml
Normal file
37
v1/Cargo.toml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
[package]
|
||||||
|
name = "board_server"
|
||||||
|
version = "2.2.1"
|
||||||
|
edition = "2018"
|
||||||
|
license = "Zlib"
|
||||||
|
repository = "https://git.solow.xyz/cgit.cgi/Boarders/"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix-web = "3.3.2"
|
||||||
|
actix-files = "0.5.0"
|
||||||
|
toml = "0.5"
|
||||||
|
serde = "1.0.125"
|
||||||
|
serde_json = "1.0"
|
||||||
|
bcrypt-bsd = "0.1.3"
|
||||||
|
rand = "0.8.3"
|
||||||
|
console = "0.14.1"
|
||||||
|
colored = "2.0.0"
|
||||||
|
tokio = {version = "1.7.1", features=["net", "rt-multi-thread", "sync", "macros"]}
|
||||||
|
futures = "0.3.15"
|
||||||
|
handlebars = "4.0.1"
|
||||||
|
maplit = "1.0.2"
|
||||||
|
log = "0.4.14"
|
||||||
|
simple_logger = "1.11.0"
|
||||||
|
chrono = "0.4.19"
|
||||||
|
ammonia = "3.1.2"
|
||||||
|
pulldown-cmark = "0.8.0"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "boarders"
|
||||||
|
path = "src/lib/mod.rs"
|
||||||
|
test = false
|
||||||
|
bench = false
|
||||||
|
doc = false
|
||||||
|
harness = false
|
||||||
|
edition = "2018"
|
8
v1/compat/2.2.0.sh
Executable file
8
v1/compat/2.2.0.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
for board in ./data/boards/*/ ; do
|
||||||
|
FILE="$board/board.json"
|
||||||
|
sed -E 's/"timestamp"\:([0-9]+)/"timestamp":{"secs_since_epoch":\1,"nanos_since_epoch":0}/g' $FILE > "${FILE}.new"
|
||||||
|
mv "${FILE}.new" $FILE
|
||||||
|
done
|
@ -61,7 +61,7 @@ pub async fn new(
|
|||||||
}
|
}
|
||||||
}, form.content.clone());
|
}, form.content.clone());
|
||||||
println!("new message");
|
println!("new message");
|
||||||
a.add_message(msg.clone());
|
a.clone().add_message(msg.clone());
|
||||||
Ok(HttpResponse::Created().body(serde_json::to_string(&msg).unwrap()))
|
Ok(HttpResponse::Created().body(serde_json::to_string(&msg).unwrap()))
|
||||||
},
|
},
|
||||||
None => Err(HttpResponse::BadRequest().body("Bad Request: Board does not exist"))
|
None => Err(HttpResponse::BadRequest().body("Bad Request: Board does not exist"))
|
3
v1/src/bin/boarders_client.rs
Normal file
3
v1/src/bin/boarders_client.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
println!("Client");
|
||||||
|
}
|
78
v1/src/html/accounts.rs
Normal file
78
v1/src/html/accounts.rs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
use actix_web::{
|
||||||
|
get, post, web,
|
||||||
|
HttpResponse,
|
||||||
|
cookie::Cookie
|
||||||
|
};
|
||||||
|
use actix_files::NamedFile;
|
||||||
|
|
||||||
|
use crate::api::types::*;
|
||||||
|
use crate::lib::users::{User, Account};
|
||||||
|
use crate::ThreadData;
|
||||||
|
|
||||||
|
use handlebars::to_json;
|
||||||
|
|
||||||
|
use maplit::btreemap;
|
||||||
|
|
||||||
|
#[post("/account/auth")]
|
||||||
|
pub async fn auth_html(
|
||||||
|
tdata: web::Data<crate::ThreadData<'_>>,
|
||||||
|
data: web::Data<AppState>,
|
||||||
|
form: web::Form<CryptoUserForm>
|
||||||
|
) -> HttpResponse {
|
||||||
|
let user = data.user_manager.lock().unwrap().get_user_by_uname(form.username.clone());
|
||||||
|
if let Some(u) = user {
|
||||||
|
match u.verify(form.password.clone()) {
|
||||||
|
Ok(a) => if a {
|
||||||
|
let mut response = HttpResponse::Ok()
|
||||||
|
.body(tdata.handlebars.render("auth", &btreemap!("user" => u.clone())).unwrap());
|
||||||
|
response.add_cookie(
|
||||||
|
&Cookie::build("auth", format!("{}&{}", u.user_id, u.secret))
|
||||||
|
.domain("localhost")
|
||||||
|
//.domain("localhost")
|
||||||
|
.path("/")
|
||||||
|
.same_site(actix_web::cookie::SameSite::Strict)
|
||||||
|
//.secure(true)
|
||||||
|
.finish()
|
||||||
|
).unwrap();
|
||||||
|
response
|
||||||
|
} else {
|
||||||
|
HttpResponse::Forbidden().body("Forbidden: Could not authenticate user")
|
||||||
|
}
|
||||||
|
Err(e) => HttpResponse::BadRequest().body(format!("Bad Request: {}", e))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
HttpResponse::BadRequest().body("Bad Request: User does not exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/account/login/")]
|
||||||
|
pub async fn login() -> actix_web::Result<NamedFile> {
|
||||||
|
Ok(NamedFile::open("static/account_login.html")?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/account/new")]
|
||||||
|
pub async fn sign_up_result(
|
||||||
|
data: web::Data<AppState>,
|
||||||
|
form: web::Form<CryptoUserForm>,
|
||||||
|
tdata: web::Data<ThreadData<'_>>
|
||||||
|
) -> HttpResponse {
|
||||||
|
println!("new user");
|
||||||
|
let mut um = data.user_manager.lock().unwrap();
|
||||||
|
let user = {
|
||||||
|
if let Some(_a) = um.get_user_by_uname(form.username.clone()) {
|
||||||
|
return HttpResponse::Conflict().body("Conflict: user with that username already exists");
|
||||||
|
} else {
|
||||||
|
let new_id = um.assign_id();
|
||||||
|
User::new(new_id, &form.username).encrypt(form.password.clone())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
um.add_user(user.clone());
|
||||||
|
HttpResponse::Created().body(tdata.handlebars.render("new_user_redirect", &btreemap! {
|
||||||
|
"user" => to_json(user.username)
|
||||||
|
}).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/account/new/")]
|
||||||
|
pub async fn sign_up() -> actix_web::Result<NamedFile> {
|
||||||
|
Ok(NamedFile::open("static/new_account.html")?)
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
time,
|
path::PathBuf,
|
||||||
time::Duration,
|
time::SystemTime
|
||||||
path::PathBuf
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
@ -14,59 +13,61 @@ use crate::{
|
|||||||
boards::Board,
|
boards::Board,
|
||||||
users::{UserCookie, User}
|
users::{UserCookie, User}
|
||||||
},
|
},
|
||||||
api::types::*
|
api::types::*,
|
||||||
|
ThreadData
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use handlebars::to_json;
|
||||||
|
use maplit::btreemap;
|
||||||
|
use super::TimeDerive;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use ammonia::clean;
|
||||||
|
use pulldown_cmark::{Parser, Options, html::push_html};
|
||||||
|
|
||||||
#[get("/boards/")]
|
#[get("/boards/")]
|
||||||
pub async fn list_boards(
|
pub async fn list_boards(
|
||||||
|
req: HttpRequest,
|
||||||
data: web::Data<AppState>,
|
data: web::Data<AppState>,
|
||||||
|
tdata: web::Data<ThreadData<'_>>
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
let now = time::SystemTime::now().duration_since(time::UNIX_EPOCH).unwrap();
|
let cookie = req.cookie("auth");
|
||||||
let mut ab = false;
|
let user = if let Some(c) = cookie {
|
||||||
|
if let Ok(v) = UserCookie::parse(c.value()) {
|
||||||
|
data.user_manager.lock().unwrap().get_user_by_id(v.id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let boards = (*data.boards.lock().unwrap()).clone();
|
let boards = (*data.boards.lock().unwrap()).clone();
|
||||||
HttpResponse::Ok().body(format!(
|
let mut boards_json = to_json(&boards);
|
||||||
r#"
|
let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap();
|
||||||
<html><head>
|
let boards_json = boards_json.as_array_mut().unwrap().iter_mut().zip(boards).map(|x| -> &serde_json::Value {
|
||||||
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
|
x.0.as_object_mut().unwrap().insert(
|
||||||
<link rel="stylesheet" type="text/css" href="/static/index.css">
|
"time_meta".to_string(),
|
||||||
</head><body>
|
if let Some(t) = x.1.last_active {
|
||||||
<div id="title" class="bar">
|
to_json(std::time::Duration::from_secs(t).derive_time_since(now).map(|d| (format!("{:.2}", d.0), d.1)))
|
||||||
<div>
|
} else {
|
||||||
<h1>Board Index</h1>
|
to_json::<Option<u8>>(None)
|
||||||
</div>
|
}
|
||||||
<div>
|
);
|
||||||
<p><a href="/account/login/">Login</a>/<a href="/account/new/">Sign Up</a></p>
|
//println!("{:?}", x.0);
|
||||||
</div>
|
x.0
|
||||||
</div><div id="index">
|
}).collect::<Vec<&serde_json::Value>>();
|
||||||
<a href="/boards/new/" style="margin-bottom: 0.5em;">New Board [+]</a>
|
HttpResponse::Ok().body(tdata.handlebars.render("board_index", &btreemap!(
|
||||||
<table style="width: 100%;" cellpadding=8px>
|
"boards" => to_json(boards_json),
|
||||||
<tr><th>Name</th><th>Description</th><th>Last Active</th></tr>
|
"user" => to_json(user)
|
||||||
{}
|
)).unwrap())
|
||||||
</table></body></html></div></body></html>
|
|
||||||
"#,
|
|
||||||
boards.iter().map(|b| {
|
|
||||||
ab = !ab;
|
|
||||||
format!(
|
|
||||||
"<tr class=\"{}\"><td><a href=\"/board/{}/\">{}</a></td><td>{}</td><td>{}</td></tr>",
|
|
||||||
if ab { "a" } else { "b" }, b.id, b.title, b.desc,
|
|
||||||
if let Some(a) = b.messages.borrow().last() {
|
|
||||||
format!("{:.2} minutes", now.checked_sub(Duration::from_secs(a.timestamp)).unwrap().as_secs()/60)
|
|
||||||
} else {
|
|
||||||
String::from("Never")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}).collect::<String>()
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/board/{board_id}/")]
|
#[get("/board/{board_id}/")]
|
||||||
pub async fn get_board(
|
pub async fn get_board(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
|
tdata: web::Data<ThreadData<'_>>,
|
||||||
data: web::Data<AppState>,
|
data: web::Data<AppState>,
|
||||||
web::Path(board_id): web::Path<u32>
|
web::Path(board_id): web::Path<u32>
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
let mut ab = false;
|
|
||||||
let now = time::SystemTime::now().duration_since(time::UNIX_EPOCH).unwrap();
|
|
||||||
let boards = data.boards.lock().unwrap();
|
let boards = data.boards.lock().unwrap();
|
||||||
let cookie = req.cookie("auth");
|
let cookie = req.cookie("auth");
|
||||||
let uid = if cookie.is_some() {
|
let uid = if cookie.is_some() {
|
||||||
@ -75,49 +76,38 @@ pub async fn get_board(
|
|||||||
UserCookie::parse("null")
|
UserCookie::parse("null")
|
||||||
};
|
};
|
||||||
if let Some(a) = Board::search(boards.iter(), board_id) {
|
if let Some(a) = Board::search(boards.iter(), board_id) {
|
||||||
HttpResponse::Ok().body(format!(
|
let data = btreemap!(
|
||||||
r#"
|
"messages" => {
|
||||||
<html><head>
|
let mut msg_json = to_json(&a.messages);
|
||||||
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
|
let mut op = Options::empty();
|
||||||
<link rel="stylesheet" type="text/css" href="/static/index.css">
|
op.insert(Options::ENABLE_TABLES);
|
||||||
</head>
|
|
||||||
<body>
|
let msg_json = msg_json.as_array_mut().unwrap().iter_mut().zip(a.messages.borrow().iter()).map(|x| {
|
||||||
<div id="title" class="bar"><h1>
|
let parse = Parser::new_ext(&x.1.text, op);
|
||||||
{} | <a href="/boards/"><- Back to index</a>
|
let o = x.0.as_object_mut().unwrap();
|
||||||
</h1></div>
|
o.insert(
|
||||||
<div id="index">
|
"time_meta".to_string(),
|
||||||
<table style="width: 100%;" cellpadding=8px>
|
to_json(DateTime::<Utc>::from(x.1.timestamp).to_rfc2822().trim_matches(|c| c=='0' || c=='+' || c==' '))
|
||||||
{}
|
);
|
||||||
</table>
|
o.insert(
|
||||||
<form action="/boards/post" method="post">
|
"render_text".to_string(),
|
||||||
<label for="content">Content:</label><br/>
|
{
|
||||||
<input type="hidden" id="board_id" name="board_id" value="{}">
|
let mut dirty_html = String::new();
|
||||||
<input type="hidden" id="user_id" name="user_id" value="{}">
|
push_html(&mut dirty_html, parse);
|
||||||
<textarea name="content" id="content" rows=12 cols=50 placeholder="Enter message here..."></textarea>
|
to_json(dirty_html)
|
||||||
<br/><input type="submit" value="Post Message" style="margin: 0.5em;">
|
}
|
||||||
</form>
|
);
|
||||||
</div></body></html>
|
x.0
|
||||||
"#,
|
}).collect::<Vec<&mut serde_json::Value>>();
|
||||||
a.title,
|
to_json(msg_json)
|
||||||
a.messages.borrow().iter().map(|x| {
|
},
|
||||||
ab = !ab;
|
"user" => match uid {
|
||||||
format!(
|
Ok(a) => to_json(data.user_manager.lock().unwrap().get_user_by_id(a.id)),
|
||||||
"<tr class=\"message {}\"><td>{}</td><td>{}</td><td>{}</td></tr>",
|
Err(_) => to_json(None::<Option<u8>>)
|
||||||
if ab { "a" } else { "b" },
|
},
|
||||||
{
|
"board" => to_json(a)
|
||||||
let author = x.author.clone().into_inner();
|
);
|
||||||
author.nick.clone().or_else(|| Some(author.username.clone())).unwrap()
|
HttpResponse::Ok().body(tdata.handlebars.render("board", &data).unwrap())
|
||||||
},
|
|
||||||
x.text.replace('\n', "<br>"),
|
|
||||||
format!("{:.2} min.", now.checked_sub(Duration::from_secs(x.timestamp)).unwrap().as_secs()/60)//x.timestamp
|
|
||||||
)
|
|
||||||
}).collect::<String>(),
|
|
||||||
board_id,
|
|
||||||
match uid {
|
|
||||||
Ok(a) => a.id.to_string(),
|
|
||||||
Err(_e) => "null".to_string()
|
|
||||||
}
|
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
HttpResponse::NotFound().body("That board could not be found")
|
HttpResponse::NotFound().body("That board could not be found")
|
||||||
}
|
}
|
||||||
@ -137,15 +127,15 @@ pub async fn board_post(
|
|||||||
Ok(a) => a,
|
Ok(a) => a,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("{}", e);
|
println!("{}", e);
|
||||||
return Err(HttpResponse::BadRequest().body("Bad Request: Could not parse cookie data"));
|
return Err(HttpResponse::BadRequest().body("Bad Request: Could not parse cookie data.\n Not logged in, or bad cookie."));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut mf = data.msg_factory.lock().unwrap();
|
let mut mf = data.msg_factory.lock().unwrap();
|
||||||
let b = data.boards.lock().unwrap();
|
let mut b = data.boards.lock().unwrap();
|
||||||
let board = b.iter().find(|x| (x.id == form.board_id));
|
let mut board = b.iter_mut().find(|x| (x.id == form.board_id));
|
||||||
let um = data.user_manager.lock().unwrap();
|
let um = data.user_manager.lock().unwrap();
|
||||||
match board {
|
match board {
|
||||||
Some(a) => {
|
Some(ref mut a) => {
|
||||||
let msg = mf.create_message({
|
let msg = mf.create_message({
|
||||||
let user = um.get_user_by_id(form.user_id).ok_or_else(|| {
|
let user = um.get_user_by_id(form.user_id).ok_or_else(|| {
|
||||||
HttpResponse::NotFound().body("Not Found: User does not exist")
|
HttpResponse::NotFound().body("Not Found: User does not exist")
|
||||||
@ -155,8 +145,7 @@ pub async fn board_post(
|
|||||||
} else {
|
} else {
|
||||||
return Err(HttpResponse::Unauthorized().body("Unauthorized: Bad login attempt, invalid auth token"));
|
return Err(HttpResponse::Unauthorized().body("Unauthorized: Bad login attempt, invalid auth token"));
|
||||||
}
|
}
|
||||||
}, form.content.clone());
|
}, clean(&form.content));
|
||||||
println!("new message");
|
|
||||||
a.add_message(msg);
|
a.add_message(msg);
|
||||||
Ok(HttpResponse::SeeOther().header("location", format!("/board/{}/", a.id)).finish())
|
Ok(HttpResponse::SeeOther().header("location", format!("/board/{}/", a.id)).finish())
|
||||||
},
|
},
|
||||||
@ -189,29 +178,7 @@ pub async fn new_board(
|
|||||||
return HttpResponse::BadRequest().body("Bad Request: User does not exist");
|
return HttpResponse::BadRequest().body("Bad Request: User does not exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpResponse::Ok().body(r#"
|
HttpResponse::Ok().body(include_str!("../../static/new_board.html"))
|
||||||
<html><head>
|
|
||||||
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
|
|
||||||
<link rel="stylesheet" type="text/css" href="/static/index.css">
|
|
||||||
</head><body>
|
|
||||||
<div id="title" class="bar">
|
|
||||||
<h1>New Board</h1>
|
|
||||||
</div><div id="index">
|
|
||||||
<div class="auth">
|
|
||||||
<form action="/boards/new" method="post">
|
|
||||||
<div>
|
|
||||||
<label for="name">Board Title: </label><br>
|
|
||||||
<input type="text" id="name" name="name" required>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="description">Board Description: </label>
|
|
||||||
<input type="text" id="description" name="description" required>
|
|
||||||
</div>
|
|
||||||
<input type="submit" value="Create Board">
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</body></html></div></body></html>
|
|
||||||
"#)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/boards/new")]
|
#[post("/boards/new")]
|
22
v1/src/html/mod.rs
Normal file
22
v1/src/html/mod.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
pub mod accounts;
|
||||||
|
pub mod boards;
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
trait TimeDerive {
|
||||||
|
fn derive_time_since(&self, time: Duration) -> Option<(f64, String)>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimeDerive for Duration {
|
||||||
|
fn derive_time_since(&self, time: Duration) -> Option<(f64, String)> {
|
||||||
|
time.checked_sub(*self).map(|a| {
|
||||||
|
match a.as_secs() {
|
||||||
|
t if t > 604800 => (t as f64/60.0/60.0/24.0/7.0, "weeks".to_string()),
|
||||||
|
t if t > 86400 => (t as f64/60.0/60.0/24.0, "days".to_string()),
|
||||||
|
t if t > 3600 => (t as f64/60.0/60.0, "hours".to_string()),
|
||||||
|
t if t > 120 => (t as f64/60.0, "mins".to_string()),
|
||||||
|
t => (t as f64, "secs".to_string())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -13,8 +13,6 @@ use super::{
|
|||||||
messages::Message
|
messages::Message
|
||||||
};
|
};
|
||||||
|
|
||||||
//use colored::*;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct Board {
|
pub struct Board {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
@ -23,15 +21,22 @@ pub struct Board {
|
|||||||
pub creation: u64,
|
pub creation: u64,
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
pub path: String,
|
pub path: String,
|
||||||
pub owner: u32
|
pub owner: u32,
|
||||||
|
pub last_active: Option<u64>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Board {
|
impl Board {
|
||||||
pub fn open<P>(path: P) -> Result<Board> where P: AsRef<Path> {
|
pub fn open<P>(path: P) -> Result<Board> where P: AsRef<Path> {
|
||||||
match File::open(&path) {
|
let mut b: std::result::Result<Board, _> = File::open(&path).map(
|
||||||
Ok(a) => Ok(serde_json::from_reader(a).unwrap()),
|
|a| serde_json::from_reader(a).unwrap()
|
||||||
Err(e) => Err(e)
|
);
|
||||||
|
/* backwards compatability */
|
||||||
|
if let Ok(ref mut b) = b {
|
||||||
|
if b.last_active.is_none() {
|
||||||
|
b.last_active = b.messages.borrow().last().map(|x| x.as_secs());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
b
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn search<'a, I>(
|
pub fn search<'a, I>(
|
||||||
@ -44,8 +49,10 @@ impl Board {
|
|||||||
iter.find(|x| x.id == id)
|
iter.find(|x| x.id == id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_message(&self, msg: Message) {
|
pub fn add_message(&mut self, msg: Message) -> &Board {
|
||||||
|
self.last_active = Some(msg.as_secs());
|
||||||
self.messages.borrow_mut().push(msg);
|
self.messages.borrow_mut().push(msg);
|
||||||
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,21 +96,10 @@ impl BoardFactory {
|
|||||||
creation: SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(),
|
creation: SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(),
|
||||||
id: self.assign_id(),
|
id: self.assign_id(),
|
||||||
path: path.to_str().unwrap().to_string(),
|
path: path.to_str().unwrap().to_string(),
|
||||||
owner: creator
|
owner: creator,
|
||||||
|
last_active: None
|
||||||
};
|
};
|
||||||
Ok(b)
|
Ok(b)
|
||||||
/*
|
|
||||||
println!("board path: {:?}", path);
|
|
||||||
fs::create_dir_all(&path).unwrap();
|
|
||||||
path.push("board.json");
|
|
||||||
Board::open(&path).or_else(|_e| -> Result<Board> {
|
|
||||||
if !path.exists() {
|
|
||||||
let mut file = File::create(&path).unwrap();
|
|
||||||
file.write_all(serde_json::to_string(&b).unwrap().as_bytes())?;
|
|
||||||
}
|
|
||||||
Board::open(path.parent().unwrap())
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -14,10 +14,16 @@ use super::{
|
|||||||
pub struct Message {
|
pub struct Message {
|
||||||
pub text: String,
|
pub text: String,
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
pub timestamp: u64,
|
pub timestamp: SystemTime,
|
||||||
pub author: RefCell<User>
|
pub author: RefCell<User>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Message {
|
||||||
|
pub fn as_secs(&self) -> u64 {
|
||||||
|
self.timestamp.duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct MessageFactory {
|
pub struct MessageFactory {
|
||||||
pub next_id: u32,
|
pub next_id: u32,
|
||||||
@ -36,11 +42,10 @@ impl MessageFactory {
|
|||||||
|
|
||||||
pub fn create_message<S>(&mut self, user: User, content: S) -> Message where S: AsRef<str> {
|
pub fn create_message<S>(&mut self, user: User, content: S) -> Message where S: AsRef<str> {
|
||||||
self.next_id += 1;
|
self.next_id += 1;
|
||||||
let time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
|
|
||||||
Message {
|
Message {
|
||||||
text: String::from(content.as_ref()),
|
text: String::from(content.as_ref()),
|
||||||
id: self.next_id-1,
|
id: self.next_id-1,
|
||||||
timestamp: time,
|
timestamp: SystemTime::now(),
|
||||||
author: RefCell::new(user)
|
author: RefCell::new(user)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -58,11 +58,7 @@ impl FactoryFactory {
|
|||||||
|
|
||||||
pub fn build(&self) -> (BoardFactory, MessageFactory, UserManager) {
|
pub fn build(&self) -> (BoardFactory, MessageFactory, UserManager) {
|
||||||
(BoardFactory::new(self.data_dir.clone()),
|
(BoardFactory::new(self.data_dir.clone()),
|
||||||
MessageFactory {
|
MessageFactory::new(self.data_dir.clone()),
|
||||||
next_id: 0,
|
|
||||||
last_write: None,
|
|
||||||
data_dir: self.data_dir.clone()
|
|
||||||
},
|
|
||||||
UserManager::new(self.data_dir.clone()))
|
UserManager::new(self.data_dir.clone()))
|
||||||
}
|
}
|
||||||
}
|
}
|
480
v1/src/main.rs
Normal file
480
v1/src/main.rs
Normal file
@ -0,0 +1,480 @@
|
|||||||
|
use std::{
|
||||||
|
env,
|
||||||
|
error::Error,
|
||||||
|
fs,
|
||||||
|
fs::ReadDir,
|
||||||
|
io,
|
||||||
|
io::{Read, Write},
|
||||||
|
path::Path,
|
||||||
|
process::Command,
|
||||||
|
sync::Mutex,
|
||||||
|
};
|
||||||
|
|
||||||
|
use actix_web::{get, web, App, HttpResponse, HttpServer, Responder};
|
||||||
|
|
||||||
|
mod lib;
|
||||||
|
use lib::{
|
||||||
|
boards::{Board, BoardFactory},
|
||||||
|
messages::MessageFactory,
|
||||||
|
users::UserManager,
|
||||||
|
Archiveable, *,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod api;
|
||||||
|
use api::types::AppState;
|
||||||
|
|
||||||
|
mod html;
|
||||||
|
use html::{accounts::*, boards::*};
|
||||||
|
|
||||||
|
use tokio::{
|
||||||
|
net::{UnixListener, UnixStream},
|
||||||
|
runtime::Runtime,
|
||||||
|
select,
|
||||||
|
sync::{oneshot, watch},
|
||||||
|
};
|
||||||
|
|
||||||
|
use console::style;
|
||||||
|
use handlebars::{handlebars_helper, Handlebars};
|
||||||
|
use maplit::btreemap;
|
||||||
|
|
||||||
|
use log::LevelFilter;
|
||||||
|
use simple_logger::SimpleLogger;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub struct ThreadData<'a> {
|
||||||
|
handlebars: Handlebars<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
async fn index(tdata: web::Data<ThreadData<'_>>) -> impl Responder {
|
||||||
|
HttpResponse::Ok().body(
|
||||||
|
tdata
|
||||||
|
.handlebars
|
||||||
|
.render(
|
||||||
|
"welcome",
|
||||||
|
&btreemap!(
|
||||||
|
"version" => env!("CARGO_PKG_VERSION").to_string()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/debug/")]
|
||||||
|
async fn debug(data: web::Data<AppState>) -> impl Responder {
|
||||||
|
HttpResponse::Ok().body(serde_json::to_string_pretty(data.as_ref()).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn socket_loop(socket: &UnixStream) -> Result<(), Box<dyn Error>> {
|
||||||
|
loop {
|
||||||
|
let mut data = vec![0; 1024];
|
||||||
|
|
||||||
|
socket.readable().await?;
|
||||||
|
let mut command = String::new();
|
||||||
|
loop {
|
||||||
|
match socket.try_read(&mut data) {
|
||||||
|
Ok(0) => break,
|
||||||
|
Ok(_n) => {
|
||||||
|
command.push_str(String::from_utf8(data.clone()).unwrap().trim());
|
||||||
|
println!("command: {}", command);
|
||||||
|
}
|
||||||
|
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.writable().await?;
|
||||||
|
match socket.try_write(&command.into_bytes()) {
|
||||||
|
Ok(_n) => {}
|
||||||
|
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
struct AppInfo<'s> {
|
||||||
|
version: &'s str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> AppInfo<'s> {
|
||||||
|
fn get_version(&self) -> Option<[u8; 3]> {
|
||||||
|
let mut a: [u8; 3] = [0; 3];
|
||||||
|
let mut ver = self.version.split('.');
|
||||||
|
a[0] = ver.next()?.parse().ok()?;
|
||||||
|
a[1] = ver.next()?.parse().ok()?;
|
||||||
|
a[2] = ver.next()?.parse().ok()?;
|
||||||
|
Some(a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static mut APP_INFO: AppInfo = AppInfo {
|
||||||
|
version: env!("CARGO_PKG_VERSION"),
|
||||||
|
};
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> io::Result<()> {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
let mut args = args.iter();
|
||||||
|
args.next().unwrap();
|
||||||
|
for arg in args {
|
||||||
|
match arg.as_str() {
|
||||||
|
"-v" | "--version" => {
|
||||||
|
println!("Boarders v{}", unsafe { APP_INFO.version });
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
println!("Unknown argument \"{}\"", arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* quick and dirty logging */
|
||||||
|
SimpleLogger::new()
|
||||||
|
.with_module_level("mio::poll", LevelFilter::Off)
|
||||||
|
.with_module_level("actix_server::worker", LevelFilter::Debug)
|
||||||
|
.with_module_level("handlebars", LevelFilter::Info)
|
||||||
|
.init()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
println!("Initializing...");
|
||||||
|
/*
|
||||||
|
* TODO: config files
|
||||||
|
* TODO: allow host to specify the data dir via a config file
|
||||||
|
* why? why not? maybe you want multiple servers running on
|
||||||
|
* the same machine
|
||||||
|
* TODO: allow host to specify config file via cmd line (overrides default name)
|
||||||
|
*/
|
||||||
|
let data_dir = Path::new("data/");
|
||||||
|
let mut pwd = env::current_dir().unwrap();
|
||||||
|
|
||||||
|
/* Getting app version and checking for backwards compatability issues */
|
||||||
|
let mut app_info_file = data_dir.to_path_buf();
|
||||||
|
app_info_file.push("app.json");
|
||||||
|
let mut app_info_file = fs::OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.open(app_info_file)
|
||||||
|
.unwrap();
|
||||||
|
let mut app_info_str = String::new();
|
||||||
|
app_info_file.read_to_string(&mut app_info_str).unwrap();
|
||||||
|
let cur_app_info: AppInfo = match serde_json::from_str(&app_info_str) {
|
||||||
|
Ok(a) => a,
|
||||||
|
Err(_) => {
|
||||||
|
println!("Malformed app.json file, backing up then creating new");
|
||||||
|
let s = unsafe { serde_json::to_string(&APP_INFO).unwrap() };
|
||||||
|
app_info_file.write_all(s.as_bytes()).unwrap();
|
||||||
|
unsafe { APP_INFO.clone() }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let ver = cur_app_info.get_version().unwrap();
|
||||||
|
match ver[0] {
|
||||||
|
x if x <= 2 => match ver[1] {
|
||||||
|
y if y <= 2 => match ver[2] {
|
||||||
|
0 => {
|
||||||
|
if Command::new("./compat/2.2.0.sh").output().is_err() {
|
||||||
|
println!("Error, could not complete compatability upgrade");
|
||||||
|
} else {
|
||||||
|
println!("Fixed compat issues for v2.2.0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
println!("Invalid version number?")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
println!("Invalid version number?")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
println!("Invalid version number?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* open/create the directory where board data is placed */
|
||||||
|
let board_files: ReadDir = fs::read_dir({
|
||||||
|
pwd.push(data_dir);
|
||||||
|
pwd.push("boards/");
|
||||||
|
&pwd
|
||||||
|
})
|
||||||
|
.unwrap_or({
|
||||||
|
fs::create_dir_all(&pwd).unwrap();
|
||||||
|
fs::read_dir(pwd).unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut boards: Vec<Board> = Vec::new();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Iterate over all the files inside the board data dir and
|
||||||
|
* open/create them. Will panic! if there is an error opening
|
||||||
|
* a board
|
||||||
|
*/
|
||||||
|
for file in board_files.map(|x| x.unwrap()) {
|
||||||
|
if file.file_type().unwrap().is_dir() {
|
||||||
|
let mut path = file.path().to_path_buf();
|
||||||
|
path.push("board.json");
|
||||||
|
println!("loading board: {:?}", path);
|
||||||
|
boards.push(
|
||||||
|
Board::open(&path)
|
||||||
|
.unwrap_or_else(|e| panic!("Could not open board {:?}\n{}", path, e)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sort boards by id so they display correctly on the board index */
|
||||||
|
boards.sort_by(|a, b| a.id.cmp(&b.id));
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
style(format!("Found and loaded {} boards!", boards.len())).green()
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut board_data = std::path::PathBuf::new();
|
||||||
|
board_data.push(data_dir);
|
||||||
|
let ff = FactoryFactory::new(data_dir);
|
||||||
|
let (mut bf, mf, um) = ff.build();
|
||||||
|
board_data.push("boards/");
|
||||||
|
bf.set_data_dir(board_data);
|
||||||
|
|
||||||
|
/* Initialize all the the shared data crap. copy/pasting code time!! */
|
||||||
|
let web_data = web::Data::new(AppState {
|
||||||
|
boards: Mutex::new(boards.clone()),
|
||||||
|
board_factory: Mutex::new(
|
||||||
|
BoardFactory::load_state({
|
||||||
|
println!("Loading board factory file...");
|
||||||
|
let mut dd = data_dir.to_path_buf();
|
||||||
|
dd.push("boards/factory.json");
|
||||||
|
dd
|
||||||
|
})
|
||||||
|
.unwrap_or(bf),
|
||||||
|
),
|
||||||
|
msg_factory: Mutex::new(
|
||||||
|
MessageFactory::load_state({
|
||||||
|
println!("Loading message factory file...");
|
||||||
|
let mut dd = data_dir.to_path_buf();
|
||||||
|
dd.push("message_factory.json");
|
||||||
|
dd
|
||||||
|
})
|
||||||
|
.unwrap_or(mf),
|
||||||
|
),
|
||||||
|
user_manager: Mutex::new(
|
||||||
|
UserManager::load_state({
|
||||||
|
println!("Loading user manager file...");
|
||||||
|
let mut dd = data_dir.to_path_buf();
|
||||||
|
dd.push("users.json");
|
||||||
|
dd
|
||||||
|
})
|
||||||
|
.unwrap_or(um),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
let wb = web_data.clone();
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
style("Loaded/Created application data files!").green()
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("Connecting to Unix socket...");
|
||||||
|
let (tx0, rx0) = oneshot::channel::<bool>();
|
||||||
|
|
||||||
|
/* NOTE: More phases will probably exist in the future */
|
||||||
|
enum AppPhase {
|
||||||
|
Normal,
|
||||||
|
Shutdown,
|
||||||
|
}
|
||||||
|
|
||||||
|
let (s_tx, mut s_rx) = watch::channel::<AppPhase>(AppPhase::Normal);
|
||||||
|
|
||||||
|
let rt = Runtime::new().unwrap();
|
||||||
|
let _guard = rt.enter();
|
||||||
|
/* spawn the thread that manages the unix socket */
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let p = Path::new("./boarders.sock");
|
||||||
|
if p.exists() {
|
||||||
|
std::fs::remove_file(p).unwrap();
|
||||||
|
}
|
||||||
|
let socket = UnixListener::bind("./boarders.sock").unwrap_or_else(|_| {
|
||||||
|
panic!("{}", style("Could not connect to Unix socket").red());
|
||||||
|
});
|
||||||
|
println!("{}", style("Listening on Unix socket!").green());
|
||||||
|
if tx0.send(true).is_err() {
|
||||||
|
panic!(
|
||||||
|
"{}",
|
||||||
|
style("[FATAL] Thread reciever was dropped! (should never happen)")
|
||||||
|
.red()
|
||||||
|
.bold()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* HACK: clone the reciever a bunch of times to get around moves
|
||||||
|
* not sure if there's a better way
|
||||||
|
*/
|
||||||
|
let s_rx_0 = s_rx.clone();
|
||||||
|
let spawn_t = tokio::spawn(async move {
|
||||||
|
let s_rx = s_rx_0.clone();
|
||||||
|
loop {
|
||||||
|
let mut s_rx = s_rx.clone();
|
||||||
|
/* accept socket then wait for exit, or shutdown signal */
|
||||||
|
let (conn, _addr) = socket.accept().await.unwrap();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
select! {
|
||||||
|
_socket = socket_loop(&conn) => 0,
|
||||||
|
_state = s_rx.changed() => 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FIXME: use enums when I feel like it (not urgent)
|
||||||
|
*/
|
||||||
|
match select! {
|
||||||
|
_st = spawn_t => 0,
|
||||||
|
state = s_rx.changed() => if state.is_ok() {
|
||||||
|
match *s_rx.borrow() {
|
||||||
|
AppPhase::Shutdown => 1,
|
||||||
|
_ => 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
} {
|
||||||
|
0 => println!("Client disconnected"),
|
||||||
|
1 => println!("Shut down client thread(s)"),
|
||||||
|
_ => println!("Unexpected value from client thread"),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* why do I want a special message for that? Honestly I don't know... */
|
||||||
|
rx0.await.unwrap_or_else(|_| {
|
||||||
|
panic!(
|
||||||
|
"{}",
|
||||||
|
style("[FATAL] Thread transmitter was dropped! (should never happen)")
|
||||||
|
.red()
|
||||||
|
.bold()
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
println!("{}", style("Finished initialization").green().bold());
|
||||||
|
println!("Starting server...");
|
||||||
|
|
||||||
|
let server =
|
||||||
|
HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.data(ThreadData {
|
||||||
|
handlebars: {
|
||||||
|
let mut h = Handlebars::new();
|
||||||
|
/*
|
||||||
|
* TODO: allow user to specify wether or not templates should be
|
||||||
|
* loaded dynamically or statically, probably via environment
|
||||||
|
* variable at compile time
|
||||||
|
*/
|
||||||
|
/* register handlebars partials here */
|
||||||
|
handlebars_helper!(msg_helper: |s: str| format!("helped: {}", s.to_string()));
|
||||||
|
h.register_helper("msg", Box::new(msg_helper));
|
||||||
|
h.register_partial("head", include_str!("res/snippets/head.hbs")).unwrap();
|
||||||
|
h.register_partial("login_status", include_str!("res/snippets/login_status.hbs")).unwrap();
|
||||||
|
h.register_partial("board_bar", include_str!("res/snippets/board_bar.hbs")).unwrap();
|
||||||
|
/* register handlebars templates here */
|
||||||
|
h.register_template_string("board_index", include_str!("res/board_index.hbs")).unwrap();
|
||||||
|
h.register_template_string("welcome", include_str!("res/welcome.hbs")).unwrap();
|
||||||
|
h.register_template_string("auth", include_str!("res/auth.hbs")).unwrap();
|
||||||
|
h.register_template_string("board", include_str!("res/board.hbs")).unwrap();
|
||||||
|
h
|
||||||
|
}
|
||||||
|
}).app_data(web_data.clone())
|
||||||
|
.service(index)
|
||||||
|
.service(debug)
|
||||||
|
.service(list_boards)
|
||||||
|
.service(login)
|
||||||
|
.service(auth_html)
|
||||||
|
.service(sign_up)
|
||||||
|
.service(sign_up_result)
|
||||||
|
.service(new_board)
|
||||||
|
.service(new_board_result)
|
||||||
|
.service(get_board)
|
||||||
|
.service(board_post)
|
||||||
|
.service(
|
||||||
|
/* add all api services under the /api/ scope */
|
||||||
|
web::scope("/api")
|
||||||
|
.service(api::boards::new)
|
||||||
|
.service(api::boards::list)
|
||||||
|
.service(api::messages::new)
|
||||||
|
.service(api::users::new)
|
||||||
|
.service(api::users::list)
|
||||||
|
.service(api::users::get)
|
||||||
|
.service(api::users::auth))
|
||||||
|
/* serve all static files inside ../static/ */
|
||||||
|
.service(actix_files::Files::new("/static/", "./static/").show_files_listing())
|
||||||
|
})
|
||||||
|
.bind("127.0.0.1:8080")?
|
||||||
|
.run();
|
||||||
|
println!("Started server");
|
||||||
|
println!(
|
||||||
|
"{}v{}",
|
||||||
|
include_str!("res/banner"),
|
||||||
|
env!("CARGO_PKG_VERSION")
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Program will block here until it is Ctrl-C 'd on the command line
|
||||||
|
* TODO: Block until cmd line exit OR client shutdown command
|
||||||
|
*/
|
||||||
|
let res = server.await;
|
||||||
|
|
||||||
|
println!("\nExiting...");
|
||||||
|
/*
|
||||||
|
* Note to self: handle all errors as much as possible
|
||||||
|
* Use unwrap() as little as possible
|
||||||
|
* If anything goes wrong while saving data data WILL get lost
|
||||||
|
*/
|
||||||
|
println!("Saving application state...");
|
||||||
|
for b in wb.boards.lock().unwrap().iter() {
|
||||||
|
fs::create_dir_all(&b.path)
|
||||||
|
.unwrap_or_else(|x| println!("Could not create directory {:?}: {}", b.path, x));
|
||||||
|
b.save_state("board.json")
|
||||||
|
.unwrap_or_else(|x| println!("Could not save board {:?}: {}", b.title, x));
|
||||||
|
}
|
||||||
|
|
||||||
|
wb.board_factory
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.save_state("factory.json")
|
||||||
|
.unwrap_or_else(|x| println!("Could not save user factory state: {}", x));
|
||||||
|
|
||||||
|
wb.msg_factory
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.save_state("message_factory.json")
|
||||||
|
.unwrap_or_else(|x| println!("Could not save message factory state: {}", x));
|
||||||
|
|
||||||
|
wb.user_manager
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.save_state("users.json")
|
||||||
|
.unwrap_or_else(|x| println!("Could not save user data: {}", x));
|
||||||
|
|
||||||
|
/* send shutdown signal to all unix socket threads */
|
||||||
|
println!("Shutting down client threads...");
|
||||||
|
if s_tx.send(AppPhase::Shutdown).is_err() {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
style("Could not update the app phase to Shutdown")
|
||||||
|
.red()
|
||||||
|
.bold()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
rt.shutdown_timeout(std::time::Duration::from_secs(10));
|
||||||
|
println!("Done!");
|
||||||
|
res
|
||||||
|
}
|
17
v1/src/res/auth.hbs
Normal file
17
v1/src/res/auth.hbs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
{{> head}}
|
||||||
|
<body>
|
||||||
|
<div id="title" class="bar">
|
||||||
|
{{#if user.nick}}
|
||||||
|
<h1>Hello, {{user.nick}}</h1>
|
||||||
|
{{else}}
|
||||||
|
<h1>Hello, {{user.nickname}}</h1>
|
||||||
|
{{/if}}
|
||||||
|
</div><div id="index">
|
||||||
|
<p>
|
||||||
|
Logged in as {{user.username}}, return to <a href="/boards/">index</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
6
v1/src/res/banner
Normal file
6
v1/src/res/banner
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
____ __
|
||||||
|
/ __ )\ ____ ____ _ _____ ____/ /\__ _____ _____
|
||||||
|
/ __ | / __ \ / __ `/\/ ___// __ // _ \ / ___// ___/\
|
||||||
|
/ /_/ / / /_/ // /_/ / / / _// /_/ // __/\/ / _/(__ )\/
|
||||||
|
/_____/ /\____/ \__,_/ /_/ / \__,_/ \___/ /_/ / /____/\)
|
||||||
|
\_____\/ \___\/ \___\/\_\/ \___\/ \__\/\_\/ \____\/
|
36
v1/src/res/board.hbs
Normal file
36
v1/src/res/board.hbs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
{{> head}}
|
||||||
|
<body style="overflow-x: hidden;">
|
||||||
|
{{> board_bar board=board user=user}}
|
||||||
|
<div id="index">
|
||||||
|
<!--messages-->
|
||||||
|
{{#each messages as | msg |}}
|
||||||
|
<div style="border-bottom: 1px solid #555" class="message">
|
||||||
|
{{#if msg.author.nick}}
|
||||||
|
<div style="display: flex; flex-direction: column; align-items: center;">
|
||||||
|
{{msg.author.nick}}<br>
|
||||||
|
<div style="color: #777777;">{{author.username}}</div>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<b>{{msg.author.username}}</b>
|
||||||
|
{{/if}}
|
||||||
|
<div class="message-content">
|
||||||
|
<pre>{{{msg.render_text}}}</pre>
|
||||||
|
</div>
|
||||||
|
<div class="message-time">
|
||||||
|
{{msg.time_meta}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
<form action="/boards/post" method="post" style="display: flex; align-items: center; flex-direction: column;">
|
||||||
|
<label for="content">Write a message:</label><br/>
|
||||||
|
<input type="hidden" id="board_id" name="board_id" value="{{board.id}}">
|
||||||
|
<input type="hidden" id="user_id" name="user_id" value="{{user.user_id}}">
|
||||||
|
<textarea name="content" id="content" rows=12 cols=50 placeholder="Enter message here..."></textarea>
|
||||||
|
<br/>
|
||||||
|
<input type="submit" value="Post Message" style="margin: 0.5em;">
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
29
v1/src/res/board_index.hbs
Normal file
29
v1/src/res/board_index.hbs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
{{> head}}
|
||||||
|
<body style="overflow-x: hidden;">
|
||||||
|
<div id="title" class="bar">
|
||||||
|
<div>
|
||||||
|
<h1>Board Index</h1>
|
||||||
|
</div>
|
||||||
|
{{> login_status}}
|
||||||
|
</div>
|
||||||
|
<div id="index">
|
||||||
|
<a href="/boards/new/" style="margin-bottom: 0.5em;">New Board [+]</a>
|
||||||
|
<table style="width: 100%;" cellpadding=8px>
|
||||||
|
<tr><th>Name</th><th>Description</th><th>Last Active</th></tr>
|
||||||
|
{{#each boards}}
|
||||||
|
<tr class="board">
|
||||||
|
<td><a href="/board/{{id}}/">{{title}}</a></td>
|
||||||
|
<td>{{desc}}</td>
|
||||||
|
{{#if time_meta}}
|
||||||
|
<td>{{time_meta.0}} {{time_meta.1}}</td>
|
||||||
|
{{else}}
|
||||||
|
<td>Never</td>
|
||||||
|
{{/if}}
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
13
v1/src/res/new_account_redirect.hbs
Normal file
13
v1/src/res/new_account_redirect.hbs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
{{> head}}
|
||||||
|
<body>
|
||||||
|
<div id="title" class="bar">
|
||||||
|
<h1>Hello, {{name}}</h1>
|
||||||
|
</div>
|
||||||
|
<div id="index">
|
||||||
|
<p>
|
||||||
|
Hooray! You can log into you new account <a href="/account/login/">here</a>
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
11
v1/src/res/snippets/board_bar.hbs
Normal file
11
v1/src/res/snippets/board_bar.hbs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<div id="title" class="bar">
|
||||||
|
<div>
|
||||||
|
<h1>
|
||||||
|
{{board.title}} | <a href="/boards/"><- Back to index</a>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div id="description">
|
||||||
|
{{board.desc}}
|
||||||
|
</div>
|
||||||
|
{{> login_status user=user}}
|
||||||
|
</div>
|
9
v1/src/res/snippets/head.hbs
Normal file
9
v1/src/res/snippets/head.hbs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
|
||||||
|
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon"/>
|
||||||
|
{{#if nolocal}}
|
||||||
|
{{else}}
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/index.css">
|
||||||
|
{{/if}}
|
||||||
|
</head>
|
17
v1/src/res/snippets/login_status.hbs
Normal file
17
v1/src/res/snippets/login_status.hbs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<div id="login-status">
|
||||||
|
<p>
|
||||||
|
{{#if user}}
|
||||||
|
<a href="/account/dashboard/">
|
||||||
|
{{#with user}}
|
||||||
|
{{#if nick}}
|
||||||
|
Welcome, {{nick}}
|
||||||
|
{{else}}
|
||||||
|
Welcome, {{username}}
|
||||||
|
{{/if}}
|
||||||
|
{{/with}}
|
||||||
|
</a>
|
||||||
|
{{else}}
|
||||||
|
<a href="/account/login/">Login</a>/<a href="/account/new/">Sign Up</a>
|
||||||
|
{{/if}}
|
||||||
|
</p>
|
||||||
|
</div>
|
49
v1/src/res/welcome.hbs
Normal file
49
v1/src/res/welcome.hbs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
{{> head nolocal=1}}
|
||||||
|
<body>
|
||||||
|
<h1>
|
||||||
|
<div style="display: flex; align-items: center; flex-direction: column;">
|
||||||
|
<pre style="font-size: 1.5rem; background-color: unset; overflow-x: unset; margin: unset; padding: unset;">
|
||||||
|
<b>
|
||||||
|
____ __
|
||||||
|
/ __ )\ ____ ____ _ _____ ____/ /\__ _____ _____
|
||||||
|
/ __ | / __ \ / __ `/\/ ___// __ // _ \ / ___// ___/\
|
||||||
|
/ /_/ / / /_/ // /_/ / / / _// /_/ // __/\/ / _/(__ )\/
|
||||||
|
/_____/ /\____/ \__,_/ /_/ / \__,_/ \___/ /_/ / /____/\)
|
||||||
|
\_____\/ \___\/ \___\/\_\/ \___\/ \__\/\_\/ \____\/
|
||||||
|
</b>
|
||||||
|
</pre>
|
||||||
|
<div style="font-size: 2rem;">
|
||||||
|
v{{version}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
<h2>What is it?</h2>
|
||||||
|
Boarders is an easily deployable message board written in Rust.
|
||||||
|
It is extremely efficient on both the frontend and backend.
|
||||||
|
On the frontent all pages are static and (at this time) have no JS, only CSS/HTML.
|
||||||
|
The backend uses the 5th fastest web framework, Actix, according to
|
||||||
|
<a href="https://www.techempower.com/benchmarks/">techempower.com</a>.
|
||||||
|
Which ensures that response times will always be snappy.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<hr>
|
||||||
|
<h2>Rules</h2>
|
||||||
|
<ol>
|
||||||
|
<li>No NSFW<br>
|
||||||
|
At least until NSFW channels are implemented</li>
|
||||||
|
<li>
|
||||||
|
Don't spam boards or user accounts<br>
|
||||||
|
It's just anoying, and if it gets too bad I may have to hard reset everything<br>
|
||||||
|
Don't ruin the fun for everyone
|
||||||
|
</li>
|
||||||
|
<li>Have Fun!<br>
|
||||||
|
Pretty much anything goes, but if people don't like you, that's your fault
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</p>
|
||||||
|
<h3 style="text-align: center;">------<a href="/boards/">[Get Started]</a>------</h3>
|
||||||
|
</body>
|
||||||
|
</html>
|
25
v1/static/account_login.html
Normal file
25
v1/static/account_login.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/index.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="title" class="bar">
|
||||||
|
<h1>Login</h1>
|
||||||
|
</div>
|
||||||
|
<div id="index">
|
||||||
|
<div class="auth">
|
||||||
|
<form action="/account/auth" method="post">
|
||||||
|
<div>
|
||||||
|
<label for="username">Username: </label>
|
||||||
|
<input type="text" id="username" name="username" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password">Password: </label>
|
||||||
|
<input type="password" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<input type="submit" value="Login">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
210
v1/static/index.css
Normal file
210
v1/static/index.css
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
body {
|
||||||
|
margin: unset !important;
|
||||||
|
padding: unset;
|
||||||
|
max-width: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
padding: 0.5em;
|
||||||
|
background-color: #111111;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar > div {
|
||||||
|
flex-grow: 1;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar > div > h1 {
|
||||||
|
font-size: xx-large;
|
||||||
|
margin: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar > div > p {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-right: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar > h1 {
|
||||||
|
width: fit-content;
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar > h1 > a {
|
||||||
|
font-size: 0.5em;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login-status {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
#description {
|
||||||
|
color: #777777;
|
||||||
|
}
|
||||||
|
|
||||||
|
#title {
|
||||||
|
width: 100%;
|
||||||
|
height: 3em;
|
||||||
|
position: inherit;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#title > h1 {
|
||||||
|
font-size: 2.5em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
border-bottom: 1px solid #555;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: inherit;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message > b {
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message > * {
|
||||||
|
padding: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message:hover {
|
||||||
|
background-color: #3a3a3a;
|
||||||
|
animation-duration: 0.1s;
|
||||||
|
animation-name: msg-hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes msg-hover {
|
||||||
|
from {
|
||||||
|
background-color: inherit;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
background-color: #3a3a3a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical-center {
|
||||||
|
margin: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
-ms-transform: translateY(-50%);
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
table, th, td {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth {
|
||||||
|
margin: auto;
|
||||||
|
width: 20em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 2px solid white;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #3A3A3A;
|
||||||
|
}
|
||||||
|
|
||||||
|
#index {
|
||||||
|
padding: 3em;
|
||||||
|
padding-top: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes board-hover {
|
||||||
|
from {
|
||||||
|
background-color: inherit;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
background-color: #444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.board {
|
||||||
|
border-bottom: 1px solid #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.board:nth-child(1n+1) {
|
||||||
|
font-weight: lighter;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.board:hover {
|
||||||
|
animation-duration: 0.25s;
|
||||||
|
animation-name: board-hover;
|
||||||
|
background-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
flex-grow: 1;
|
||||||
|
/*max-width: 90%; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content > pre {
|
||||||
|
margin-top: 0.1rem;
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
white-space: pre-wrap; /* Since CSS 2.1 */
|
||||||
|
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
|
||||||
|
white-space: -pre-wrap; /* Opera 4-6 */
|
||||||
|
white-space: -o-pre-wrap; /* Opera 7 */
|
||||||
|
word-wrap: break-word; /* Internet Explorer 5.5+ */
|
||||||
|
}
|
||||||
|
|
||||||
|
pre > h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre > h2 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre > h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre > h4 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre > h5 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre > *:last-child {
|
||||||
|
margin-bottom: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background-color: #222;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-time {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #555
|
||||||
|
}
|
24
v1/static/new_account.html
Normal file
24
v1/static/new_account.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/index.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="title" class="bar">
|
||||||
|
<h1>Sign Up</h1>
|
||||||
|
</div><div id="index">
|
||||||
|
<div class="auth">
|
||||||
|
<form action="/account/new" method="post">
|
||||||
|
<div>
|
||||||
|
<label for="username">Username: </label>
|
||||||
|
<input type="text" id="username" name="username" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password">Password: </label>
|
||||||
|
<input type="password" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<input type="submit" value="Sign Up">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
28
v1/static/new_board.html
Normal file
28
v1/static/new_board.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/index.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="title" class="bar">
|
||||||
|
<h1>New Board</h1>
|
||||||
|
</div>
|
||||||
|
<div id="index">
|
||||||
|
<div class="auth">
|
||||||
|
<form action="/boards/new" method="post">
|
||||||
|
<div>
|
||||||
|
<label for="name">Board Title: </label><br>
|
||||||
|
<input type="text" id="name" name="name" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="description">Board Description: </label>
|
||||||
|
<input type="text" id="description" name="description" required>
|
||||||
|
</div>
|
||||||
|
<input type="submit" value="Create Board">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
1
v2/.gitignore
vendored
Normal file
1
v2/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
boarders.v2
|
9
v2/main.go
Normal file
9
v2/main.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("Hello Boarders!")
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user