diff --git a/crates/app/src/assets.rs b/crates/app/src/assets.rs index 5196916..98a0479 100644 --- a/crates/app/src/assets.rs +++ b/crates/app/src/assets.rs @@ -41,11 +41,13 @@ pub const AUTH_REGISTER: &str = include_str!("./public/html/auth/register.html") pub const PROFILE_BASE: &str = include_str!("./public/html/profile/base.html"); pub const PROFILE_POSTS: &str = include_str!("./public/html/profile/posts.html"); +pub const PROFILE_SETTINGS: &str = include_str!("./public/html/profile/settings.html"); pub const COMMUNITIES_LIST: &str = include_str!("./public/html/communities/list.html"); pub const COMMUNITIES_BASE: &str = include_str!("./public/html/communities/base.html"); pub const COMMUNITIES_FEED: &str = include_str!("./public/html/communities/feed.html"); pub const COMMUNITIES_POST: &str = include_str!("./public/html/communities/post.html"); +pub const COMMUNITIES_SETTINGS: &str = include_str!("./public/html/communities/settings.html"); // langs pub const LANG_EN_US: &str = include_str!("./langs/en-US.toml"); @@ -153,11 +155,13 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD { write_template!(html_path->"profile/base.html"(crate::assets::PROFILE_BASE) -d "profile" --config=config); write_template!(html_path->"profile/posts.html"(crate::assets::PROFILE_POSTS) --config=config); + write_template!(html_path->"profile/settings.html"(crate::assets::PROFILE_SETTINGS) --config=config); write_template!(html_path->"communities/list.html"(crate::assets::COMMUNITIES_LIST) -d "communities" --config=config); write_template!(html_path->"communities/base.html"(crate::assets::COMMUNITIES_BASE) --config=config); write_template!(html_path->"communities/feed.html"(crate::assets::COMMUNITIES_FEED) --config=config); write_template!(html_path->"communities/post.html"(crate::assets::COMMUNITIES_POST) --config=config); + write_template!(html_path->"communities/settings.html"(crate::assets::COMMUNITIES_SETTINGS) --config=config); html_path } diff --git a/crates/app/src/langs/en-US.toml b/crates/app/src/langs/en-US.toml index 17080de..0527138 100644 --- a/crates/app/src/langs/en-US.toml +++ b/crates/app/src/langs/en-US.toml @@ -4,7 +4,9 @@ version = "1.0.0" [data] "general:link.home" = "Home" "general:link.communities" = "Communities" +"general:action.save" = "Save" "general:action.delete" = "Delete" +"general:action.back" = "Back" "dialog:action.okay" = "Ok" "dialog:action.continue" = "Continue" @@ -41,3 +43,15 @@ version = "1.0.0" "notifs:action.mark_as_read" = "Mark as read" "notifs:action.mark_as_unread" = "Mark as unread" "notifs:action.clear" = "Clear" + +"settings:tab.general" = "General" +"settings:tab.account" = "Account" +"settings:tab.profile" = "Profile" +"settings:tab.sessions" = "Sessions" +"settings:label.change_password" = "Change password" +"settings:label.current_password" = "Current password" +"settings:label.new_password" = "New password" +"settings:label.change_username" = "Change username" +"settings:label.new_username" = "New username" +"settings:label.change_avatar" = "Change avatar" +"settings:label.change_banner" = "Change banner" diff --git a/crates/app/src/public/css/style.css b/crates/app/src/public/css/style.css index 17c9836..a87d218 100644 --- a/crates/app/src/public/css/style.css +++ b/crates/app/src/public/css/style.css @@ -335,6 +335,10 @@ table ol { background: var(--color-surface); } +.card.tertiary { + background: var(--color-lowered); +} + .card-nest { box-shadow: var(--shadow-x-offset) var(--shadow-y-offset) var(--shadow-size) var(--color-shadow); @@ -375,6 +379,8 @@ button, background: var(--color-primary); color: var(--color-text-primary); font-weight: 600; + font-size: 0.9rem; + text-decoration: none !important; } button.small, @@ -413,6 +419,17 @@ button.tertiary:hover, background: var(--color-super-raised); } +button.quaternary, +.button.quaternary { + background: var(--color-lowered); + color: var(--color-text-lowered); +} + +button.quaternary:hover, +.button.quaternary:hover { + background: var(--color-super-lowered); +} + button.camo, .button.camo { background: transparent; @@ -508,6 +525,25 @@ select:focus { border-bottom-right-radius: var(--radius); } +@media screen and (max-width: 900px) { + .pillmenu { + /* convert into a sidemenu */ + flex-direction: column; + } + + .pillmenu a:first-child { + border-top-left-radius: var(--radius); + border-top-right-radius: var(--radius); + border-bottom-left-radius: 0; + } + + .pillmenu a:last-child { + border-top-right-radius: 0; + border-bottom-left-radius: var(--radius); + border-bottom-right-radius: var(--radius); + } +} + /* notification */ .notification { text-decoration: none; diff --git a/crates/app/src/public/html/communities/base.html b/crates/app/src/public/html/communities/base.html index 5a4ffc7..2ebbfaa 100644 --- a/crates/app/src/public/html/communities/base.html +++ b/crates/app/src/public/html/communities/base.html @@ -40,155 +40,15 @@ {{ text "communities:action.leave" }} {% endif %} {% else %} - - - -
-
-
-
- Read access -
- -
- -
-
- -
-
- Write access -
- -
- -
-
-
- -
- - -
-
- - + {% endif %} {% endif %} diff --git a/crates/app/src/public/html/communities/list.html b/crates/app/src/public/html/communities/list.html index 20477af..6f9252a 100644 --- a/crates/app/src/public/html/communities/list.html +++ b/crates/app/src/public/html/communities/list.html @@ -13,7 +13,7 @@ onsubmit="create_community_from_form(event)" >
- + Community settings - {{ config.name }} +{% endblock %} {% block body %} {{ macros::nav() }} +
+ + +
+
+
+
+ Read access +
+ +
+ +
+
+ +
+
+ Write access +
+ +
+ +
+
+
+
+ + + +
+ + + + {{ icon "arrow-left" }} + {{ text "general:action.back" }} + +
+
+ + +{% endblock %} diff --git a/crates/app/src/public/html/components.html b/crates/app/src/public/html/components.html index b94db3b..3816ee4 100644 --- a/crates/app/src/public/html/components.html +++ b/crates/app/src/public/html/components.html @@ -4,6 +4,7 @@ src="/api/v1/auth/profile/{{ username }}/avatar?selector_type={{ selector_type }}" alt="@{{ username }}" class="avatar shadow" + loading="lazy" style="--size: {{ size }}" /> {%- endmacro %} {% macro community_avatar(id, community=false, size="24px") -%} @@ -12,6 +13,7 @@ src="/api/v1/communities/{{ id }}/avatar" alt="{{ community.title }}" class="avatar shadow" + loading="lazy" style="--size: {{ size }}" /> {% else %} @@ -19,6 +21,7 @@ src="/api/v1/communities/{{ id }}/avatar" alt="{{ id }}" class="avatar shadow" + loading="lazy" style="--size: {{ size }}" /> {% endif %} {%- endmacro %} {% macro community_listing_card(community) -%} @@ -28,7 +31,7 @@ > {{ components::community_avatar(id=community.id, community=community, size="48px") }} -
+

{{ community.context.display_name }}

{{ community.member_count }} members
diff --git a/crates/app/src/public/html/macros.html b/crates/app/src/public/html/macros.html index 081496d..3b9242a 100644 --- a/crates/app/src/public/html/macros.html +++ b/crates/app/src/public/html/macros.html @@ -31,7 +31,11 @@ show_lhs=true) -%} + +
+
+ {{ text "settings:label.change_username" }} +
+ +
+
+ + +
+ + +
+
+
+
+ + + + + + + + + +{% endblock %} diff --git a/crates/app/src/public/js/atto.js b/crates/app/src/public/js/atto.js index e2823e6..fa5fa5e 100644 --- a/crates/app/src/public/js/atto.js +++ b/crates/app/src/public/js/atto.js @@ -636,10 +636,20 @@ media_theme_pref(); (() => { const self = reg_ns("ui"); + self.define("refresh_container", (_, element, keep) => { + for (const child of element.children) { + if (keep.includes(child.getAttribute("ui_ident"))) { + continue; + } + + child.remove(); + } + }); + self.define("render_settings_ui_field", (_, into_element, option) => { into_element.innerHTML += `
- ${option.label.replaceAll("_", " ")} +
@@ -647,6 +657,8 @@ media_theme_pref(); type="text" onchange="window.set_setting_field('${option.key}', event.target.value)" placeholder="${option.key}" + name="${option.key}" + id="${option.key}" ${option.input_element_type === "input" ? `value="${option.value}"/>` : ">"} ${option.input_element_type === "textarea" ? `${option.value}` : ""}
diff --git a/crates/app/src/routes/api/v1/auth/images.rs b/crates/app/src/routes/api/v1/auth/images.rs index a0dcb48..bd2954d 100644 --- a/crates/app/src/routes/api/v1/auth/images.rs +++ b/crates/app/src/routes/api/v1/auth/images.rs @@ -72,8 +72,11 @@ pub async fn avatar_request( } }; - let path = - PathBufD::current().extend(&["avatars", &data.0.dirs.media, &format!("{}.avif", &user.id)]); + let path = PathBufD::current().extend(&[ + data.0.dirs.media.as_str(), + "avatars", + &format!("{}.avif", &user.id), + ]); if !exists(&path).unwrap() { return ( @@ -114,8 +117,11 @@ pub async fn banner_request( } }; - let path = - PathBufD::current().extend(&["banners", &data.0.dirs.media, &format!("{}.avif", &user.id)]); + let path = PathBufD::current().extend(&[ + data.0.dirs.media.as_str(), + "banners", + &format!("{}.avif", &user.id), + ]); if !exists(&path).unwrap() { return ( diff --git a/crates/app/src/routes/api/v1/auth/profile.rs b/crates/app/src/routes/api/v1/auth/profile.rs index 1c87600..abd3de7 100644 --- a/crates/app/src/routes/api/v1/auth/profile.rs +++ b/crates/app/src/routes/api/v1/auth/profile.rs @@ -1,7 +1,7 @@ use crate::{ State, get_user_from_token, model::{ApiReturn, Error}, - routes::api::v1::UpdateUserIsVerified, + routes::api::v1::{UpdateUserIsVerified, UpdateUserPassword, UpdateUserUsername}, }; use axum::{ Extension, Json, @@ -59,6 +59,70 @@ pub async fn update_profile_settings_request( } } +/// Update the password of the given user. +pub async fn update_profile_password_request( + jar: CookieJar, + Path(id): Path, + Extension(data): Extension, + Json(req): Json, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + if user.id != id { + if !user.permissions.check(FinePermission::MANAGE_USERS) { + return Json(Error::NotAllowed.into()); + } + } + + match data + .update_user_password(id, req.from, req.to, user, false) + .await + { + Ok(_) => Json(ApiReturn { + ok: true, + message: "Password updated".to_string(), + payload: (), + }), + Err(e) => Json(e.into()), + } +} + +pub async fn update_profile_username_request( + jar: CookieJar, + Path(id): Path, + Extension(data): Extension, + Json(req): Json, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + if user.id != id { + if !user.permissions.check(FinePermission::MANAGE_USERS) { + return Json(Error::NotAllowed.into()); + } + } + + if data.get_user_by_username(&req.to).await.is_ok() { + return Json(Error::UsernameInUse.into()); + } + + match data.update_user_username(id, req.to, user).await { + Ok(_) => Json(ApiReturn { + ok: true, + message: "Username updated".to_string(), + payload: (), + }), + Err(e) => Json(e.into()), + } +} + /// Update the tokens of the given user. pub async fn update_profile_tokens_request( jar: CookieJar, diff --git a/crates/app/src/routes/api/v1/communities/images.rs b/crates/app/src/routes/api/v1/communities/images.rs index 4ebbbac..8907740 100644 --- a/crates/app/src/routes/api/v1/communities/images.rs +++ b/crates/app/src/routes/api/v1/communities/images.rs @@ -34,8 +34,8 @@ pub async fn avatar_request( }; let path = PathBufD::current().extend(&[ + data.0.dirs.media.as_str(), "community_avatars", - &data.0.dirs.media, &format!("{}.avif", &community.id), ]); @@ -79,8 +79,8 @@ pub async fn banner_request( }; let path = PathBufD::current().extend(&[ + data.0.dirs.media.as_str(), "community_banners", - &data.0.dirs.media, &format!("{}.avif", &community.id), ]); @@ -132,7 +132,7 @@ pub async fn upload_avatar_request( let path = pathd!( "{}/community_avatars/{}.avif", data.0.dirs.media, - &auth_user.id + &community.id ); // check file size @@ -188,7 +188,7 @@ pub async fn upload_banner_request( let path = pathd!( "{}/community_banners/{}.avif", data.0.dirs.media, - &auth_user.id + &community.id ); // check file size diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index 437cafb..dd3d399 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -109,6 +109,14 @@ pub fn routes() -> Router { "/auth/profile/{id}/settings", post(auth::profile::update_profile_settings_request), ) + .route( + "/auth/profile/{id}/password", + post(auth::profile::update_profile_password_request), + ) + .route( + "/auth/profile/{id}/username", + post(auth::profile::update_profile_username_request), + ) .route( "/auth/profile/{id}/tokens", post(auth::profile::update_profile_tokens_request), @@ -189,6 +197,17 @@ pub struct CreateReaction { pub is_like: bool, } +#[derive(Deserialize)] +pub struct UpdateUserPassword { + pub from: String, + pub to: String, +} + +#[derive(Deserialize)] +pub struct UpdateUserUsername { + pub to: String, +} + #[derive(Deserialize)] pub struct UpdateUserIsVerified { pub is_verified: bool, diff --git a/crates/app/src/routes/pages/communities.rs b/crates/app/src/routes/pages/communities.rs index 371d454..6cf9821 100644 --- a/crates/app/src/routes/pages/communities.rs +++ b/crates/app/src/routes/pages/communities.rs @@ -80,15 +80,6 @@ pub fn community_context( context.insert("community", &community); context.insert("is_owner", &is_owner); context.insert("is_joined", &is_joined); - - if is_owner { - context.insert( - "community_context_serde", - &serde_json::to_string(&community.context) - .unwrap() - .replace("\"", "\\\""), - ); - } } /// `/community/{title}` @@ -152,6 +143,53 @@ pub async fn feed_request( )) } +/// `/community/{title}/manage` +pub async fn settings_request( + jar: CookieJar, + Path(title): Path, + Extension(data): Extension, +) -> impl IntoResponse { + let data = data.read().await; + let user = match get_user_from_token!(jar, data.0) { + Some(ua) => ua, + None => { + return Err(Html( + render_error(Error::NotAllowed, &jar, &data, &None).await, + )); + } + }; + + let community = match data.0.get_community_by_title(&title.to_lowercase()).await { + Ok(ua) => ua, + Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), + }; + + if user.id != community.owner { + return Err(Html( + render_error(Error::NotAllowed, &jar, &data, &None).await, + )); + } + + // init context + let lang = get_lang!(jar, data.0); + let mut context = initial_context(&data.0.0, lang, &Some(user)).await; + + context.insert("community", &community); + context.insert( + "community_context_serde", + &serde_json::to_string(&community.context) + .unwrap() + .replace("\"", "\\\""), + ); + + // return + Ok(Html( + data.1 + .render("communities/settings.html", &mut context) + .unwrap(), + )) +} + /// `/post/{id}` pub async fn post_request( jar: CookieJar, diff --git a/crates/app/src/routes/pages/misc.rs b/crates/app/src/routes/pages/misc.rs index 751512e..8dba087 100644 --- a/crates/app/src/routes/pages/misc.rs +++ b/crates/app/src/routes/pages/misc.rs @@ -7,6 +7,20 @@ use axum::{ use axum_extra::extract::CookieJar; use tetratto_core::model::Error; +pub async fn not_found(jar: CookieJar, Extension(data): Extension) -> impl IntoResponse { + let data = data.read().await; + let user = get_user_from_token!(jar, data.0); + Html( + render_error( + Error::GeneralNotFound("page".to_string()), + &jar, + &data, + &user, + ) + .await, + ) +} + /// `/` pub async fn index_request(jar: CookieJar, Extension(data): Extension) -> impl IntoResponse { let data = data.read().await; diff --git a/crates/app/src/routes/pages/mod.rs b/crates/app/src/routes/pages/mod.rs index 3c7d58d..33b881a 100644 --- a/crates/app/src/routes/pages/mod.rs +++ b/crates/app/src/routes/pages/mod.rs @@ -18,14 +18,20 @@ pub fn routes() -> Router { // misc .route("/", get(misc::index_request)) .route("/notifs", get(misc::notifications_request)) + .fallback_service(get(misc::not_found)) // auth .route("/auth/register", get(auth::register_request)) .route("/auth/login", get(auth::login_request)) // profile + .route("/settings", get(profile::settings_request)) .route("/user/{username}", get(profile::posts_request)) // communities .route("/communities", get(communities::list_request)) .route("/community/{title}", get(communities::feed_request)) + .route( + "/community/{title}/manage", + get(communities::settings_request), + ) .route("/post/{id}", get(communities::post_request)) } diff --git a/crates/app/src/routes/pages/profile.rs b/crates/app/src/routes/pages/profile.rs index 9713f79..2061d7e 100644 --- a/crates/app/src/routes/pages/profile.rs +++ b/crates/app/src/routes/pages/profile.rs @@ -9,6 +9,48 @@ use axum_extra::extract::CookieJar; use tera::Context; use tetratto_core::model::{Error, auth::User, communities::Community}; +/// `/settings` +pub async fn settings_request( + jar: CookieJar, + Extension(data): Extension, +) -> impl IntoResponse { + let data = data.read().await; + let user = match get_user_from_token!(jar, data.0) { + Some(ua) => ua, + None => { + return Err(Html( + render_error(Error::NotAllowed, &jar, &data, &None).await, + )); + } + }; + + let settings = user.settings.clone(); + let tokens = user.tokens.clone(); + + let lang = get_lang!(jar, data.0); + let mut context = initial_context(&data.0.0, lang, &Some(user)).await; + + context.insert( + "user_settings_serde", + &serde_json::to_string(&settings) + .unwrap() + .replace("\"", "\\\""), + ); + context.insert( + "user_tokens_serde", + &serde_json::to_string(&tokens) + .unwrap() + .replace("\"", "\\\""), + ); + + // return + Ok(Html( + data.1 + .render("profile/settings.html", &mut context) + .unwrap(), + )) +} + pub fn profile_context( context: &mut Context, profile: &User, diff --git a/crates/core/src/database/auth.rs b/crates/core/src/database/auth.rs index b3e7df9..bd86f0b 100644 --- a/crates/core/src/database/auth.rs +++ b/crates/core/src/database/auth.rs @@ -6,7 +6,7 @@ use crate::model::{ permissions::FinePermission, }; use crate::{auto_method, execute, get, query_row}; -use tetratto_shared::hash::hash_salted; +use tetratto_shared::hash::{hash_salted, salt}; #[cfg(feature = "sqlite")] use rusqlite::Row; @@ -86,7 +86,7 @@ impl DataManager { // make sure username isn't taken if self.get_user_by_username(&data.username).await.is_ok() { - return Err(Error::MiscError("Username in use".to_string())); + return Err(Error::UsernameInUse); } // ... @@ -130,7 +130,7 @@ impl DataManager { pub async fn delete_user(&self, id: usize, password: &str, force: bool) -> Result<()> { let user = self.get_user_by_id(id).await?; - if (hash_salted(password.to_string(), user.salt) != user.password) && !force { + if (hash_salted(password.to_string(), user.salt.clone()) != user.password) && !force { return Err(Error::IncorrectPassword); } @@ -145,8 +145,7 @@ impl DataManager { return Err(Error::DatabaseError(e.to_string())); } - self.2.remove(format!("atto.user:{}", id)).await; - self.2.remove(format!("atto.user:{}", user.username)).await; + self.cache_clear_user(&user).await; Ok(()) } @@ -166,7 +165,8 @@ impl DataManager { "UPDATE users SET is_verified = $1 WHERE id = $2", &[ &(if x { 1 } else { 0 }).to_string().as_str(), - &serde_json::to_string(&x).unwrap().as_str() + &serde_json::to_string(&x).unwrap().as_str(), + &id.to_string().as_str() ] ); @@ -174,20 +174,86 @@ impl DataManager { return Err(Error::DatabaseError(e.to_string())); } - self.2.remove(format!("atto.user:{}", id)).await; + self.cache_clear_user(&user).await; Ok(()) } - auto_method!(update_user_tokens(Vec) -> "UPDATE users SET tokens = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.user:{}"); - auto_method!(update_user_settings(UserSettings) -> "UPDATE users SET settings = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.user:{}"); + pub async fn update_user_password( + &self, + id: usize, + from: String, + to: String, + user: User, + force: bool, + ) -> Result<()> { + // verify password + if (hash_salted(from.clone(), user.salt.clone()) != user.password) && !force { + return Err(Error::MiscError("Password does not match".to_string())); + } - auto_method!(incr_user_notifications() -> "UPDATE users SET notification_count = notification_count + 1 WHERE id = $1" --cache-key-tmpl="atto.user:{}" --incr); - auto_method!(decr_user_notifications() -> "UPDATE users SET notification_count = notification_count - 1 WHERE id = $1" --cache-key-tmpl="atto.user:{}" --decr); + // ... + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; - auto_method!(incr_user_follower_count() -> "UPDATE users SET follower_count = follower_count + 1 WHERE id = $1" --cache-key-tmpl="atto.user:{}" --incr); - auto_method!(decr_user_follower_count() -> "UPDATE users SET follower_count = follower_count - 1 WHERE id = $1" --cache-key-tmpl="atto.user:{}" --decr); + let new_salt = salt(); + let new_password = hash_salted(to, new_salt.clone()); + let res = execute!( + &conn, + "UPDATE users SET password = $1, salt = $2 WHERE id = $3", + &[ + &new_password.as_str(), + &new_salt.as_str(), + &id.to_string().as_str() + ] + ); - auto_method!(incr_user_following_count() -> "UPDATE users SET following_count = following_count + 1 WHERE id = $1" --cache-key-tmpl="atto.user:{}" --incr); - auto_method!(decr_user_following_count() -> "UPDATE users SET following_count = following_count - 1 WHERE id = $1" --cache-key-tmpl="atto.user:{}" --decr); + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + self.cache_clear_user(&user).await; + + Ok(()) + } + + pub async fn update_user_username(&self, id: usize, to: String, user: User) -> Result<()> { + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!( + &conn, + "UPDATE users SET username = $1 WHERE id = $3", + &[&to.as_str(), &id.to_string().as_str()] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + self.cache_clear_user(&user).await; + + Ok(()) + } + + pub async fn cache_clear_user(&self, user: &User) { + self.2.remove(format!("atto.user:{}", user.id)).await; + self.2.remove(format!("atto.user:{}", user.username)).await; + } + + auto_method!(update_user_tokens(Vec)@get_user_by_id -> "UPDATE users SET tokens = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user); + auto_method!(update_user_settings(UserSettings)@get_user_by_id -> "UPDATE users SET settings = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user); + + auto_method!(incr_user_notifications()@get_user_by_id -> "UPDATE users SET notification_count = notification_count + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --incr); + auto_method!(decr_user_notifications()@get_user_by_id -> "UPDATE users SET notification_count = notification_count - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --decr); + + auto_method!(incr_user_follower_count()@get_user_by_id -> "UPDATE users SET follower_count = follower_count + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --incr); + auto_method!(decr_user_follower_count()@get_user_by_id -> "UPDATE users SET follower_count = follower_count - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --decr); + + auto_method!(incr_user_following_count()@get_user_by_id -> "UPDATE users SET following_count = following_count + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --incr); + auto_method!(decr_user_following_count()@get_user_by_id -> "UPDATE users SET following_count = following_count - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --decr); } diff --git a/crates/core/src/database/common.rs b/crates/core/src/database/common.rs index bbe0740..1e35cbb 100644 --- a/crates/core/src/database/common.rs +++ b/crates/core/src/database/common.rs @@ -465,6 +465,31 @@ macro_rules! auto_method { } }; + ($name:ident($x:ty)@$select_fn:ident -> $query:literal --serde --cache-key-tmpl=$cache_key_tmpl:ident) => { + pub async fn $name(&self, id: usize, x: $x) -> Result<()> { + let y = self.$select_fn(id).await?; + + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!( + &conn, + $query, + &[&serde_json::to_string(&x).unwrap(), &id.to_string()] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + self.$cache_key_tmpl(&y).await; + + Ok(()) + } + }; + ($name:ident($x:ty)@$select_fn:ident:$permission:ident -> $query:literal --serde --cache-key-tmpl=$cache_key_tmpl:ident) => { pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> { let y = self.$select_fn(id).await?; diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs index 831e774..caadf19 100644 --- a/crates/core/src/model/auth.rs +++ b/crates/core/src/model/auth.rs @@ -25,7 +25,7 @@ pub struct User { pub following_count: usize, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct UserSettings { #[serde(default)] pub display_name: String, diff --git a/crates/core/src/model/mod.rs b/crates/core/src/model/mod.rs index afa77b8..06277b3 100644 --- a/crates/core/src/model/mod.rs +++ b/crates/core/src/model/mod.rs @@ -29,6 +29,7 @@ pub enum Error { AlreadyAuthenticated, DataTooLong(String), DataTooShort(String), + UsernameInUse, Unknown, } @@ -46,6 +47,7 @@ impl ToString for Error { Self::AlreadyAuthenticated => "Already authenticated".to_string(), Self::DataTooLong(name) => format!("Given {name} is too long!"), Self::DataTooShort(name) => format!("Given {name} is too short!"), + Self::UsernameInUse => "Username in use".to_string(), _ => format!("An unknown error as occurred: ({:?})", self), } }