[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:
hkau 2024-03-29 23:55:15 -04:00
parent 1ad5aafe1a
commit 5035123a27
26 changed files with 98 additions and 6885 deletions

View file

@ -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"

View file

@ -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.

View file

@ -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());
}

View file

@ -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%;\">&lpar;edited <span class=\"date-time-to-localize\">{}</span>&rpar;</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());
}

View file

@ -1,4 +1,3 @@
//! API Routes ("/api/...")
pub mod auth;
pub mod boards;
pub mod pastes;

View file

@ -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,

View file

@ -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)}
/>
}
}

View file

@ -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>", "&lt;style>")
.replace("</style>", "&lt;/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>
};
}

View file

@ -1,3 +1,2 @@
pub mod avatar;
pub mod message;
pub mod navigation;

View file

@ -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>

File diff suppressed because it is too large Load diff

View file

@ -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)

View file

@ -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>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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");

View file

@ -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;

View file

@ -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");

View file

@ -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");

View file

@ -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());
}

View file

@ -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 {};

View file

@ -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 {};

View file

@ -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 {};

View file

@ -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 {};

View file

@ -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 {};

View file

@ -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: {