working login

This commit is contained in:
Brandon Thomas committed 2021-05-17 22:08:48 -04:00
1 parent 0c9751bea5
commit 245ef66969
9 files changed
+159 -94

No files matched your search

File renamed without changes.
+3
View File
@@ -0,0 +1,3 @@
pub mod users;
pub mod badges;
pub mod sessions;
+41
View File
@@ -0,0 +1,41 @@
use anyhow::anyhow;
use log::{info, warn};
use crate::AnyhowResult;
use crate::util::tokens::random_token;
use sqlx::MySqlPool;
pub async fn create_session_for_user(user_token: &str, ip_address: &str, mysql_pool: &MySqlPool)
-> AnyhowResult<String>
{
let session_token = random_token(32);
let query_result = sqlx::query!(
r#"
INSERT INTO user_sessions (
token,
user_token,
ip_address_creation,
expires_at
)
VALUES ( ?, ?, ?, NOW() + interval 1 year )
"#,
session_token.to_string(),
user_token.to_string(),
ip_address.to_string(),
)
.execute(mysql_pool)
.await;
let record_id = match query_result {
Ok(res) => {
res.last_insert_id()
},
Err(err) => {
return Err(anyhow!("session creation DB error: {:?}", err));
}
};
info!("Created session id: {}", record_id);
Ok(session_token)
}
File renamed without changes.
+4 -1
View File
@@ -112,8 +112,11 @@ pub async fn create_account_handler(
let username = request.username.to_lowercase();
let display_name = request.username.to_string();
let email_address = request.email_address.to_lowercase();
let profile_markdown = "";
let profile_rendered_html = "";
let ip_address = get_request_ip(&http_request);
let query_result = sqlx::query!(
@@ -136,7 +139,7 @@ VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
user_token.to_string(),
username,
display_name,
request.email_address.to_string(),
email_address,
profile_markdown,
profile_rendered_html,
NEW_USER_ROLE,
+104 -84
View File
@@ -18,6 +18,9 @@ use std::sync::Arc;
use crate::validations::username::validate_username;
use crate::validations::passwords::validate_passwords;
use actix_web::cookie::Cookie;
use sqlx::MySqlPool;
use crate::AnyhowResult;
use crate::common_queries::sessions::create_session_for_user;
#[derive(Deserialize)]
pub struct LoginRequest {
@@ -37,29 +40,23 @@ pub struct ErrorResponse {
}
#[derive(Debug, Display)]
pub enum CreateAccountError {
BadInput(String),
UsernameTaken,
EmailTaken,
pub enum LoginError {
WrongCredentials,
ServerError,
}
impl ResponseError for CreateAccountError {
impl ResponseError for LoginError {
fn status_code(&self) -> StatusCode {
match *self {
CreateAccountError::BadInput(_) => StatusCode::BAD_REQUEST,
CreateAccountError::UsernameTaken => StatusCode::BAD_REQUEST,
CreateAccountError::EmailTaken => StatusCode::BAD_REQUEST,
CreateAccountError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
LoginError::WrongCredentials => StatusCode::BAD_REQUEST,
LoginError::ServerError=> StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_response(&self) -> HttpResponse {
let error_reason = match self {
BadInput(reason) => reason.to_string(),
CreateAccountError::UsernameTaken => "username already taken".to_string(),
CreateAccountError::EmailTaken => "email already taken".to_string(),
CreateAccountError::ServerError => "server error".to_string(),
LoginError::WrongCredentials => "wrong credentials".to_string(),
LoginError::ServerError => "server error".to_string(),
};
let response = ErrorResponse {
@@ -81,96 +78,119 @@ impl ResponseError for CreateAccountError {
pub async fn login_handler(
http_request: HttpRequest,
request: web::Json<LoginRequest>,
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, CreateAccountError>
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, LoginError>
{
let try_password_hash = match bcrypt::hash(&request.password, bcrypt::DEFAULT_COST) {
let check_username_or_email = request.username_or_email.to_lowercase();
let maybe_user = if check_username_or_email.contains("@") {
lookup_by_email(&check_username_or_email, &server_state.mysql_pool).await
} else {
lookup_by_username(&check_username_or_email, &server_state.mysql_pool).await
};
let user = match maybe_user {
Ok(user) => user,
Err(e) => {
warn!("Login lookup error: {:?}", e);
return Err(LoginError::WrongCredentials); // TODO: This isn't necessarily user error.
}
};
info!("login user found");
let actual_hash = match String::from_utf8(user.password_hash.clone()) {
Ok(hash) => hash,
Err(err) => {
warn!("Bcrypt error: {:?}", err);
return Err(ServerError);
Err(e) => {
warn!("Login hash hydration error: {:?}", e);
return Err(LoginError::ServerError);
}
};
let query_result = sqlx::query!(
r#"
INSERT INTO users (
token,
username,
display_name,
email_address,
profile_markdown,
profile_rendered_html,
user_role_slug,
password_hash,
ip_address_creation,
ip_address_last_login,
ip_address_last_update
)
VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
"#,
user_token.to_string(),
username,
display_name,
request.email_address.to_string(),
profile_markdown,
profile_rendered_html,
NEW_USER_ROLE,
password_hash,
ip_address.to_string(),
ip_address.to_string(),
ip_address.to_string(),
)
.execute(&server_state.mysql_pool)
.await;
let record_id = match query_result {
Ok(res) => {
res.last_insert_id()
},
Err(err) => {
warn!("New user creation DB error: {:?}", err);
// NB: SQLSTATE[23000]: Integrity constraint violation
// NB: MySQL Error Code 1062: Duplicate key insertion (this is harder to access)
match err {
Database(err) => {
let maybe_code = err.code().map(|c| c.into_owned());
match maybe_code.as_deref() {
Some("23000") => {
if err.message().contains("username") {
return Err(UsernameTaken);
} else if err.message().contains("email_address") {
return Err(EmailTaken);
}
}
_ => {},
}
},
_ => {},
let is_valid = match bcrypt::verify(&request.password, &actual_hash) {
Ok(is_valid) => {
if !is_valid {
info!("invalid credentials");
return Err(LoginError::WrongCredentials);
}
return Err(ServerError);
},
Err(e) => {
warn!("Login hash comparison error: {:?}", e);
return Err(LoginError::ServerError);
}
};
let ip_address = get_request_ip(&http_request);
info!("login session created");
let create_session_result =
create_session_for_user(&user.token, &ip_address, &server_state.mysql_pool).await;
let session_cookie = Cookie::build("session", session_token)
.domain(".vo.codes")
.path("/")
.secure(true) // HTTPS-only
.http_only(true) // Not exposed to Javascript
.finish();
let session_token = match create_session_result {
Ok(token) => token,
Err(e) => {
warn!("login create session error : {:?}", e);
return Err(LoginError::ServerError);
}
};
let response = CreateAccountSuccessResponse {
let session_cookie = match server_state.cookie_manager.create_cookie(&session_token) {
Ok(cookie) => cookie,
Err(_) => return Err(LoginError::ServerError),
};
let response = LoginSuccessResponse {
success: true,
};
let body = serde_json::to_string(&response)
.map_err(|e| BadInput("".to_string()))?;
.map_err(|e| LoginError::ServerError)?;
Ok(HttpResponse::Ok()
.cookie(session_cookie)
.content_type("application/json")
.body(body))
}
#[derive(Debug)]
pub struct UserRecordForLogin {
token: String,
username: String,
email_address: String,
password_hash: Vec<u8>,
}
async fn lookup_by_username(username: &str, pool: &MySqlPool) -> AnyhowResult<UserRecordForLogin>
{
// NB: Lookup failure is Err(RowNotFound).
let record = sqlx::query_as!(
UserRecordForLogin,
r#"
SELECT token, username, email_address, password_hash
FROM users
WHERE username = ?
LIMIT 1
"#,
username.to_string(),
)
.fetch_one(pool)
.await?;
Ok(record)
}
async fn lookup_by_email(email: &str, pool: &MySqlPool) -> AnyhowResult<UserRecordForLogin> {
// NB: Lookup failure is Err(RowNotFound).
let record = sqlx::query_as!(
UserRecordForLogin,
r#"
SELECT token, username, email_address, password_hash
FROM users
WHERE email_address = ?
LIMIT 1
"#,
email.to_string(),
)
.fetch_one(pool)
.await?;
Ok(record)
}
+2 -1
View File
@@ -1,3 +1,4 @@
//pub mod login;
//pub mod logout;
pub mod create_account;
pub mod create_account;
pub mod login;
+5 -6
View File
@@ -1,9 +1,9 @@
#[macro_use] extern crate lazy_static;
#[macro_use] extern crate serde_derive;
pub mod server_state;
pub mod common_queries;
pub mod endpoints;
pub mod queries;
pub mod server_state;
pub mod util;
pub mod validations;
@@ -16,9 +16,8 @@ use std::sync::Arc;
use crate::server_state::{ServerState, EnvConfig};
use sqlx::MySqlPool;
use sqlx::mysql::MySqlPoolOptions;
use crate::queries::badges::NewBadge;
use crate::endpoints::users::create_account::create_account_handler;
//use crate::endpoints::users::login::login_handler;
use crate::endpoints::users::login::login_handler;
use crate::util::cookies::CookieManager;
const DEFAULT_BIND_ADDRESS : &'static str = "0.0.0.0:12345";
@@ -110,11 +109,11 @@ pub async fn serve(server_state: ServerState) -> AnyhowResult<()>
.route(web::post().to(create_account_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
/*.service(
.service(
web::resource("/login")
.route(web::post().to(login_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)*/
)
.service(
web::scope("/foo")
.service(
-2
View File
@@ -1,2 +0,0 @@
pub mod users;
pub mod badges;