mirror of
https://github.com/johrpan/wolfgang.git
synced 2025-10-28 03:27:24 +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"
|
dotenv = "0.15.0"
|
||||||
env_logger = "0.8.1"
|
env_logger = "0.8.1"
|
||||||
jsonwebtoken = "7.2.0"
|
jsonwebtoken = "7.2.0"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
r2d2 = "0.8.9"
|
r2d2 = "0.8.9"
|
||||||
rand = "0.7.3"
|
rand = "0.7.3"
|
||||||
serde = { version = "1.0.117", features = ["derive"] }
|
serde = { version = "1.0.117", features = ["derive"] }
|
||||||
serde_json = "1.0.59"
|
serde_json = "1.0.59"
|
||||||
sodiumoxide = "0.2.6"
|
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]
|
#[macro_use]
|
||||||
extern crate diesel_migrations;
|
extern crate diesel_migrations;
|
||||||
|
|
||||||
use actix_web::{App, HttpServer};
|
use actix_web::{web, App, HttpServer};
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
mod database;
|
mod database;
|
||||||
mod error;
|
mod error;
|
||||||
|
|
@ -15,18 +16,22 @@ mod routes;
|
||||||
use routes::*;
|
use routes::*;
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> Result<()> {
|
||||||
dotenv::dotenv().ok();
|
dotenv::dotenv().ok();
|
||||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||||
sodiumoxide::init().expect("Failed to init crypto library!");
|
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 || {
|
let server = HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
.data(db_pool.clone())
|
.app_data(db_pool.clone())
|
||||||
|
.app_data(captcha_manager.clone())
|
||||||
.wrap(actix_web::middleware::Logger::new(
|
.wrap(actix_web::middleware::Logger::new(
|
||||||
"%t: %r -> %s; %b B; %D ms",
|
"%t: %r -> %s; %b B; %D ms",
|
||||||
))
|
))
|
||||||
|
.service(get_captcha)
|
||||||
.service(register_user)
|
.service(register_user)
|
||||||
.service(login_user)
|
.service(login_user)
|
||||||
.service(put_user)
|
.service(put_user)
|
||||||
|
|
@ -58,5 +63,7 @@ async fn main() -> std::io::Result<()> {
|
||||||
.service(delete_medium)
|
.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;
|
||||||
use crate::database::{DbConn, DbPool, User, UserInsertion};
|
use crate::database::{DbConn, DbPool, User, UserInsertion};
|
||||||
use crate::error::ServerError;
|
use crate::error::ServerError;
|
||||||
|
|
@ -14,6 +15,8 @@ pub struct UserRegistration {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
|
pub captcha_id: String,
|
||||||
|
pub answer: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request body data for user login.
|
/// Request body data for user login.
|
||||||
|
|
@ -52,24 +55,31 @@ struct Claims {
|
||||||
#[post("/users")]
|
#[post("/users")]
|
||||||
pub async fn register_user(
|
pub async fn register_user(
|
||||||
db: web::Data<DbPool>,
|
db: web::Data<DbPool>,
|
||||||
|
captcha_manager: web::Data<CaptchaManager>,
|
||||||
data: web::Json<UserRegistration>,
|
data: web::Json<UserRegistration>,
|
||||||
) -> Result<HttpResponse, ServerError> {
|
) -> Result<HttpResponse, ServerError> {
|
||||||
web::block(move || {
|
let captcha_manager = captcha_manager.into_inner();
|
||||||
let conn = db.into_inner().get().or(Err(ServerError::Internal))?;
|
|
||||||
|
|
||||||
database::insert_user(
|
if captcha_manager.check_captcha(&data.captcha_id, &data.answer)? {
|
||||||
&conn,
|
web::block(move || {
|
||||||
&data.username,
|
let conn = db.into_inner().get().or(Err(ServerError::Internal))?;
|
||||||
&UserInsertion {
|
|
||||||
password_hash: hash_password(&data.password).or(Err(ServerError::Internal))?,
|
|
||||||
email: data.email.clone(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.or(Err(ServerError::Internal))
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
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
|
/// 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 mod auth;
|
||||||
pub use auth::*;
|
pub use auth::*;
|
||||||
|
|
||||||
|
pub mod captcha;
|
||||||
|
pub use captcha::*;
|
||||||
|
|
||||||
pub mod ensembles;
|
pub mod ensembles;
|
||||||
pub use ensembles::*;
|
pub use ensembles::*;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue