From 1242b016f2f720ab8ac92060a41a868b5d029a00 Mon Sep 17 00:00:00 2001 From: Aleks Todorov Date: Sat, 10 Feb 2024 19:27:37 +0000 Subject: [PATCH] Implement login endpoint --- backend-rs/src/controllers/errors.rs | 6 +++++ backend-rs/src/controllers/user.rs | 23 +++++++++++++++++++ backend-rs/src/main.rs | 1 + backend-rs/src/models/user.rs | 33 ++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+) diff --git a/backend-rs/src/controllers/errors.rs b/backend-rs/src/controllers/errors.rs index cd2ddbc..9ee125d 100644 --- a/backend-rs/src/controllers/errors.rs +++ b/backend-rs/src/controllers/errors.rs @@ -61,6 +61,8 @@ pub enum HandlerError { UsernameTaken, #[error("Email Address must not be taken.")] EmailTaken, + #[error("Login credentials are invalid.")] + InvalidCredentials, #[error("Authentication token is invalid.")] DecodeJwt(jsonwebtoken::errors::Error), @@ -71,6 +73,8 @@ pub enum HandlerError { Database(#[from] DbErr), #[error("Could not create user.")] CreateUser(#[from] user::CreateError), + #[error("Could not validate credentials.")] + ValidateCredentials(#[from] user::ValidateCredentialsError), #[error("Could not encode JWT.")] EncodeJwt(jsonwebtoken::errors::Error), } @@ -128,12 +132,14 @@ impl IntoResponse for HandlerError { }), HandlerError::UsernameTaken => self.failed_validation(StatusCode::BAD_REQUEST, "username"), HandlerError::EmailTaken => self.failed_validation(StatusCode::BAD_REQUEST, "emailAddress"), + HandlerError::InvalidCredentials => self.into_generic(StatusCode::BAD_REQUEST), HandlerError::DecodeJwt(_) => self.into_generic(StatusCode::BAD_REQUEST), HandlerError::UserRequired => self.into_generic(StatusCode::UNAUTHORIZED), HandlerError::Database(_) => self.into_generic(StatusCode::INTERNAL_SERVER_ERROR), HandlerError::CreateUser(_) => self.into_generic(StatusCode::INTERNAL_SERVER_ERROR), + HandlerError::ValidateCredentials(_) => self.into_generic(StatusCode::INTERNAL_SERVER_ERROR), HandlerError::EncodeJwt(_) => self.into_generic(StatusCode::INTERNAL_SERVER_ERROR), } .into_response() diff --git a/backend-rs/src/controllers/user.rs b/backend-rs/src/controllers/user.rs index 4122df3..349ca38 100644 --- a/backend-rs/src/controllers/user.rs +++ b/backend-rs/src/controllers/user.rs @@ -86,6 +86,29 @@ pub async fn register( Ok((StatusCode::CREATED, cookies)) } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LoginUser { + username: String, + password: String, +} + +pub async fn login( + State(state): State, + cookies: CookieJar, + Json(details): Json, +) -> Result<(StatusCode, CookieJar), HandlerError> { + if let Some(user) = + user::login_with_credentials(&state.connection, &details.username, &details.password).await? + { + let cookies = authenticate(cookies, &user, state.config.app.token_lifespan)?; + + Ok((StatusCode::CREATED, cookies)) + } else { + Err(HandlerError::InvalidCredentials) + } +} + #[async_trait] impl FromRequestParts for User { type Rejection = HandlerError; diff --git a/backend-rs/src/main.rs b/backend-rs/src/main.rs index f272c05..d81a899 100644 --- a/backend-rs/src/main.rs +++ b/backend-rs/src/main.rs @@ -67,6 +67,7 @@ async fn main() -> Result<(), AppError> { let app = Router::new() .route("/auth/register", routing::post(user::register)) + .route("/auth/login", routing::post(user::login)) .route("/user", routing::get(user::get_user)) .layer( TraceLayer::new_for_http() diff --git a/backend-rs/src/models/user.rs b/backend-rs/src/models/user.rs index 05b23b1..1764806 100644 --- a/backend-rs/src/models/user.rs +++ b/backend-rs/src/models/user.rs @@ -89,6 +89,39 @@ pub async fn create( ) } +#[derive(Error, Debug)] +pub enum ValidateCredentialsError { + #[error(transparent)] + Database(#[from] DbErr), + #[error(transparent)] + Hash(#[from] BcryptError), +} + +pub async fn login_with_credentials( + connection: &DatabaseConnection, + name: &str, + password: &str, +) -> Result, ValidateCredentialsError> { + let user = Entity::find() + .filter(Column::Username.eq(name)) + .one(connection) + .await?; + + match user { + None => Ok(None), + Some(user) => match &user.password { + None => Ok(None), + Some(hash) => { + if bcrypt::verify(password, hash)? { + Ok(Some(user)) + } else { + Ok(None) + } + } + }, + } +} + pub async fn find(connection: &DatabaseConnection, id: Id) -> Result, DbErr> { Entity::find_by_id(id).one(connection).await }