diff --git a/Cargo.lock b/Cargo.lock index bc76ea83..90cb3eb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2475,7 +2475,7 @@ dependencies = [ [[package]] name = "revolt" -version = "0.4.0-alpha.3" +version = "0.4.0-alpha.4" dependencies = [ "async-std", "async-tungstenite", diff --git a/Cargo.toml b/Cargo.toml index cc28e92e..26f7933a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt" -version = "0.4.0-alpha.3" +version = "0.4.0-alpha.4" authors = ["Paul Makles "] edition = "2018" diff --git a/src/database/entities/user.rs b/src/database/entities/user.rs index 81d87c74..16f9ecd0 100644 --- a/src/database/entities/user.rs +++ b/src/database/entities/user.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; use mongodb::options::{Collation, FindOneOptions}; use crate::database::*; +use validator::Validate; use crate::util::result::{Error, Result}; use crate::notifications::websocket::is_online; use crate::database::permissions::user::UserPermissions; @@ -27,16 +28,23 @@ pub struct Relationship { /* pub enum Badge { - Developer = 1 + Developer = 1, + Translator = 2, } */ -#[derive(Serialize, Deserialize, Debug)] -#[serde(tag = "type")] -pub enum UserStatus { - Text { - text: String - } +#[derive(Validate, Serialize, Deserialize, Debug)] +pub struct UserStatus { + #[validate(length(min = 1, max = 128))] + #[serde(skip_serializing_if = "Option::is_none")] + text: Option +} + +#[derive(Validate, Serialize, Deserialize, Debug)] +pub struct UserProfile { + #[validate(length(min = 1, max = 2000))] + #[serde(skip_serializing_if = "Option::is_none")] + content: Option } #[derive(Serialize, Deserialize, Debug)] @@ -46,13 +54,13 @@ pub struct User { pub username: String, #[serde(skip_serializing_if = "Option::is_none")] pub relations: Option>, - + #[serde(skip_serializing_if = "Option::is_none")] pub badges: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub status: Option>, + pub status: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub profile: Option, + pub profile: Option, // ? This should never be pushed to the collection. #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src/notifications/events.rs b/src/notifications/events.rs index de643e72..d097fa7d 100644 --- a/src/notifications/events.rs +++ b/src/notifications/events.rs @@ -78,6 +78,10 @@ pub enum ClientboundNotification { user: String }, + UserUpdate { + id: String, + data: JsonValue, + }, UserRelationship { id: String, user: String, diff --git a/src/routes/channels/edit_channel.rs b/src/routes/channels/edit_channel.rs index 6e74de15..8c8e8604 100644 --- a/src/routes/channels/edit_channel.rs +++ b/src/routes/channels/edit_channel.rs @@ -2,7 +2,7 @@ use crate::database::*; use crate::util::result::{Error, Result}; use crate::notifications::events::ClientboundNotification; -use mongodb::bson::doc; +use mongodb::bson::{doc, to_document}; use validator::Validate; use rocket_contrib::json::Json; use serde::{Serialize, Deserialize}; @@ -10,8 +10,10 @@ use serde::{Serialize, Deserialize}; #[derive(Validate, Serialize, Deserialize)] pub struct Data { #[validate(length(min = 1, max = 32))] + #[serde(skip_serializing_if = "Option::is_none")] name: Option, #[validate(length(min = 0, max = 1024))] + #[serde(skip_serializing_if = "Option::is_none")] description: Option, } @@ -36,19 +38,10 @@ pub async fn req(user: User, target: Ref, info: Json) -> Result<()> { match &target { Channel::Group { id, .. } => { - let col = get_collection("channels"); - let mut set = doc! {}; - if let Some(name) = &info.name { - set.insert("name", name.clone()); - } - - if let Some(description) = &info.description { - set.insert("description", description.clone()); - } - - col.update_one( + get_collection("channels") + .update_one( doc! { "_id": &id }, - doc! { "$set": set }, + doc! { "$set": to_document(&info.0).map_err(|_| Error::DatabaseError { operation: "to_document", with: "data" })? }, None ) .await diff --git a/src/routes/root.rs b/src/routes/root.rs index 18c9c53e..41375c15 100644 --- a/src/routes/root.rs +++ b/src/routes/root.rs @@ -9,7 +9,7 @@ use rocket_contrib::json::JsonValue; #[get("/")] pub async fn root() -> JsonValue { json!({ - "revolt": "0.4.0-alpha.3", + "revolt": "0.4.0-alpha.4", "features": { "registration": !*DISABLE_REGISTRATION, "captcha": { diff --git a/src/routes/users/change_username.rs b/src/routes/users/change_username.rs new file mode 100644 index 00000000..ede13761 --- /dev/null +++ b/src/routes/users/change_username.rs @@ -0,0 +1,62 @@ +use crate::database::*; +use crate::util::result::{Error, Result}; +use crate::notifications::events::ClientboundNotification; + +use rauth::auth::{Auth, Session}; +use regex::Regex; +use mongodb::bson::doc; +use rocket::State; +use validator::Validate; +use rocket_contrib::json::Json; +use serde::{Serialize, Deserialize}; + +// ! FIXME: should be global somewhere; maybe use config(?) +lazy_static! { + static ref RE_USERNAME: Regex = Regex::new(r"^[a-zA-Z0-9_.]+$").unwrap(); +} + +#[derive(Validate, Serialize, Deserialize)] +pub struct Data { + #[validate(length(min = 2, max = 32), regex = "RE_USERNAME")] + username: Option, + #[validate(length(min = 8, max = 72))] + password: String, +} + +#[patch("/username", data = "")] +pub async fn req(auth: State<'_, Auth>, session: Session, user: User, data: Json) -> Result<()> { + data.validate() + .map_err(|error| Error::FailedValidation { error })?; + + auth.verify_password(&session, data.password.clone()) + .await + .map_err(|_| Error::InvalidCredentials)?; + + let mut set = doc! {}; + if let Some(username) = &data.username { + if User::is_username_taken(&username).await? { + return Err(Error::UsernameTaken) + } + + set.insert("username", username.clone()); + } + + get_collection("users") + .update_one( + doc! { "_id": &user.id }, + doc! { "$set": set }, + None + ) + .await + .map_err(|_| Error::DatabaseError { operation: "update_one", with: "user" })?; + + ClientboundNotification::UserUpdate { + id: user.id.clone(), + data: json!(data.0) + } + .publish(user.id.clone()) + .await + .ok(); + + Ok(()) +} diff --git a/src/routes/users/edit_user.rs b/src/routes/users/edit_user.rs new file mode 100644 index 00000000..468748ae --- /dev/null +++ b/src/routes/users/edit_user.rs @@ -0,0 +1,41 @@ +use crate::database::*; +use crate::util::result::{Error, Result}; +use crate::notifications::events::ClientboundNotification; + +use mongodb::bson::{doc, to_document}; +use validator::Validate; +use rocket_contrib::json::Json; +use serde::{Serialize, Deserialize}; + +#[derive(Validate, Serialize, Deserialize)] +pub struct Data { + #[serde(skip_serializing_if = "Option::is_none")] + status: Option, + #[serde(skip_serializing_if = "Option::is_none")] + profile: Option +} + +#[patch("/", data = "")] +pub async fn req(user: User, data: Json) -> Result<()> { + data.validate() + .map_err(|error| Error::FailedValidation { error })?; + + get_collection("users") + .update_one( + doc! { "_id": &user.id }, + doc! { "$set": to_document(&data.0).map_err(|_| Error::DatabaseError { operation: "to_document", with: "data" })? }, + None + ) + .await + .map_err(|_| Error::DatabaseError { operation: "update_one", with: "user" })?; + + ClientboundNotification::UserUpdate { + id: user.id.clone(), + data: json!(data.0) + } + .publish(user.id.clone()) + .await + .ok(); + + Ok(()) +} diff --git a/src/routes/users/mod.rs b/src/routes/users/mod.rs index 6945bcbe..331186db 100644 --- a/src/routes/users/mod.rs +++ b/src/routes/users/mod.rs @@ -2,6 +2,8 @@ use rocket::Route; mod add_friend; mod block_user; +mod change_username; +mod edit_user; mod fetch_dms; mod fetch_profile; mod fetch_relationship; @@ -18,6 +20,8 @@ pub fn routes() -> Vec { routes![ // User Information fetch_user::req, + edit_user::req, + change_username::req, get_default_avatar::req, get_avatar::req, fetch_profile::req, diff --git a/src/util/result.rs b/src/util/result.rs index e13a25a7..b7edccef 100644 --- a/src/util/result.rs +++ b/src/util/result.rs @@ -67,6 +67,8 @@ pub enum Error { MissingPermission, #[snafu(display("Operation cannot be performed on this object."))] InvalidOperation, + #[snafu(display("Email or password is incorrect."))] + InvalidCredentials, #[snafu(display("Already created an object with this nonce."))] DuplicateNonce, #[snafu(display("Voso is not enabled on this instance."))] @@ -108,6 +110,7 @@ impl<'r> Responder<'r, 'static> for Error { Error::MissingPermission => Status::Forbidden, Error::InvalidOperation => Status::BadRequest, Error::TooManyIds => Status::BadRequest, + Error::InvalidCredentials => Status::Forbidden, Error::DuplicateNonce => Status::Conflict, Error::VosoUnavailable => Status::BadRequest, Error::NoEffect => Status::Ok,