#
mcaptcha/api/v1/mcaptcha/
update.rs

    1// Copyright (C) 2022  Aravinth Manivannan <realaravinth@batsense.net>
    2// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
    3//
    4// SPDX-License-Identifier: AGPL-3.0-or-later
    5
    6use actix_identity::Identity;
    7use actix_web::{web, HttpResponse, Responder};
    8use libmcaptcha::defense::Level;
    9use libmcaptcha::master::messages::RenameBuilder;
    10use serde::{Deserialize, Serialize};
    11
    12use db_core::errors::DBError;
    13use db_core::CreateCaptcha;
    14
    15use super::create::MCaptchaDetails;
    16use super::get_random;
    17use crate::errors::*;
    18use crate::AppData;
    19
    20#[my_codegen::post(
    21    path = "crate::V1_API_ROUTES.captcha.update_key",
    22    wrap = "crate::api::v1::get_middleware()"
    23)]
    24pub async fn update_key(
    25    payload: web::Json<MCaptchaDetails>,
    26    data: AppData,
    27    id: Identity,
    28) -> ServiceResult<impl Responder> {
    29    let username = id.identity().unwrap();
    30    let mut key;
    31
    32    loop {
    33        key = get_random(32);
    34
    35        match data
    36            .db
    37            .update_captcha_key(&username, &payload.key, &key)
    38            .await
    39        {
    40            Ok(_) => break,
    41            Err(DBError::SecretTaken) => continue,
    42            Err(e) => return Err(e.into()),
    43        }
    44    }
    45
    46    let payload = payload.into_inner();
    47    let rename = RenameBuilder::default()
    48        .name(payload.key)
    49        .rename_to(key.clone())
    50        .build()
    51        .unwrap();
    52    data.captcha.rename(rename).await?;
    53
    54    let resp = MCaptchaDetails {
    55        key,
    56        name: payload.name,
    57    };
    58
    59    Ok(HttpResponse::Ok().json(resp))
    60}
    61
    62#[derive(Serialize, Deserialize)]
    63pub struct UpdateCaptcha {
    64    pub levels: Vec<Level>,
    65    pub duration: u32,
    66    pub description: String,
    67    pub key: String,
    68    pub publish_benchmarks: bool,
    69}
    70
    71#[my_codegen::post(
    72    path = "crate::V1_API_ROUTES.captcha.update",
    73    wrap = "crate::api::v1::get_middleware()"
    74)]
    75pub async fn update_captcha(
    76    payload: web::Json<UpdateCaptcha>,
    77    data: AppData,
    78    id: Identity,
    79) -> ServiceResult<impl Responder> {
    80    let username = id.identity().unwrap();
    81    runner::update_captcha(&payload, &data, &username).await?;
    82    Ok(HttpResponse::Ok())
    83}
    84
    85pub mod runner {
    86    use libmcaptcha::{master::messages::RemoveCaptcha, DefenseBuilder};
    87
    88    use super::*;
    89
    90    pub async fn update_captcha(
    91        payload: &UpdateCaptcha,
    92        data: &AppData,
    93        username: &str,
    94    ) -> ServiceResult<()> {
    95        let mut defense = DefenseBuilder::default();
    96
    97        for level in payload.levels.iter() {
    98            defense.add_level(*level)?;
    99        }
    100
    101        // I feel this is necessary as both difficulty factor _and_ visitor threshold of a
    102        // level could change so doing this would not require us to send level_id to client
    103        // still, needs to be benchmarked
    104        defense.build()?;
    105
    106        data.db
    107            .delete_captcha_levels(username, &payload.key)
    108            .await?;
    109
    110        let m = CreateCaptcha {
    111            key: &payload.key,
    112            duration: payload.duration as i32,
    113            description: &payload.description,
    114        };
    115
    116        data.db.update_captcha_metadata(username, &m).await?;
    117
    118        data.db
    119            .add_captcha_levels(username, &payload.key, &payload.levels)
    120            .await?;
    121        if let Err(ServiceError::CaptchaError(e)) = data
    122            .captcha
    123            .remove(RemoveCaptcha(payload.key.clone()))
    124            .await
    125        {
    126            log::error!(
    127                "Deleting captcha key {} while updating it, error: {:?}",
    128                &payload.key,
    129                e
    130            );
    131        }
    132
    133        if payload.publish_benchmarks {
    134            data.db
    135                .analytics_create_psuedo_id_if_not_exists(&payload.key)
    136                .await?;
    137        } else {
    138            data.db
    139                .analytics_delete_all_records_for_campaign(&payload.key)
    140                .await?;
    141        }
    142        Ok(())
    143    }
    144}
    145
    146#[cfg(test)]
    147mod tests {
    148    use actix_web::http::StatusCode;
    149    use actix_web::test;
    150
    151    use crate::api::v1::mcaptcha::create::MCaptchaDetails;
    152    use crate::api::v1::mcaptcha::stats::StatsPayload;
    153    use crate::api::v1::ROUTES;
    154    use crate::tests::*;
    155    use crate::*;
    156
    157    #[actix_rt::test]
    158    async fn update_and_get_mcaptcha_works_pg() {
    159        let data = crate::tests::pg::get_data().await;
    160        update_and_get_mcaptcha_works(data).await;
    161    }
    162
    163    #[actix_rt::test]
    164    async fn update_and_get_mcaptcha_works_maria() {
    165        let data = crate::tests::maria::get_data().await;
    166        update_and_get_mcaptcha_works(data).await;
    167    }
    168
    169    async fn update_and_get_mcaptcha_works(data: ArcData) {
    170        const NAME: &str = "updateusermcaptcha";
    171        const PASSWORD: &str = "longpassworddomain";
    172        const EMAIL: &str = "testupdateusermcaptcha@a.com";
    173        let data = &data;
    174        delete_user(data, NAME).await;
    175
    176        // 1. add mcaptcha token
    177        register_and_signin(data, NAME, EMAIL, PASSWORD).await;
    178        let (_, signin_resp, token_key) = add_levels_util(data, NAME, PASSWORD).await;
    179        let cookies = get_cookie!(signin_resp);
    180        let app = get_app!(data).await;
    181
    182        // 2. update token key
    183        let update_token_resp = test::call_service(
    184            &app,
    185            post_request!(&token_key, ROUTES.captcha.update_key)
    186                .cookie(cookies.clone())
    187                .to_request(),
    188        )
    189        .await;
    190        assert_eq!(update_token_resp.status(), StatusCode::OK);
    191        let updated_token: MCaptchaDetails =
    192            test::read_body_json(update_token_resp).await;
    193
    194        // get levels with updated key
    195        let get_token_resp = test::call_service(
    196            &app,
    197            post_request!(&updated_token, ROUTES.captcha.get)
    198                .cookie(cookies.clone())
    199                .to_request(),
    200        )
    201        .await;
    202        // if updated key doesn't exist in database, a non 200 result will bereturned
    203        assert_eq!(get_token_resp.status(), StatusCode::OK);
    204
    205        // get stats
    206        let paylod = StatsPayload { key: token_key.key };
    207        let get_statis_resp = test::call_service(
    208            &app,
    209            post_request!(&paylod, ROUTES.captcha.stats.get)
    210                .cookie(cookies.clone())
    211                .to_request(),
    212        )
    213        .await;
    214        // if updated key doesn't exist in database, a non 200 result will bereturned
    215        assert_eq!(get_statis_resp.status(), StatusCode::OK);
    216    }
    217}