feat(api): implemented /project/create

This commit is contained in:
antifallobst 2023-08-23 18:16:36 +02:00
parent 45cdf93536
commit 83a2dabf87
Signed by: antifallobst
GPG Key ID: 2B4F402172791BAF
14 changed files with 216 additions and 19 deletions

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -56,7 +56,7 @@ Date: Sun, 20 Aug 2023 13:37:35 GMT
Server: nginx/1.24.0 Server: nginx/1.24.0
Strict-Transport-Security: max-age=31536000; includeSubDomains 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`. Such a token looks like this: `f68b0ee33bbe4850991993c361997003`.
### 2. Verify the created account ### 2. Verify the created account

View File

@ -21,6 +21,8 @@ __Content - JSON:__
| id | The created projects unique id. | | id | The created projects unique id. |
### 400 - Error: Bad Request ### 400 - Error: Bad Request
The request was malformed. The request was malformed.
### 401 - Error: Unauthorized
The provided access token doesn't allow you to perfrom this operation.
### 403 - Error: Forbidden ### 403 - Error: Forbidden
Blocked for security reasons. Blocked for security reasons.
### 409 - Error: Conflict ### 409 - Error: Conflict

View File

@ -1,4 +1,5 @@
mod account; mod account;
mod project;
use actix_web::{web, App, HttpServer}; use actix_web::{web, App, HttpServer};
use anyhow::Result; use anyhow::Result;
@ -61,9 +62,9 @@ pub async fn start(port: u16, pool: PgPool) -> Result<()> {
CREATE TABLE IF NOT EXISTS Projects ( CREATE TABLE IF NOT EXISTS Projects (
id SERIAL8 NOT NULL, id SERIAL8 NOT NULL,
name VARCHAR(32) NOT NULL, name VARCHAR(32) NOT NULL,
desription TEXT, description TEXT NOT NULL,
created TIMESTAMP NOT NULL, created TIMESTAMP NOT NULL,
members BIGINT[][] NOT NULL, members BIGINT[] NOT NULL,
PRIMARY KEY(id) PRIMARY KEY(id)
); );
"# "#
@ -90,6 +91,7 @@ pub async fn start(port: u16, pool: PgPool) -> Result<()> {
.service(account::calls::delete) .service(account::calls::delete)
.service(account::calls::tokens_delete) .service(account::calls::tokens_delete)
.service(account::calls::tokens_get) .service(account::calls::tokens_get)
.service(project::calls::create)
.app_data(web::Data::new(ApiState { pool: pool.clone() })) .app_data(web::Data::new(ApiState { pool: pool.clone() }))
}) })
.bind(("0.0.0.0", port))? .bind(("0.0.0.0", port))?

21
src/api/project/calls.rs Normal file
View File

@ -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<ApiState>, auth: BearerAuth, body: web::Json<data::CreateRequest>) -> 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()
}
}
}

20
src/api/project/data.rs Normal file
View File

@ -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,
}

View File

@ -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<data::CreateResponse> {
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,
}))
}

3
src/api/project/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod calls;
pub mod data;
mod handlers;

View File

@ -1,5 +1,6 @@
mod accounts; mod accounts;
mod api; mod api;
mod projects;
mod security; mod security;
mod tokens; mod tokens;

48
src/projects.rs Normal file
View File

@ -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<i64>,
}
impl Project {
pub async fn new(
pool: &PgPool,
owner_id: i64,
name: String,
description: String,
) -> Result<Self> {
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<Option<Self>> {
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)),
}
}
}