mirror of
https://github.com/Jackzmc/storage.git
synced 2025-05-06 01:43:20 +00:00
Implement login form validation
This commit is contained in:
parent
b2889eac1d
commit
a60f80cdda
6 changed files with 117 additions and 44 deletions
|
@ -5,7 +5,7 @@ use rocket::data::ByteUnit;
|
|||
pub const MAX_UPLOAD_SIZE: ByteUnit = ByteUnit::Mebibyte(100_000);
|
||||
|
||||
/// The number of encryption rounds
|
||||
pub const ENCRYPTION_ROUNDS: u32 = 20;
|
||||
pub const ENCRYPTION_ROUNDS: u32 = 12;
|
||||
|
||||
pub const SESSION_LIFETIME_SECONDS: u64 = 3600 * 24 * 14; // 14 days
|
||||
|
||||
|
|
|
@ -13,11 +13,18 @@ pub(crate) fn bytes(h: &Helper< '_>, _: &Handlebars<'_>, _: &Context, rc:
|
|||
|
||||
pub(crate) fn debug(h: &Helper< '_>, _: &Handlebars<'_>, _: &Context, rc:
|
||||
&mut RenderContext<'_, '_>, out: &mut dyn Output) -> HelperResult {
|
||||
let param = h.param(0)
|
||||
.and_then(|v| v.value().as_object())
|
||||
.ok_or::<RenderError>(RenderErrorReason::ParamNotFoundForIndex("", 0).into())?;
|
||||
let output = serde_json::to_string(param).unwrap();
|
||||
out.write(&output)?;
|
||||
if let Some(param) = h.param(0) {
|
||||
if let Some(obj) = param.value().as_object() {
|
||||
let output = serde_json::to_string(obj).unwrap();
|
||||
out.write(&output)?;
|
||||
} else if let Some(str) = param.value().as_str() {
|
||||
out.write(str)?;
|
||||
} else {
|
||||
out.write("[unknown]")?;
|
||||
}
|
||||
} else {
|
||||
out.write("undefined")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use bcrypt::BcryptError;
|
||||
use chrono::NaiveDateTime;
|
||||
use rocket::form::Context;
|
||||
use rocket::http::Status;
|
||||
use rocket::Request;
|
||||
use rocket::{form, Request};
|
||||
use rocket::form::error::Entity;
|
||||
use rocket::response::Responder;
|
||||
use rocket::serde::Serialize;
|
||||
use rocket::serde::uuid::Uuid;
|
||||
use sqlx::{query_as, FromRow};
|
||||
use crate::consts::ENCRYPTION_ROUNDS;
|
||||
use crate::DB;
|
||||
use crate::{LoginSessionData, SessionData, DB};
|
||||
use crate::models::repo::RepoModel;
|
||||
use crate::util::JsonErrorResponse;
|
||||
|
||||
|
@ -37,6 +41,16 @@ pub enum UserAuthError {
|
|||
PasswordInvalid,
|
||||
EncryptionError(BcryptError),
|
||||
}
|
||||
|
||||
impl Display for UserAuthError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}: {}", self.get_err_code(), self.get_err_msg())
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for UserAuthError {
|
||||
|
||||
}
|
||||
impl UserAuthError {
|
||||
fn get_err_code(&self) -> String {
|
||||
match self {
|
||||
|
@ -82,7 +96,23 @@ pub async fn get_user(pool: &DB, user_id: &str) -> Result<Option<UserModel>, any
|
|||
.fetch_optional(pool)
|
||||
.await.map_err(anyhow::Error::from)
|
||||
}
|
||||
/// Validates user login form, returning Some on success or None (with ctx containing errors) on failure
|
||||
pub async fn validate_user_form(ctx: &mut Context<'_>, pool: &DB) -> Option<UserModel> {
|
||||
let username = ctx.field_value("username").unwrap();
|
||||
let password = ctx.field_value("password").unwrap(); // TODO: no unwrap
|
||||
match validate_user(pool, username, password).await {
|
||||
Ok(u) => Some(u),
|
||||
Err(UserAuthError::PasswordInvalid | UserAuthError::UserNotFound) => {
|
||||
ctx.push_error(form::Error::validation("Username or password is incorrect").with_entity(Entity::Form));
|
||||
None
|
||||
},
|
||||
Err(e) => {
|
||||
ctx.push_error(form::Error::custom(e));
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
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
|
||||
|
|
|
@ -1,59 +1,72 @@
|
|||
use std::net::IpAddr;
|
||||
use rocket::{get, post, FromForm, Route, State};
|
||||
use rocket::form::Form;
|
||||
use log::debug;
|
||||
use rocket::{get, post, uri, FromForm, Route, State};
|
||||
use rocket::form::{Context, Contextual, Error, Form};
|
||||
use rocket::form::error::Entity;
|
||||
use rocket::http::Status;
|
||||
use rocket::response::Redirect;
|
||||
use rocket_dyn_templates::{context, Template};
|
||||
use rocket_session_store::Session;
|
||||
use crate::models::user::{validate_user, UserAuthError, UserModel};
|
||||
use crate::models::user::{validate_user, validate_user_form, UserAuthError, UserModel};
|
||||
use crate::{LoginSessionData, SessionData, DB};
|
||||
use crate::util::{gen_csrf_token, set_csrf, validate_csrf, JsonErrorResponse, ResponseError};
|
||||
use crate::routes::ui;
|
||||
use crate::routes::ui::user::list_library_files;
|
||||
use crate::util::{gen_csrf_token, set_csrf, validate_csrf_form, JsonErrorResponse, ResponseError};
|
||||
|
||||
#[get("/login")]
|
||||
pub async fn login(route: &Route, session: Session<'_, SessionData>) -> Template {
|
||||
let csrf_token = set_csrf(&session).await;
|
||||
Template::render("auth/login", context! {
|
||||
route: route.uri.path(),
|
||||
csrf_token: csrf_token
|
||||
csrf_token: csrf_token,
|
||||
form: &Context::default(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[derive(FromForm)]
|
||||
#[derive(Debug)]
|
||||
struct LoginForm<'r> {
|
||||
_csrf: &'r str,
|
||||
#[field(validate = len(1..))]
|
||||
username: &'r str,
|
||||
#[field(validate = len(1..))]
|
||||
password: &'r str,
|
||||
#[field(default = false)]
|
||||
remember_me: bool
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[post("/login", data = "<form>")]
|
||||
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()) {
|
||||
mut form: Form<Contextual<'_, LoginForm<'_>>>,
|
||||
) -> Result<Redirect, Template> {
|
||||
validate_csrf_form(&mut form.context, &session).await;
|
||||
let user = validate_user_form(&mut form.context, &pool).await;
|
||||
if form.context.status() == Status::Ok {
|
||||
if let Some(submission) = &form.value {
|
||||
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(),
|
||||
},
|
||||
user: user.expect("failed to acquire user but no errors"), // if validate_user_form returned None, form had errors, this shouldnt run,
|
||||
ip_address: ip_addr,
|
||||
}),
|
||||
}).await.unwrap();
|
||||
return Ok(format!("login success"))
|
||||
|
||||
return Ok(Redirect::to(uri!(ui::user::index())))
|
||||
}
|
||||
}
|
||||
Err(ResponseError::CSRFError)
|
||||
|
||||
let csrf_token = set_csrf(&session).await;
|
||||
let ctx = context! {
|
||||
csrf_token,
|
||||
form: &form.context
|
||||
};
|
||||
Err(Template::render("auth/login", &ctx))
|
||||
}
|
||||
|
||||
#[get("/register")]
|
||||
|
|
|
@ -5,7 +5,9 @@ use rand::rngs::OsRng;
|
|||
use rand::{rng, Rng, TryRngCore};
|
||||
use rand::distr::Alphanumeric;
|
||||
use rocket::http::{ContentType, Status};
|
||||
use rocket::{response, Request, Response};
|
||||
use rocket::{form, response, Request, Response};
|
||||
use rocket::form::Context;
|
||||
use rocket::form::error::Entity;
|
||||
use rocket::fs::relative;
|
||||
use rocket::response::Responder;
|
||||
use rocket::serde::Serialize;
|
||||
|
@ -41,19 +43,25 @@ pub async fn set_csrf(session: &Session<'_, SessionData>) -> String {
|
|||
session.set(sess).await.unwrap();
|
||||
token
|
||||
}
|
||||
|
||||
pub async fn validate_csrf(session: &Session<'_, SessionData>, form_csrf_token: &str) -> Result<bool, SessionError> {
|
||||
if let Some(mut sess) = session.get().await? {
|
||||
if let Some(sess_token) = sess.csrf_token {
|
||||
let success = sess_token == form_csrf_token;
|
||||
if success {
|
||||
sess.csrf_token = None;
|
||||
session.set(sess).await?;
|
||||
return Ok(true)
|
||||
pub(crate) async fn validate_csrf_form(ctx: &mut Context<'_>, session: &Session<'_, SessionData>) -> bool {
|
||||
if let Some(form_token) = ctx.field_value("_csrf") {
|
||||
if let Some(mut sess) = session.get().await.unwrap() {
|
||||
if let Some(sess_token) = sess.csrf_token {
|
||||
let success = sess_token == form_token;
|
||||
if success {
|
||||
sess.csrf_token = None;
|
||||
session.set(sess).await.unwrap();
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ctx.push_error(form::Error::validation("_csrf token invalid").with_entity(Entity::Form));
|
||||
return false
|
||||
}
|
||||
Ok(false)
|
||||
// CSRF failed, set error
|
||||
ctx.push_error(rocket::form::Error::validation("CSRF Validation failed").with_entity(Entity::Form));
|
||||
false
|
||||
}
|
||||
|
||||
pub fn gen_csrf_token() -> String {
|
||||
|
|
|
@ -4,27 +4,42 @@
|
|||
<h1 class="title is-1 has-text-centered">storage-app</h1>
|
||||
<div class="box is-radiusless">
|
||||
<h4 class="title is-4 has-text-centered">Login</h4>
|
||||
{{debug this}}
|
||||
{{#if (len form.form_errors) includeZero=true }}
|
||||
<div class="notification is-danger is-light">
|
||||
<b>Login failed with errors:</b>
|
||||
<ul>
|
||||
{{#each form.form_errors}}
|
||||
<li>{{msg}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
{{/if}}
|
||||
<form method="post" action="/auth/login">
|
||||
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
|
||||
<div class="field">
|
||||
<label class="label">Username / Email</label>
|
||||
<div class="control has-icons-left">
|
||||
<input required name="username" class="input" type="text" placeholder="Username or Email">
|
||||
<input required name="username" class="input {{#if errors.username}}is-danger{{/if}}" type="text" placeholder="Username or Email">
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fas fa-user"></i>
|
||||
</span>
|
||||
</div>
|
||||
{{!-- <p class="help is-success">This username is available</p> --}}
|
||||
{{#if errors.username }}
|
||||
<p class="help is-danger">{{errors.username}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Password</label>
|
||||
<div class="control has-icons-left">
|
||||
<input required name="password" class="input" type="password" placeholder="hunter2">
|
||||
<input required name="password" class="input {{#if errors.password}}is-danger{{/if}}" type="password" placeholder="hunter2">
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fas fa-key"></i>
|
||||
</span>
|
||||
</div>
|
||||
{{!-- <p class="help is-success">This username is available</p> --}}
|
||||
{{#if errors.username }}
|
||||
<p class="help is-danger">{{errors.password}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue