mirror of
https://github.com/johrpan/wolfgang.git
synced 2025-10-26 02:37:25 +01:00
Add framework for captchas
This commit is contained in:
parent
2066b9c423
commit
59acc460f8
5 changed files with 174 additions and 19 deletions
|
|
@ -13,8 +13,10 @@ diesel_migrations = "1.4.0"
|
|||
dotenv = "0.15.0"
|
||||
env_logger = "0.8.1"
|
||||
jsonwebtoken = "7.2.0"
|
||||
lazy_static = "1.4.0"
|
||||
r2d2 = "0.8.9"
|
||||
rand = "0.7.3"
|
||||
serde = { version = "1.0.117", features = ["derive"] }
|
||||
serde_json = "1.0.59"
|
||||
sodiumoxide = "0.2.6"
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
|
|
|
|||
17
src/main.rs
17
src/main.rs
|
|
@ -6,7 +6,8 @@ extern crate diesel;
|
|||
#[macro_use]
|
||||
extern crate diesel_migrations;
|
||||
|
||||
use actix_web::{App, HttpServer};
|
||||
use actix_web::{web, App, HttpServer};
|
||||
use anyhow::Result;
|
||||
|
||||
mod database;
|
||||
mod error;
|
||||
|
|
@ -15,18 +16,22 @@ mod routes;
|
|||
use routes::*;
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
async fn main() -> Result<()> {
|
||||
dotenv::dotenv().ok();
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||
sodiumoxide::init().expect("Failed to init crypto library!");
|
||||
let db_pool = database::connect().expect("Failed to create database interface!");
|
||||
|
||||
let db_pool = web::Data::new(database::connect()?);
|
||||
let captcha_manager = web::Data::new(CaptchaManager::new());
|
||||
|
||||
let server = HttpServer::new(move || {
|
||||
App::new()
|
||||
.data(db_pool.clone())
|
||||
.app_data(db_pool.clone())
|
||||
.app_data(captcha_manager.clone())
|
||||
.wrap(actix_web::middleware::Logger::new(
|
||||
"%t: %r -> %s; %b B; %D ms",
|
||||
))
|
||||
.service(get_captcha)
|
||||
.service(register_user)
|
||||
.service(login_user)
|
||||
.service(put_user)
|
||||
|
|
@ -58,5 +63,7 @@ async fn main() -> std::io::Result<()> {
|
|||
.service(delete_medium)
|
||||
});
|
||||
|
||||
server.bind("127.0.0.1:8087")?.run().await
|
||||
server.bind("127.0.0.1:8087")?.run().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use super::CaptchaManager;
|
||||
use crate::database;
|
||||
use crate::database::{DbConn, DbPool, User, UserInsertion};
|
||||
use crate::error::ServerError;
|
||||
|
|
@ -14,6 +15,8 @@ pub struct UserRegistration {
|
|||
pub username: String,
|
||||
pub password: String,
|
||||
pub email: Option<String>,
|
||||
pub captcha_id: String,
|
||||
pub answer: String,
|
||||
}
|
||||
|
||||
/// Request body data for user login.
|
||||
|
|
@ -52,24 +55,31 @@ struct Claims {
|
|||
#[post("/users")]
|
||||
pub async fn register_user(
|
||||
db: web::Data<DbPool>,
|
||||
captcha_manager: web::Data<CaptchaManager>,
|
||||
data: web::Json<UserRegistration>,
|
||||
) -> Result<HttpResponse, ServerError> {
|
||||
web::block(move || {
|
||||
let conn = db.into_inner().get().or(Err(ServerError::Internal))?;
|
||||
let captcha_manager = captcha_manager.into_inner();
|
||||
|
||||
database::insert_user(
|
||||
&conn,
|
||||
&data.username,
|
||||
&UserInsertion {
|
||||
password_hash: hash_password(&data.password).or(Err(ServerError::Internal))?,
|
||||
email: data.email.clone(),
|
||||
},
|
||||
)
|
||||
.or(Err(ServerError::Internal))
|
||||
})
|
||||
.await?;
|
||||
if captcha_manager.check_captcha(&data.captcha_id, &data.answer)? {
|
||||
web::block(move || {
|
||||
let conn = db.into_inner().get().or(Err(ServerError::Internal))?;
|
||||
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
database::insert_user(
|
||||
&conn,
|
||||
&data.username,
|
||||
&UserInsertion {
|
||||
password_hash: hash_password(&data.password).or(Err(ServerError::Internal))?,
|
||||
email: data.email.clone(),
|
||||
},
|
||||
)
|
||||
.or(Err(ServerError::Internal))
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
} else {
|
||||
Err(ServerError::Forbidden)
|
||||
}
|
||||
}
|
||||
|
||||
/// Update an existing user. This doesn't use a JWT for authentication but requires the client to
|
||||
|
|
|
|||
133
src/routes/captcha.rs
Normal file
133
src/routes/captcha.rs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
use crate::error::ServerError;
|
||||
use actix_web::{get, web, HttpResponse};
|
||||
use anyhow::{anyhow, Result};
|
||||
use lazy_static::lazy_static;
|
||||
use rand::seq::SliceRandom;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
// TODO/INFO: These hardcoded questions are a placeholder for a future mechanism to autogenerate
|
||||
// questions from the database. This will require a easily accissible web interface for Musicus.
|
||||
// There may also be another, better solution. However, the current framework of question-answer
|
||||
// pairs with randomly generated identifiers will most likely stay in place.
|
||||
|
||||
/// A question to identify users as human.
|
||||
#[derive(Clone, Debug)]
|
||||
struct Question {
|
||||
/// The question that will be sent to the client.
|
||||
pub question: &'static str,
|
||||
|
||||
/// The answer that the client has to provide.
|
||||
pub answer: &'static str,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
/// All available captcha questions.
|
||||
static ref QUESTIONS: Vec<Question> = vec![
|
||||
Question {
|
||||
question: "In welchem Jahr wurde Johannes Brahms geboren?",
|
||||
answer: "1833",
|
||||
},
|
||||
Question {
|
||||
question: "In welchem Jahr ist Johannes Brahms gestorben?",
|
||||
answer: "1897",
|
||||
},
|
||||
Question {
|
||||
question: "In welchem Jahr wurde Ludwig van Beethoven geboren?",
|
||||
answer: "1770",
|
||||
},
|
||||
Question {
|
||||
question: "In welchem Jahr ist Ludwig van Beethoven gestorben?",
|
||||
answer: "1827",
|
||||
},
|
||||
Question {
|
||||
question: "In welchem Jahr wurde Claude Debussy geboren?",
|
||||
answer: "1862",
|
||||
},
|
||||
Question {
|
||||
question: "In welchem Jahr ist Claude Debussy gestorben?",
|
||||
answer: "1918",
|
||||
},
|
||||
Question {
|
||||
question: "In welchem Jahr wurde Sergei Rachmaninow geboren?",
|
||||
answer: "1873",
|
||||
},
|
||||
Question {
|
||||
question: "In welchem Jahr ist Sergei Rachmaninow gestorben?",
|
||||
answer: "1943",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/// Response body data for captcha requests.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Captcha {
|
||||
pub id: String,
|
||||
pub question: String,
|
||||
}
|
||||
|
||||
/// A generator and manager for captchas. This will keep track of the captchas that where created
|
||||
/// for clients and delete them, once the client has tried to solve them.
|
||||
pub struct CaptchaManager {
|
||||
captchas: Mutex<HashMap<String, &'static Question>>,
|
||||
}
|
||||
|
||||
impl CaptchaManager {
|
||||
/// Create a new captcha manager.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
captchas: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new captcha with a random ID.
|
||||
pub fn generate_captcha(&self) -> Result<Captcha> {
|
||||
let mut buffer = uuid::Uuid::encode_buffer();
|
||||
let id = uuid::Uuid::new_v4().to_simple().encode_lower(&mut buffer).to_owned();
|
||||
|
||||
let question = QUESTIONS.choose(&mut rand::thread_rng())
|
||||
.ok_or_else(|| anyhow!("Failed to get random question!"))?;
|
||||
|
||||
let captchas = &mut self.captchas.lock()
|
||||
.or_else(|_| Err(anyhow!("Failed to aquire lock!")))?;
|
||||
|
||||
captchas.insert(id.clone(), question);
|
||||
|
||||
let captcha = Captcha {
|
||||
id,
|
||||
question: question.question.to_owned(),
|
||||
};
|
||||
|
||||
Ok(captcha)
|
||||
}
|
||||
|
||||
/// Check whether the provided answer is correct and delete the captcha eitherway.
|
||||
pub fn check_captcha(&self, id: &str, answer: &str) -> Result<bool> {
|
||||
let captchas = &mut self.captchas.lock()
|
||||
.or_else(|_| Err(anyhow!("Failed to aquire lock!")))?;
|
||||
|
||||
let question = captchas.get(id);
|
||||
|
||||
let result = if let Some(question) = question {
|
||||
let result = answer == question.answer;
|
||||
captchas.remove(id);
|
||||
result
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// Request a new captcha.
|
||||
#[get("/captcha")]
|
||||
pub async fn get_captcha(manager: web::Data<CaptchaManager>) -> Result<HttpResponse, ServerError> {
|
||||
let manager = manager.into_inner();
|
||||
let captcha = manager.generate_captcha()?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(captcha))
|
||||
}
|
||||
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
pub mod auth;
|
||||
pub use auth::*;
|
||||
|
||||
pub mod captcha;
|
||||
pub use captcha::*;
|
||||
|
||||
pub mod ensembles;
|
||||
pub use ensembles::*;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue