diff --git a/.sqlx/query-12a6fa98cf544f2aad6c2fcf6f2b895ddf17ef53fd6dfd8d7373ad8e4cdfcb1e.json b/.sqlx/query-12a6fa98cf544f2aad6c2fcf6f2b895ddf17ef53fd6dfd8d7373ad8e4cdfcb1e.json new file mode 100644 index 0000000..6bd9b57 --- /dev/null +++ b/.sqlx/query-12a6fa98cf544f2aad6c2fcf6f2b895ddf17ef53fd6dfd8d7373ad8e4cdfcb1e.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "\n CREATE TABLE IF NOT EXISTS Projects (\n id SERIAL8 NOT NULL,\n name VARCHAR(32) NOT NULL,\n description TEXT NOT NULL,\n created TIMESTAMP NOT NULL,\n members BIGINT[] NOT NULL,\n PRIMARY KEY(id)\n );\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "12a6fa98cf544f2aad6c2fcf6f2b895ddf17ef53fd6dfd8d7373ad8e4cdfcb1e" +} diff --git a/.sqlx/query-2b90457c4ec97c1559cab9dba5f2547e27a2778debfc158106e365678a0f2141.json b/.sqlx/query-2b90457c4ec97c1559cab9dba5f2547e27a2778debfc158106e365678a0f2141.json new file mode 100644 index 0000000..34ef536 --- /dev/null +++ b/.sqlx/query-2b90457c4ec97c1559cab9dba5f2547e27a2778debfc158106e365678a0f2141.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT * FROM Projects WHERE name = $1;", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "created", + "type_info": "Timestamp" + }, + { + "ordinal": 4, + "name": "members", + "type_info": "Int8Array" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "2b90457c4ec97c1559cab9dba5f2547e27a2778debfc158106e365678a0f2141" +} diff --git a/.sqlx/query-5a65288ceaee7044d547a04600d50408249ae7f03356121968abf1039407ec3e.json b/.sqlx/query-5a65288ceaee7044d547a04600d50408249ae7f03356121968abf1039407ec3e.json deleted file mode 100644 index b18b87f..0000000 --- a/.sqlx/query-5a65288ceaee7044d547a04600d50408249ae7f03356121968abf1039407ec3e.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n CREATE TABLE IF NOT EXISTS Projects (\n id SERIAL8 NOT NULL,\n name VARCHAR(32) NOT NULL,\n desription TEXT,\n created TIMESTAMP NOT NULL,\n members BIGINT[][] NOT NULL,\n PRIMARY KEY(id)\n );\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [] - }, - "nullable": [] - }, - "hash": "5a65288ceaee7044d547a04600d50408249ae7f03356121968abf1039407ec3e" -} diff --git a/.sqlx/query-714a62e127cb828940bf21d21ad1fb2a37a8ad7a7458e6d727009a3513f8879c.json b/.sqlx/query-714a62e127cb828940bf21d21ad1fb2a37a8ad7a7458e6d727009a3513f8879c.json new file mode 100644 index 0000000..96b8b49 --- /dev/null +++ b/.sqlx/query-714a62e127cb828940bf21d21ad1fb2a37a8ad7a7458e6d727009a3513f8879c.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO Projects (name, description, created, members) VALUES ($1, $2, $3, $4);", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Text", + "Timestamp", + "Int8Array" + ] + }, + "nullable": [] + }, + "hash": "714a62e127cb828940bf21d21ad1fb2a37a8ad7a7458e6d727009a3513f8879c" +} diff --git a/Cargo.toml b/Cargo.toml index c6bc59c..38d5999 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,4 +21,4 @@ sqlx = { version = "0.7.1", features = ["runtime-tokio", "postgres", "chrono"] } uuid = { version = "1.4.1", features = ["v4"] } chrono = "0.4" mail-send = "0.4.0" -regex = "1.9.3" \ No newline at end of file +regex = "1.9.3" diff --git a/docs/README.md b/docs/README.md index d666bf5..e3f4e36 100644 --- a/docs/README.md +++ b/docs/README.md @@ -56,7 +56,7 @@ Date: Sun, 20 Aug 2023 13:37:35 GMT Server: nginx/1.24.0 Strict-Transport-Security: max-age=31536000; includeSubDomains ``` -This sends an verification token to the email you specified in the request body. +This sends a verification token to the email you specified in the request body. Such a token looks like this: `f68b0ee33bbe4850991993c361997003`. ### 2. Verify the created account diff --git a/docs/project/create.md b/docs/project/create.md index 310518c..a6cb089 100644 --- a/docs/project/create.md +++ b/docs/project/create.md @@ -21,6 +21,8 @@ __Content - JSON:__ | id | The created projects unique id. | ### 400 - Error: Bad Request The request was malformed. +### 401 - Error: Unauthorized +The provided access token doesn't allow you to perfrom this operation. ### 403 - Error: Forbidden Blocked for security reasons. ### 409 - Error: Conflict diff --git a/src/api/mod.rs b/src/api/mod.rs index 1b4880a..429a9e6 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,4 +1,5 @@ mod account; +mod project; use actix_web::{web, App, HttpServer}; use anyhow::Result; @@ -59,11 +60,11 @@ pub async fn start(port: u16, pool: PgPool) -> Result<()> { sqlx::query!( r#" CREATE TABLE IF NOT EXISTS Projects ( - id SERIAL8 NOT NULL, - name VARCHAR(32) NOT NULL, - desription TEXT, - created TIMESTAMP NOT NULL, - members BIGINT[][] NOT NULL, + id SERIAL8 NOT NULL, + name VARCHAR(32) NOT NULL, + description TEXT NOT NULL, + created TIMESTAMP NOT NULL, + members BIGINT[] NOT NULL, PRIMARY KEY(id) ); "# @@ -90,6 +91,7 @@ pub async fn start(port: u16, pool: PgPool) -> Result<()> { .service(account::calls::delete) .service(account::calls::tokens_delete) .service(account::calls::tokens_get) + .service(project::calls::create) .app_data(web::Data::new(ApiState { pool: pool.clone() })) }) .bind(("0.0.0.0", port))? diff --git a/src/api/project/calls.rs b/src/api/project/calls.rs new file mode 100644 index 0000000..5713b1f --- /dev/null +++ b/src/api/project/calls.rs @@ -0,0 +1,21 @@ +use crate::api::ApiState; +use crate::api::project::{data, handlers}; +use actix_web::{get, post, web, HttpResponse, Responder}; +use actix_web_httpauth::extractors::bearer::BearerAuth; +use log::error; + +#[post("/project/create")] +async fn create(data: web::Data, auth: BearerAuth, body: web::Json) -> impl Responder { + match handlers::create(&data.pool, auth.token().to_string(), body.into_inner()).await { + Ok(resp) => match resp { + data::CreateResponse::Success(b) => HttpResponse::Ok().json(web::Json(b)), + data::CreateResponse::Conflict => HttpResponse::Conflict().finish(), + data::CreateResponse::Unauthorized => HttpResponse::Unauthorized().finish(), + data::CreateResponse::Blocked => HttpResponse::Forbidden().finish(), + }, + Err(e) => { + error!("While handling register request: {e}"); + HttpResponse::InternalServerError().finish() + } + } +} diff --git a/src/api/project/data.rs b/src/api/project/data.rs new file mode 100644 index 0000000..fd017af --- /dev/null +++ b/src/api/project/data.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize)] +pub struct CreateRequest { + pub name: String, + pub description: String, +} + +#[derive(Debug, Serialize)] +pub struct CreateSuccess { + pub id: i64, +} + +#[derive(Debug)] +pub enum CreateResponse { + Success(CreateSuccess), + Unauthorized, + Blocked, + Conflict, +} diff --git a/src/api/project/handlers.rs b/src/api/project/handlers.rs new file mode 100644 index 0000000..e93f8bc --- /dev/null +++ b/src/api/project/handlers.rs @@ -0,0 +1,37 @@ +use crate::{ + api::project::data, + projects::Project, + security::{is_sql_injection, AlphaExt}, + tokens::AuthToken, +}; +use anyhow::Result; +use sqlx::PgPool; + +pub async fn create( + pool: &PgPool, + auth: String, + request: data::CreateRequest, +) -> Result { + if !auth.is_alpha() { + return Ok(data::CreateResponse::Blocked); + } + + let token = match AuthToken::check(pool, &auth).await? { + Some(t) => t, + None => return Ok(data::CreateResponse::Unauthorized), + }; + + if is_sql_injection(&request.name) || is_sql_injection(&request.description) { + return Ok(data::CreateResponse::Blocked); + } + + if let Some(_) = Project::from_name(pool, &request.name).await? { + return Ok(data::CreateResponse::Conflict); + } + + let project = Project::new(pool, token.account, request.name, request.description).await?; + + Ok(data::CreateResponse::Success(data::CreateSuccess { + id: project.id, + })) +} diff --git a/src/api/project/mod.rs b/src/api/project/mod.rs new file mode 100644 index 0000000..6ffa288 --- /dev/null +++ b/src/api/project/mod.rs @@ -0,0 +1,3 @@ +pub mod calls; +pub mod data; +mod handlers; diff --git a/src/main.rs b/src/main.rs index 0e70afa..ba300ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod accounts; mod api; +mod projects; mod security; mod tokens; diff --git a/src/projects.rs b/src/projects.rs new file mode 100644 index 0000000..f1ef948 --- /dev/null +++ b/src/projects.rs @@ -0,0 +1,48 @@ +use anyhow::{Error, Result}; +use sqlx::postgres::PgPool; + +#[derive(Debug)] +pub struct Project { + pub id: i64, + pub name: String, + pub description: String, + pub created: chrono::NaiveDateTime, + pub members: Vec, +} + +impl Project { + pub async fn new( + pool: &PgPool, + owner_id: i64, + name: String, + description: String, + ) -> Result { + let members = vec![owner_id]; + + sqlx::query!( + r#"INSERT INTO Projects (name, description, created, members) VALUES ($1, $2, $3, $4);"#, + name, + description, + chrono::Utc::now().naive_utc(), + &members, + ).execute(pool).await?; + + match Project::from_name(pool, &name).await? { + Some(project) => Ok(project), + None => Err(Error::msg( + "The just created project can't be found in the database!", + )), + } + } + + pub async fn from_name(pool: &PgPool, name: &String) -> Result> { + match sqlx::query_as!(Project, r#"SELECT * FROM Projects WHERE name = $1;"#, name) + .fetch_one(pool) + .await + { + Ok(project) => Ok(Some(project)), + Err(sqlx::Error::RowNotFound) => Ok(None), + Err(e) => Err(Error::new(e)), + } + } +}