login with bcrypt

This commit is contained in:
Jackzie 2025-04-16 10:58:12 -05:00
parent dc21e50a8a
commit b2889eac1d
9 changed files with 201 additions and 37 deletions

43
server/Cargo.lock generated
View file

@ -143,6 +143,19 @@ version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3"
[[package]]
name = "bcrypt"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92758ad6077e4c76a6cadbce5005f666df70d4f13b19976b1a8062eef880040f"
dependencies = [
"base64",
"blowfish",
"getrandom 0.3.2",
"subtle",
"zeroize",
]
[[package]]
name = "binascii"
version = "0.1.4"
@ -173,6 +186,16 @@ dependencies = [
"generic-array",
]
[[package]]
name = "blowfish"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
dependencies = [
"byteorder",
"cipher",
]
[[package]]
name = "bumpalo"
version = "3.17.0"
@ -227,6 +250,16 @@ dependencies = [
"windows-link",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
@ -1055,6 +1088,15 @@ dependencies = [
"libc",
]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array",
]
[[package]]
name = "int-enum"
version = "1.2.0"
@ -2318,6 +2360,7 @@ name = "storage-server"
version = "0.1.0"
dependencies = [
"anyhow",
"bcrypt",
"chrono",
"dotenvy",
"humanize-bytes",

View file

@ -20,3 +20,4 @@ humanize-bytes = "1.0.6"
rocket-session-store = "0.2.1"
uuid = { version = "1.16.0", features = ["v4"] }
rand = { version = "0.9.0", features = ["thread_rng"] }
bcrypt = "0.17.0"

12
server/src/consts.rs Normal file
View file

@ -0,0 +1,12 @@
use std::time::Duration;
use rocket::data::ByteUnit;
/// The maximum amount of bytes that can be uploaded at once
pub const MAX_UPLOAD_SIZE: ByteUnit = ByteUnit::Mebibyte(100_000);
/// The number of encryption rounds
pub const ENCRYPTION_ROUNDS: u32 = 20;
pub const SESSION_LIFETIME_SECONDS: u64 = 3600 * 24 * 14; // 14 days
pub const SESSION_COOKIE_NAME: &'static str = "storage-session";

View file

@ -22,6 +22,7 @@ use crate::managers::repos::RepoManager;
use crate::objs::library::Library;
use crate::util::{setup_logger, JsonErrorResponse, ResponseError};
use routes::api;
use crate::consts::{SESSION_COOKIE_NAME, SESSION_LIFETIME_SECONDS};
use crate::models::user::UserModel;
use crate::routes::ui;
@ -34,10 +35,10 @@ mod models;
mod managers;
mod objs;
mod helpers;
mod consts;
pub type DB = Pool<Postgres>;
const MAX_UPLOAD_SIZE: ByteUnit = ByteUnit::Mebibyte(100_000);
#[derive(Clone, Debug, Serialize, Default)]
struct SessionData {
@ -89,8 +90,8 @@ async fn rocket() -> _ {
let memory_store: MemoryStore::<SessionData> = MemoryStore::default();
let store: SessionStore<SessionData> = SessionStore {
store: Box::new(memory_store),
name: "storage-session".into(),
duration: Duration::from_secs(3600 * 24 * 14),
name: SESSION_COOKIE_NAME.into(),
duration: Duration::from_secs(SESSION_LIFETIME_SECONDS),
// The cookie builder is used to set the cookie's path and other options.
// Name and value don't matter, they'll be overridden on each request.
cookie_builder: CookieBuilder::new("", "")

View file

@ -1,22 +1,120 @@
use bcrypt::BcryptError;
use chrono::NaiveDateTime;
use rocket::http::Status;
use rocket::Request;
use rocket::response::Responder;
use rocket::serde::Serialize;
use rocket::serde::uuid::Uuid;
use sqlx::query_as;
use sqlx::{query_as, FromRow};
use crate::consts::ENCRYPTION_ROUNDS;
use crate::DB;
use crate::models::repo::RepoModel;
use crate::util::JsonErrorResponse;
#[derive(Serialize, Clone, Debug)]
#[derive(Serialize, Clone, Debug, FromRow)]
pub struct UserModel {
pub id: Uuid,
// email
// password
pub username: String,
pub email: String,
pub created_at: NaiveDateTime,
pub name: String
}
#[derive(Serialize, Clone, Debug, FromRow)]
pub struct UserModelWithPassword {
pub id: Uuid,
pub username: String,
pub email: String,
pub password: Option<String>,
pub created_at: NaiveDateTime,
pub name: String
}
#[derive(Debug)]
pub enum UserAuthError {
DatabaseError(sqlx::Error),
UserNotFound,
UserAlreadyExists,
PasswordInvalid,
EncryptionError(BcryptError),
}
impl UserAuthError {
fn get_err_code(&self) -> String {
match self {
UserAuthError::DatabaseError(_) => "DATABASE_ERROR",
UserAuthError::UserNotFound => "USER_NOT_FOUND",
UserAuthError::UserAlreadyExists => "USER_EXISTS",
UserAuthError::PasswordInvalid => "PASSWORD_INVALID",
UserAuthError::EncryptionError(_) => "ENCRYPTION_ERROR",
}.to_string()
}
fn get_err_msg(&self) -> String {
match self {
UserAuthError::DatabaseError(e) => format!("Error from database: {}", e.to_string()),
UserAuthError::UserNotFound => "No user found with provided username or email".to_string(),
UserAuthError::UserAlreadyExists => "User already exists".to_string(),
UserAuthError::PasswordInvalid => "Password is invalid or incorrect".to_string(),
UserAuthError::EncryptionError(_) => "Error occurred during password encryption".to_string()
}.to_string()
}
pub(crate) fn get_response_code(&self) -> Status {
match self {
UserAuthError::DatabaseError(_) => Status::InternalServerError,
UserAuthError::UserNotFound => Status::NotFound,
UserAuthError::UserAlreadyExists => Status::Conflict,
UserAuthError::PasswordInvalid => Status::Unauthorized,
UserAuthError::EncryptionError(_) => Status::InternalServerError
}
}
pub(crate) fn into_response_err(self) -> JsonErrorResponse {
JsonErrorResponse {
code: self.get_err_code(),
message: self.get_err_msg(),
}
}
}
pub async fn get_user(pool: &DB, user_id: &str) -> Result<Option<UserModel>, anyhow::Error> {
let user_id = Uuid::parse_str(user_id)?;
query_as!(UserModel, "select id, created_at, name from storage.users where id = $1", user_id)
query_as!(UserModel, "select id, username, created_at, email, name from storage.users where id = $1", user_id)
.fetch_optional(pool)
.await.map_err(anyhow::Error::from)
}
pub async fn validate_user(pool: &DB, email_or_usrname: &str, password: &str) -> Result<UserModel, UserAuthError> {
let user = query_as!(UserModelWithPassword,
"select id, username, password, created_at, email, name from storage.users where email = $1 OR username = $1", email_or_usrname
)
.fetch_optional(pool)
.await
.map_err(|e| UserAuthError::DatabaseError(e))?;
let Some(user) = user else {
return Err(UserAuthError::UserNotFound);
};
if let Some(db_password) = user.password {
if bcrypt::verify(password, &db_password).map_err(|e| UserAuthError::EncryptionError(e))? {
return Ok(UserModel {
id: user.id,
email: user.email,
username: user.username,
created_at: user.created_at,
name: user.name
})
}
}
Err(UserAuthError::PasswordInvalid)
}
pub struct CreateUserModel {
pub username: String,
pub email: String,
pub password: String,
pub name: String
}
pub async fn create_user(pool: &DB, user: CreateUserModel) -> Result<UserModel, UserAuthError> {
let encrypted_pass = bcrypt::hash(user.password, ENCRYPTION_ROUNDS)
.map_err(|e| UserAuthError::EncryptionError(e))?;
todo!()
}

View file

@ -1,22 +1,2 @@
use std::path::PathBuf;
use std::sync::Arc;
use log::debug;
use rocket::{get, post, Data, State};
use rocket::fs::TempFile;
use rocket::http::Status;
use rocket::response::status;
use rocket::serde::json::Json;
use sqlx::{query, Postgres};
use sqlx::types::{Uuid};
use tokio::io::AsyncReadExt;
use tokio::sync::Mutex;
use crate::{library, models, DB, MAX_UPLOAD_SIZE};
use crate::managers::libraries::LibraryManager;
use crate::managers::repos::RepoManager;
use crate::models::library::{LibraryModel, LibraryWithRepoModel};
use crate::models::user;
use crate::storage::FileEntry;
use crate::util::{JsonErrorResponse, ResponseError};
pub mod api;
pub mod ui;

View file

@ -10,7 +10,8 @@ use sqlx::{query, Postgres};
use sqlx::types::{Uuid};
use tokio::io::AsyncReadExt;
use tokio::sync::Mutex;
use crate::{library, models, DB, MAX_UPLOAD_SIZE};
use crate::{library, models, DB};
use crate::consts::MAX_UPLOAD_SIZE;
use crate::managers::libraries::LibraryManager;
use crate::managers::repos::RepoManager;
use crate::models::library::{LibraryModel, LibraryWithRepoModel};

View file

@ -1,11 +1,11 @@
use std::net::IpAddr;
use rocket::{get, post, FromForm, Route};
use rocket::{get, post, FromForm, Route, State};
use rocket::form::Form;
use rocket_dyn_templates::{context, Template};
use rocket_session_store::Session;
use crate::models::user::UserModel;
use crate::{LoginSessionData, SessionData};
use crate::util::{gen_csrf_token, set_csrf, validate_csrf};
use crate::models::user::{validate_user, UserAuthError, UserModel};
use crate::{LoginSessionData, SessionData, DB};
use crate::util::{gen_csrf_token, set_csrf, validate_csrf, JsonErrorResponse, ResponseError};
#[get("/login")]
pub async fn login(route: &Route, session: Session<'_, SessionData>) -> Template {
@ -27,24 +27,33 @@ struct LoginForm<'r> {
remember_me: bool
}
#[post("/login", data = "<form>")]
pub async fn login_handler(route: &Route, ip_addr: IpAddr, form: Form<LoginForm<'_>>, session: Session<'_, SessionData>) -> String {
pub async fn login_handler(
pool: &State<DB>,
route: &Route,
ip_addr: IpAddr,
session: Session<'_, SessionData>,
form: Form<LoginForm<'_>>,
) -> Result<String, ResponseError> {
if let Ok(true) = validate_csrf(&session, &form._csrf).await {
let user = validate_user(pool, form.username, form.password).await?;
if let Ok(sess) = session.get().await.map(|s| s.unwrap_or_default()) {
session.set(SessionData {
csrf_token: None,
login: Some(LoginSessionData {
user: UserModel {
id: Default::default(),
username: "".to_string(),
email: "".to_string(),
created_at: Default::default(),
name: form.username.to_string(),
},
ip_address: ip_addr,
}),
}).await.unwrap();
return format!("login success")
return Ok(format!("login success"))
}
}
format!("login bad. csrf failed!")
Err(ResponseError::CSRFError)
}
#[get("/register")]

View file

@ -15,6 +15,7 @@ use sqlx::Error;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use uuid::Uuid;
use crate::models::user::{UserAuthError,};
use crate::SessionData;
use crate::util::ResponseError::DatabaseError;
@ -95,6 +96,8 @@ pub enum ResponseError {
GenericError,
InternalServerError(JsonErrorResponse),
DatabaseError(JsonErrorResponse),
AuthError(UserAuthError),
CSRFError
}
impl ResponseError {
@ -103,6 +106,9 @@ impl ResponseError {
ResponseError::InternalServerError(_) => Status::InternalServerError,
ResponseError::GenericError => Status::InternalServerError,
ResponseError::NotFound(_) => Status::NotFound,
ResponseError::DatabaseError(_) => Status::InternalServerError,
ResponseError::AuthError(e) => e.get_response_code(),
ResponseError::CSRFError => Status::Unauthorized,
_ => Status::BadRequest,
}
}
@ -118,6 +124,13 @@ impl ResponseError {
},
ResponseError::InternalServerError(e) => e,
DatabaseError(e) => e,
ResponseError::AuthError(e) => e.into_response_err(),
ResponseError::CSRFError => {
JsonErrorResponse {
code: "CSRF_VALIDATION_FAILED".to_string(),
message: "CSRF Token is invalid / expired or does not exist. Reload the form and try again".to_string(),
}
}
}
}
}
@ -131,6 +144,12 @@ impl From<sqlx::Error> for ResponseError {
}
}
impl From<UserAuthError> for ResponseError {
fn from(value: UserAuthError) -> Self {
ResponseError::AuthError(value)
}
}
impl std::fmt::Display for ResponseError {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(fmt, "Error {}.", self.get_http_status())