diff options
Diffstat (limited to 'src/warning')
| -rw-r--r-- | src/warning/mod.rs | 2 | ||||
| -rw-r--r-- | src/warning/models.rs | 382 | ||||
| -rw-r--r-- | src/warning/routes.rs | 175 |
3 files changed, 559 insertions, 0 deletions
diff --git a/src/warning/mod.rs b/src/warning/mod.rs new file mode 100644 index 0000000..a0e1883 --- /dev/null +++ b/src/warning/mod.rs @@ -0,0 +1,2 @@ +pub mod models; +pub mod routes; diff --git a/src/warning/models.rs b/src/warning/models.rs new file mode 100644 index 0000000..c420dd0 --- /dev/null +++ b/src/warning/models.rs @@ -0,0 +1,382 @@ +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<i32>, + pub model_id: Option<i32>, + pub resolved_by: Option<i32>, + 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<i32>, + pub model_id: Option<i32>, + pub resolved_by: Option<i32>, + pub note: String, + pub admin_note: String, + pub created: NaiveDateTime, + pub updated: NaiveDateTime, + user: Option<JsonValue>, + resolved: Option<JsonValue>, +} + +/// Impl conversion from `WarningUser` to `Warning` +impl From<WarningUser> 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<i32>, + pub resolved_by: Option<i32>, +} + +/// Struct used as argument for filtering by the backend +#[derive(Debug)] +pub struct WarningFilter { + pub model_id: Option<i32>, + pub resolved_by: Option<i32>, + pub user_id: Option<i32>, +} + +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<i32>) -> Result<Vec<WarningUser>, 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<WarningUser> = 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<WarningUser, AppError> { + 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<i32>) -> Result<i64, AppError> { + 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<Warning, AppError> { + 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<Vec<WarningUser>, 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<WarningUser> = 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<i64, AppError> { + 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(()) + } +} diff --git a/src/warning/routes.rs b/src/warning/routes.rs new file mode 100644 index 0000000..81173e6 --- /dev/null +++ b/src/warning/routes.rs @@ -0,0 +1,175 @@ +use crate::{ + auth::models::Claims, + errors::AppError, + model::models::Model, + pagination::{Pagination, WarningPagination}, + routes::JsonCreate, + user::models::User, + warning::models::*, +}; +use axum::{ + extract::{Path, Query}, + http::StatusCode, + routing::{get, post}, + Json, Router, +}; + +/// Create routes for `/v1/warnings/` namespace +pub fn create_route() -> Router { + Router::new() + .route("/", get(list_warnings).post(create_warning)) + .route( + "/:id", + get(get_warning).put(edit_warning).delete(delete_warning), + ) + .route("/filter", post(filter_warnings)) +} + +/// List warnings. A staffer can see everything. +async fn list_warnings( + pagination: Query<Pagination>, + claims: Claims, +) -> Result<Json<WarningPagination>, AppError> { + let page = pagination.0.page.unwrap_or_default(); + + let user = User::find_by_id(claims.user_id).await?; + + let (results, count) = match user.is_staff.unwrap() { + true => ( + Warning::list(page, None).await?, + Warning::count(None).await?, + ), + false => ( + Warning::list(page, Some(user.id)).await?, + Warning::count(Some(user.id)).await?, + ), + }; + + Ok(Json(WarningPagination { count, results })) +} + +/// Get a warning with id = `model_id` +async fn get_warning( + Path(warning_id): Path<i32>, + claims: Claims, +) -> Result<Json<Warning>, AppError> { + let user = User::find_by_id(claims.user_id).await?; + + if !(user.is_staff.unwrap()) { + return Err(AppError::Unauthorized); + } + + match Warning::find_by_id(warning_id).await { + Ok(warning) => Ok(Json(warning.into())), + Err(_) => Err(AppError::NotFound("Warning not found".to_string())), + } +} + +/// Create a warning. Checks Authorization token +async fn create_warning( + Json(payload): Json<WarningCreate>, + claims: Claims, +) -> Result<JsonCreate<Warning>, AppError> { + let model = match Model::find_by_id(payload.model_id).await { + Ok(model) => model, + Err(_) => return Err(AppError::NotFound("Report not found".to_string())), + }; + + let warning = Warning::new(claims.user_id, model.id, payload.note); + + let warning_new = Warning::create(warning).await?; + + Ok(JsonCreate(warning_new)) +} + +/// Staffers can edit a warning +async fn edit_warning( + Json(payload): Json<WarningEdit>, + claims: Claims, + Path(warning_id): Path<i32>, +) -> Result<Json<Warning>, AppError> { + let mut warning: Warning = match Warning::find_by_id(warning_id).await { + Ok(warning) => warning.into(), + Err(_) => { + return Err(AppError::NotFound("Report not found".to_string())); + } + }; + + let user = User::find_by_id(claims.user_id).await?; + + if !(user.is_staff.unwrap()) { + return Err(AppError::Unauthorized); + } + + warning.edit(user.id, payload).await?; + + Ok(Json(warning)) +} + +/// A staffer can delete a warning +async fn delete_warning( + claims: Claims, + Path(warning_id): Path<i32>, +) -> Result<StatusCode, AppError> { + let user = User::find_by_id(claims.user_id).await?; + + if !user.is_staff.unwrap() { + return Err(AppError::Unauthorized); + } + + if Warning::delete(warning_id).await.is_ok() { + Ok(StatusCode::NO_CONTENT) + } else { + Ok(StatusCode::BAD_REQUEST) + } +} + +/// Apply a filter to warnings list +async fn filter_warnings( + Json(payload): Json<WarningFilterPayload>, + pagination: Query<Pagination>, + claims: Claims, +) -> Result<Json<WarningPagination>, AppError> { + let page = pagination.0.page.unwrap_or_default(); + + let user = User::find_by_id(claims.user_id).await?; + + let (results, count) = match user.is_staff.unwrap() { + true => ( + Warning::filter( + page, + WarningFilter { + model_id: payload.model_id, + resolved_by: payload.resolved_by, + user_id: None, + }, + ) + .await?, + Warning::count_by_model_id(WarningFilter { + model_id: payload.model_id, + resolved_by: payload.resolved_by, + user_id: None, + }) + .await?, + ), + false => ( + Warning::filter( + page, + WarningFilter { + model_id: payload.model_id, + resolved_by: payload.resolved_by, + user_id: Some(user.id), + }, + ) + .await?, + Warning::count_by_model_id(WarningFilter { + model_id: payload.model_id, + resolved_by: payload.resolved_by, + user_id: Some(user.id), + }) + .await?, + ), + }; + + Ok(Json(WarningPagination { count, results })) +} |
