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}