add(backend): move to dorsal for database

add(boards ui): change open button to expand
add(posts ui): action button icons
fix(pagination): make next page button clickable
This commit is contained in:
hkau 2024-04-08 12:32:36 -04:00
parent e364a7f0a3
commit 53443da9d8
17 changed files with 311 additions and 1232 deletions

2
.cargo/config Normal file
View file

@ -0,0 +1,2 @@
[registries]
stellular = { index = "sparse+https://code.stellular.org/api/packages/stellular/cargo/" }

View file

@ -3,16 +3,16 @@ name = "puffer"
authors = ["hkau"]
license = "MIT"
version = "0.1.1"
version = "0.1.2"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
postgres = []
mysql = []
sqlite = []
default = ["sqlite"]
postgres = ["dorsal/postgres"]
mysql = ["dorsal/mysql"]
sqlite = ["dorsal/sqlite"]
default = ["dorsal/sqlite"]
[dependencies]
actix-cors = "0.7.0"
@ -20,21 +20,13 @@ actix-files = "0.6.5"
actix-web = "4.5.1"
askama = "0.12.1"
awc = { version = "3.4.0", features = ["rustls"] }
dorsal = { version = "0.1.0", registry = "stellular", default-features = false } # crates: disable-check
comrak = "0.22.0"
dotenv = "0.15.0"
env_logger = "0.11.3"
hex_fmt = "0.3.0"
redis = "0.25.2"
regex = "1.10.4"
serde = "1.0.197"
serde_json = "1.0.115"
sha2 = "0.10.8"
sqlx = { version = "0.7.3", features = [
"sqlite",
"postgres",
"mysql",
"any",
"runtime-tokio",
"tls-native-tls",
] }
uuid = { version = "1.8.0", features = ["v4"] }

View file

@ -1,7 +1,6 @@
use crate::db::AppData;
use actix_web::{get, web, HttpRequest, HttpResponse, Responder};
use crate::db::pufferdb::AppData;
#[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

View file

@ -1,12 +1,6 @@
use crate::db::{AppData, Board, BoardMetadata, BoardPostLog, DefaultReturn};
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse, Responder};
use crate::{
db::pufferdb::{
AppData, Board, BoardMetadata, BoardPostLog, DefaultReturn, UserMailStreamIdentifier,
},
// pages::boards,
};
#[derive(Default, PartialEq, serde::Deserialize)]
pub struct OffsetQueryProps {
pub offset: Option<i32>,
@ -89,54 +83,6 @@ pub async fn create_request(
.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()
.append_header(("Content-Type", "text/plain"))
.body("Invalid token");
}
} else {
return HttpResponse::NotAcceptable()
.append_header(("Content-Type", "text/plain"))
.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,
@ -418,11 +364,13 @@ pub async fn pin_post_request(req: HttpRequest, data: web::Data<AppData>) -> imp
// 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
.base
.cachedb
.remove_starting_with(format!("post-replies:{}:*", post.reply.as_ref().unwrap()))
.await;
} else {
data.db
.base
.cachedb
.remove_starting_with(format!("board-posts:{}:*", post.board))
.await;
@ -548,11 +496,13 @@ pub async fn update_post_request(
// 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
.base
.cachedb
.remove_starting_with(format!("post-replies:{}:*", post.reply.as_ref().unwrap()))
.await;
} else {
data.db
.base
.cachedb
.remove_starting_with(format!("board-posts:{}:*", post.board))
.await;
@ -736,11 +686,13 @@ pub async fn delete_post_request(req: HttpRequest, data: web::Data<AppData>) ->
// update cache
if post.reply.is_some() {
data.db
.base
.cachedb
.remove(format!("post-replies:{}", post.reply.as_ref().unwrap()))
.await;
data.db
.base
.cachedb
.remove(format!(
"post-replies:{}:offset0",
@ -752,11 +704,13 @@ pub async fn delete_post_request(req: HttpRequest, data: web::Data<AppData>) ->
// to update the number when viewing this post in a feed, but that's a waste of time and memory
} else {
data.db
.base
.cachedb
.remove(format!("board-posts:{}", post.board))
.await;
data.db
.base
.cachedb
.remove(format!("board-posts:{}:offset0", post.board))
.await;

File diff suppressed because it is too large Load diff

View file

@ -1,112 +0,0 @@
//! # CacheDB
//!
//! Redis connection.
//!
//! Identifiers should be a string following this format: `TYPE_OF_OBJECT:OBJECT_ID`. For pastes this would look like: `paste:{custom_url}`
use redis::Commands;
#[derive(Clone)]
pub struct CacheDB {
pub client: redis::Client,
}
impl CacheDB {
pub async fn new() -> CacheDB {
return CacheDB {
client: redis::Client::open("redis://127.0.0.1:6379").unwrap(),
};
}
pub async fn get_con(&self) -> redis::Connection {
self.client.get_connection().unwrap()
}
// GET
/// Get a cache object by its identifier
///
/// # Arguments:
/// * `id` - `String` of the object's id
pub async fn get(&self, id: String) -> Option<String> {
// fetch from database
let mut c = self.get_con().await;
let res = c.get(id);
if res.is_err() {
return Option::None;
}
// return
Option::Some(res.unwrap())
}
// SET
/// Set a cache object by its identifier and content
///
/// # Arguments:
/// * `id` - `String` of the object's id
/// * `content` - `String` of the object's content
pub async fn set(&self, id: String, content: String) -> bool {
// set
let mut c = self.get_con().await;
let res: Result<String, redis::RedisError> = c.set(id, content);
if res.is_err() {
return false;
}
// return
true
}
/// Update a cache object by its identifier and content
///
/// # Arguments:
/// * `id` - `String` of the object's id
/// * `content` - `String` of the object's content
pub async fn update(&self, id: String, content: String) -> bool {
self.set(id, content).await
}
/// Remove a cache object by its identifier
///
/// # Arguments:
/// * `id` - `String` of the object's id
pub async fn remove(&self, id: String) -> bool {
// remove
let mut c = self.get_con().await;
let res: Result<String, redis::RedisError> = c.del(id);
if res.is_err() {
return false;
}
// return
true
}
/// Remove a cache object by its identifier('s start)
///
/// # Arguments:
/// * `id` - `String` of the object's id('s start)
pub async fn remove_starting_with(&self, id: String) -> bool {
let mut c = self.get_con().await;
// get keys
let mut cmd = redis::cmd("DEL");
let keys: Result<Vec<String>, redis::RedisError> = c.keys(id);
for key in keys.unwrap() {
cmd.arg(key);
}
// remove
let res: Result<String, redis::RedisError> = cmd.query(&mut c);
if res.is_err() {
return false;
}
// return
true
}
}

View file

@ -1,3 +0,0 @@
pub mod pufferdb;
pub mod cachedb;
pub mod sql;

View file

@ -1,100 +0,0 @@
#[derive(Debug, Clone)]
pub struct DatabaseOpts {
pub _type: Option<String>,
pub host: Option<String>,
pub user: String,
pub pass: String,
pub name: String,
}
// ...
#[derive(Clone)]
pub struct Database<T> {
pub client: T,
pub _type: String,
}
// ...
#[cfg(feature = "mysql")]
/// Create a new "mysql" database
pub async fn create_db(options: DatabaseOpts) -> Database<sqlx::MySqlPool> {
// mysql
let opts = sqlx::mysql::MySqlPoolOptions::new()
.max_connections(25)
.acquire_timeout(std::time::Duration::from_millis(2000))
.idle_timeout(Some(std::time::Duration::from_secs(60 * 5)));
// .max_lifetime(Some(std::time::Duration::from_secs(120)));
let client = opts
.connect(&format!(
"mysql://{}:{}@{}/{}",
options.user,
options.pass,
if options.host.is_some() {
options.host.unwrap()
} else {
"localhost".to_string()
},
options.name
))
.await;
if client.is_err() {
panic!("failed to connect to database: {}", client.err().unwrap());
}
return Database {
client: client.unwrap(),
_type: String::from("mysql"),
};
}
#[cfg(feature = "postgres")]
/// Create a new "postgres" database
pub async fn create_db(options: DatabaseOpts) -> Database<sqlx::PgPool> {
// postgres
let opts = sqlx::postgres::PgPoolOptions::new()
.max_connections(25)
.acquire_timeout(std::time::Duration::from_millis(2000))
.idle_timeout(Some(std::time::Duration::from_secs(60 * 5)));
// .max_lifetime(Some(std::time::Duration::from_secs(120)));
let client = opts
.connect(&format!(
"postgres://{}:{}@{}/{}",
options.user,
options.pass,
if options.host.is_some() {
options.host.unwrap()
} else {
"localhost".to_string()
},
options.name
))
.await;
if client.is_err() {
panic!("failed to connect to database: {}", client.err().unwrap());
}
return Database {
client: client.unwrap(),
_type: String::from("postgres"),
};
}
#[cfg(feature = "sqlite")]
/// Create a new "sqlite" database
pub async fn create_db(_options: DatabaseOpts) -> Database<sqlx::SqlitePool> {
// sqlite
let client = sqlx::SqlitePool::connect("sqlite://pufferbb.db").await;
if client.is_err() {
panic!("Failed to connect to database!");
}
return Database {
client: client.unwrap(),
_type: String::from("sqlite"),
};
}

View file

@ -10,8 +10,6 @@ use actix_files as fs;
use actix_web::{web, App, HttpServer};
use dotenv;
use sqlx;
pub mod config;
pub mod db;
pub mod utility;
@ -21,8 +19,8 @@ pub mod pages;
pub mod markup;
use crate::db::pufferdb::{AppData, PufferDB};
use crate::db::sql::DatabaseOpts;
use crate::db::{AppData, Database};
use dorsal::DatabaseOpts;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
@ -58,8 +56,7 @@ async fn main() -> std::io::Result<()> {
panic!("Missing required database config settings!");
}
sqlx::any::install_default_drivers(); // install database drivers
let db: PufferDB = PufferDB::new(DatabaseOpts {
let db: Database = Database::new(DatabaseOpts {
_type: db_type,
host: db_host,
user: if db_is_other {
@ -127,7 +124,6 @@ async fn main() -> std::io::Result<()> {
.service(crate::api::boards::update_post_tags_request)
.service(crate::api::boards::metadata_request)
.service(crate::api::boards::pin_post_request)
.service(crate::api::boards::create_mail_stream_request)
// DELETE boards api
.service(crate::api::boards::delete_post_request)
.service(crate::api::boards::delete_board_request)

View file

@ -1,3 +1,6 @@
use crate::db::AppData;
use actix_web::{web::Data, HttpRequest};
pub struct BaseTemplate {
pub info: String,
pub auth_state: bool,
@ -32,3 +35,40 @@ pub fn get_base_values(token_cookie: bool) -> BaseTemplate {
body_embed,
}
}
pub async fn check_auth_status(
req: HttpRequest,
data: Data<AppData>,
) -> (
String,
Option<actix_web::cookie::Cookie<'static>>,
Option<dorsal::DefaultReturn<Option<dorsal::db::special::auth_db::FullUser<String>>>>,
) {
// verify auth status
let token_cookie = req.cookie("__Secure-Token");
let mut set_cookie: &str = "";
let mut token_user: Option<
dorsal::DefaultReturn<Option<dorsal::db::special::auth_db::FullUser<String>>>,
> = if token_cookie.is_some() {
Option::Some(
data.db
.auth
.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, refresh token if not
if token_user.as_ref().unwrap().success == false {
set_cookie = "__Secure-Token=refresh; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=0";
token_user = Option::None;
}
}
// return
(set_cookie.to_string(), token_cookie, token_user)
}

View file

@ -1,11 +1,11 @@
use actix_web::HttpRequest;
use actix_web::{get, web, HttpResponse, Responder};
use dorsal::DefaultReturn;
use super::base;
use askama::Template;
use crate::db;
use crate::db::pufferdb::{self, Board, BoardMetadata, BoardPostLog, Log};
use crate::db::{AppData, Board, BoardIdentifier, BoardMetadata, BoardPostLog, Log};
#[derive(Template)]
#[template(path = "boards/new.html")]
@ -130,7 +130,7 @@ struct SettingsTemplate {
#[derive(Template)]
#[template(path = "dashboard.html")]
struct DashboardTemplate {
pub boards: Vec<pufferdb::BoardIdentifier>,
pub boards: Vec<BoardIdentifier>,
// required fields (super::base)
info: String,
auth_state: bool,
@ -141,7 +141,7 @@ struct DashboardTemplate {
/* #[derive(Default, PartialEq, serde::Deserialize)]
struct SearchProps {
pub boards: Vec<pufferdb::BoardIdentifier>,
pub boards: Vec<BoardIdentifier>,
pub tags: String,
pub offset: i32,
pub auth_state: Option<bool>,
@ -155,44 +155,14 @@ pub struct SearchQueryProps {
#[get("/new")]
/// Available at "/new"
pub async fn new_request(req: HttpRequest, data: web::Data<pufferdb::AppData>) -> impl Responder {
pub async fn new_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
// verify auth status
let token_cookie = req.cookie("__Secure-Token");
let mut set_cookie: &str = "";
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, refresh token if not
if token_user.as_ref().unwrap().success == false {
set_cookie = "__Secure-Token=refresh; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=0";
// token_user = Option::None;
}
}
let (set_cookie, _, token_user) = base::check_auth_status(req.clone(), data.clone()).await;
if token_user.is_none() {
// you must have an account to create boards
let base = base::get_base_values(token_user.is_some());
let props = AuthPromptTemplate {
// required fields
info: base.info,
auth_state: base.auth_state,
bundlrs: base.bundlrs,
guppy: base.guppy,
body_embed: base.body_embed,
};
return HttpResponse::NotFound()
.append_header(("Content-Type", "text/html"))
.body(props.render().unwrap());
return HttpResponse::NotAcceptable()
.append_header(("Content-Type", "text/plain"))
.body("An account is required to do this.");
}
// ...
@ -218,36 +188,17 @@ pub async fn new_request(req: HttpRequest, data: web::Data<pufferdb::AppData>) -
/// Available at "/{name}"
pub async fn view_board_request(
req: HttpRequest,
data: web::Data<pufferdb::AppData>,
data: web::Data<AppData>,
info: web::Query<ViewBoardQueryProps>,
) -> impl Responder {
// verify auth status
let token_cookie = req.cookie("__Secure-Token");
let mut set_cookie: &str = "";
let mut 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, refresh token if not
if token_user.as_ref().unwrap().success == false {
set_cookie = "__Secure-Token=refresh; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=0";
token_user = Option::None;
}
}
let (set_cookie, token_cookie, token_user) =
base::check_auth_status(req.clone(), data.clone()).await;
// get board
let name: String = req.match_info().get("name").unwrap().to_string();
let board: pufferdb::DefaultReturn<Option<Board<String>>> =
data.db.get_board_by_name(name.clone()).await;
let board: DefaultReturn<Option<Board<String>>> = data.db.get_board_by_name(name.clone()).await;
if board.success == false {
return HttpResponse::NotFound()
@ -258,8 +209,7 @@ pub async fn view_board_request(
// check if board is private
// if it is, only the owner and users with the "staff" role can view it
let metadata =
serde_json::from_str::<pufferdb::BoardMetadata>(&board.payload.as_ref().unwrap().metadata)
.unwrap();
serde_json::from_str::<BoardMetadata>(&board.payload.as_ref().unwrap().metadata).unwrap();
if metadata.is_private == "yes" {
// anonymous
@ -326,7 +276,7 @@ pub async fn view_board_request(
}
// ...
let posts: pufferdb::DefaultReturn<Option<Vec<Log>>> =
let posts: DefaultReturn<Option<Vec<Log>>> =
if info.tags.is_some() && (info.tags.as_ref().unwrap().len() > 0) {
data.db
.get_board_posts_by_tag(
@ -340,7 +290,7 @@ pub async fn view_board_request(
data.db.get_board_posts(name.clone(), info.offset).await
};
let pinned: pufferdb::DefaultReturn<Option<Vec<Log>>> =
let pinned: DefaultReturn<Option<Vec<Log>>> =
data.db.get_pinned_board_posts(name.clone()).await;
// ...
@ -389,36 +339,17 @@ pub async fn view_board_request(
/// Available at "/{name}/new"
pub async fn create_board_post_request(
req: HttpRequest,
data: web::Data<db::pufferdb::AppData>,
data: web::Data<AppData>,
info: web::Query<ViewBoardQueryProps>,
) -> impl Responder {
// verify auth status
let token_cookie = req.cookie("__Secure-Token");
let mut set_cookie: &str = "";
let mut 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, refresh token if not
if token_user.as_ref().unwrap().success == false {
set_cookie = "__Secure-Token=refresh; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=0";
token_user = Option::None;
}
}
let (set_cookie, token_cookie, token_user) =
base::check_auth_status(req.clone(), data.clone()).await;
// get board
let name: String = req.match_info().get("name").unwrap().to_string();
let board: pufferdb::DefaultReturn<Option<Board<String>>> =
data.db.get_board_by_name(name.clone()).await;
let board: DefaultReturn<Option<Board<String>>> = data.db.get_board_by_name(name.clone()).await;
if board.success == false {
return HttpResponse::NotFound()
@ -429,8 +360,7 @@ pub async fn create_board_post_request(
// check if board is private
// if it is, only the owner and users with the "staff" role can view it
let metadata =
serde_json::from_str::<pufferdb::BoardMetadata>(&board.payload.as_ref().unwrap().metadata)
.unwrap();
serde_json::from_str::<BoardMetadata>(&board.payload.as_ref().unwrap().metadata).unwrap();
if metadata.is_private == "yes" {
// anonymous
@ -522,38 +452,19 @@ pub async fn create_board_post_request(
/// Available at "/{name}/posts/{id:.*}"
pub async fn view_board_post_request(
req: HttpRequest,
data: web::Data<db::pufferdb::AppData>,
data: web::Data<AppData>,
info: web::Query<ViewBoardPostQueryProps>,
) -> impl Responder {
// you're able to do this even if the board is private ON PURPOSE
// verify auth status
let token_cookie = req.cookie("__Secure-Token");
let mut set_cookie: &str = "";
let mut 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, refresh token if not
if token_user.as_ref().unwrap().success == false {
set_cookie = "__Secure-Token=refresh; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=0";
token_user = Option::None;
}
}
let (set_cookie, token_cookie, token_user) =
base::check_auth_status(req.clone(), data.clone()).await;
// get board
let name: String = req.match_info().get("name").unwrap().to_string();
let board: pufferdb::DefaultReturn<Option<Board<String>>> =
data.db.get_board_by_name(name.clone()).await;
let board: DefaultReturn<Option<Board<String>>> = data.db.get_board_by_name(name.clone()).await;
if board.success == false {
return HttpResponse::NotFound()
@ -566,7 +477,7 @@ pub async fn view_board_post_request(
// get post
let id: String = req.match_info().get("id").unwrap().to_string();
let p: pufferdb::DefaultReturn<Option<Log>> = data.db.get_log_by_id(id.clone()).await;
let p: DefaultReturn<Option<Log>> = data.db.get_log_by_id(id.clone()).await;
if p.success == false {
return HttpResponse::NotFound()
@ -578,7 +489,7 @@ pub async fn view_board_post_request(
let post = serde_json::from_str::<BoardPostLog>(&p.content).unwrap();
// get replies
let replies: pufferdb::DefaultReturn<Option<Vec<Log>>> = data
let replies: DefaultReturn<Option<Vec<Log>>> = data
.db
.get_post_replies_limited(id.clone(), false, info.offset)
.await;
@ -665,15 +576,11 @@ pub async fn view_board_post_request(
#[get("/{name:.*}/manage")]
/// Available at "/{name}/manage"
pub async fn board_settings_request(
req: HttpRequest,
data: web::Data<pufferdb::AppData>,
) -> impl Responder {
pub async fn board_settings_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
// get board
let name: String = req.match_info().get("name").unwrap().to_string();
let board: pufferdb::DefaultReturn<Option<Board<String>>> =
data.db.get_board_by_name(name).await;
let board: DefaultReturn<Option<Board<String>>> = data.db.get_board_by_name(name).await;
if board.success == false {
return HttpResponse::NotFound()
@ -682,37 +589,18 @@ pub async fn board_settings_request(
}
// verify auth status
let token_cookie = req.cookie("__Secure-Token");
let mut set_cookie: &str = "";
let mut 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, refresh token if not
if token_user.as_ref().unwrap().success == false {
set_cookie = "__Secure-Token=refresh; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=0";
token_user = Option::None;
}
}
let (set_cookie, token_cookie, token_user) =
base::check_auth_status(req.clone(), data.clone()).await;
if token_user.is_none() {
return HttpResponse::NotAcceptable()
.append_header(("Content-Type", "text/plain"))
.body("An account is required to do this");
.body("An account is required to do this.");
}
// ...
let metadata =
serde_json::from_str::<pufferdb::BoardMetadata>(&board.payload.as_ref().unwrap().metadata)
.unwrap();
serde_json::from_str::<BoardMetadata>(&board.payload.as_ref().unwrap().metadata).unwrap();
let user = token_user.unwrap().payload.unwrap();
let can_view: bool = (user.user.username == metadata.owner)
@ -835,7 +723,7 @@ fn build_search_renderer_with_props(props: SearchProps) -> ServerRenderer<Search
/// Available at "/d/browse"
pub async fn search_by_tags_request(
req: HttpRequest,
data: web::Data<db::pufferdb::AppData>,
data: web::Data<AppData>,
info: web::Query<SearchQueryProps>,
) -> impl Responder {
// verify auth status
@ -912,31 +800,10 @@ You can create an account at: /d/auth/register",
#[get("/d")]
/// Available at "/d"
pub async fn dashboard_request(
req: HttpRequest,
data: web::Data<db::pufferdb::AppData>,
) -> impl Responder {
pub async fn dashboard_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
// verify auth status
let token_cookie = req.cookie("__Secure-Token");
let mut set_cookie: &str = "";
let mut 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, refresh token if not
if token_user.as_ref().unwrap().success == false {
set_cookie = "__Secure-Token=refresh; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=0";
token_user = Option::None;
}
}
let (set_cookie, token_cookie, token_user) =
base::check_auth_status(req.clone(), data.clone()).await;
if token_user.is_none() {
// you must have an account to use boards

View file

@ -1,6 +1,6 @@
use actix_web::{get, web, HttpRequest, HttpResponse, Responder};
use crate::db::pufferdb;
use crate::db::AppData;
use super::base;
use askama::Template;
@ -13,32 +13,13 @@ struct HomeTemplate {
auth_state: bool,
bundlrs: String,
guppy: String,
body_embed: String,
body_embed: String,
}
#[get("/")]
pub async fn home_request(req: HttpRequest, data: web::Data<pufferdb::AppData>) -> impl Responder {
pub async fn home_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
// verify auth status
let token_cookie = req.cookie("__Secure-Token");
let mut set_cookie: &str = "";
let mut 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, refresh token if not
if token_user.as_ref().unwrap().success == false {
set_cookie = "__Secure-Token=refresh; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=0";
token_user = Option::None;
}
}
let (set_cookie, _, token_user) = base::check_auth_status(req.clone(), data.clone()).await;
// ...
let base = base::get_base_values(token_user.is_some());
@ -50,9 +31,9 @@ pub async fn home_request(req: HttpRequest, data: web::Data<pufferdb::AppData>)
// required fields
info: base.info,
auth_state: base.auth_state,
bundlrs: base.bundlrs,
guppy: base.guppy,
body_embed: base.body_embed,
bundlrs: base.bundlrs,
guppy: base.guppy,
body_embed: base.body_embed,
}
.render()
.unwrap(),

View file

@ -103,6 +103,12 @@ button.puffer-primary:hover,
background: var(--primary-low);
}
button[disabled="false"],
.button[disabled="false"] {
opacity: 100%;
cursor: pointer;
}
/* input modifications */
button + input,
.button + input {

View file

@ -86,6 +86,13 @@ setTimeout(() => {
).toLocaleString();
}, 50);
// disabled="false"
for (const element of Array.from(
document.querySelectorAll('[disabled="false"]')
) as HTMLButtonElement[]) {
element.removeAttribute("disabled");
}
// disable "a"
setTimeout(() => {
for (const element of Array.from(

View file

@ -1,7 +1,7 @@
<div class="flex mobile:flex-column g-4 full message-box">
{% if show_open == true && post.topic.is_some() %}
<div class="card message topic round full flex justify-space-between align-center flex-wrap g-4"
style="background: var(--background-surface0-5)">
style="background: var(--background-surface0-5);">
<a class="flex align-center g-4" href="/{{ post.board }}/posts/{{ p.id }}" title="Expand Topic">
{% if post.pinned.is_some() && post.pinned.as_ref().unwrap().to_owned() == true %}
<div class="flex align-center" style="color: var(--primary);" title="Pinned Post">
@ -58,7 +58,7 @@
</span>
</div>
<div class="card message round full flex g-4" , style="background: var(--background-surface0-5)">
<div class="card message round full flex g-4" style="background: var(--background-surface0-5);">
<div class="flex g-4 full justify-space-between">
<div class="full">
{{ post.content_html|safe }}
@ -92,13 +92,14 @@
{% if show_open == true %}
<div class="flex g-4 flex-wrap">
<a class="button invisible round" href="/{{ post.board }}/posts/{{ p.id }}"
style="color: var(--text-color);" target="_blank" title="open/manage">
style="color: var(--text-color);" title="Expand 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-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" />
class="lucide lucide-expand">
<path d="m21 21-6-6m6 6v-4.8m0 4.8h-4.8" />
<path d="M3 16.2V21m0 0h4.8M3 21l6-6" />
<path d="M21 7.8V3m0 0h-4.8M21 3l-6 6" />
<path d="M3 7.8V3m0 0h4.8M3 3l6 6" />
</svg>
</a>
</div>

View file

@ -36,7 +36,7 @@
<div class="full flex justify-space-between align-center g-4">
<h5 class="no-margin">
{% if board.name.starts_with("inbox-") %}
{% let mailstream = crate::db::pufferdb::deserialize_mailstream(board_m.about.as_ref().unwrap()) %}
{% let mailstream = crate::db::deserialize_mailstream(board_m.about.as_ref().unwrap()) %}
<!-- show OTHER USER's username if we're in an inbox -->
{% if me == mailstream.user1 %}
{{ mailstream.user2 }}
@ -94,7 +94,7 @@
<!-- don't look here, my formatter doesn't support whitespace for this -->
{% if pinned.len() > 0 %}
{% for p in pinned %}
{% let post = crate::db::pufferdb::deserialize_post(p.to_owned()) %}
{% let post = crate::db::deserialize_post(p.to_owned()) %}
{% let show_open = true %}
{% include "message_component.html" %}
{% endfor %}
@ -104,7 +104,7 @@
{% if !topic_required %}
{% for p in posts %}
{% let post = crate::db::pufferdb::deserialize_post(p.to_owned()) %}
{% let post = crate::db::deserialize_post(p.to_owned()) %}
{% let show_open = true %}
{% include "message_component.html" %}
{% endfor %}
@ -118,7 +118,7 @@
<tbody>
{% for p in posts %}
{% let post = crate::db::pufferdb::deserialize_post(p.to_owned()) %}
{% let post = crate::db::deserialize_post(p.to_owned()) %}
<tr>
<td>
<a class="flex align-center g-4" href="/{{ post.board }}/posts/{{ p.id }}" title="Expand Topic">
@ -144,7 +144,8 @@
<td class="flex align-center g-4">
{% if post.author != "Anonymous" %}
<img class="avatar" style="--size: 25px;" src="{{ guppy }}/api/auth/users/{{ post.author }}/avatar" />
<img class="avatar" style="--size: 25px;"
src="{{ guppy }}/api/auth/users/{{ post.author }}/avatar" />
{% endif %}
<span class="chip mention round" style="width: max-content;">
@ -177,7 +178,7 @@
</a>
<a class="button round" href="?tags={{tags}}&offset={{offset + 50}}&view={{view}}"
disabled="{{ posts.len()==0 }}">
disabled="{{ posts.len() == 0 }}">
Next
<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"

View file

@ -89,19 +89,55 @@
<div class="flex flex-wrap g-4">
{% if can_manage %}
<button class="border round" id="delete-post"
data-endpoint="/api/board/{{ post.board }}/posts/{{ p.id }}">Delete</button>
<a class="button border round" href="?edit_tags=true">Edit Tags</a>
<button class="round red" id="delete-post" data-endpoint="/api/board/{{ post.board }}/posts/{{ 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-trash-2">
<path d="M3 6h18" />
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
<line x1="10" x2="10" y1="11" y2="17" />
<line x1="14" x2="14" y1="11" y2="17" />
</svg>
Delete
</button>
<a class="button round" href="?edit_tags=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-tag">
<path
d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z" />
<circle cx="7.5" cy="7.5" r=".5" fill="currentColor" />
</svg>
Edit Tags
</a>
{% endif %}
{% if can_manage_board %}
<button class="border round" id="pin-post"
data-endpoint="/api/board/{{ post.board }}/posts/{{ p.id }}/pin">Pin</button>
<button class="round" id="pin-post" data-endpoint="/api/board/{{ post.board }}/posts/{{ p.id }}/pin">
<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>
Pin
</button>
{% endif %}
{% if can_edit %}
<a class="button border round" href="?edit_text=true">Edit</a>
<a class="button round" href="?edit_text=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-pencil">
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
<path d="m15 5 4 4" />
</svg>
Edit
</a>
{% endif %}
</div>
@ -146,7 +182,7 @@
{% endif %}
{% for p in replies %}
{% let post = crate::db::pufferdb::deserialize_post(p.to_owned()) %}
{% let post = crate::db::deserialize_post(p.to_owned()) %}
{% let show_open = true %}
{% include "message_component.html" %}
{% endfor %}