mirror of
https://github.com/johrpan/wolfgang.git
synced 2025-10-28 03:27:24 +01:00
245 lines
7.4 KiB
Rust
245 lines
7.4 KiB
Rust
use super::CaptchaManager;
|
|
use crate::database;
|
|
use crate::database::{DbConn, DbPool, User, UserInsertion};
|
|
use crate::error::ServerError;
|
|
use actix_web::{get, post, put, web, HttpResponse};
|
|
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
|
use anyhow::{anyhow, Result};
|
|
use serde::{Deserialize, Serialize};
|
|
use sodiumoxide::crypto::pwhash::argon2id13;
|
|
|
|
/// Request body data for user registration.
|
|
#[derive(Deserialize, Debug, Clone)]
|
|
#[serde(rename_all = "camelCase")]
|
|
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.
|
|
#[derive(Deserialize, Debug, Clone)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Login {
|
|
pub username: String,
|
|
pub password: String,
|
|
}
|
|
|
|
/// Request body data for changing user details.
|
|
#[derive(Deserialize, Debug, Clone)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PutUser {
|
|
pub old_password: String,
|
|
pub new_password: Option<String>,
|
|
pub email: Option<String>,
|
|
}
|
|
|
|
/// Response body data for getting a user.
|
|
#[derive(Serialize, Debug, Clone)]
|
|
pub struct GetUser {
|
|
pub username: String,
|
|
pub email: Option<String>,
|
|
}
|
|
|
|
/// Claims for issued JWTs.
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
struct Claims {
|
|
pub iat: u64,
|
|
pub exp: u64,
|
|
pub username: String,
|
|
}
|
|
|
|
/// Register a new user.
|
|
#[post("/users")]
|
|
pub async fn register_user(
|
|
db: web::Data<DbPool>,
|
|
captcha_manager: web::Data<CaptchaManager>,
|
|
data: web::Json<UserRegistration>,
|
|
) -> Result<HttpResponse, ServerError> {
|
|
let captcha_manager = captcha_manager.into_inner();
|
|
|
|
if captcha_manager.check_captcha(&data.captcha_id, &data.answer)? {
|
|
web::block(move || {
|
|
let conn = db.into_inner().get().or(Err(ServerError::Internal))?;
|
|
|
|
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
|
|
/// resent the old password.
|
|
#[put("/users/{username}")]
|
|
pub async fn put_user(
|
|
db: web::Data<DbPool>,
|
|
username: web::Path<String>,
|
|
data: web::Json<PutUser>,
|
|
) -> Result<HttpResponse, ServerError> {
|
|
let conn = db.into_inner().get().or(Err(ServerError::Internal))?;
|
|
|
|
web::block(move || {
|
|
let user = database::get_user(&conn, &username)
|
|
.or(Err(ServerError::Internal))?
|
|
.ok_or(ServerError::Unauthorized)?;
|
|
|
|
if verify_password(&data.old_password, &user.password_hash) {
|
|
let password_hash = match &data.new_password {
|
|
Some(password) => hash_password(password).or(Err(ServerError::Unauthorized))?,
|
|
None => user.password_hash.clone(),
|
|
};
|
|
|
|
database::update_user(
|
|
&conn,
|
|
&username,
|
|
&UserInsertion {
|
|
email: data.email.clone(),
|
|
password_hash,
|
|
},
|
|
)
|
|
.or(Err(ServerError::Internal))?;
|
|
|
|
Ok(())
|
|
} else {
|
|
Err(ServerError::Forbidden)
|
|
}
|
|
})
|
|
.await?;
|
|
|
|
Ok(HttpResponse::Ok().finish())
|
|
}
|
|
|
|
/// Get an existing user. This requires a valid JWT authenticating that user.
|
|
#[get("/users/{username}")]
|
|
pub async fn get_user(
|
|
db: web::Data<DbPool>,
|
|
username: web::Path<String>,
|
|
auth: BearerAuth,
|
|
) -> Result<HttpResponse, ServerError> {
|
|
let user = web::block(move || {
|
|
let conn = db.into_inner().get().or(Err(ServerError::Internal))?;
|
|
authenticate(&conn, auth.token()).or(Err(ServerError::Unauthorized))
|
|
})
|
|
.await?;
|
|
|
|
if username.into_inner() != user.username {
|
|
Err(ServerError::Forbidden)?;
|
|
}
|
|
|
|
Ok(HttpResponse::Ok().json(GetUser {
|
|
username: user.username,
|
|
email: user.email,
|
|
}))
|
|
}
|
|
|
|
/// Login an already existing user. This will respond with a newly issued JWT.
|
|
#[post("/login")]
|
|
pub async fn login_user(
|
|
db: web::Data<DbPool>,
|
|
data: web::Json<Login>,
|
|
) -> Result<HttpResponse, ServerError> {
|
|
let token = web::block(move || {
|
|
let conn = db.into_inner().get().or(Err(ServerError::Internal))?;
|
|
|
|
let user = database::get_user(&conn, &data.username)
|
|
.or(Err(ServerError::Internal))?
|
|
.ok_or(ServerError::Unauthorized)?;
|
|
|
|
if verify_password(&data.password, &user.password_hash) {
|
|
issue_jwt(&user.username).or(Err(ServerError::Internal))
|
|
} else {
|
|
Err(ServerError::Unauthorized)
|
|
}
|
|
})
|
|
.await?;
|
|
|
|
Ok(HttpResponse::Ok().body(token))
|
|
}
|
|
|
|
/// Authenticate a user by verifying the provided token. The environemtn variable "WOLFGANG_SECRET"
|
|
/// will be used as the secret key and has to be set.
|
|
pub fn authenticate(conn: &DbConn, token: &str) -> Result<User> {
|
|
let username = verify_jwt(token)?.username;
|
|
database::get_user(conn, &username)?.ok_or(anyhow!("User doesn't exist: {}", &username))
|
|
}
|
|
|
|
/// Return a hash for a password that can be stored in the database.
|
|
fn hash_password(password: &str) -> Result<String> {
|
|
let hash = argon2id13::pwhash(
|
|
password.as_bytes(),
|
|
argon2id13::OPSLIMIT_INTERACTIVE,
|
|
argon2id13::MEMLIMIT_INTERACTIVE,
|
|
)
|
|
.or(Err(anyhow!("Failed to hash password!")))?;
|
|
|
|
// Strip trailing null bytes to facilitate database storage.
|
|
Ok(std::str::from_utf8(&hash.0)?
|
|
.trim_end_matches('\u{0}')
|
|
.to_string())
|
|
}
|
|
|
|
/// Verify whether a hash is valid for a password.
|
|
fn verify_password(password: &str, hash: &str) -> bool {
|
|
// Readd the trailing null bytes padding.
|
|
let mut bytes = [0u8; 128];
|
|
for (index, byte) in hash.as_bytes().iter().enumerate() {
|
|
bytes[index] = *byte;
|
|
}
|
|
|
|
argon2id13::pwhash_verify(
|
|
&argon2id13::HashedPassword::from_slice(&bytes).unwrap(),
|
|
password.as_bytes(),
|
|
)
|
|
}
|
|
|
|
/// Issue a JWT that allows to claim to be a user. This uses the value of the environment variable
|
|
/// "WOLFGANG_SECRET" as the secret key. This needs to be set.
|
|
fn issue_jwt(username: &str) -> Result<String> {
|
|
let now = std::time::SystemTime::now();
|
|
let expiry = now + std::time::Duration::new(86400, 0);
|
|
|
|
let iat = now.duration_since(std::time::UNIX_EPOCH)?.as_secs();
|
|
let exp = expiry.duration_since(std::time::UNIX_EPOCH)?.as_secs();
|
|
|
|
let secret = std::env::var("WOLFGANG_SECRET")?;
|
|
|
|
let token = jsonwebtoken::encode(
|
|
&jsonwebtoken::Header::default(),
|
|
&Claims {
|
|
iat,
|
|
exp,
|
|
username: username.to_string(),
|
|
},
|
|
&jsonwebtoken::EncodingKey::from_secret(&secret.as_bytes()),
|
|
)?;
|
|
|
|
Ok(token)
|
|
}
|
|
|
|
/// Verify a JWT and return the claims that are made by it. This uses the value of the environment
|
|
/// variable "WOLFGANG_SECRET" as the secret key. This needs to be set.
|
|
fn verify_jwt(token: &str) -> Result<Claims> {
|
|
let secret = std::env::var("WOLFGANG_SECRET")?;
|
|
|
|
let jwt = jsonwebtoken::decode::<Claims>(
|
|
token,
|
|
&jsonwebtoken::DecodingKey::from_secret(&secret.as_bytes()),
|
|
&jsonwebtoken::Validation::default(),
|
|
)?;
|
|
|
|
Ok(jwt.claims)
|
|
}
|