summaryrefslogtreecommitdiffstats
path: root/src/warning
diff options
context:
space:
mode:
Diffstat (limited to 'src/warning')
-rw-r--r--src/warning/mod.rs2
-rw-r--r--src/warning/models.rs382
-rw-r--r--src/warning/routes.rs175
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 }))
+}