[add] guppy connection
[remove] boards (https://code.stellular.org/stellular/puffer) [remove] users (https://code.stellular.org/stellular/guppy) [chore] bump version (v0.11.8 -> v0.12.0)
This commit is contained in:
parent
1ad5aafe1a
commit
5035123a27
|
@ -3,7 +3,7 @@ name = "bundlrs"
|
|||
authors = ["hkau"]
|
||||
license = "MIT"
|
||||
|
||||
version = "0.11.8"
|
||||
version = "0.12.0"
|
||||
edition = "2021"
|
||||
|
||||
rust-version = "1.75"
|
||||
|
@ -43,5 +43,6 @@ sqlx = { version = "0.7.3", features = [
|
|||
] }
|
||||
uuid = { version = "1.6.1", features = ["v4"] }
|
||||
yew = { version = "0.21.0", features = ["ssr"] }
|
||||
handlebars = "5.1.2"
|
||||
redis = "0.25.2"
|
||||
handlebars = "5.1.2"
|
||||
actix-cors = "0.7.0"
|
||||
|
|
|
@ -6,6 +6,8 @@ Bundlrs is a *super* lightweight and [anonymous](#user-accounts) social markdown
|
|||
|
||||
For migration from Bundles, please see [#3](https://code.stellular.org/stellular/bundlrs/issues/3).
|
||||
|
||||
> Also see [Puffer](https://code.stellular.org/stellular/puffer) and [Guppy](https://code.stellular.org/stellular/guppy)! (required)
|
||||
|
||||
## Install
|
||||
|
||||
Bundlrs provides build scripts using [just](https://github.com/casey/just). It is required that `bun`, `just`, `redis`, and (obviously) Rust are installed before running.
|
||||
|
|
553
src/api/auth.rs
553
src/api/auth.rs
|
@ -1,100 +1,32 @@
|
|||
use actix_web::{get, post, web, HttpMessage, HttpRequest, HttpResponse, Responder};
|
||||
use crate::db::bundlesdb::{self, AppData};
|
||||
use actix_web::{get, post, web, HttpRequest, HttpResponse, Responder};
|
||||
|
||||
use crate::{
|
||||
db::bundlesdb::{self, AppData, DefaultReturn, FullUser, UserFollow, UserMetadata},
|
||||
utility,
|
||||
};
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct RegisterInfo {
|
||||
username: String,
|
||||
#[derive(Default, PartialEq, serde::Deserialize)]
|
||||
pub struct CallbackQueryProps {
|
||||
pub uid: Option<String>, // this uid will need to be sent to the client as a token
|
||||
// the uid will also be sent to the client as a token on GUPPY_ROOT, meaning we'll have signed in here and there!
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct LoginInfo {
|
||||
uid: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct UpdateAboutInfo {
|
||||
about: String,
|
||||
}
|
||||
|
||||
#[post("/api/auth/register")]
|
||||
pub async fn register(body: web::Json<RegisterInfo>, data: web::Data<AppData>) -> impl Responder {
|
||||
// if server disabled registration, return
|
||||
let disabled = crate::config::get_var("REGISTRATION_DISABLED");
|
||||
|
||||
if disabled.is_some() {
|
||||
return HttpResponse::NotAcceptable()
|
||||
.body("This server requires has registration disabled");
|
||||
}
|
||||
|
||||
// ...
|
||||
let username = &body.username.trim();
|
||||
let res = data.db.create_user(username.to_string()).await;
|
||||
|
||||
let c = res.clone();
|
||||
let set_cookie = if res.success && res.payload.is_some() {
|
||||
format!("__Secure-Token={}; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}", c.message, 60 * 60 * 24 * 365)
|
||||
#[get("/api/auth/callback")]
|
||||
pub async fn callback_request(info: web::Query<CallbackQueryProps>) -> impl Responder {
|
||||
let set_cookie = if info.uid.is_some() {
|
||||
format!("__Secure-Token={}; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}", info.uid.as_ref().unwrap(), 60 * 60 * 24 * 365)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Set-Cookie", if res.success { &set_cookie } else { "" }))
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&res).unwrap());
|
||||
}
|
||||
|
||||
#[post("/api/auth/login")]
|
||||
pub async fn login(body: web::Json<LoginInfo>, data: web::Data<AppData>) -> impl Responder {
|
||||
let id = body.uid.trim();
|
||||
let id_hashed = utility::hash(id.to_string());
|
||||
|
||||
let res = data
|
||||
.db
|
||||
.get_user_by_hashed(id_hashed) // if the user is returned, that means the ID is valid
|
||||
.await;
|
||||
|
||||
let set_cookie = if res.success && res.payload.is_some() {
|
||||
format!("__Secure-Token={}; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}", body.uid, 60 * 60 * 24 * 365)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Set-Cookie", if res.success { &set_cookie } else { "" }))
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&res).unwrap());
|
||||
}
|
||||
|
||||
#[post("/api/auth/login-st")]
|
||||
pub async fn login_secondary_token(
|
||||
body: web::Json<LoginInfo>,
|
||||
data: web::Data<AppData>,
|
||||
) -> impl Responder {
|
||||
let id = body.uid.trim();
|
||||
let id_unhashed = id.to_string();
|
||||
|
||||
let res = data
|
||||
.db
|
||||
.get_user_by_unhashed_st(id_unhashed) // if the user is returned, that means the token is valid
|
||||
.await;
|
||||
|
||||
let set_cookie = if res.success && res.payload.is_some() {
|
||||
format!("__Secure-Token={}; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}", body.uid, 60 * 60 * 24 * 365)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Set-Cookie", if res.success { &set_cookie } else { "" }))
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&res).unwrap());
|
||||
.append_header((
|
||||
"Set-Cookie",
|
||||
if info.uid.is_some() { &set_cookie } else { "" },
|
||||
))
|
||||
.append_header(("Content-Type", "text/html"))
|
||||
.body(
|
||||
"<head>
|
||||
<meta http-equiv=\"Refresh\" content=\"0; URL=/d\" />
|
||||
</head>",
|
||||
);
|
||||
}
|
||||
|
||||
#[get("/api/auth/logout")]
|
||||
|
@ -121,298 +53,30 @@ pub async fn logout(req: HttpRequest, data: web::Data<AppData>) -> impl Responde
|
|||
.body("You have been signed out. You can now close this tab.");
|
||||
}
|
||||
|
||||
#[post("/api/auth/users/{name:.*}/about")]
|
||||
pub async fn edit_about_request(
|
||||
#[get("/api/auth/users/{name:.*?}/pastes")]
|
||||
/// Get all pastes by owner
|
||||
pub async fn get_from_owner_request(
|
||||
req: HttpRequest,
|
||||
body: web::Json<UpdateAboutInfo>,
|
||||
data: web::Data<AppData>,
|
||||
data: web::Data<bundlesdb::AppData>,
|
||||
info: web::Query<crate::api::pastes::OffsetQueryProps>,
|
||||
) -> impl Responder {
|
||||
let name: String = req.match_info().get("name").unwrap().to_string();
|
||||
|
||||
// get token user
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
return HttpResponse::NotFound().body("Invalid token");
|
||||
}
|
||||
} else {
|
||||
return HttpResponse::NotAcceptable().body("An account is required to do this");
|
||||
}
|
||||
|
||||
let token_user = token_user.unwrap().payload.unwrap();
|
||||
|
||||
// make sure profile exists
|
||||
let profile: DefaultReturn<Option<FullUser<String>>> =
|
||||
data.db.get_user_by_username(name.to_owned()).await;
|
||||
|
||||
if !profile.success {
|
||||
return HttpResponse::NotFound()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(
|
||||
serde_json::to_string::<DefaultReturn<Option<String>>>(&DefaultReturn {
|
||||
success: false,
|
||||
message: String::from("Profile does not exist!"),
|
||||
payload: Option::None,
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let profile = profile.payload.unwrap();
|
||||
let mut user = serde_json::from_str::<UserMetadata>(&profile.user.metadata).unwrap();
|
||||
|
||||
// check if we can update this user
|
||||
// must be authenticated AND same user OR staff
|
||||
let can_update: bool = (token_user.user.username == profile.user.username)
|
||||
| (token_user
|
||||
.level
|
||||
.permissions
|
||||
.contains(&String::from("ManageUsers")));
|
||||
|
||||
if can_update == false {
|
||||
return HttpResponse::NotFound()
|
||||
.body("You do not have permission to manage this user's contents.");
|
||||
}
|
||||
|
||||
// (check length)
|
||||
if (body.about.len() < 2) | (body.about.len() > 200_000) {
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(
|
||||
serde_json::to_string::<DefaultReturn<Option<String>>>(&DefaultReturn {
|
||||
success: false,
|
||||
message: String::from("Content is invalid"),
|
||||
payload: Option::None,
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
// update about
|
||||
user.about = body.about.clone();
|
||||
|
||||
// ...
|
||||
let res = data.db.edit_user_metadata_by_name(name, user).await;
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&res).unwrap());
|
||||
}
|
||||
|
||||
#[post("/api/auth/users/{name:.*}/secondary-token")]
|
||||
pub async fn refresh_secondary_token_request(
|
||||
req: HttpRequest,
|
||||
data: web::Data<AppData>,
|
||||
) -> impl Responder {
|
||||
let name: String = req.match_info().get("name").unwrap().to_string();
|
||||
|
||||
// get token user
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
return HttpResponse::NotFound().body("Invalid token");
|
||||
}
|
||||
} else {
|
||||
return HttpResponse::NotAcceptable().body("An account is required to do this");
|
||||
}
|
||||
|
||||
let token_user = token_user.unwrap().payload.unwrap();
|
||||
|
||||
// make sure profile exists
|
||||
let profile: DefaultReturn<Option<FullUser<String>>> =
|
||||
data.db.get_user_by_username(name.to_owned()).await;
|
||||
|
||||
if !profile.success {
|
||||
return HttpResponse::NotFound()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(
|
||||
serde_json::to_string::<DefaultReturn<Option<String>>>(&DefaultReturn {
|
||||
success: false,
|
||||
message: String::from("Profile does not exist!"),
|
||||
payload: Option::None,
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let profile = profile.payload.unwrap();
|
||||
let mut user = serde_json::from_str::<UserMetadata>(&profile.user.metadata).unwrap();
|
||||
|
||||
// check if we can update this user
|
||||
// must be authenticated AND same user OR staff
|
||||
let can_update: bool = (token_user.user.username == profile.user.username)
|
||||
| (token_user
|
||||
.level
|
||||
.permissions
|
||||
.contains(&String::from("ManageUsers")));
|
||||
|
||||
if can_update == false {
|
||||
return HttpResponse::NotFound()
|
||||
.body("You do not have permission to manage this user's contents.");
|
||||
}
|
||||
|
||||
// update secondary token
|
||||
let token = crate::utility::uuid();
|
||||
user.secondary_token = Option::Some(crate::utility::hash(token.clone())); // this is essentially just a second ID the user can signin with
|
||||
|
||||
// ...
|
||||
let res = data.db.edit_user_metadata_by_name(name, user).await;
|
||||
// get pastes
|
||||
let res: bundlesdb::DefaultReturn<Option<Vec<bundlesdb::PasteIdentifier>>> =
|
||||
data.db.get_pastes_by_owner_limited(name, info.offset).await;
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(
|
||||
serde_json::to_string::<DefaultReturn<String>>(&DefaultReturn {
|
||||
success: res.success,
|
||||
message: res.message,
|
||||
payload: token,
|
||||
})
|
||||
serde_json::to_string::<
|
||||
bundlesdb::DefaultReturn<Option<Vec<bundlesdb::PasteIdentifier>>>,
|
||||
>(&res)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
#[post("/api/auth/users/{name:.*}/follow")]
|
||||
pub async fn follow_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
|
||||
let name: String = req.match_info().get("name").unwrap().to_string();
|
||||
|
||||
// get token user
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
return HttpResponse::NotFound().body("Invalid token");
|
||||
}
|
||||
} else {
|
||||
return HttpResponse::NotAcceptable().body("An account is required to do this");
|
||||
}
|
||||
|
||||
let token_user = token_user.unwrap().payload.unwrap();
|
||||
|
||||
// ...
|
||||
let res = data
|
||||
.db
|
||||
.toggle_user_follow(&mut UserFollow {
|
||||
user: token_user.user.username,
|
||||
is_following: name,
|
||||
})
|
||||
.await;
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&res).unwrap());
|
||||
}
|
||||
|
||||
#[post("/api/auth/users/{name:.*}/update")]
|
||||
pub async fn update_request(
|
||||
req: HttpRequest,
|
||||
body: web::Json<UserMetadata>,
|
||||
data: web::Data<AppData>,
|
||||
) -> impl Responder {
|
||||
let name: String = req.match_info().get("name").unwrap().to_string();
|
||||
|
||||
// get token user
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
return HttpResponse::NotFound().body("Invalid token");
|
||||
}
|
||||
} else {
|
||||
return HttpResponse::NotAcceptable().body("An account is required to do this");
|
||||
}
|
||||
|
||||
// make sure profile exists
|
||||
let profile: DefaultReturn<Option<FullUser<String>>> =
|
||||
data.db.get_user_by_username(name.to_owned()).await;
|
||||
|
||||
if !profile.success {
|
||||
return HttpResponse::NotFound()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(
|
||||
serde_json::to_string::<DefaultReturn<Option<String>>>(&DefaultReturn {
|
||||
success: false,
|
||||
message: String::from("Profile does not exist!"),
|
||||
payload: Option::None,
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let token_user = token_user.unwrap().payload.unwrap();
|
||||
let profile = profile.payload.unwrap();
|
||||
|
||||
// check if we can update this user
|
||||
// must be authenticated AND same user OR staff
|
||||
let can_update: bool = (token_user.user.username == profile.user.username)
|
||||
| (token_user
|
||||
.level
|
||||
.permissions
|
||||
.contains(&String::from("ManageUsers")));
|
||||
|
||||
if can_update == false {
|
||||
return HttpResponse::NotFound()
|
||||
.body("You do not have permission to manage this user's contents.");
|
||||
}
|
||||
|
||||
// ...
|
||||
let res = data
|
||||
.db
|
||||
.edit_user_metadata_by_name(
|
||||
name, // select user
|
||||
body.to_owned(), // new metadata
|
||||
)
|
||||
.await;
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&res).unwrap());
|
||||
}
|
||||
|
||||
#[post("/api/auth/users/{name:.*?}/ban")]
|
||||
/// Ban user
|
||||
pub async fn ban_request(req: HttpRequest, data: web::Data<bundlesdb::AppData>) -> impl Responder {
|
||||
|
@ -459,158 +123,3 @@ pub async fn ban_request(req: HttpRequest, data: web::Data<bundlesdb::AppData>)
|
|||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string::<bundlesdb::DefaultReturn<Option<String>>>(&res).unwrap());
|
||||
}
|
||||
|
||||
#[get("/api/auth/users/{name:.*}/followers")]
|
||||
pub async fn followers_request(
|
||||
req: HttpRequest,
|
||||
data: web::Data<AppData>,
|
||||
info: web::Query<crate::pages::boards::ViewBoardQueryProps>,
|
||||
) -> impl Responder {
|
||||
let name: String = req.match_info().get("name").unwrap().to_string();
|
||||
|
||||
// get followers
|
||||
let res: DefaultReturn<Option<Vec<bundlesdb::Log>>> = data
|
||||
.db
|
||||
.get_user_followers(name.to_owned(), info.offset)
|
||||
.await;
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string::<DefaultReturn<Option<Vec<bundlesdb::Log>>>>(&res).unwrap());
|
||||
}
|
||||
|
||||
#[get("/api/auth/users/{name:.*}/following")]
|
||||
pub async fn following_request(
|
||||
req: HttpRequest,
|
||||
data: web::Data<AppData>,
|
||||
info: web::Query<crate::pages::boards::ViewBoardQueryProps>,
|
||||
) -> impl Responder {
|
||||
let name: String = req.match_info().get("name").unwrap().to_string();
|
||||
|
||||
// get following
|
||||
let res: DefaultReturn<Option<Vec<bundlesdb::Log>>> = data
|
||||
.db
|
||||
.get_user_following(name.to_owned(), info.offset)
|
||||
.await;
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string::<DefaultReturn<Option<Vec<bundlesdb::Log>>>>(&res).unwrap());
|
||||
}
|
||||
|
||||
#[get("/api/auth/users/{name:.*}/avatar")]
|
||||
pub async fn avatar_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
|
||||
let name: String = req.match_info().get("name").unwrap().to_string();
|
||||
|
||||
// make sure profile exists
|
||||
let profile: DefaultReturn<Option<FullUser<String>>> =
|
||||
data.db.get_user_by_username(name.to_owned()).await;
|
||||
|
||||
if !profile.success {
|
||||
return HttpResponse::NotFound()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(
|
||||
serde_json::to_string::<DefaultReturn<Option<String>>>(&DefaultReturn {
|
||||
success: false,
|
||||
message: String::from("Profile does not exist!"),
|
||||
payload: Option::None,
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let profile = profile.payload.unwrap();
|
||||
let user = serde_json::from_str::<UserMetadata>(&profile.user.metadata).unwrap();
|
||||
|
||||
if user.avatar_url.is_none() {
|
||||
return HttpResponse::NotFound().body("User does not have an avatar set");
|
||||
}
|
||||
|
||||
let avatar = user.avatar_url.unwrap();
|
||||
|
||||
// fetch avatar
|
||||
let res = data
|
||||
.http_client
|
||||
.get(avatar)
|
||||
.timeout(std::time::Duration::from_millis(5_000))
|
||||
.insert_header(("User-Agent", "stellular-bundlrs/1.0"))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if res.is_err() {
|
||||
return HttpResponse::NotFound().body(format!(
|
||||
"Failed to fetch avatar on server: {}",
|
||||
res.err().unwrap()
|
||||
));
|
||||
}
|
||||
|
||||
// ...
|
||||
let mut res = res.unwrap();
|
||||
let body = res.body().limit(10_000_000).await;
|
||||
|
||||
if body.is_err() {
|
||||
return HttpResponse::NotFound().body(
|
||||
"Failed to fetch avatar on server (image likely too large, please keep under 10 MB)",
|
||||
);
|
||||
}
|
||||
|
||||
let body = body.unwrap();
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", res.content_type()))
|
||||
.body(body);
|
||||
}
|
||||
|
||||
#[get("/api/auth/users/{name:.*?}/pastes")]
|
||||
/// Get all pastes by owner
|
||||
pub async fn get_from_owner_request(
|
||||
req: HttpRequest,
|
||||
data: web::Data<bundlesdb::AppData>,
|
||||
info: web::Query<crate::pages::boards::ViewBoardQueryProps>,
|
||||
) -> impl Responder {
|
||||
let name: String = req.match_info().get("name").unwrap().to_string();
|
||||
|
||||
// get pastes
|
||||
let res: bundlesdb::DefaultReturn<Option<Vec<bundlesdb::PasteIdentifier>>> =
|
||||
data.db.get_pastes_by_owner_limited(name, info.offset).await;
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(
|
||||
serde_json::to_string::<
|
||||
bundlesdb::DefaultReturn<Option<Vec<bundlesdb::PasteIdentifier>>>,
|
||||
>(&res)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
#[get("/api/auth/users/{name:.*}/level")]
|
||||
pub async fn level_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
|
||||
let name: String = req.match_info().get("name").unwrap().to_string();
|
||||
|
||||
// get user
|
||||
let res: DefaultReturn<Option<FullUser<String>>> =
|
||||
data.db.get_user_by_username(name.to_owned()).await;
|
||||
|
||||
if res.success == false {
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(
|
||||
serde_json::to_string::<bundlesdb::RoleLevel>(&bundlesdb::RoleLevel {
|
||||
elevation: -1000,
|
||||
name: String::from("anonymous"),
|
||||
permissions: Vec::new(),
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string::<bundlesdb::RoleLevel>(&res.payload.unwrap().level).unwrap());
|
||||
}
|
||||
|
|
|
@ -1,839 +0,0 @@
|
|||
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse, Responder};
|
||||
|
||||
use crate::{
|
||||
db::bundlesdb::{
|
||||
AppData, Board, BoardMetadata, BoardPostLog, DefaultReturn, UserMailStreamIdentifier,
|
||||
},
|
||||
pages::boards,
|
||||
};
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct CreateInfo {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct CreatePostInfo {
|
||||
content: String,
|
||||
reply: Option<String>,
|
||||
topic: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct UpdatePostInfo {
|
||||
content: String,
|
||||
topic: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct UpdatePostTagsInfo {
|
||||
tags: String,
|
||||
}
|
||||
|
||||
#[post("/api/board/new")]
|
||||
pub async fn create_request(
|
||||
req: HttpRequest,
|
||||
body: web::Json<CreateInfo>,
|
||||
data: web::Data<AppData>,
|
||||
) -> impl Responder {
|
||||
// get token user
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
return HttpResponse::NotFound().body("Invalid token");
|
||||
}
|
||||
} else {
|
||||
return HttpResponse::NotAcceptable().body("An account is required to do this");
|
||||
}
|
||||
|
||||
// ...
|
||||
let res = data
|
||||
.db
|
||||
.create_board(
|
||||
&mut Board {
|
||||
name: body.name.clone(),
|
||||
timestamp: 0,
|
||||
metadata: String::new(),
|
||||
},
|
||||
if token_user.is_some() {
|
||||
Option::Some(token_user.unwrap().payload.unwrap().user.username)
|
||||
} else {
|
||||
Option::None
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&res).unwrap());
|
||||
}
|
||||
|
||||
#[post("/api/auth/users/{name:.*}/mail")]
|
||||
pub async fn create_mail_stream_request(
|
||||
req: HttpRequest,
|
||||
data: web::Data<AppData>,
|
||||
) -> impl Responder {
|
||||
let name: String = req.match_info().get("name").unwrap().to_string();
|
||||
|
||||
// get token user
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
return HttpResponse::NotFound().body("Invalid token");
|
||||
}
|
||||
} else {
|
||||
return HttpResponse::NotAcceptable().body("An account is required to do this");
|
||||
}
|
||||
|
||||
// ...
|
||||
let res = data
|
||||
.db
|
||||
.create_mail_stream(&mut UserMailStreamIdentifier {
|
||||
_is_user_mail_stream: true,
|
||||
user1: token_user.unwrap().payload.unwrap().user.username,
|
||||
user2: name,
|
||||
})
|
||||
.await;
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&res).unwrap());
|
||||
}
|
||||
|
||||
#[get("/api/board/{name:.*}/posts")]
|
||||
pub async fn get_posts_request(
|
||||
req: HttpRequest,
|
||||
data: web::Data<AppData>,
|
||||
info: web::Query<boards::ViewBoardQueryProps>,
|
||||
) -> impl Responder {
|
||||
let name: String = req.match_info().get("name").unwrap().to_string();
|
||||
|
||||
let board: DefaultReturn<Option<Board<String>>> = data.db.get_board_by_name(name.clone()).await;
|
||||
|
||||
// get
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
|
||||
let token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
// check if board is private
|
||||
// if it is, only the owner and users with the "staff" role can view it
|
||||
if board.payload.is_some() {
|
||||
let metadata =
|
||||
serde_json::from_str::<BoardMetadata>(&board.payload.as_ref().unwrap().metadata)
|
||||
.unwrap();
|
||||
|
||||
if metadata.is_private == String::from("yes") {
|
||||
// anonymous
|
||||
if token_user.is_none() {
|
||||
return HttpResponse::NotFound()
|
||||
.body("You do not have permission to view this board's contents.");
|
||||
}
|
||||
|
||||
// not owner
|
||||
let user = token_user.unwrap().payload.unwrap();
|
||||
|
||||
if (user.user.username != metadata.owner)
|
||||
&& (user
|
||||
.level
|
||||
.permissions
|
||||
.contains(&String::from("ManageBoards")))
|
||||
{
|
||||
return HttpResponse::NotFound()
|
||||
.body("You do not have permission to view this board's contents.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
let res = data.db.get_board_posts(name, info.offset).await;
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&res).unwrap());
|
||||
}
|
||||
|
||||
#[get("/api/board/{name:.*}/posts/{id:.*}")]
|
||||
pub async fn get_post_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
|
||||
let name: String = req.match_info().get("name").unwrap().to_string();
|
||||
let id: String = req.match_info().get("id").unwrap().to_string();
|
||||
|
||||
let board: DefaultReturn<Option<Board<String>>> = data.db.get_board_by_name(name.clone()).await;
|
||||
|
||||
// get
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
|
||||
let token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
// check if board is private
|
||||
// if it is, only the owner and users with the "staff" role can view it
|
||||
if board.payload.is_some() {
|
||||
let metadata =
|
||||
serde_json::from_str::<BoardMetadata>(&board.payload.as_ref().unwrap().metadata)
|
||||
.unwrap();
|
||||
|
||||
if metadata.is_private == String::from("yes") {
|
||||
// anonymous
|
||||
if token_user.is_none() {
|
||||
return HttpResponse::NotFound()
|
||||
.body("You do not have permission to view this board's contents.");
|
||||
}
|
||||
|
||||
// not owner
|
||||
let user = token_user.unwrap().payload.unwrap();
|
||||
|
||||
if (user.user.username != metadata.owner)
|
||||
&& (user
|
||||
.level
|
||||
.permissions
|
||||
.contains(&String::from("ManageBoards")))
|
||||
{
|
||||
return HttpResponse::NotFound()
|
||||
.body("You do not have permission to view this board's contents.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
let res = data.db.get_log_by_id(id).await;
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&res).unwrap());
|
||||
}
|
||||
|
||||
#[post("/api/board/{name:.*}/posts")]
|
||||
pub async fn create_post_request(
|
||||
req: HttpRequest,
|
||||
body: web::Json<CreatePostInfo>,
|
||||
data: web::Data<AppData>,
|
||||
) -> impl Responder {
|
||||
let name: String = req.match_info().get("name").unwrap().to_string();
|
||||
|
||||
// get token user
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
return HttpResponse::NotFound().body("Invalid token");
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
let token_user = if token_user.is_some() {
|
||||
Option::Some(token_user.unwrap().payload.unwrap())
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
let res = data
|
||||
.db
|
||||
.create_board_post(
|
||||
&mut BoardPostLog {
|
||||
author: String::new(),
|
||||
content: body.content.clone(), // use given content
|
||||
content_html: String::new(),
|
||||
topic: body.topic.clone(),
|
||||
board: name,
|
||||
is_hidden: false,
|
||||
reply: if body.reply.is_some() {
|
||||
Option::Some(body.reply.as_ref().unwrap().to_string())
|
||||
} else {
|
||||
Option::None
|
||||
},
|
||||
pinned: Option::Some(false),
|
||||
replies: Option::None,
|
||||
tags: Option::None,
|
||||
},
|
||||
if token_user.is_some() {
|
||||
Option::Some(token_user.clone().unwrap().user.username)
|
||||
} else {
|
||||
Option::None
|
||||
},
|
||||
if token_user.is_some() {
|
||||
Option::Some(token_user.clone().unwrap().user.role)
|
||||
} else {
|
||||
Option::None
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&res).unwrap());
|
||||
}
|
||||
|
||||
#[post("/api/board/{name:.*}/posts/{id:.*}/pin")]
|
||||
pub async fn pin_post_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
|
||||
let name: String = req.match_info().get("name").unwrap().to_string();
|
||||
let id: String = req.match_info().get("id").unwrap().to_string();
|
||||
|
||||
// get token user
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
return HttpResponse::NotFound().body("Invalid token");
|
||||
}
|
||||
} else {
|
||||
return HttpResponse::NotAcceptable().body("An account is required to do this");
|
||||
}
|
||||
|
||||
// make sure board exists
|
||||
let board: DefaultReturn<Option<Board<String>>> =
|
||||
data.db.get_board_by_name(name.to_owned()).await;
|
||||
|
||||
if !board.success {
|
||||
return HttpResponse::NotFound()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(
|
||||
serde_json::to_string::<DefaultReturn<Option<String>>>(&DefaultReturn {
|
||||
success: false,
|
||||
message: String::from("Board does not exist!"),
|
||||
payload: Option::None,
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let board = serde_json::from_str::<BoardMetadata>(&board.payload.unwrap().metadata).unwrap();
|
||||
|
||||
// get post
|
||||
let p = data.db.get_log_by_id(id.to_owned()).await;
|
||||
let mut post = serde_json::from_str::<BoardPostLog>(&p.payload.unwrap().content).unwrap();
|
||||
|
||||
// check if we can pin this post
|
||||
// must be authenticated AND board owner OR staff
|
||||
let user = token_user.unwrap().payload.unwrap();
|
||||
|
||||
let can_pin: bool = (user.user.username != String::from("Anonymous"))
|
||||
&& ((user.user.username == board.owner)
|
||||
| (user
|
||||
.level
|
||||
.permissions
|
||||
.contains(&String::from("ManageBoards"))));
|
||||
|
||||
if can_pin == false {
|
||||
return HttpResponse::NotFound()
|
||||
.body("You do not have permission to manage this board's contents.");
|
||||
}
|
||||
|
||||
// toggle pinned
|
||||
if post.pinned.is_some() {
|
||||
post.pinned = Option::Some(!post.pinned.unwrap())
|
||||
} else {
|
||||
// update to "true" by default
|
||||
post.pinned = Option::Some(true);
|
||||
}
|
||||
|
||||
// ...
|
||||
let res = data
|
||||
.db
|
||||
.edit_log(id, serde_json::to_string::<BoardPostLog>(&post).unwrap())
|
||||
.await;
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&res).unwrap());
|
||||
}
|
||||
|
||||
#[post("/api/board/{name:.*}/posts/{id:.*}/update")]
|
||||
pub async fn update_post_request(
|
||||
req: HttpRequest,
|
||||
body: web::Json<UpdatePostInfo>,
|
||||
data: web::Data<AppData>,
|
||||
) -> impl Responder {
|
||||
let name: String = req.match_info().get("name").unwrap().to_string();
|
||||
let id: String = req.match_info().get("id").unwrap().to_string();
|
||||
|
||||
// get token user
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
return HttpResponse::NotFound().body("Invalid token");
|
||||
}
|
||||
} else {
|
||||
return HttpResponse::NotAcceptable().body("An account is required to do this");
|
||||
}
|
||||
|
||||
// make sure board exists
|
||||
let board: DefaultReturn<Option<Board<String>>> =
|
||||
data.db.get_board_by_name(name.to_owned()).await;
|
||||
|
||||
if !board.success {
|
||||
return HttpResponse::NotFound()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(
|
||||
serde_json::to_string::<DefaultReturn<Option<String>>>(&DefaultReturn {
|
||||
success: false,
|
||||
message: String::from("Board does not exist!"),
|
||||
payload: Option::None,
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let board = serde_json::from_str::<BoardMetadata>(&board.payload.unwrap().metadata).unwrap();
|
||||
|
||||
// get post
|
||||
let p = data.db.get_log_by_id(id.to_owned()).await;
|
||||
let mut post = serde_json::from_str::<BoardPostLog>(&p.payload.unwrap().content).unwrap();
|
||||
|
||||
// check board "topic_required" setting (make sure we can't edit to remove topic)
|
||||
// if it is set to "yes", make sure we provided a topic AND this is not a reply (replies to not count)
|
||||
if board.topic_required.is_some()
|
||||
&& board.topic_required.unwrap() == "yes"
|
||||
&& post.reply.is_none()
|
||||
&& body.topic.is_none()
|
||||
{
|
||||
return HttpResponse::NotFound()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(
|
||||
serde_json::to_string::<DefaultReturn<Option<String>>>(&DefaultReturn {
|
||||
success: false,
|
||||
message: String::from("This board requires a topic to be set before posting"),
|
||||
payload: Option::None,
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
// check if we can update this post
|
||||
// must be authenticated AND post author OR staff
|
||||
let user = token_user.unwrap().payload.unwrap();
|
||||
|
||||
let can_update: bool = (user.user.username != String::from("Anonymous"))
|
||||
&& ((user.user.username == post.author)
|
||||
| (user
|
||||
.level
|
||||
.permissions
|
||||
.contains(&String::from("EditBoardPosts"))));
|
||||
|
||||
if can_update == false {
|
||||
return HttpResponse::NotFound()
|
||||
.body("You do not have permission to manage this post's contents.");
|
||||
}
|
||||
|
||||
// update content
|
||||
post.content = body.content.clone();
|
||||
post.content_html = format!(
|
||||
// we'll add the "(edited)" tag to this post in its rendered content so it doesn't impact the markdown content
|
||||
"{}<hr /><p style=\"opacity: 75%;\">(edited <span class=\"date-time-to-localize\">{}</span>)</p>",
|
||||
crate::markdown::render::parse_markdown(&body.content),
|
||||
crate::utility::unix_epoch_timestamp()
|
||||
);
|
||||
|
||||
post.topic = body.topic.clone();
|
||||
|
||||
// ...
|
||||
let res = data
|
||||
.db
|
||||
.edit_log(id, serde_json::to_string::<BoardPostLog>(&post).unwrap())
|
||||
.await;
|
||||
|
||||
// update cache
|
||||
if post.reply.is_some() {
|
||||
// this doesn't change the number of posts so we only need to refresh ALL OFFSETS
|
||||
// TODO: maybe do something to figure out the post offset if possible so we don't clear all offsets every time
|
||||
data.db
|
||||
.cachedb
|
||||
.remove_starting_with(format!("post-replies:{}:*", post.reply.as_ref().unwrap()))
|
||||
.await;
|
||||
} else {
|
||||
data.db
|
||||
.cachedb
|
||||
.remove_starting_with(format!("board-posts:{}:*", post.board))
|
||||
.await;
|
||||
}
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&res).unwrap());
|
||||
}
|
||||
|
||||
#[post("/api/board/{name:.*}/posts/{id:.*}/tags")]
|
||||
pub async fn update_post_tags_request(
|
||||
req: HttpRequest,
|
||||
body: web::Json<UpdatePostTagsInfo>,
|
||||
data: web::Data<AppData>,
|
||||
) -> impl Responder {
|
||||
let name: String = req.match_info().get("name").unwrap().to_string();
|
||||
let id: String = req.match_info().get("id").unwrap().to_string();
|
||||
|
||||
// get token user
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
return HttpResponse::NotFound().body("Invalid token");
|
||||
}
|
||||
} else {
|
||||
return HttpResponse::NotAcceptable().body("An account is required to do this");
|
||||
}
|
||||
|
||||
// make sure board exists
|
||||
let board: DefaultReturn<Option<Board<String>>> =
|
||||
data.db.get_board_by_name(name.to_owned()).await;
|
||||
|
||||
if !board.success {
|
||||
return HttpResponse::NotFound()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(
|
||||
serde_json::to_string::<DefaultReturn<Option<String>>>(&DefaultReturn {
|
||||
success: false,
|
||||
message: String::from("Board does not exist!"),
|
||||
payload: Option::None,
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let board = serde_json::from_str::<BoardMetadata>(&board.payload.unwrap().metadata).unwrap();
|
||||
|
||||
// get post
|
||||
let p = data.db.get_log_by_id(id.to_owned()).await;
|
||||
let mut post = serde_json::from_str::<BoardPostLog>(&p.payload.unwrap().content).unwrap();
|
||||
|
||||
// check if we can update this post
|
||||
// must be authenticated AND post author OR staff OR board owner
|
||||
let user = token_user.unwrap().payload.unwrap();
|
||||
|
||||
let can_update: bool = (user.user.username != String::from("Anonymous"))
|
||||
&& ((user.user.username == board.owner)
|
||||
| (user.user.username == post.author)
|
||||
| (user
|
||||
.level
|
||||
.permissions
|
||||
.contains(&String::from("ManageBoards"))));
|
||||
|
||||
if can_update == false {
|
||||
return HttpResponse::NotFound()
|
||||
.body("You do not have permission to manage this post's contents.");
|
||||
}
|
||||
|
||||
// update tags
|
||||
post.tags = Option::Some(body.tags.clone());
|
||||
|
||||
// ...
|
||||
let res = data
|
||||
.db
|
||||
.edit_log(id, serde_json::to_string::<BoardPostLog>(&post).unwrap())
|
||||
.await;
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&res).unwrap());
|
||||
}
|
||||
|
||||
#[delete("/api/board/{name:.*}/posts/{id:.*}")]
|
||||
pub async fn delete_post_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
|
||||
let name: String = req.match_info().get("name").unwrap().to_string();
|
||||
let id: String = req.match_info().get("id").unwrap().to_string();
|
||||
|
||||
// get token user
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
return HttpResponse::NotFound().body("Invalid token");
|
||||
}
|
||||
} else {
|
||||
return HttpResponse::NotAcceptable().body("An account is required to do this");
|
||||
}
|
||||
|
||||
// make sure board exists
|
||||
let board: DefaultReturn<Option<Board<String>>> =
|
||||
data.db.get_board_by_name(name.to_owned()).await;
|
||||
|
||||
if !board.success {
|
||||
return HttpResponse::NotFound()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(
|
||||
serde_json::to_string::<DefaultReturn<Option<String>>>(&DefaultReturn {
|
||||
success: false,
|
||||
message: String::from("Board does not exist!"),
|
||||
payload: Option::None,
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let board = serde_json::from_str::<BoardMetadata>(&board.payload.unwrap().metadata).unwrap();
|
||||
|
||||
// get post
|
||||
let p = data.db.get_log_by_id(id.to_owned()).await;
|
||||
|
||||
if p.success == false {
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&p).unwrap());
|
||||
}
|
||||
|
||||
let post = serde_json::from_str::<BoardPostLog>(&p.payload.unwrap().content).unwrap();
|
||||
|
||||
// check if we can delete this post
|
||||
// must be authenticated AND board owner OR staff OR post author
|
||||
let user = token_user.unwrap().payload.unwrap();
|
||||
|
||||
let can_delete: bool = (user.user.username != String::from("Anonymous"))
|
||||
&& ((user.user.username == board.owner)
|
||||
| (user
|
||||
.level
|
||||
.permissions
|
||||
.contains(&String::from("ManageBoardPosts")))
|
||||
| (user.user.username == post.author));
|
||||
|
||||
if can_delete == false {
|
||||
return HttpResponse::NotFound()
|
||||
.body("You do not have permission to manage this board's contents.");
|
||||
}
|
||||
|
||||
// ...
|
||||
let res = data.db.delete_log(id).await;
|
||||
|
||||
// update cache
|
||||
if post.reply.is_some() {
|
||||
data.db
|
||||
.cachedb
|
||||
.remove(format!("post-replies:{}", post.reply.as_ref().unwrap()))
|
||||
.await;
|
||||
|
||||
data.db
|
||||
.cachedb
|
||||
.remove(format!(
|
||||
"post-replies:{}:offset0",
|
||||
post.reply.as_ref().unwrap()
|
||||
))
|
||||
.await;
|
||||
|
||||
// technically we should do that whole reply parent thing from create_board_post
|
||||
// to update the number when viewing this post in a feed, but that's a waste of time and memory
|
||||
} else {
|
||||
data.db
|
||||
.cachedb
|
||||
.remove(format!("board-posts:{}", post.board))
|
||||
.await;
|
||||
|
||||
data.db
|
||||
.cachedb
|
||||
.remove(format!("board-posts:{}:offset0", post.board))
|
||||
.await;
|
||||
}
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&res).unwrap());
|
||||
}
|
||||
|
||||
#[post("/api/board/{name:.*}/update")]
|
||||
pub async fn metadata_request(
|
||||
req: HttpRequest,
|
||||
body: web::Json<BoardMetadata>,
|
||||
data: web::Data<AppData>,
|
||||
) -> impl Responder {
|
||||
let name: String = req.match_info().get("name").unwrap().to_string();
|
||||
|
||||
// get board
|
||||
let board: DefaultReturn<Option<Board<String>>> = data.db.get_board_by_name(name.clone()).await;
|
||||
|
||||
if board.success == false {
|
||||
return HttpResponse::NotFound().body(board.message);
|
||||
}
|
||||
|
||||
// get token user
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
return HttpResponse::NotFound().body("Invalid token");
|
||||
}
|
||||
} else {
|
||||
return HttpResponse::NotAcceptable().body("An account is required to do this");
|
||||
}
|
||||
|
||||
// make sure we have permission to do this
|
||||
let metadata =
|
||||
serde_json::from_str::<BoardMetadata>(&board.payload.as_ref().unwrap().metadata).unwrap();
|
||||
|
||||
let user = token_user.as_ref().unwrap().payload.as_ref().unwrap();
|
||||
let can_edit: bool = (user.user.username == metadata.owner)
|
||||
| (user
|
||||
.level
|
||||
.permissions
|
||||
.contains(&String::from("ManageBoards")));
|
||||
|
||||
if can_edit == false {
|
||||
return HttpResponse::NotFound()
|
||||
.body("You do not have permission to manage this board's contents.");
|
||||
}
|
||||
|
||||
// ...
|
||||
let res = data
|
||||
.db
|
||||
.edit_board_metadata_by_name(
|
||||
name, // select board
|
||||
body.to_owned(), // new metadata
|
||||
if token_user.is_some() {
|
||||
Option::Some(token_user.unwrap().payload.unwrap().user.username)
|
||||
} else {
|
||||
Option::None
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&res).unwrap());
|
||||
}
|
||||
|
||||
#[delete("/api/board/{name:.*}")]
|
||||
pub async fn delete_board_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
|
||||
let name: String = req.match_info().get("name").unwrap().to_string();
|
||||
|
||||
let board: DefaultReturn<Option<Board<String>>> = data.db.get_board_by_name(name.clone()).await;
|
||||
let board = serde_json::from_str::<BoardMetadata>(&board.payload.unwrap().metadata).unwrap();
|
||||
|
||||
// get
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
|
||||
let token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
// check if we can delete this board
|
||||
// must be authenticated AND board owner OR staff
|
||||
let user = token_user.unwrap().payload.unwrap();
|
||||
|
||||
let can_delete: bool = (user.user.username != String::from("Anonymous"))
|
||||
&& ((user.user.username == board.owner)
|
||||
| (user
|
||||
.level
|
||||
.permissions
|
||||
.contains(&String::from("ManageBoards"))));
|
||||
|
||||
if can_delete == false {
|
||||
return HttpResponse::NotFound().body("You do not have permission to manage this board.");
|
||||
}
|
||||
|
||||
// ...
|
||||
let res = data.db.delete_board(name).await;
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&res).unwrap());
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
//! API Routes ("/api/...")
|
||||
pub mod auth;
|
||||
pub mod boards;
|
||||
pub mod pastes;
|
||||
|
|
|
@ -3,6 +3,11 @@ use actix_web::{get, post, web, HttpRequest, HttpResponse, Responder};
|
|||
use crate::db::bundlesdb::{self, AtomicPasteFSFile, DefaultReturn, FullPaste, PasteMetadata};
|
||||
use crate::{markdown, ssm, utility};
|
||||
|
||||
#[derive(Default, PartialEq, serde::Deserialize)]
|
||||
pub struct OffsetQueryProps {
|
||||
pub offset: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct RenderInfo {
|
||||
text: String,
|
||||
|
|
|
@ -12,7 +12,7 @@ pub fn AvatarDisplay(props: &AvatarProps) -> Html {
|
|||
<img
|
||||
class="avatar"
|
||||
style={format!("--size: {}px;", props.size)}
|
||||
src={format!("/api/auth/users/{}/avatar", props.username)}
|
||||
src={format!("::GUPPY_ROOT::/api/auth/users/{}/avatar", props.username)}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,248 +0,0 @@
|
|||
use super::avatar::AvatarDisplay;
|
||||
use crate::db::bundlesdb::{BoardPostLog, Log};
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Properties, Default, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||
pub struct MessageProps {
|
||||
pub post: Log,
|
||||
pub show_open: bool,
|
||||
pub pinned: bool,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn Message(props: &MessageProps) -> Html {
|
||||
let p = &props.post;
|
||||
|
||||
let post = serde_json::from_str::<BoardPostLog>(&p.content).unwrap();
|
||||
let content = Html::from_html_unchecked(AttrValue::from(
|
||||
post.content_html
|
||||
.clone()
|
||||
.replace("<style>", "<style>")
|
||||
.replace("</style>", "</style>"),
|
||||
));
|
||||
|
||||
let pinned = (props.pinned == true) | (post.pinned.is_some() && post.pinned.unwrap() == true); // show pin icon even when post is not in pinned section
|
||||
|
||||
// ...
|
||||
return html! {
|
||||
<div class="flex mobile:flex-column g-4 full message-box">
|
||||
{if props.show_open == true && post.topic.is_some() {
|
||||
html! {
|
||||
<div class={format!(
|
||||
"card message topic {} {} round full flex justify-space-between align-center flex-wrap g-4",
|
||||
if post.reply.is_some() { "reply" } else { "" },
|
||||
if pinned == true { "pinned" } else { "" }
|
||||
)}
|
||||
title={if post.tags.is_some() {
|
||||
post.tags.unwrap()
|
||||
} else {
|
||||
String::new()
|
||||
}}
|
||||
style="background: var(--background-surface0-5)"
|
||||
>
|
||||
<a
|
||||
class="flex align-center g-4"
|
||||
href={format!("/b/{}/posts/{}", post.board, p.id)}
|
||||
title="Expand Topic"
|
||||
>
|
||||
{if pinned == true {
|
||||
html! {
|
||||
<div class="flex align-center" style="color: var(--primary);" title="Pinned Post">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pin"><line x1="12" x2="12" y1="17" y2="22"/><path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z"/></svg>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}}
|
||||
|
||||
<span>{post.topic.unwrap()}</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
|
||||
</a>
|
||||
|
||||
<div class="flex align-center g-4">
|
||||
<span class="chip mention round" style="width: max-content;">
|
||||
{if post.author != "Anonymous" {
|
||||
html! {<a href={format!("/~{}", &post.author)} style="color: inherit;">{&post.author}</a>}
|
||||
} else {
|
||||
html! {<span>{"Anonymous"}</span>}
|
||||
}}
|
||||
</span>
|
||||
|
||||
<span class="date-time-to-localize" style="opacity: 75%;">{&p.timestamp}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
<>
|
||||
// info box
|
||||
<div class="card secondary round flex flex-column g-4 mobile:max" style="width: 250px;">
|
||||
<div class="full flex justify-center">
|
||||
{if post.author != "Anonymous" {
|
||||
html! {<AvatarDisplay size={100} username={post.author.clone()} />}
|
||||
} else {
|
||||
html! {}
|
||||
}}
|
||||
</div>
|
||||
|
||||
<span class="chip mention round" style="width: max-content;">
|
||||
{if post.author != "Anonymous" {
|
||||
html! {<a href={format!("/~{}", &post.author)} style="color: inherit;">{&post.author}</a>}
|
||||
} else {
|
||||
html! {<span>{"Anonymous"}</span>}
|
||||
}}
|
||||
</span>
|
||||
|
||||
<span>
|
||||
{"Posted: "}
|
||||
<span class="date-time-to-localize" style="opacity: 75%;">{&p.timestamp}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
// message content
|
||||
<div class={format!(
|
||||
"card message {} {} round full flex g-4",
|
||||
if post.reply.is_some() { "reply" } else { "" },
|
||||
if pinned == true { "pinned" } else { "" }
|
||||
)}
|
||||
title={if post.tags.is_some() {
|
||||
post.tags.unwrap()
|
||||
} else {
|
||||
String::new()
|
||||
}}
|
||||
style="background: var(--background-surface0-5)"
|
||||
>
|
||||
<div class="flex g-4 full justify-space-between">
|
||||
<div class="full">
|
||||
{content}
|
||||
</div>
|
||||
|
||||
<div class="flex g-4 flex-wrap align-center flex-column">
|
||||
{if post.replies.is_some() && post.replies.unwrap() > 0 {
|
||||
html! { <>
|
||||
<div class="flex align-center g-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-messages-square"><path d="M14 9a2 2 0 0 1-2 2H6l-4 4V4c0-1.1.9-2 2-2h8a2 2 0 0 1 2 2z"/><path d="M18 9h2a2 2 0 0 1 2 2v11l-4-4h-6a2 2 0 0 1-2-2v-1"/></svg>
|
||||
<span title="Reply Count">{&post.replies.unwrap()}</span>
|
||||
</div>
|
||||
</> }
|
||||
} else {
|
||||
html! {}
|
||||
}}
|
||||
|
||||
{if pinned == true {
|
||||
html! {
|
||||
<div class="flex align-center" style="color: var(--primary);" title="Pinned Post">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pin"><line x1="12" x2="12" y1="17" y2="22"/><path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z"/></svg>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}}
|
||||
|
||||
{if props.show_open == true {
|
||||
html! {
|
||||
<div class="flex g-4 flex-wrap">
|
||||
<a
|
||||
class="button invisible round"
|
||||
href={format!("/b/{}/posts/{}", post.board, p.id)}
|
||||
style="color: var(--text-color);"
|
||||
target="_blank"
|
||||
title="open/manage"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right-from-square"><path d="M21 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h6"/><path d="m21 3-9 9"/><path d="M15 3h6v6"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}}
|
||||
</div>
|
||||
};
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn TopicForumMessage(props: &MessageProps) -> Html {
|
||||
let p = &props.post;
|
||||
|
||||
let post = serde_json::from_str::<BoardPostLog>(&p.content).unwrap();
|
||||
let pinned = (props.pinned == true) | (post.pinned.is_some() && post.pinned.unwrap() == true); // show pin icon even when post is not in pinned section
|
||||
|
||||
if post.topic.is_none() {
|
||||
return html! { <tr><td>
|
||||
<a
|
||||
class="flex align-center g-4"
|
||||
href={format!("/b/{}/posts/{}", post.board, p.id)}
|
||||
title="Expand Topic"
|
||||
>
|
||||
{if pinned == true {
|
||||
html! {
|
||||
<div class="flex align-center" style="color: var(--primary);" title="Pinned Post">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pin"><line x1="12" x2="12" y1="17" y2="22"/><path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z"/></svg>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}}
|
||||
|
||||
<span>{"Invalid post"}</span>
|
||||
</a>
|
||||
</td></tr> };
|
||||
}
|
||||
|
||||
// ...
|
||||
return html! {
|
||||
<tr
|
||||
title={if post.tags.is_some() {
|
||||
post.tags.unwrap()
|
||||
} else {
|
||||
String::new()
|
||||
}}
|
||||
>
|
||||
<td>
|
||||
<a
|
||||
class="flex align-center g-4"
|
||||
href={format!("/b/{}/posts/{}", post.board, p.id)}
|
||||
title="Expand Topic"
|
||||
>
|
||||
{if pinned == true {
|
||||
html! {
|
||||
<div class="flex align-center" style="color: var(--primary);" title="Pinned Post">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pin"><line x1="12" x2="12" y1="17" y2="22"/><path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z"/></svg>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}}
|
||||
|
||||
<span>{post.topic.unwrap()}</span>
|
||||
</a>
|
||||
</td>
|
||||
|
||||
<td class="flex align-center g-4">
|
||||
{if post.author != "Anonymous" {
|
||||
html! { <AvatarDisplay size={25} username={post.author.clone()} /> }
|
||||
} else {
|
||||
html! {}
|
||||
}}
|
||||
|
||||
<span class="chip mention round" style="width: max-content;">
|
||||
{if post.author != "Anonymous" {
|
||||
html! {<a href={format!("/~{}", &post.author)} style="color: inherit;">{&post.author}</a>}
|
||||
} else {
|
||||
html! {<span>{"Anonymous"}</span>}
|
||||
}}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="device:desktop">
|
||||
<span class="date-time-to-localize" style="opacity: 75%;">{&p.timestamp}</span>
|
||||
</td>
|
||||
</tr>
|
||||
};
|
||||
}
|
|
@ -1,3 +1,2 @@
|
|||
pub mod avatar;
|
||||
pub mod message;
|
||||
pub mod navigation;
|
||||
|
|
|
@ -39,14 +39,14 @@ pub fn Footer(props: &FooterProps) -> Html {
|
|||
</div>
|
||||
|
||||
<div class="item">
|
||||
<a href="/d/auth/register" class="flex align-center g-4">
|
||||
<a href="::GUPPY_ROOT::/d/auth/register" class="flex align-center g-4" data-wants-redirect="true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-at-sign"><circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8"/></svg>
|
||||
{"register"}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<a href="/d/auth/login" class="flex align-center g-4">
|
||||
<a href="::GUPPY_ROOT::/d/auth/login" class="flex align-center g-4" data-wants-redirect="true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-log-in"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" x2="3" y1="12" y2="12"/></svg>
|
||||
{"login"}
|
||||
</a>
|
||||
|
@ -129,12 +129,12 @@ pub fn GlobalMenu(props: &FooterProps) -> Html {
|
|||
{"new"}
|
||||
</a>
|
||||
|
||||
<a href="/d/auth/register" class="button green full round border justify-start">
|
||||
<a href="::GUPPY_ROOT::/d/auth/register" class="button green full round border justify-start" data-wants-redirect="true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-at-sign"><circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8"/></svg>
|
||||
{"register"}
|
||||
</a>
|
||||
|
||||
<a href="/d/auth/login" class="button green full round border justify-start">
|
||||
<a href="::GUPPY_ROOT::/d/auth/login" class="button green full round border justify-start" data-wants-redirect="true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-log-in"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" x2="3" y1="12" y2="12"/></svg>
|
||||
{"login"}
|
||||
</a>
|
||||
|
|
2056
src/db/bundlesdb.rs
2056
src/db/bundlesdb.rs
File diff suppressed because it is too large
Load diff
45
src/main.rs
45
src/main.rs
|
@ -88,10 +88,13 @@ async fn main() -> std::io::Result<()> {
|
|||
http_client: client,
|
||||
});
|
||||
|
||||
let cors = actix_cors::Cors::default().send_wildcard();
|
||||
|
||||
App::new()
|
||||
.app_data(web::Data::clone(&data))
|
||||
// middleware
|
||||
.wrap(actix_web::middleware::Logger::default())
|
||||
.wrap(cors)
|
||||
// static dir
|
||||
.service(
|
||||
fs::Files::new(
|
||||
|
@ -107,15 +110,6 @@ async fn main() -> std::io::Result<()> {
|
|||
// docs
|
||||
.service(fs::Files::new("/api/docs", "./target/doc").show_files_listing())
|
||||
// POST api
|
||||
// POST auth
|
||||
.service(crate::api::auth::register)
|
||||
.service(crate::api::auth::login)
|
||||
.service(crate::api::auth::login_secondary_token)
|
||||
.service(crate::api::auth::edit_about_request)
|
||||
.service(crate::api::auth::refresh_secondary_token_request)
|
||||
.service(crate::api::auth::update_request)
|
||||
.service(crate::api::auth::follow_request)
|
||||
.service(crate::api::boards::create_mail_stream_request)
|
||||
.service(crate::api::auth::ban_request)
|
||||
// POST api::pastes
|
||||
.service(crate::api::pastes::render_request)
|
||||
|
@ -131,14 +125,12 @@ async fn main() -> std::io::Result<()> {
|
|||
.service(crate::api::pastes::get_from_url_request)
|
||||
.service(crate::api::pastes::get_from_id_request)
|
||||
.service(crate::api::pastes::exists_request)
|
||||
.service(crate::api::auth::callback_request)
|
||||
.service(crate::api::auth::logout)
|
||||
// GET dashboard
|
||||
.service(crate::pages::home::dashboard_request)
|
||||
.service(crate::pages::home::notifications_request)
|
||||
.service(crate::pages::home::inbox_request)
|
||||
.service(crate::pages::auth::register_request)
|
||||
.service(crate::pages::auth::login_request)
|
||||
.service(crate::pages::auth::login_secondary_token_request)
|
||||
.service(crate::pages::settings::user_settings_request)
|
||||
.service(crate::pages::settings::paste_settings_request)
|
||||
.service(crate::pages::paste_view::dashboard_request)
|
||||
|
@ -146,40 +138,11 @@ async fn main() -> std::io::Result<()> {
|
|||
.service(crate::pages::atomic_editor::dashboard_request)
|
||||
.service(crate::pages::atomic_editor::new_request)
|
||||
.service(crate::pages::atomic_editor::edit_request)
|
||||
// GET boards
|
||||
.service(crate::pages::boards::dashboard_request)
|
||||
.service(crate::pages::boards::search_by_tags_request)
|
||||
.service(crate::pages::boards::new_request)
|
||||
.service(crate::pages::boards::view_board_post_request)
|
||||
.service(crate::pages::boards::board_settings_request)
|
||||
.service(crate::pages::boards::create_board_post_request)
|
||||
.service(crate::pages::boards::view_board_request)
|
||||
// GET boards api
|
||||
.service(crate::api::boards::get_posts_request)
|
||||
.service(crate::api::boards::get_post_request)
|
||||
// POST boards api
|
||||
.service(crate::api::boards::create_request)
|
||||
.service(crate::api::boards::create_post_request)
|
||||
.service(crate::api::boards::update_post_request)
|
||||
.service(crate::api::boards::update_post_tags_request)
|
||||
.service(crate::api::boards::metadata_request)
|
||||
.service(crate::api::boards::pin_post_request)
|
||||
// DELETE boards api
|
||||
.service(crate::api::boards::delete_post_request)
|
||||
.service(crate::api::boards::delete_board_request)
|
||||
// GET staff
|
||||
.service(crate::pages::staff::dashboard_request)
|
||||
.service(crate::pages::staff::staff_boards_dashboard_request)
|
||||
.service(crate::pages::staff::staff_users_dashboard_request)
|
||||
// GET users
|
||||
.service(crate::pages::auth::followers_request)
|
||||
.service(crate::pages::auth::following_request)
|
||||
.service(crate::pages::auth::user_settings_request)
|
||||
.service(crate::pages::auth::profile_view_request)
|
||||
.service(crate::api::auth::avatar_request)
|
||||
.service(crate::api::auth::followers_request)
|
||||
.service(crate::api::auth::following_request)
|
||||
.service(crate::api::auth::level_request)
|
||||
.service(crate::api::auth::get_from_owner_request)
|
||||
// GET root
|
||||
.service(crate::pages::home::home_request)
|
||||
|
|
|
@ -69,7 +69,7 @@ fn Dashboard(props: &Props) -> Html {
|
|||
<a href="/d" class="button">{"Home"}</a>
|
||||
<a href="/d/pastes" class="button">{"Pastes"}</a>
|
||||
<a href="/d/atomic" class="button active">{"Atomic"}</a>
|
||||
<a href="/d/boards" class="button">{"Boards"}</a>
|
||||
<a href="::PUFFER_ROOT::d" class="button">{"Boards"}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
1154
src/pages/auth.rs
1154
src/pages/auth.rs
File diff suppressed because it is too large
Load diff
1701
src/pages/boards.rs
1701
src/pages/boards.rs
File diff suppressed because it is too large
Load diff
|
@ -347,7 +347,7 @@ Disallow: /*?",
|
|||
|
||||
#[function_component]
|
||||
fn Dashboard(props: &DashboardProps) -> Html {
|
||||
return html! {
|
||||
html! {
|
||||
<div class="flex flex-column" style="height: 100dvh;">
|
||||
<GlobalMenu auth_state={props.auth_state} />
|
||||
|
||||
|
@ -376,7 +376,7 @@ fn Dashboard(props: &DashboardProps) -> Html {
|
|||
<a href="/d" class="button active">{"Home"}</a>
|
||||
<a href="/d/pastes" class="button">{"Pastes"}</a>
|
||||
<a href="/d/atomic" class="button">{"Atomic"}</a>
|
||||
<a href="/d/boards" class="button">{"Boards"}</a>
|
||||
<a href="::PUFFER_ROOT::d" class="button">{"Boards"}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -433,20 +433,20 @@ fn Dashboard(props: &DashboardProps) -> Html {
|
|||
<div class="card secondary round flex justify-space-between align-center g-4">
|
||||
<b>{"My Boards"}</b>
|
||||
|
||||
<a class="button bundles-primary round" href="/d/boards">
|
||||
<a class="button bundles-primary round" href="::PUFFER_ROOT::d">
|
||||
{"Go"}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card secondary round flex justify-space-between align-center g-4">
|
||||
<b>{"Browse Boards"}</b>
|
||||
// <div class="card secondary round flex justify-space-between align-center g-4">
|
||||
// <b>{"Browse Boards"}</b>
|
||||
|
||||
<a class="button bundles-primary round" href="/d/boards/browse">
|
||||
{"Go"}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
// <a class="button bundles-primary round" href="::PUFFER_ROOT::d/browse">
|
||||
// {"Go"}
|
||||
// <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
|
||||
// </a>
|
||||
// </div>
|
||||
|
||||
<div class="card secondary round flex justify-space-between align-center g-4">
|
||||
<b>{"My Inboxes"}</b>
|
||||
|
@ -460,7 +460,7 @@ fn Dashboard(props: &DashboardProps) -> Html {
|
|||
<div class="card secondary round flex justify-space-between align-center g-4">
|
||||
<b>{"My Profile"}</b>
|
||||
|
||||
<a class="button bundles-primary round" href={format!("/~{}", props.user.username)}>
|
||||
<a class="button bundles-primary round" href={format!("::GUPPY_ROOT::{}", props.user.username)}>
|
||||
{"Go"}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
|
||||
</a>
|
||||
|
@ -469,7 +469,7 @@ fn Dashboard(props: &DashboardProps) -> Html {
|
|||
<div class="card secondary round flex justify-space-between align-center g-4">
|
||||
<b>{"Account Settings"}</b>
|
||||
|
||||
<a class="button bundles-primary round" href={format!("/~{}/settings", props.user.username)}>
|
||||
<a class="button bundles-primary round" href={format!("::GUPPY_ROOT::{}/settings", props.user.username)}>
|
||||
{"Go"}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
|
||||
</a>
|
||||
|
@ -479,7 +479,7 @@ fn Dashboard(props: &DashboardProps) -> Html {
|
|||
</main>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn build_dashboard_renderer_with_props(props: DashboardProps) -> ServerRenderer<Dashboard> {
|
||||
|
@ -581,7 +581,7 @@ fn Notifications(props: &NotificationsProps) -> Html {
|
|||
<a href="/d" class="button">{"Home"}</a>
|
||||
<a href="/d/pastes" class="button">{"Pastes"}</a>
|
||||
<a href="/d/atomic" class="button">{"Atomic"}</a>
|
||||
<a href="/d/boards" class="button">{"Boards"}</a>
|
||||
<a href="::PUFFER_ROOT::d" class="button">{"Boards"}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -632,7 +632,7 @@ fn build_notifications_renderer_with_props(
|
|||
pub async fn notifications_request(
|
||||
req: HttpRequest,
|
||||
data: web::Data<db::bundlesdb::AppData>,
|
||||
info: web::Query<super::boards::ViewBoardQueryProps>,
|
||||
info: web::Query<crate::api::pastes::OffsetQueryProps>,
|
||||
) -> impl Responder {
|
||||
// verify auth status
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
|
@ -696,7 +696,7 @@ You can create an account at: /d/auth/register",
|
|||
|
||||
#[function_component]
|
||||
fn Inbox(props: &InboxProps) -> Html {
|
||||
return html! {
|
||||
html! {
|
||||
<div class="flex flex-column" style="height: 100dvh;">
|
||||
<GlobalMenu auth_state={props.auth_state} />
|
||||
|
||||
|
@ -725,7 +725,7 @@ fn Inbox(props: &InboxProps) -> Html {
|
|||
<a href="/d" class="button">{"Home"}</a>
|
||||
<a href="/d/pastes" class="button">{"Pastes"}</a>
|
||||
<a href="/d/atomic" class="button">{"Atomic"}</a>
|
||||
<a href="/d/boards" class="button">{"Boards"}</a>
|
||||
<a href="::PUFFER_ROOT::d" class="button">{"Boards"}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -745,7 +745,7 @@ fn Inbox(props: &InboxProps) -> Html {
|
|||
html! {
|
||||
<tr>
|
||||
<td>
|
||||
<a class="flex full g-4" href={format!("/b/{}", b.name)}>
|
||||
<a class="flex full g-4" href={format!("::PUFFER_ROOT::{}", b.name)}>
|
||||
<AvatarDisplay size={25} username={b.tags.clone()} />
|
||||
{b.tags.clone()}
|
||||
</a>
|
||||
|
@ -772,7 +772,7 @@ fn Inbox(props: &InboxProps) -> Html {
|
|||
</main>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn build_inbox_renderer_with_props(props: InboxProps) -> ServerRenderer<Inbox> {
|
||||
|
@ -784,7 +784,7 @@ fn build_inbox_renderer_with_props(props: InboxProps) -> ServerRenderer<Inbox> {
|
|||
pub async fn inbox_request(
|
||||
req: HttpRequest,
|
||||
data: web::Data<db::bundlesdb::AppData>,
|
||||
info: web::Query<super::boards::ViewBoardQueryProps>,
|
||||
info: web::Query<crate::api::pastes::OffsetQueryProps>,
|
||||
) -> impl Responder {
|
||||
// verify auth status
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
//! Page Routes ("/...")
|
||||
pub mod atomic_editor;
|
||||
pub mod auth;
|
||||
pub mod boards;
|
||||
pub mod errors;
|
||||
pub mod home;
|
||||
pub mod paste_view;
|
||||
|
|
|
@ -91,7 +91,7 @@ fn PasteView(props: &Props) -> Html {
|
|||
Config
|
||||
</a>", &props.paste.custom_url);
|
||||
|
||||
let owner_button = format!("<a href=\"/~{}\">{}</a>", &metadata.owner, {
|
||||
let owner_button = format!("<a href=\"::GUPPY_ROOT::{}\">{}</a>", &metadata.owner, {
|
||||
if user_metadata.is_some() && user_metadata.as_ref().unwrap().nickname.is_some() {
|
||||
user_metadata.as_ref().unwrap().nickname.as_ref().unwrap()
|
||||
} else {
|
||||
|
@ -476,7 +476,7 @@ fn Dashboard(props: &DashboardProps) -> Html {
|
|||
<a href="/d" class="button">{"Home"}</a>
|
||||
<a href="/d/pastes" class="button active">{"Pastes"}</a>
|
||||
<a href="/d/atomic" class="button">{"Atomic"}</a>
|
||||
<a href="/d/boards" class="button">{"Boards"}</a>
|
||||
<a href="::PUFFER_ROOT::d" class="button">{"Boards"}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -527,7 +527,7 @@ fn build_dashboard_renderer_with_props(props: DashboardProps) -> ServerRenderer<
|
|||
pub async fn dashboard_request(
|
||||
req: HttpRequest,
|
||||
data: web::Data<bundlesdb::AppData>,
|
||||
info: web::Query<super::boards::ViewBoardQueryProps>,
|
||||
info: web::Query<crate::api::pastes::OffsetQueryProps>,
|
||||
) -> impl Responder {
|
||||
// verify auth status
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
|
|
|
@ -9,7 +9,7 @@ use crate::db::bundlesdb::{DefaultReturn, FullUser};
|
|||
use crate::db::{self, bundlesdb};
|
||||
use crate::utility::format_html;
|
||||
|
||||
use crate::pages::boards::ViewBoardQueryProps;
|
||||
use crate::api::pastes::OffsetQueryProps;
|
||||
|
||||
#[derive(Default, Properties, PartialEq, serde::Deserialize)]
|
||||
struct DashboardProps {
|
||||
|
@ -147,7 +147,7 @@ You can create an account at: /d/auth/register",
|
|||
|
||||
#[function_component]
|
||||
fn BoardsDashboard(props: &BoardsProps) -> Html {
|
||||
return html! {
|
||||
html! {
|
||||
<div class="flex flex-column" style="height: 100dvh;">
|
||||
<GlobalMenu auth_state={props.auth_state} />
|
||||
|
||||
|
@ -199,7 +199,7 @@ fn BoardsDashboard(props: &BoardsProps) -> Html {
|
|||
let post = serde_json::from_str::<bundlesdb::BoardPostLog>(&p.content).unwrap();
|
||||
|
||||
html! {
|
||||
<a class="button secondary round full justify-start" href={format!("/b/{}/posts/{}", &post.board, &p.id)}>
|
||||
<a class="button secondary round full justify-start" href={format!("::PUFFER_ROOT::/{}/posts/{}", &post.board, &p.id)}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-square-text"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="M13 8H7"/><path d="M17 12H7"/></svg>
|
||||
{&post.board}
|
||||
|
||||
|
@ -215,7 +215,7 @@ fn BoardsDashboard(props: &BoardsProps) -> Html {
|
|||
</main>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn build_boards_dashboard_renderer_with_props(
|
||||
|
@ -229,7 +229,7 @@ fn build_boards_dashboard_renderer_with_props(
|
|||
pub async fn staff_boards_dashboard_request(
|
||||
req: HttpRequest,
|
||||
data: web::Data<db::bundlesdb::AppData>,
|
||||
info: web::Query<ViewBoardQueryProps>,
|
||||
info: web::Query<OffsetQueryProps>,
|
||||
) -> impl Responder {
|
||||
// verify auth status
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
|
|
|
@ -47,6 +47,8 @@ pub fn format_html(input: String, head: &str) -> String {
|
|||
|
||||
// ...
|
||||
let site_name = config::get_var("SITE_NAME");
|
||||
let guppy = config::get_var("GUPPY_ROOT");
|
||||
let puffer = config::get_var("PUFFER_ROOT");
|
||||
|
||||
// ...
|
||||
return format!(
|
||||
|
@ -82,5 +84,13 @@ pub fn format_html(input: String, head: &str) -> String {
|
|||
site_name.unwrap()
|
||||
} else {
|
||||
"Bundlrs".to_string()
|
||||
}.as_str()).replace("::GUPPY_ROOT::", if guppy.is_some() {
|
||||
guppy.unwrap()
|
||||
} else {
|
||||
"".to_string()
|
||||
}.as_str()).replace("::PUFFER_ROOT::", if puffer.is_some() {
|
||||
puffer.unwrap()
|
||||
} else {
|
||||
"".to_string()
|
||||
}.as_str());
|
||||
}
|
||||
|
|
|
@ -1,117 +0,0 @@
|
|||
const error: HTMLElement = document.getElementById("error")!;
|
||||
const success: HTMLElement = document.getElementById("success")!;
|
||||
const forms: HTMLElement = document.getElementById("forms")!;
|
||||
const switch_button: HTMLElement = document.getElementById("switch-button")!;
|
||||
|
||||
const register_form: HTMLFormElement | null = document.getElementById(
|
||||
"register-user"
|
||||
) as HTMLFormElement | null;
|
||||
|
||||
const login_form: HTMLFormElement | null = document.getElementById(
|
||||
"login-user"
|
||||
) as HTMLFormElement | null;
|
||||
|
||||
const login_st_form: HTMLFormElement | null = document.getElementById(
|
||||
"login-user-st"
|
||||
) as HTMLFormElement | null;
|
||||
|
||||
if (register_form) {
|
||||
// register
|
||||
register_form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const res = await fetch("/api/auth/register", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
username: register_form.username.value,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (json.success === false) {
|
||||
error.style.display = "block";
|
||||
error.innerHTML = `<div class="mdnote-title">${json.message}</div>`;
|
||||
} else {
|
||||
success.style.display = "block";
|
||||
success.innerHTML = `<p>Account created! You can login using this code:</p>
|
||||
|
||||
<p class="card border round flex justify-center align-center">${json.message}</p>
|
||||
|
||||
<p><b>Do not lose it!</b> This code is required for you to sign into your account, <b>it cannot be reset!</b></p>
|
||||
|
||||
<hr />
|
||||
<a href="/d" class="button round bundles-primary">Continue</a>`;
|
||||
forms.style.display = "none";
|
||||
}
|
||||
});
|
||||
} else if (login_form) {
|
||||
// login
|
||||
login_form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const res = await fetch("/api/auth/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
uid: login_form.uid.value,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (json.success === false) {
|
||||
error.style.display = "block";
|
||||
error.innerHTML = `<div class="mdnote-title">${json.message}</div>`;
|
||||
} else {
|
||||
success.style.display = "block";
|
||||
success.innerHTML = `<p>Successfully logged into account.</p>
|
||||
|
||||
<hr />
|
||||
<a href="/d" class="button round bundles-primary">Continue</a>`;
|
||||
forms.style.display = "none";
|
||||
|
||||
if (switch_button) {
|
||||
switch_button.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (login_st_form) {
|
||||
// login (secondary token)
|
||||
login_st_form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const res = await fetch("/api/auth/login-st", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
uid: login_st_form.uid.value,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (json.success === false) {
|
||||
error.style.display = "block";
|
||||
error.innerHTML = `<div class="mdnote-title">${json.message}</div>`;
|
||||
} else {
|
||||
success.style.display = "block";
|
||||
success.innerHTML = `<p>Successfully logged into account.</p>
|
||||
|
||||
<hr />
|
||||
<a href="/d" class="button round bundles-primary">Continue</a>`;
|
||||
forms.style.display = "none";
|
||||
|
||||
if (switch_button) {
|
||||
switch_button.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// default export
|
||||
export default {};
|
|
@ -1,38 +0,0 @@
|
|||
const error: HTMLElement = document.getElementById("error")!;
|
||||
const create_form: HTMLFormElement | null = document.getElementById(
|
||||
"create-post"
|
||||
) as HTMLFormElement | null;
|
||||
|
||||
const board_name: string = (document.getElementById(
|
||||
"board-name"
|
||||
) as HTMLFormElement | null)!.innerText;
|
||||
|
||||
if (create_form) {
|
||||
// create board
|
||||
create_form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const res = await fetch(`/api/board/${board_name}/posts`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
content: create_form.content.value,
|
||||
topic: create_form.topic.value || null,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (json.success === false) {
|
||||
error.style.display = "block";
|
||||
error.innerHTML = `<div class="mdnote-title">${json.message}</div>`;
|
||||
} else {
|
||||
create_form.reset();
|
||||
window.location.href = `/b/${board_name}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// default export
|
||||
export default {};
|
|
@ -254,5 +254,14 @@ for (const element of onclick) {
|
|||
}
|
||||
};
|
||||
|
||||
// wants redirect
|
||||
for (const element of Array.from(
|
||||
document.querySelectorAll('[data-wants-redirect="true"]')
|
||||
) as HTMLAnchorElement[]) {
|
||||
element.href = `${element.href}?callback=${encodeURIComponent(
|
||||
`${window.location.origin}/api/auth/callback`
|
||||
)}`;
|
||||
}
|
||||
|
||||
// default export
|
||||
export default {};
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
const error: HTMLElement = document.getElementById("error")!;
|
||||
const create_form: HTMLFormElement | null = document.getElementById(
|
||||
"create-board"
|
||||
) as HTMLFormElement | null;
|
||||
|
||||
if (create_form) {
|
||||
// create board
|
||||
create_form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const res = await fetch("/api/board/new", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
name: create_form._name.value,
|
||||
timestamp: 0,
|
||||
metadata: "",
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (json.success === false) {
|
||||
error.style.display = "block";
|
||||
error.innerHTML = `<div class="mdnote-title">${json.message}</div>`;
|
||||
} else {
|
||||
window.location.href = `/b/${json.payload.name}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// default export
|
||||
export default {};
|
|
@ -1,91 +0,0 @@
|
|||
const error: HTMLElement = document.getElementById("error")!;
|
||||
const success: HTMLElement = document.getElementById("success")!;
|
||||
|
||||
// edit about
|
||||
const edit_form: HTMLFormElement | null = document.getElementById(
|
||||
"edit-about"
|
||||
) as HTMLFormElement | null;
|
||||
|
||||
if (edit_form) {
|
||||
// edit user about
|
||||
edit_form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const res = await fetch(edit_form.getAttribute("data-endpoint")!, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
about: edit_form.about.value,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (json.success === false) {
|
||||
error.style.display = "block";
|
||||
error.innerHTML = `<div class="mdnote-title">${json.message}</div>`;
|
||||
} else {
|
||||
edit_form.reset();
|
||||
window.location.href = "?";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// follow
|
||||
const follow_button: HTMLButtonElement | null = document.getElementById(
|
||||
"follow-user"
|
||||
) as HTMLButtonElement | null;
|
||||
|
||||
if (follow_button) {
|
||||
// follow user
|
||||
follow_button.addEventListener("click", async (e) => {
|
||||
e.preventDefault();
|
||||
const res = await fetch(follow_button.getAttribute("data-endpoint")!, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (json.success === false) {
|
||||
error.style.display = "block";
|
||||
error.innerHTML = `<div class="mdnote-title">${json.message}</div>`;
|
||||
} else {
|
||||
success.style.display = "block";
|
||||
success.innerHTML = `<div class="mdnote-title">${json.message}</div>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// mail
|
||||
const mail_button: HTMLButtonElement | null = document.getElementById(
|
||||
"mail-user"
|
||||
) as HTMLButtonElement | null;
|
||||
|
||||
if (mail_button) {
|
||||
// mail user
|
||||
mail_button.addEventListener("click", async (e) => {
|
||||
e.preventDefault();
|
||||
const res = await fetch(mail_button.getAttribute("data-endpoint")!, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (json.success === false) {
|
||||
error.style.display = "block";
|
||||
error.innerHTML = `<div class="mdnote-title">${json.message}</div>`;
|
||||
} else {
|
||||
window.location.href = `/b/${json.payload.name}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// default export
|
||||
export default {};
|
|
@ -13,13 +13,9 @@ const output = await build({
|
|||
"./static/ts/editors/MarkdownEditor.ts",
|
||||
"./static/ts/editors/ClientFixMarkdown.ts",
|
||||
"./static/ts/editors/SettingsEditor.ts",
|
||||
"./static/ts/pages/AuthPages.ts",
|
||||
"./static/ts/pages/Footer.ts",
|
||||
"./static/ts/pages/NewAtomic.ts",
|
||||
"./static/ts/pages/NewBoard.ts",
|
||||
"./static/ts/pages/BoardView.ts",
|
||||
"./static/ts/pages/ManageBoardPost.ts",
|
||||
"./static/ts/pages/ProfileView.ts",
|
||||
"./static/ts/pages/SDManageUser.ts",
|
||||
],
|
||||
minify: {
|
||||
|
|
Loading…
Reference in a new issue