lacoctelera/routes/recipe/
post.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// Copyright 2024 Felipe Torres González
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use crate::{
    authentication::{check_access, AuthData},
    domain::Recipe,
    routes::recipe::utils::register_new_recipe,
};
use actix_web::{
    post,
    web::{Data, Json, Query},
    HttpResponse,
};
use serde_json::json;
use sqlx::MySqlPool;
use std::error::Error;
use tracing::{debug, info, instrument};

/// POST method for the /recipe endpoint (Restricted)
///
/// # Description
///
/// This method creates new recipes in the DB using the data provided by authors. Recipes are identified by an unique
/// ID that is generated by the backend sw before inserting a recipe into the DB. This means that the same recipe
/// can be pushed several times either by the same author or by another one.
///
/// A previous search in the DB is advised to avoid having many similar recipes. However, every author is free to
/// add a recipe that is quite similar to another one if he/she likes to.
///
/// A new recipe shall populate all the fields marked with a red asterisk in the recipe's schema. Fields that are
/// optional (not marked with the red asterisk) are, most likely, being filled by the backend logic. However, the
/// following optional fields are meant to be populated by the author, but they are not mandatory:
/// - *author_tags*: Tags that can be freely assigned by the author.
/// - *description*: A free text input in which the author can describe in detail the recipe.
/// - *url*: Useful to link the recipe entry to another web resource.
#[utoipa::path(
    post,
    path = "/recipe",
    tag = "Recipe",
    security(
        ("api_key" = [])
    ),
    responses(
        (
            status = 200,
            description = "The Recipe was inserted in the DB.",
            content_type = "application/json",
            example = json!({"id": "0192e8d9-36cf-7ce3-82ef-0a7c9b2deefe"}),
            headers(
                ("Content-Length"),
                ("Content-Type"),
                ("Date"),
                ("Vary", description = "Origin,Access-Control-Request-Method,Access-Control-Request-Headers")
            ),
        ),
        (
            status = 400,
            description = "Missing API key. This endpoint is restricted to public access.",
        ),
        (
            status = 429, description = "**Too many requests.**",
            headers(
                ("Cache-Control", description = "Cache control is set to *no-cache*."),
                ("Access-Control-Allow-Origin"),
                ("Retry-After", description = "Amount of time between requests (seconds).")
            )
        )
    )
)]
#[instrument(skip(pool, token))]
#[post("")]
pub async fn post_recipe(
    req: Json<Recipe>,
    pool: Data<MySqlPool>,
    token: Query<AuthData>,
) -> Result<HttpResponse, Box<dyn Error>> {
    info!("Post new recipe: {:#?}", req.0);

    // Access control
    check_access(&pool, &token.api_key).await?;
    debug!("Access granted");

    let id = register_new_recipe(&pool, &req.0).await?;

    Ok(HttpResponse::Ok().json(json!({"id": id.to_string()})))
}