From cc7014b04bc4714cb2de7af39e62ff9762827489 Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Sat, 9 Apr 2022 10:04:12 +0200 Subject: [PATCH] add validation to user registration --- planetwars-server/Cargo.toml | 1 + planetwars-server/src/db/users.rs | 8 ++- planetwars-server/src/routes/users.rs | 85 ++++++++++++++++++++++++++- 3 files changed, 89 insertions(+), 5 deletions(-) diff --git a/planetwars-server/Cargo.toml b/planetwars-server/Cargo.toml index be34a5f..4c6ddfc 100644 --- a/planetwars-server/Cargo.toml +++ b/planetwars-server/Cargo.toml @@ -25,6 +25,7 @@ zip = "0.5" toml = "0.5" planetwars-matchrunner = { path = "../planetwars-matchrunner" } config = { version = "0.12", features = ["toml"] } +thiserror = "1.0.31" # TODO: remove me shlex = "1.1" diff --git a/planetwars-server/src/db/users.rs b/planetwars-server/src/db/users.rs index 3c071de..3a74c53 100644 --- a/planetwars-server/src/db/users.rs +++ b/planetwars-server/src/db/users.rs @@ -57,10 +57,14 @@ pub fn create_user(credentials: &Credentials, conn: &PgConnection) -> QueryResul .get_result::(conn) } -pub fn authenticate_user(credentials: &Credentials, db_conn: &PgConnection) -> Option { +pub fn find_user(username: &str, db_conn: &PgConnection) -> QueryResult { users::table - .filter(users::username.eq(&credentials.username)) + .filter(users::username.eq(username)) .first::(db_conn) +} + +pub fn authenticate_user(credentials: &Credentials, db_conn: &PgConnection) -> Option { + find_user(credentials.username, db_conn) .optional() .unwrap() .and_then(|user| { diff --git a/planetwars-server/src/routes/users.rs b/planetwars-server/src/routes/users.rs index 967710e..54ddd09 100644 --- a/planetwars-server/src/routes/users.rs +++ b/planetwars-server/src/routes/users.rs @@ -8,6 +8,8 @@ use axum::http::StatusCode; use axum::response::{Headers, IntoResponse, Response}; use axum::{async_trait, Json}; use serde::{Deserialize, Serialize}; +use serde_json::json; +use thiserror::Error; type AuthorizationHeader = TypedHeader>; @@ -53,16 +55,93 @@ pub struct RegistrationParams { pub password: String, } +#[derive(Debug, Error)] +pub enum RegistrationError { + #[error("database error")] + DatabaseError(#[from] diesel::result::Error), + #[error("validation failed")] + ValidationFailed(Vec), +} + +impl RegistrationParams { + fn validate(&self, conn: &DatabaseConnection) -> Result<(), RegistrationError> { + let mut errors = Vec::new(); + + // TODO: do we want to support cased usernames? + // this could be done by allowing casing in names, but requiring case-insensitive uniqueness + if !self + .username + .chars() + .all(|c| c.is_ascii_alphanumeric() && !c.is_uppercase()) + { + errors.push("username must be alphanumeric and lowercase".to_string()); + } + + if self.username.len() < 3 { + errors.push("username must be at least 3 characters".to_string()); + } + + if self.username.len() > 32 { + errors.push("username must be at most 32 characters".to_string()); + } + + if self.password.len() < 8 { + errors.push("password must be at least 8 characters".to_string()); + } + + if users::find_user(&self.username, &conn).is_ok() { + errors.push("username is already taken".to_string()); + } + + if errors.is_empty() { + Ok(()) + } else { + Err(RegistrationError::ValidationFailed(errors)) + } + } +} + +impl IntoResponse for RegistrationError { + fn into_response(self) -> Response { + let (status, json_body) = match self { + RegistrationError::DatabaseError(_e) => { + // TODO: create an error response struct + ( + StatusCode::INTERNAL_SERVER_ERROR, + json!({ + "error": { + "type": "internal_server_error", + } + }), + ) + } + RegistrationError::ValidationFailed(errors) => ( + StatusCode::UNPROCESSABLE_ENTITY, + json!({ + "error": { + "type": "validation_failed", + "validation_errors": errors, + } + }), + ), + }; + + (status, Json(json_body)).into_response() + } +} + pub async fn register( conn: DatabaseConnection, params: Json, -) -> Json { +) -> Result, RegistrationError> { + params.validate(&conn)?; + let credentials = Credentials { username: ¶ms.username, password: ¶ms.password, }; - let user = users::create_user(&credentials, &conn).unwrap(); - Json(user.into()) + let user = users::create_user(&credentials, &conn)?; + Ok(Json(user.into())) } #[derive(Deserialize)]