Skip to content

Commit

Permalink
Merge pull request #7 from MiSArch/additional-order-fields
Browse files Browse the repository at this point in the history
Additional order fields
  • Loading branch information
legendofa authored May 12, 2024
2 parents 9b15963 + 98f47b2 commit 6765937
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 124 deletions.
83 changes: 0 additions & 83 deletions src/authentication.rs

This file was deleted.

95 changes: 95 additions & 0 deletions src/authorization.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use async_graphql::{Context, Error, Result};
use axum::http::HeaderMap;
use bson::Uuid;
use serde::{Deserialize, Serialize};

/// `Authorized-User` HTTP header.
#[derive(Deserialize, Debug, Serialize)]
pub struct AuthorizedUserHeader {
id: Uuid,
roles: Vec<Role>,
}

/// Extraction of `Authorized-User` header from header map.
impl TryFrom<&HeaderMap> for AuthorizedUserHeader {
type Error = Error;

/// Tries to extract the `Authorized-User` header from a header map.
///
/// Returns a GraphQL error if the extraction fails.
fn try_from(header_map: &HeaderMap) -> Result<Self, Self::Error> {
if let Some(authorized_user_header_value) = header_map.get("Authorized-User") {
if let Ok(authorized_user_header_str) = authorized_user_header_value.to_str() {
let authorized_user_header: AuthorizedUserHeader =
serde_json::from_str(authorized_user_header_str)?;
return Ok(authorized_user_header);
}
}
Err(Error::new(
"Authorization failed. Authorized-User header is not set or could not be parsed.",
))
}
}

/// Role of user.
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)]
#[serde(rename_all = "snake_case")]
enum Role {
Buyer,
Admin,
Employee,
}

impl Role {
/// Defines if user has a permissive role.
fn is_permissive(self) -> bool {
match self {
Self::Buyer => false,
Self::Admin => true,
Self::Employee => true,
}
}
}

/// Authorize user of UUID for a context.
///
/// * `context` - GraphQL context containing the `Authorized-User` header.
/// * `id` - Option of UUID of the user to authorize.
pub fn authorize_user(ctx: &Context, id: Option<Uuid>) -> Result<()> {
match ctx.data::<AuthorizedUserHeader>() {
Ok(authorized_user_header) => check_permissions(&authorized_user_header, id),
Err(_) => Err(Error::new(
"Authentication failed. Authorized-User header is not set or could not be parsed.",
)),
}
}

/// Check if user of UUID has a valid permission according to the `Authorized-User` header.
///
/// Permission is valid if the user has `Role::Buyer` and the same UUID as provided in the function parameter.
/// Permission is valid if the user has a permissive role: `user.is_permissive() == true`, regardless of the users UUID.
///
/// * `authorized_user_header` - `Authorized-User` header containing the users UUID and role.
/// * `id` - Option of UUID of the user to authorize.
pub fn check_permissions(
authorized_user_header: &AuthorizedUserHeader,
id: Option<Uuid>,
) -> Result<()> {
let id_contained_in_header = id
.and_then(|id| Some(authorized_user_header.id == id))
.unwrap_or(false);
if authorized_user_header
.roles
.iter()
.any(|role| role.is_permissive())
|| id_contained_in_header
{
return Ok(());
} else {
let message = format!(
"Authentication failed for user of UUID: `{}`. Operation not permitted.",
authorized_user_header.id
);
return Err(Error::new(message));
}
}
5 changes: 3 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ use http_event_service::{
on_user_address_creation_event, HttpEventServiceState,
};

mod authentication;
use authentication::AuthorizedUserHeader;
mod authorization;
use authorization::AuthorizedUserHeader;

mod order_compensation;

Expand All @@ -58,6 +58,7 @@ mod mutation_input_structs;
mod order_connection;
mod order_datatypes;
mod order_item_connection;
mod payment_authorization;
mod product_variant_version_connection;

/// Builds the GraphiQL frontend.
Expand Down
61 changes: 43 additions & 18 deletions src/mutation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ use std::collections::HashMap;
use std::time::Duration;
use std::time::SystemTime;

use crate::authentication::authenticate_user;
use crate::authentication::AuthorizedUserHeader;
use crate::authorization::authorize_user;
use crate::authorization::AuthorizedUserHeader;
use crate::foreign_types::Coupon;
use crate::foreign_types::Discount;
use crate::foreign_types::ProductVariant;
Expand All @@ -28,9 +28,11 @@ use crate::foreign_types::TaxRateVersion;
use crate::foreign_types::UserAddress;
use crate::mutation_input_structs::CreateOrderInput;
use crate::mutation_input_structs::OrderItemInput;
use crate::mutation_input_structs::PlaceOrderInput;
use crate::order::OrderDTO;
use crate::order::OrderStatus;
use crate::order_item::OrderItem;
use crate::payment_authorization::PaymentAuthorization;
use crate::query::query_object;
use crate::query::query_objects;
use crate::user::User;
Expand All @@ -51,7 +53,7 @@ impl Mutation {
ctx: &Context<'a>,
#[graphql(desc = "CreateOrderInput")] input: CreateOrderInput,
) -> Result<Order> {
authenticate_user(&ctx, input.user_id)?;
authorize_user(&ctx, Some(input.user_id))?;
let db_client = ctx.data::<Database>()?;
let collection: Collection<Order> = db_client.collection::<Order>("orders");
validate_order_input(db_client, &input).await?;
Expand All @@ -74,33 +76,57 @@ impl Mutation {
invoice_address,
compensatable_order_amount,
payment_information_id: input.payment_information_id,
vat_number: input.vat_number,
};
match collection.insert_one(order, None).await {
Ok(result) => {
let id = uuid_from_bson(result.inserted_id)?;
query_order(&collection, id).await
}
Err(_) => Err(Error::new("Adding order failed in MongoDB.")),
}
insert_order_in_mongodb(&collection, order).await
}

/// Places an existing order by changing its status to `OrderStatus::Placed`.
///
/// Adds optional payment authorization input to order DTO when placing order.
async fn place_order<'a>(
&self,
ctx: &Context<'a>,
#[graphql(desc = "Uuid of order to place")] id: Uuid,
#[graphql(desc = "PlaceOrderInput")] input: PlaceOrderInput,
) -> Result<Order> {
let db_client = ctx.data::<Database>()?;
let collection: Collection<Order> = db_client.collection::<Order>("orders");
let mut order = query_order(&collection, id).await?;
authenticate_user(&ctx, order.user._id)?;
set_status_placed(&collection, id).await?;
order = query_order(&collection, id).await?;
send_order_created_event(order.clone()).await?;
let mut order = query_order(&collection, input.id).await?;
authorize_user(&ctx, Some(order.user._id))?;
let payment_authorization = build_payment_authorization(&input);
set_status_placed(&collection, input.id).await?;
order = query_order(&collection, input.id).await?;
let order_dto = OrderDTO::try_from((order.clone(), payment_authorization))?;
send_order_created_event(order_dto).await?;
Ok(order)
}
}

/// Builds payment authorization from place order input.
///
/// `input` - The place order input to build the payment authorization from.
fn build_payment_authorization(input: &PlaceOrderInput) -> Option<PaymentAuthorization> {
input
.payment_authorization
.clone()
.and_then(|definitely_payment_authorization| {
Option::<PaymentAuthorization>::from(definitely_payment_authorization)
})
}

/// Inserts order in MongoDB and returns the order itself.
///
/// * `collection` - MongoDB collection to insert order in.
/// * `order` - Order to insert.
async fn insert_order_in_mongodb(collection: &Collection<Order>, order: Order) -> Result<Order> {
match collection.insert_one(order, None).await {
Ok(result) => {
let id = uuid_from_bson(result.inserted_id)?;
query_order(&collection, id).await
}
Err(_) => Err(Error::new("Adding order failed in MongoDB.")),
}
}
/// Calculates the total compensatable amount of all order items in the input by summing up their `compensatable_amount` attributes.
fn calculate_compensatable_order_amount(order_items: &Vec<OrderItem>) -> u64 {
order_items.iter().map(|o| o.compensatable_amount).sum()
Expand Down Expand Up @@ -927,9 +953,8 @@ fn build_calculate_shipment_fees_input(
}

/// Sends an `order/order/created` created event containing the order context.
async fn send_order_created_event(order: Order) -> Result<()> {
async fn send_order_created_event(order_dto: OrderDTO) -> Result<()> {
let client = reqwest::Client::new();
let order_dto = OrderDTO::try_from(order)?;
client
.post("http://localhost:3500/v1.0/publish/pubsub/order/order/created")
.json(&order_dto)
Expand Down
22 changes: 19 additions & 3 deletions src/mutation_input_structs.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use async_graphql::{InputObject, SimpleObject};
use async_graphql::InputObject;
use bson::Uuid;
use std::{
cmp::Ordering,
collections::{BTreeSet, HashSet},
};

#[derive(SimpleObject, InputObject)]
#[derive(Debug, InputObject)]
pub struct CreateOrderInput {
/// UUID of user owning the order.
pub user_id: Uuid,
Expand All @@ -17,9 +17,11 @@ pub struct CreateOrderInput {
pub invoice_address_id: Uuid,
/// UUID of payment information that the order should be processed with.
pub payment_information_id: Uuid,
/// VAT number.
pub vat_number: String,
}

#[derive(SimpleObject, InputObject, PartialEq, Eq, Clone)]
#[derive(Debug, InputObject, PartialEq, Eq, Clone)]
pub struct OrderItemInput {
/// UUID of shopping cart item associated with order item.
pub shopping_cart_item_id: Uuid,
Expand All @@ -29,6 +31,12 @@ pub struct OrderItemInput {
pub coupon_ids: HashSet<Uuid>,
}

#[derive(Debug, InputObject, Clone)]
pub struct PaymentAuthorizationInput {
/// CVC/CVV number of 3-4 digits.
pub cvc: Option<u16>,
}

impl PartialOrd for OrderItemInput {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.shopping_cart_item_id
Expand All @@ -41,3 +49,11 @@ impl Ord for OrderItemInput {
self.shopping_cart_item_id.cmp(&other.shopping_cart_item_id)
}
}

#[derive(Debug, InputObject)]
pub struct PlaceOrderInput {
/// UUID of order to place.
pub id: Uuid,
/// Optional payment authorization data.
pub payment_authorization: Option<PaymentAuthorizationInput>,
}
Loading

0 comments on commit 6765937

Please sign in to comment.