From 74976dab57887a4d7e29b426cdf7422722fa58ee Mon Sep 17 00:00:00 2001 From: Santo Cariotti Date: Mon, 17 Oct 2022 22:08:09 +0200 Subject: Refactoring of mods --- src/models/auth.rs | 122 ------------- src/models/likes.rs | 91 ---------- src/models/mod.rs | 5 - src/models/model.rs | 479 -------------------------------------------------- src/models/user.rs | 250 -------------------------- src/models/warning.rs | 382 ---------------------------------------- 6 files changed, 1329 deletions(-) delete mode 100644 src/models/auth.rs delete mode 100644 src/models/likes.rs delete mode 100644 src/models/mod.rs delete mode 100644 src/models/model.rs delete mode 100644 src/models/user.rs delete mode 100644 src/models/warning.rs (limited to 'src/models') diff --git a/src/models/auth.rs b/src/models/auth.rs deleted file mode 100644 index 8a673dd..0000000 --- a/src/models/auth.rs +++ /dev/null @@ -1,122 +0,0 @@ -use crate::errors::AppError; -use axum::{ - async_trait, - extract::{FromRequest, RequestParts, TypedHeader}, - headers::{authorization::Bearer, Authorization}, -}; -use chrono::{Duration, Local}; -use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; -use once_cell::sync::Lazy; -use serde::{Deserialize, Serialize}; - -struct Keys { - encoding: EncodingKey, - decoding: DecodingKey, -} - -/// Claims struct -#[derive(Serialize, Deserialize)] -pub struct Claims { - /// ID from the user model - pub user_id: i32, - /// Expiration timestamp - exp: usize, -} - -/// Body used as response to login -#[derive(Serialize)] -pub struct AuthBody { - /// Access token string - access_token: String, - /// "Bearer" string - token_type: String, -} - -/// Payload used for login -#[derive(Deserialize)] -pub struct LoginCredentials { - pub username: String, - pub password: String, -} - -/// Paylod used for user creation -#[derive(Deserialize)] -pub struct SignUpForm { - pub name: String, - pub email: String, - pub username: String, - pub password1: String, - pub password2: String, -} - -static KEYS: Lazy = Lazy::new(|| { - let secret = &crate::config::CONFIG.jwt_secret; - Keys::new(secret.as_bytes()) -}); - -impl Keys { - fn new(secret: &[u8]) -> Self { - Self { - encoding: EncodingKey::from_secret(secret), - decoding: DecodingKey::from_secret(secret), - } - } -} - -impl Claims { - /// Create a new Claim using the `user_id` and the current timestamp + 2 days - pub fn new(user_id: i32) -> Self { - let expiration = Local::now() + Duration::days(1); - - Self { - user_id, - exp: expiration.timestamp() as usize, - } - } - - /// Returns the token as a string. If a token is not encoded, raises an - /// `AppError::TokenCreation` - pub fn get_token(&self) -> Result { - let token = encode(&Header::default(), &self, &KEYS.encoding) - .map_err(|_| AppError::TokenCreation)?; - - Ok(token) - } -} - -impl AuthBody { - pub fn new(access_token: String) -> Self { - Self { - access_token, - token_type: "Bearer".to_string(), - } - } -} - -/// Parse a request to get the Authorization header and then decode it checking its validation -#[async_trait] -impl FromRequest for Claims -where - B: Send, -{ - type Rejection = AppError; - - async fn from_request(req: &mut RequestParts) -> Result { - // Extract the token from the authorization header - let TypedHeader(Authorization(bearer)) = - TypedHeader::>::from_request(req) - .await - .map_err(|_| AppError::InvalidToken)?; - // Decode the user data - let token_data = decode::(bearer.token(), &KEYS.decoding, &Validation::default()) - .map_err(|_| AppError::InvalidToken)?; - - let now = Local::now().timestamp() as usize; - - if token_data.claims.exp < now { - return Err(AppError::InvalidToken); - } - - Ok(token_data.claims) - } -} diff --git a/src/models/likes.rs b/src/models/likes.rs deleted file mode 100644 index 56001d9..0000000 --- a/src/models/likes.rs +++ /dev/null @@ -1,91 +0,0 @@ -use crate::{db::get_client, errors::AppError}; -use chrono::{Local, NaiveDateTime}; -use serde::{Deserialize, Serialize}; -use sqlx::Row; - -/// Likes model -#[derive(Serialize, Deserialize, sqlx::FromRow)] -pub struct Like { - id: i32, - user_id: i32, - model_id: i32, - created: NaiveDateTime, -} - -impl Like { - /// Create a new like - pub fn new(user_id: i32, model_id: i32) -> Self { - let now = Local::now().naive_utc(); - Self { - id: 0, - user_id, - model_id, - created: now, - } - } - - /// Returns `true` if an user has already assigned a like to a model - pub async fn exists(&self) -> Result { - let pool = unsafe { get_client() }; - let cursor = sqlx::query( - r#" - SELECT COUNT(id) as count FROM likes WHERE user_id = $1 AND model_id = $2 - "#, - ) - .bind(self.user_id) - .bind(self.model_id) - .fetch_one(pool) - .await?; - - let count: i64 = cursor.try_get(0).unwrap(); - - Ok(count > 0) - } - - /// Save new like into db - pub async fn save(&self) -> Result { - let pool = unsafe { get_client() }; - - if self.exists().await? { - return Err(AppError::BadRequest( - "This user already likes this model".to_string(), - )); - } - - let rec: Like = sqlx::query_as( - r#" - INSERT INTO likes (user_id, model_id, created) - VALUES ($1, $2, $3) - RETURNING * - "#, - ) - .bind(self.user_id) - .bind(self.model_id) - .bind(self.created) - .fetch_one(pool) - .await?; - - Ok(rec) - } - - /// Remove a like - pub async fn remove(&self) -> Result<(), AppError> { - let pool = unsafe { get_client() }; - - if !self.exists().await? { - return Err(AppError::NotFound("Like not found".to_string())); - } - - sqlx::query( - r#" - DELETE FROM likes WHERE user_id = $1 AND model_id = $2 - "#, - ) - .bind(self.user_id) - .bind(self.model_id) - .execute(pool) - .await?; - - Ok(()) - } -} diff --git a/src/models/mod.rs b/src/models/mod.rs deleted file mode 100644 index 8062fe3..0000000 --- a/src/models/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod auth; -pub mod likes; -pub mod model; -pub mod user; -pub mod warning; diff --git a/src/models/model.rs b/src/models/model.rs deleted file mode 100644 index 87e1997..0000000 --- a/src/models/model.rs +++ /dev/null @@ -1,479 +0,0 @@ -use crate::{config::CONFIG, db::get_client, errors::AppError, json::number_from_string}; -use serde_json::json; -use sqlx::types::JsonValue; -use sqlx::Row; - -use chrono::{Local, NaiveDateTime}; -use serde::{Deserialize, Serialize}; -use validator::Validate; - -/// Model for models. -#[derive(Deserialize, Serialize, Validate, sqlx::FromRow)] -pub struct Model { - id: i32, - #[validate(length(min = 2, message = "Can not be empty"))] - name: String, - description: Option, - duration: f64, - height: f64, - weight: f64, - printer: Option, - material: Option, - author_id: i32, - created: NaiveDateTime, - updated: NaiveDateTime, -} - -/// Payload used for model creation -#[derive(Deserialize)] -pub struct ModelCreate { - pub name: String, - pub description: Option, - #[serde(deserialize_with = "number_from_string")] - pub duration: f64, - #[serde(deserialize_with = "number_from_string")] - pub height: f64, - #[serde(deserialize_with = "number_from_string")] - pub weight: f64, - pub printer: Option, - pub material: Option, -} - -/// Payload used for model searching -#[derive(Deserialize)] -pub struct ModelFilter { - /// Stands for "query" - pub q: String, -} - -#[derive(Serialize, sqlx::FromRow)] -pub struct ModelUser { - pub id: i32, - name: String, - description: Option, - duration: f64, - height: f64, - weight: f64, - printer: Option, - material: Option, - author_id: i32, - created: NaiveDateTime, - updated: NaiveDateTime, - author: Option, - uploads: Option, - likes: Option, -} - -#[derive(Deserialize, Serialize, sqlx::FromRow)] -pub struct ModelUpload { - id: i32, - pub model_id: i32, - pub filepath: String, - created: NaiveDateTime, -} - -impl Model { - #[allow(clippy::too_many_arguments)] - pub fn new( - name: String, - description: Option, - duration: f64, - height: f64, - weight: f64, - printer: Option, - material: Option, - author_id: i32, - ) -> Self { - let now = Local::now().naive_utc(); - Self { - id: 0, - name, - description, - duration, - height, - weight, - printer, - material, - author_id, - created: now, - updated: now, - } - } - - /// Create a new model - pub async fn create(model: Model) -> Result { - let pool = unsafe { get_client() }; - - model - .validate() - .map_err(|error| AppError::BadRequest(error.to_string()))?; - - let rec: Model = sqlx::query_as( - r#" - INSERT INTO models (name, description, duration, height, weight, printer, material, author_id, created, updated) - VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10) - RETURNING * - "#) - .bind(model.name) - .bind(model.description) - .bind(model.duration) - .bind(model.height) - .bind(model.weight) - .bind(model.printer) - .bind(model.material) - .bind(model.author_id) - .bind(model.created) - .bind(model.updated) - .fetch_one(pool) - .await?; - - Ok(rec) - } - - /// Edit a model - pub async fn edit(id: i32, model: Model) -> Result { - let pool = unsafe { get_client() }; - - model - .validate() - .map_err(|error| AppError::BadRequest(error.to_string()))?; - - let rec: Model = sqlx::query_as( - r#" - UPDATE models SET name = $1, description = $2, duration = $3, height = $4, weight = $5, printer = $6, material = $7, updated = $8 - WHERE id = $9 - RETURNING * - "#) - .bind(model.name) - .bind(model.description) - .bind(model.duration) - .bind(model.height) - .bind(model.weight) - .bind(model.printer) - .bind(model.material) - .bind(model.updated) - .bind(id) - .fetch_one(pool) - .await?; - - Ok(rec) - } - - /// Returns the model with id = `model_id` - pub async fn find_by_id(model_id: i32) -> Result { - let pool = unsafe { get_client() }; - - let rec: ModelUser = sqlx::query_as( - r#" - WITH model_uploads AS ( - SELECT models.id, json_agg(uploads.*) filter(WHERE uploads.* IS NOT NULL) AS uploads - FROM models - LEFT JOIN uploads ON uploads.model_id = models.id - GROUP BY models.id - ), - model_likes AS ( - SELECT models.id, json_agg(likes.*) filter(WHERE likes.* IS NOT NULL) AS likes - FROM models - LEFT JOIN likes ON likes.model_id = models.id - GROUP BY models.id - ), - model_author AS ( - SELECT models.id, json_build_object('id', users.id, 'name', users.name, 'email', users.email, 'username', users.username, 'is_staff', users.is_staff, 'avatar', users.avatar) as author - FROM models - JOIN users ON users.id = models.author_id - ) - SELECT models.*, author, uploads, likes - FROM models - INNER JOIN model_author using (id) - INNER JOIN model_uploads using (id) - INNER JOIN model_likes using (id) - WHERE models.id = $1 - "#) - .bind(model_id) - .fetch_one(pool) - .await?; - - Ok(rec) - } - - /// List all models - pub async fn list(page: i64) -> Result, AppError> { - let pool = unsafe { get_client() }; - let rows: Vec = sqlx::query_as( - r#" - WITH model_uploads AS ( - SELECT models.id, json_agg(uploads.*) filter(WHERE uploads.* IS NOT NULL) AS uploads - FROM models - LEFT JOIN uploads ON uploads.model_id = models.id - GROUP BY models.id - ), - model_likes AS ( - SELECT models.id, json_agg(likes.*) filter(WHERE likes.* IS NOT NULL) AS likes - FROM models - LEFT JOIN likes ON likes.model_id = models.id - GROUP BY models.id - ), - model_author AS ( - SELECT models.id, json_build_object('id', users.id, 'name', users.name, 'email', users.email, 'username', users.username, 'is_staff', users.is_staff, 'avatar', users.avatar) as author - FROM models - JOIN users ON users.id = models.author_id - ) - SELECT models.*, author, uploads, likes - FROM models - INNER JOIN model_author using (id) - INNER JOIN model_uploads using (id) - INNER JOIN model_likes using (id) - ORDER BY id DESC - LIMIT $1 OFFSET $2 - "#) - .bind(CONFIG.page_limit) - .bind(CONFIG.page_limit * page) - .fetch_all(pool) - .await?; - - Ok(rows) - } - - /// Filter models by some cols - pub async fn filter(page: i64, query: String) -> Result, AppError> { - let pool = unsafe { get_client() }; - let rows: Vec = sqlx::query_as( - r#" - WITH model_uploads AS ( - SELECT models.id, json_agg(uploads.*) filter(WHERE uploads.* IS NOT NULL) AS uploads - FROM models - LEFT JOIN uploads ON uploads.model_id = models.id - GROUP BY models.id - ), - model_likes AS ( - SELECT models.id, json_agg(likes.*) filter(WHERE likes.* IS NOT NULL) AS likes - FROM models - LEFT JOIN likes ON likes.model_id = models.id - GROUP BY models.id - ), - model_author AS ( - SELECT models.id, json_build_object('id', users.id, 'name', users.name, 'email', users.email, 'username', users.username, 'is_staff', users.is_staff, 'avatar', users.avatar) as author - FROM models - JOIN users ON users.id = models.author_id - ) - SELECT models.*, author, uploads, likes - FROM models - INNER JOIN model_author using (id) - INNER JOIN model_uploads using (id) - INNER JOIN model_likes using (id) - WHERE models.name ILIKE $1 OR description ILIKE $1 OR printer ILIKE $1 OR material ILIKE $1 - ORDER BY id DESC - LIMIT $2 OFFSET $3 - "#) - .bind(format!("%{}%", query)) - .bind(CONFIG.page_limit) - .bind(CONFIG.page_limit * page) - .fetch_all(pool) - .await?; - - Ok(rows) - } - - /// List author's models - pub async fn list_from_author(page: i64, author: i32) -> Result, AppError> { - let pool = unsafe { get_client() }; - let rows: Vec = sqlx::query_as( - r#" - WITH model_uploads AS ( - SELECT models.id, json_agg(uploads.*) filter(WHERE uploads.* IS NOT NULL) AS uploads - FROM models - LEFT JOIN uploads ON uploads.model_id = models.id - GROUP BY models.id - ), - model_likes AS ( - SELECT models.id, json_agg(likes.*) filter(WHERE likes.* IS NOT NULL) AS likes - FROM models - LEFT JOIN likes ON likes.model_id = models.id - GROUP BY models.id - ), - model_author AS ( - SELECT models.id, json_build_object('id', users.id, 'name', users.name, 'email', users.email, 'username', users.username, 'is_staff', users.is_staff, 'avatar', users.avatar) as author - FROM models - JOIN users ON users.id = models.author_id - ) - SELECT models.*, author, uploads, likes - FROM models - INNER JOIN model_author using (id) - INNER JOIN model_uploads using (id) - INNER JOIN model_likes using (id) - WHERE models.author_id = $1 - ORDER BY id DESC - LIMIT $2 OFFSET $3 - "#) - .bind(author) - .bind(CONFIG.page_limit) - .bind(CONFIG.page_limit * page) - .fetch_all(pool) - .await?; - - Ok(rows) - } - - /// Delete a model - pub async fn delete(model_id: i32) -> Result<(), AppError> { - let pool = unsafe { get_client() }; - - sqlx::query( - r#" - DELETE FROM models WHERE id = $1 - "#, - ) - .bind(model_id) - .execute(pool) - .await?; - - Ok(()) - } - - /// Return the number of models. - pub async fn count() -> Result { - let pool = unsafe { get_client() }; - let cursor = sqlx::query(r#"SELECT COUNT(id) as count FROM models"#) - .fetch_one(pool) - .await?; - - let count: i64 = cursor.try_get(0).unwrap(); - Ok(count) - } - - /// Return the number of author models - pub async fn count_filter_by_author(author: i32) -> Result { - let pool = unsafe { get_client() }; - let cursor = sqlx::query(r#"SELECT COUNT(id) as count FROM models WHERE author_id = $1"#) - .bind(author) - .fetch_one(pool) - .await?; - - let count: i64 = cursor.try_get(0).unwrap(); - Ok(count) - } - - /// Return the number of models filtered by query - pub async fn count_filter(query: String) -> Result { - let pool = unsafe { get_client() }; - let cursor = sqlx::query( - r#" - SELECT COUNT(id) as count FROM models - WHERE models.name ILIKE $1 OR description ILIKE $1 OR printer ILIKE $1 OR material ILIKE $1 - "# - ) - .bind(format!("%{}%", query)) - .fetch_one(pool) - .await?; - - let count: i64 = cursor.try_get(0).unwrap(); - Ok(count) - } -} - -impl ModelUser { - /// Returns the author id from the `JsonValue` - pub fn author_id(&self) -> JsonValue { - match &self.author { - Some(json) => json.get("id").unwrap().clone(), - None => json!(0), - } - } - - /// Returns a vec of string made by all the filepaths from the model - pub async fn list_upload_filepaths(&self) -> Option> { - // Raise a `None` if `self.uploads` is `None` - self.uploads.as_ref()?; - - let uploads = ModelUpload::find_by_model(self.id) - .await - .unwrap_or_default(); - - let paths = uploads - .iter() - .map(|x| x.filepath.clone()) - .collect::>(); - - Some(paths) - } -} - -impl ModelUpload { - pub fn new(filepath: String, model_id: i32) -> Self { - let now = Local::now().naive_utc(); - Self { - id: 0, - filepath, - model_id, - created: now, - } - } - - /// Create a new upload for model - pub async fn create(file: ModelUpload) -> Result { - let pool = unsafe { get_client() }; - - let rec: ModelUpload = sqlx::query_as( - r#" - INSERT INTO uploads (filepath, model_id, created) - VALUES ( $1, $2, $3) - RETURNING * - "#, - ) - .bind(file.filepath) - .bind(file.model_id) - .bind(file.created) - .fetch_one(pool) - .await?; - - Ok(rec) - } - - /// Find all paths of a model - pub async fn find_by_model(model_id: i32) -> Result, AppError> { - let pool = unsafe { get_client() }; - - let rec: Vec = sqlx::query_as( - r#" - SELECT * FROM uploads WHERE model_id = $1 - "#, - ) - .bind(model_id) - .fetch_all(pool) - .await?; - - Ok(rec) - } - - /// Returns the model upload with id = `upload_id` - pub async fn find_by_id(id: i32) -> Result { - let pool = unsafe { get_client() }; - - let rec: ModelUpload = sqlx::query_as( - r#" - SELECT * FROM uploads WHERE id = $1 - "#, - ) - .bind(id) - .fetch_one(pool) - .await?; - - Ok(rec) - } - - /// Delete a model upload - pub async fn delete(upload_id: i32) -> Result<(), AppError> { - let pool = unsafe { get_client() }; - - sqlx::query( - r#" - DELETE FROM uploads WHERE id = $1 - "#, - ) - .bind(upload_id) - .execute(pool) - .await?; - - Ok(()) - } -} diff --git a/src/models/user.rs b/src/models/user.rs deleted file mode 100644 index d09394b..0000000 --- a/src/models/user.rs +++ /dev/null @@ -1,250 +0,0 @@ -use crate::{ - config::CONFIG, - db::get_client, - errors::AppError, - models::model::{Model, ModelUser}, -}; - -use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, NoneAsEmptyString}; -use sqlx::Row; -use validator::Validate; - -/// User model -#[derive(Deserialize, Serialize, Validate)] -pub struct User { - id: i32, - name: String, - #[validate(length(min = 4, message = "Can not be empty"))] - email: String, - #[validate(length(min = 2, message = "Can not be empty"))] - username: String, - #[validate(length(min = 8, message = "Must be min 8 chars length"))] - password: String, - is_staff: Option, - avatar: Option, -} - -/// Paylod used for user editing -#[derive(Deserialize)] -pub struct UserEdit { - pub name: String, - pub email: String, - pub username: String, - pub is_staff: Option, -} - -/// Response used to print a user (or a users list) -#[serde_as] -#[derive(Deserialize, Serialize, sqlx::FromRow, Validate)] -pub struct UserList { - pub id: i32, - pub name: String, - #[validate(length(min = 4, message = "Can not be empty"))] - pub email: String, - #[validate(length(min = 2, message = "Can not be empty"))] - pub username: String, - pub is_staff: Option, - #[serde_as(as = "NoneAsEmptyString")] - pub avatar: Option, -} - -impl User { - /// By default an user has id = 0. It is not created yet - pub fn new(name: String, email: String, username: String, password: String) -> Self { - Self { - id: 0, - name, - email, - username, - password, - is_staff: Some(false), - avatar: None, - } - } - - /// Create a new user from the model using a SHA256 crypted password - pub async fn create(user: User) -> Result { - let pool = unsafe { get_client() }; - - user.validate() - .map_err(|error| AppError::BadRequest(error.to_string()))?; - - let crypted_password = sha256::digest(user.password); - - let rec: UserList = sqlx::query_as( - r#" - INSERT INTO users (name, email, username, password) - VALUES ( $1, $2, $3, $4) - RETURNING id, name, email, username, is_staff, avatar - "#, - ) - .bind(user.name) - .bind(user.email) - .bind(user.username) - .bind(crypted_password) - .fetch_one(pool) - .await?; - - Ok(rec) - } - - /// Find a user using the model. It used for login - pub async fn find(user: User) -> Result { - let pool = unsafe { get_client() }; - - let crypted_password = sha256::digest(user.password); - - let rec: UserList = sqlx::query_as( - r#" - SELECT id, name, email, username, is_staff, avatar FROM "users" - WHERE username = $1 AND password = $2 - "#, - ) - .bind(user.username) - .bind(crypted_password) - .fetch_one(pool) - .await?; - - Ok(rec) - } - - /// Returns the user with id = `user_id` - pub async fn find_by_id(user_id: i32) -> Result { - let pool = unsafe { get_client() }; - - let rec: UserList = sqlx::query_as( - r#" - SELECT id, name, email, username, is_staff, avatar FROM "users" - WHERE id = $1 - "#, - ) - .bind(user_id) - .fetch_one(pool) - .await?; - - Ok(rec) - } - - /// List all users - pub async fn list(page: i64) -> Result, AppError> { - let pool = unsafe { get_client() }; - let rows: Vec = sqlx::query_as( - r#"SELECT id, name, email, username, is_staff, avatar FROM users - ORDER BY id DESC - LIMIT $1 OFFSET $2 - "#, - ) - .bind(CONFIG.page_limit) - .bind(CONFIG.page_limit * page) - .fetch_all(pool) - .await?; - - Ok(rows) - } - - /// Return the number of users. - pub async fn count() -> Result { - let pool = unsafe { get_client() }; - let cursor = sqlx::query(r#"SELECT COUNT(id) as count FROM users"#) - .fetch_one(pool) - .await?; - - let count: i64 = cursor.try_get(0).unwrap(); - Ok(count) - } - - /// Prevent the "uniquess" Postgres fields check. Check if username has been taken - pub async fn username_has_taken(username: &String) -> Result { - let pool = unsafe { get_client() }; - let cursor = sqlx::query( - r#" - SELECT COUNT(id) as count FROM users WHERE username = $1 - "#, - ) - .bind(username) - .fetch_one(pool) - .await?; - - let count: i64 = cursor.try_get(0).unwrap(); - - Ok(count > 0) - } - - /// Prevent the "uniquess" Postgres fields check. Check if email has been taken - pub async fn email_has_taken(email: &String) -> Result { - let pool = unsafe { get_client() }; - let cursor = sqlx::query( - r#" - SELECT COUNT(id) as count FROM users WHERE email = $1 - "#, - ) - .bind(email) - .fetch_one(pool) - .await?; - - let count: i64 = cursor.try_get(0).unwrap(); - - Ok(count > 0) - } -} - -impl UserList { - /// Edit an user avatar - pub async fn edit_avatar(&mut self, avatar: Option) -> Result<(), AppError> { - let pool = unsafe { get_client() }; - sqlx::query( - r#" - UPDATE users SET avatar = $1 WHERE id = $2 - "#, - ) - .bind(&avatar) - .bind(self.id) - .execute(pool) - .await?; - - self.avatar = avatar; - - Ok(()) - } - - /// Edit an user - pub async fn edit(&mut self, payload: UserEdit) -> Result<(), AppError> { - let pool = unsafe { get_client() }; - - // Make assignments before the `sqlx::query()` so to perform validation. - // If the `AppError::BadRequest` is raised, the query (and then the update) will be skipped - self.name = payload.name.clone(); - self.username = payload.username.clone(); - self.email = payload.email.clone(); - self.is_staff = payload.is_staff; - - self.validate() - .map_err(|error| AppError::BadRequest(error.to_string()))?; - - sqlx::query( - r#" - UPDATE users SET name = $1, username = $2, email = $3, is_staff = $4 WHERE id = $5 - "#, - ) - .bind(&payload.name) - .bind(&payload.username) - .bind(&payload.email) - .bind(payload.is_staff.unwrap_or_default()) - .bind(self.id) - .execute(pool) - .await?; - - Ok(()) - } - - /// Get all models created by an user - pub async fn get_models(&self, page: i64) -> Result, AppError> { - Model::list_from_author(page, self.id).await - } - - /// Returns the number of models for an user - pub async fn count_models(&self) -> Result { - Model::count_filter_by_author(self.id).await - } -} diff --git a/src/models/warning.rs b/src/models/warning.rs deleted file mode 100644 index c420dd0..0000000 --- a/src/models/warning.rs +++ /dev/null @@ -1,382 +0,0 @@ -use crate::{config::CONFIG, db::get_client, errors::AppError}; -use chrono::{Local, NaiveDateTime}; -use serde::{Deserialize, Serialize}; -use sqlx::types::JsonValue; -use sqlx::Row; -use std::convert::From; - -/// Model for warnings. -#[derive(Deserialize, Serialize, sqlx::FromRow)] -pub struct Warning { - pub id: i32, - pub user_id: Option, - pub model_id: Option, - pub resolved_by: Option, - pub note: String, - pub admin_note: String, - pub created: NaiveDateTime, - pub updated: NaiveDateTime, -} - -#[derive(Serialize, sqlx::FromRow)] -pub struct WarningUser { - pub id: i32, - pub user_id: Option, - pub model_id: Option, - pub resolved_by: Option, - pub note: String, - pub admin_note: String, - pub created: NaiveDateTime, - pub updated: NaiveDateTime, - user: Option, - resolved: Option, -} - -/// Impl conversion from `WarningUser` to `Warning` -impl From for Warning { - fn from(item: WarningUser) -> Self { - Self { - id: item.id, - user_id: item.user_id, - model_id: item.model_id, - resolved_by: item.resolved_by, - note: item.note, - admin_note: item.admin_note, - created: item.created, - updated: item.created, - } - } -} - -/// Payload used to create a new warning -#[derive(Deserialize)] -pub struct WarningCreate { - pub model_id: i32, - pub note: String, -} - -/// Payload used to edit a warning -#[derive(Deserialize)] -pub struct WarningEdit { - pub admin_note: String, -} - -/// Payload used for warning filtering -#[derive(Deserialize)] -pub struct WarningFilterPayload { - pub model_id: Option, - pub resolved_by: Option, -} - -/// Struct used as argument for filtering by the backend -#[derive(Debug)] -pub struct WarningFilter { - pub model_id: Option, - pub resolved_by: Option, - pub user_id: Option, -} - -impl Warning { - /// Create a warning means create an object which has an `user_id` (creator of the warning), a - /// `model_id` (suspect model) and a `note` - pub fn new(user_id: i32, model_id: i32, note: String) -> Self { - let now = Local::now().naive_utc(); - Self { - id: 0, - user_id: Some(user_id), - model_id: Some(model_id), - resolved_by: None, - note, - admin_note: String::new(), - created: now, - updated: now, - } - } - - /// Delete a report - pub async fn delete(warning_id: i32) -> Result<(), AppError> { - let pool = unsafe { get_client() }; - - sqlx::query( - r#" - DELETE FROM warnings WHERE id = $1 - "#, - ) - .bind(warning_id) - .execute(pool) - .await?; - - Ok(()) - } - - /// List all warnings. A staffer can see all the warnings, a user cannot - pub async fn list(page: i64, user_id: Option) -> Result, AppError> { - let pool = unsafe { get_client() }; - let query = r#" - SELECT - warnings.*, - json_build_object('id', users.id, 'name', users.name, 'email', users.email, 'username', users.username, 'is_staff', users.is_staff, 'avatar', users.avatar) as user, - coalesce(r.data, '{}'::json) as resolved - FROM warnings - JOIN users ON users.id = warnings.user_id - LEFT JOIN ( - SELECT id, json_build_object('id', r.id, 'name', r.name, 'email', r.email, 'username', r.username, 'is_staff', r.is_staff, 'avatar', r.avatar) as data - FROM users r - ) r ON r.id = warnings.resolved_by - "#; - - let rows: Vec = match user_id { - Some(id) => { - sqlx::query_as(&format!( - r#"{} WHERE user_id = $1 ORDER BY id DESC LIMIT $2 OFFSET $3"#, - query - )) - .bind(id) - .bind(CONFIG.page_limit) - .bind(CONFIG.page_limit * page) - .fetch_all(pool) - .await? - } - None => { - sqlx::query_as(&format!(r#"{} ORDER BY id DESC LIMIT $1 OFFSET $2"#, query)) - .bind(CONFIG.page_limit) - .bind(CONFIG.page_limit * page) - .fetch_all(pool) - .await? - } - }; - - Ok(rows) - } - - /// Returns the warning with id = `warning_id` - pub async fn find_by_id(warning_id: i32) -> Result { - let pool = unsafe { get_client() }; - - let rec: WarningUser = sqlx::query_as( - r#" - SELECT - warnings.*, - json_build_object('id', users.id, 'name', users.name, 'email', users.email, 'username', users.username, 'is_staff', users.is_staff, 'avatar', users.avatar) as user, - coalesce(r.data, '{}'::json) as resolved - FROM warnings - JOIN users ON users.id = warnings.user_id - LEFT JOIN ( - SELECT id, json_build_object('id', r.id, 'name', r.name, 'email', r.email, 'username', r.username, 'is_staff', r.is_staff, 'avatar', r.avatar) as data - FROM users r - ) r ON r.id = warnings.resolved_by - WHERE warnings.id = $1 - "#) - .bind(warning_id) - .fetch_one(pool) - .await?; - - Ok(rec) - } - - /// Return the number of warnings. - pub async fn count(user_id: Option) -> Result { - let pool = unsafe { get_client() }; - - let cursor = match user_id { - Some(id) => { - sqlx::query(r#"SELECT COUNT(id) as count FROM warnings WHERE user_id = $1"#) - .bind(id) - .fetch_one(pool) - .await? - } - None => { - sqlx::query(r#"SELECT COUNT(id) as count FROM warnings"#) - .fetch_one(pool) - .await? - } - }; - - let count: i64 = cursor.try_get(0).unwrap(); - Ok(count) - } - - /// Create a new upload for model - pub async fn create(warning: Warning) -> Result { - let pool = unsafe { get_client() }; - - let rec: Warning = sqlx::query_as( - r#" - INSERT INTO warnings (user_id, model_id, resolved_by, note, admin_note, created, updated) - VALUES ( $1, $2, $3, $4, $5, $6, $7) - RETURNING * - "#, - ) - .bind(warning.user_id) - .bind(warning.model_id) - .bind(warning.resolved_by) - .bind(warning.note) - .bind(warning.admin_note) - .bind(warning.created) - .bind(warning.updated) - .fetch_one(pool) - .await?; - - Ok(rec) - } - - /// Filter warnings. Pass a `WarningFilter` argument. You can filter only by model_id or (not - /// both) resolved by - pub async fn filter(page: i64, args: WarningFilter) -> Result, AppError> { - let pool = unsafe { get_client() }; - - let mut query = r#" - SELECT - warnings.*, - json_build_object('id', users.id, 'name', users.name, 'email', users.email, 'username', users.username, 'is_staff', users.is_staff, 'avatar', users.avatar) as user, - coalesce(r.data, '{}'::json) as resolved - FROM warnings - JOIN users ON users.id = warnings.user_id - LEFT JOIN ( - SELECT id, json_build_object('id', r.id, 'name', r.name, 'email', r.email, 'username', r.username, 'is_staff', r.is_staff, 'avatar', r.avatar) as data - FROM users r - ) r ON r.id = warnings.resolved_by - "#.to_string(); - - if args.model_id.is_some() { - query += r#"WHERE model_id = $1"#; - } else { - match args.resolved_by { - Some(_) => { - query += r#" WHERE warnings.resolved_by = $1"#; - } - None => { - query += r#" WHERE warnings.resolved_by IS NULL"#; - } - }; - } - - let rows: Vec = match args.user_id { - Some(id) => { - let q = if args.model_id.is_some() { - query = format!( - r#"{} AND user_id = $2 ORDER BY id DESC LIMIT $3 OFFSET $4"#, - query - ); - sqlx::query_as(&query).bind(args.model_id.unwrap()) - } else if args.resolved_by.is_some() { - query = format!( - r#"{} AND user_id = $2 ORDER BY id DESC LIMIT $3 OFFSET $4"#, - query - ); - sqlx::query_as(&query).bind(args.resolved_by.unwrap()) - } else { - query = format!( - r#"{} AND user_id = $1 ORDER BY id DESC LIMIT $2 OFFSET $3"#, - query - ); - sqlx::query_as(&query) - }; - - q.bind(id) - .bind(CONFIG.page_limit) - .bind(CONFIG.page_limit * page) - .fetch_all(pool) - .await? - } - None => { - let q = if args.model_id.is_some() { - query = format!(r#"{} ORDER BY id DESC LIMIT $2 OFFSET $3"#, query); - sqlx::query_as(&query).bind(args.model_id.unwrap()) - } else if args.resolved_by.is_some() { - query = format!(r#"{} ORDER BY id DESC LIMIT $2 OFFSET $3"#, query); - sqlx::query_as(&query).bind(args.resolved_by.unwrap()) - } else { - query = format!(r#"{} ORDER BY id DESC LIMIT $1 OFFSET $2"#, query); - sqlx::query_as(&query) - }; - - q.bind(CONFIG.page_limit) - .bind(CONFIG.page_limit * page) - .fetch_all(pool) - .await? - } - }; - - Ok(rows) - } - - /// Return the number of filtered warnings. - pub async fn count_by_model_id(args: WarningFilter) -> Result { - let pool = unsafe { get_client() }; - - let mut query = r#" - SELECT COUNT(id) as count FROM warnings - "# - .to_string(); - - if args.model_id.is_some() { - query += r#" WHERE model_id = $1"#; - } else { - match args.resolved_by { - Some(_) => { - query += r#" WHERE warnings.resolved_by = $1"#; - } - None => { - query += r#" WHERE warnings.resolved_by IS NULL"#; - } - }; - } - - let cursor = match args.user_id { - Some(id) => { - let q = if args.model_id.is_some() { - query = format!(r#"{} AND user_id = $2"#, query); - sqlx::query(&query).bind(args.model_id.unwrap()) - } else if args.resolved_by.is_some() { - query = format!(r#"{} AND user_id = $2"#, query); - sqlx::query(&query).bind(args.resolved_by.unwrap()) - } else { - query = format!(r#"{} AND user_id = $1"#, query); - sqlx::query(&query) - }; - - q.bind(id).fetch_one(pool).await? - } - None => { - let q = if args.model_id.is_some() { - sqlx::query(&query).bind(args.model_id.unwrap()) - } else if args.resolved_by.is_some() { - sqlx::query(&query).bind(args.resolved_by.unwrap()) - } else { - sqlx::query(&query) - }; - - q.fetch_one(pool).await? - } - }; - - let count: i64 = cursor.try_get(0).unwrap(); - Ok(count) - } - - /// Edit a warning - pub async fn edit(&mut self, resolver: i32, payload: WarningEdit) -> Result<(), AppError> { - let pool = unsafe { get_client() }; - - let now = Local::now().naive_utc(); - - sqlx::query( - r#" - UPDATE warnings SET admin_note = $1, resolved_by = $2, updated = $3 WHERE id = $4 - "#, - ) - .bind(&payload.admin_note) - .bind(resolver) - .bind(now) - .bind(self.id) - .execute(pool) - .await?; - - self.admin_note = payload.admin_note; - self.resolved_by = Some(resolver); - self.updated = now; - - Ok(()) - } -} -- cgit v1.2.3-71-g8e6c