From 5cfca4979380cb196777f02fe5dfe5be5687b476 Mon Sep 17 00:00:00 2001 From: trisua Date: Wed, 26 Mar 2025 21:46:21 -0400 Subject: [PATCH] add: profile is_verified add: better profile not found page TODO: use error page for fallback service --- crates/app/src/assets.rs | 2 + crates/app/src/public/css/style.css | 4 ++ crates/app/src/public/html/misc/error.html | 21 +++++++ crates/app/src/public/html/profile/base.html | 31 ++++++--- crates/app/src/routes/api/v1/auth/profile.rs | 63 ++++++++++++++++++- crates/app/src/routes/api/v1/mod.rs | 13 ++++ crates/app/src/routes/pages/mod.rs | 26 ++++++++ crates/app/src/routes/pages/profile.rs | 17 ++++- crates/core/src/database/auth.rs | 38 +++++++++-- .../src/database/drivers/sql/create_users.sql | 1 + crates/core/src/database/posts.rs | 35 ++++++++++- crates/core/src/model/auth.rs | 2 + crates/core/src/model/permissions.rs | 1 + 13 files changed, 234 insertions(+), 20 deletions(-) create mode 100644 crates/app/src/public/html/misc/error.html diff --git a/crates/app/src/assets.rs b/crates/app/src/assets.rs index 4fb39c8..bdbd1ba 100644 --- a/crates/app/src/assets.rs +++ b/crates/app/src/assets.rs @@ -31,6 +31,7 @@ pub const ROOT: &str = include_str!("./public/html/root.html"); pub const MACROS: &str = include_str!("./public/html/macros.html"); pub const MISC_INDEX: &str = include_str!("./public/html/misc/index.html"); +pub const MISC_ERROR: &str = include_str!("./public/html/misc/error.html"); pub const AUTH_BASE: &str = include_str!("./public/html/auth/base.html"); pub const AUTH_LOGIN: &str = include_str!("./public/html/auth/login.html"); @@ -135,6 +136,7 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD { write_template!(html_path->"macros.html"(crate::assets::MACROS) --config=config); write_template!(html_path->"misc/index.html"(crate::assets::MISC_INDEX) -d "misc" --config=config); + write_template!(html_path->"misc/error.html"(crate::assets::MISC_ERROR) --config=config); write_template!(html_path->"auth/base.html"(crate::assets::AUTH_BASE) -d "auth" --config=config); write_template!(html_path->"auth/login.html"(crate::assets::AUTH_LOGIN) --config=config); diff --git a/crates/app/src/public/css/style.css b/crates/app/src/public/css/style.css index 574e165..4420cff 100644 --- a/crates/app/src/public/css/style.css +++ b/crates/app/src/public/css/style.css @@ -324,6 +324,10 @@ table ol { border-radius: var(--radius); } +.card.small { + padding: 0.5rem 1rem; +} + .card.secondary { background: var(--color-surface); } diff --git a/crates/app/src/public/html/misc/error.html b/crates/app/src/public/html/misc/error.html new file mode 100644 index 0000000..1ea6627 --- /dev/null +++ b/crates/app/src/public/html/misc/error.html @@ -0,0 +1,21 @@ +{% import "macros.html" as macros %} {% extends "root.html" %} {% block head %} +{{ error_text }} - Tetratto +{% endblock %} {% block body %} {{ macros::nav(selected="home") }} +
+
+
+ Error! 😦 +
+ +
+

{{ error_text }}

+
+ Home + Back +
+
+
+
+{% endblock %} diff --git a/crates/app/src/public/html/profile/base.html b/crates/app/src/public/html/profile/base.html index dc37db5..1ceb5d0 100644 --- a/crates/app/src/public/html/profile/base.html +++ b/crates/app/src/public/html/profile/base.html @@ -13,10 +13,19 @@ {{ macros::avatar(username=profile.username,size="72px") }}
-

- {% if profile.settings.display_name %} {{ - profile.settings.display_name }} {% else %} {{ - profile.username }} {% endif %} + +

+ {% if profile.settings.display_name %} + {{ profile.settings.display_name }} + {% else %} + {{ profile.username }} + {% endif %} + + {% if profile.is_verified %} + + {{ icon "badge-check" }} + + {% endif %}

{{ profile.username }} @@ -24,23 +33,25 @@
-

{{ profile.follower_count }}

{{ text "auth:label.followers" }} -
-
+

{{ profile.following_count }}

{{ text "auth:label.following" }} -
+
-
+
{{ profile.settings.biography }}
@@ -64,7 +75,7 @@
-
+
{{ icon "users-round" }} {{ text "auth:label.joined_journals" }}
diff --git a/crates/app/src/routes/api/v1/auth/profile.rs b/crates/app/src/routes/api/v1/auth/profile.rs index 148ce02..f471fb6 100644 --- a/crates/app/src/routes/api/v1/auth/profile.rs +++ b/crates/app/src/routes/api/v1/auth/profile.rs @@ -1,10 +1,14 @@ use crate::{ State, get_user_from_token, model::{ApiReturn, Error}, + routes::api::v1::UpdateUserIsVerified, }; use axum::{Extension, Json, extract::Path, response::IntoResponse}; use axum_extra::extract::CookieJar; -use tetratto_core::model::{auth::UserSettings, permissions::FinePermission}; +use tetratto_core::model::{ + auth::{Token, UserSettings}, + permissions::FinePermission, +}; /// Update the settings of the given user. pub async fn update_profile_settings_request( @@ -28,7 +32,62 @@ pub async fn update_profile_settings_request( match data.update_user_settings(id, req).await { Ok(_) => Json(ApiReturn { ok: true, - message: "User unfollowed".to_string(), + message: "Settings 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, + 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_tokens(id, req).await { + Ok(_) => Json(ApiReturn { + ok: true, + message: "Tokens updated".to_string(), + payload: (), + }), + Err(e) => Json(e.into()), + } +} + +/// Update the verification status of the given user. +pub async fn update_profile_is_verified_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()), + }; + + match data + .update_user_verified_status(id, req.is_verified, user) + .await + { + Ok(_) => Json(ApiReturn { + ok: true, + message: "Verified status updated".to_string(), payload: (), }), Err(e) => Json(e.into()), diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index 8280024..5311fcb 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -82,6 +82,14 @@ pub fn routes() -> Router { "/auth/profile/{id}/settings", post(auth::profile::update_profile_settings_request), ) + .route( + "/auth/profile/{id}/tokens", + post(auth::profile::update_profile_tokens_request), + ) + .route( + "/auth/profile/{id}/verified", + post(auth::profile::update_profile_is_verified_request), + ) } #[derive(Deserialize)] @@ -140,3 +148,8 @@ pub struct CreateReaction { pub asset_type: AssetType, pub is_like: bool, } + +#[derive(Deserialize)] +pub struct UpdateUserIsVerified { + pub is_verified: bool, +} diff --git a/crates/app/src/routes/pages/mod.rs b/crates/app/src/routes/pages/mod.rs index f0925de..be3bb18 100644 --- a/crates/app/src/routes/pages/mod.rs +++ b/crates/app/src/routes/pages/mod.rs @@ -3,6 +3,14 @@ pub mod misc; pub mod profile; use axum::{Router, routing::get}; +use axum_extra::extract::CookieJar; +use serde::Deserialize; +use tetratto_core::{ + DataManager, + model::{Error, auth::User}, +}; + +use crate::{assets::initial_context, get_lang}; pub fn routes() -> Router { Router::new() @@ -14,3 +22,21 @@ pub fn routes() -> Router { // profile .route("/user/{username}", get(profile::posts_request)) } + +pub fn render_error( + e: Error, + jar: &CookieJar, + data: &(DataManager, tera::Tera), + user: &Option, +) -> String { + let lang = get_lang!(jar, data.0); + let mut context = initial_context(&data.0.0, lang, &user); + context.insert("error_text", &e.to_string()); + data.1.render("misc/error.html", &mut context).unwrap() +} + +#[derive(Deserialize)] +pub struct PaginatedQuery { + #[serde(default)] + pub page: usize, +} diff --git a/crates/app/src/routes/pages/profile.rs b/crates/app/src/routes/pages/profile.rs index eb59396..68bc6cf 100644 --- a/crates/app/src/routes/pages/profile.rs +++ b/crates/app/src/routes/pages/profile.rs @@ -1,7 +1,8 @@ +use super::{PaginatedQuery, render_error}; use crate::{State, assets::initial_context, get_lang, get_user_from_token}; use axum::{ Extension, - extract::Path, + extract::{Path, Query}, response::{Html, IntoResponse}, }; use axum_extra::extract::CookieJar; @@ -10,6 +11,7 @@ use axum_extra::extract::CookieJar; pub async fn posts_request( jar: CookieJar, Path(username): Path, + Query(props): Query, Extension(data): Extension, ) -> impl IntoResponse { let data = data.read().await; @@ -17,12 +19,23 @@ pub async fn posts_request( let other_user = match data.0.get_user_by_username(&username).await { Ok(ua) => ua, - Err(e) => return Err(Html(e.to_string())), + Err(e) => return Err(Html(render_error(e, &jar, &data, &user))), + }; + + let posts = match data + .0 + .get_posts_by_user(other_user.id, 12, props.page) + .await + { + Ok(p) => p, + Err(e) => return Err(Html(render_error(e, &jar, &data, &user))), }; let lang = get_lang!(jar, data.0); let mut context = initial_context(&data.0.0, lang, &user); + context.insert("profile", &other_user); + context.insert("posts", &posts); Ok(Html( data.1.render("profile/posts.html", &mut context).unwrap(), diff --git a/crates/core/src/database/auth.rs b/crates/core/src/database/auth.rs index a54836b..bd17247 100644 --- a/crates/core/src/database/auth.rs +++ b/crates/core/src/database/auth.rs @@ -29,10 +29,11 @@ impl DataManager { settings: serde_json::from_str(&get!(x->5(String)).to_string()).unwrap(), tokens: serde_json::from_str(&get!(x->6(String)).to_string()).unwrap(), permissions: FinePermission::from_bits(get!(x->7(u32))).unwrap(), + is_verified: if get!(x->8(i8)) == 1 { true } else { false }, // counts - notification_count: get!(x->8(i64)) as usize, - follower_count: get!(x->9(i64)) as usize, - following_count: get!(x->10(i64)) as usize, + notification_count: get!(x->9(i64)) as usize, + follower_count: get!(x->10(i64)) as usize, + following_count: get!(x->11(i64)) as usize, } } @@ -91,7 +92,7 @@ impl DataManager { let res = execute!( &conn, - "INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)", + "INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", &[ &data.id.to_string().as_str(), &data.created.to_string().as_str(), @@ -101,6 +102,7 @@ impl DataManager { &serde_json::to_string(&data.settings).unwrap().as_str(), &serde_json::to_string(&data.tokens).unwrap().as_str(), &(FinePermission::DEFAULT.bits()).to_string().as_str(), + &(if data.is_verified { 1 } else { 0 }).to_string().as_str(), &0.to_string().as_str(), &0.to_string().as_str(), &0.to_string().as_str() @@ -144,6 +146,34 @@ impl DataManager { Ok(()) } + pub async fn update_user_verified_status(&self, id: usize, x: bool, user: User) -> Result<()> { + if !user.permissions.check(FinePermission::MANAGE_VERIFIED) { + return Err(Error::NotAllowed); + } + + 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 is_verified = $1 WHERE id = $2", + &[ + &(if x { 1 } else { 0 }).to_string().as_str(), + &serde_json::to_string(&x).unwrap().as_str() + ] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + self.2.remove(format!("atto.user:{}", id)).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:{}"); diff --git a/crates/core/src/database/drivers/sql/create_users.sql b/crates/core/src/database/drivers/sql/create_users.sql index f1098ac..752ba68 100644 --- a/crates/core/src/database/drivers/sql/create_users.sql +++ b/crates/core/src/database/drivers/sql/create_users.sql @@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS users ( settings TEXT NOT NULL, tokens TEXT NOT NULL, permissions INTEGER NOT NULL, + verified INTEGER NOT NULL, -- counts notification_count INTEGER NOT NULL, follower_count INTEGER NOT NULL, diff --git a/crates/core/src/database/posts.rs b/crates/core/src/database/posts.rs index 8476319..88428e5 100644 --- a/crates/core/src/database/posts.rs +++ b/crates/core/src/database/posts.rs @@ -5,7 +5,7 @@ use crate::model::{ Error, Result, auth::User, journal::JournalPost, journal::JournalWriteAccess, permissions::FinePermission, }; -use crate::{auto_method, execute, get, query_row}; +use crate::{auto_method, execute, get, query_row, query_rows}; #[cfg(feature = "sqlite")] use rusqlite::Row; @@ -60,7 +60,7 @@ impl DataManager { let res = query_row!( &conn, - "SELECT * FROM posts WHERE replying_to = $1 LIMIT $2 OFFSET $3", + "SELECT * FROM posts WHERE replying_to = $1 ORDER BY created DESC LIMIT $2 OFFSET $3", &[&(id as i64), &(batch as i64), &((page * batch) as i64)], |x| { Ok(Self::get_post_from_row(x)) } ); @@ -72,6 +72,37 @@ impl DataManager { Ok(res.unwrap()) } + /// Get all posts from the given user (from most recent). + /// + /// # Arguments + /// * `id` - the ID of the user the requested posts belong to + /// * `batch` - the limit of posts in each page + /// * `page` - the page number + pub async fn get_posts_by_user( + &self, + id: usize, + batch: usize, + page: usize, + ) -> Result> { + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = query_rows!( + &conn, + "SELECT * FROM posts WHERE owner = $1 ORDER BY created DESC LIMIT $2 OFFSET $3", + &[&(id as i64), &(batch as i64), &((page * batch) as i64)], + |x| { Self::get_post_from_row(x) } + ); + + if res.is_err() { + return Err(Error::GeneralNotFound("post".to_string())); + } + + Ok(res.unwrap()) + } + /// Create a new journal entry in the database. /// /// # Arguments diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs index 91fa638..86d21f4 100644 --- a/crates/core/src/model/auth.rs +++ b/crates/core/src/model/auth.rs @@ -19,6 +19,7 @@ pub struct User { pub settings: UserSettings, pub tokens: Vec, pub permissions: FinePermission, + pub is_verified: bool, pub notification_count: usize, pub follower_count: usize, pub following_count: usize, @@ -62,6 +63,7 @@ impl User { settings: UserSettings::default(), tokens: Vec::new(), permissions: FinePermission::DEFAULT, + is_verified: false, notification_count: 0, follower_count: 0, following_count: 0, diff --git a/crates/core/src/model/permissions.rs b/crates/core/src/model/permissions.rs index d5423f2..907cbcb 100644 --- a/crates/core/src/model/permissions.rs +++ b/crates/core/src/model/permissions.rs @@ -22,6 +22,7 @@ bitflags! { const MANAGE_MEMBERSHIPS = 1 << 11; const MANAGE_REACTIONS = 1 << 12; const MANAGE_FOLLOWS = 1 << 13; + const MANAGE_VERIFIED = 1 << 14; const _ = !0; }