mirror of
https://github.com/Jackzmc/storage.git
synced 2025-05-06 14:53:22 +00:00
login with bcrypt
This commit is contained in:
parent
dc21e50a8a
commit
b2889eac1d
9 changed files with 201 additions and 37 deletions
43
server/Cargo.lock
generated
43
server/Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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
12
server/src/consts.rs
Normal 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";
|
|
@ -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("", "")
|
||||
|
|
|
@ -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!()
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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())
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue