mirror of
https://github.com/Jackzmc/storage.git
synced 2025-05-08 16:53:22 +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);
|
pub const MAX_UPLOAD_SIZE: ByteUnit = ByteUnit::Mebibyte(100_000);
|
||||||
|
|
||||||
/// The number of encryption rounds
|
/// 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
|
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:
|
pub(crate) fn debug(h: &Helper< '_>, _: &Handlebars<'_>, _: &Context, rc:
|
||||||
&mut RenderContext<'_, '_>, out: &mut dyn Output) -> HelperResult {
|
&mut RenderContext<'_, '_>, out: &mut dyn Output) -> HelperResult {
|
||||||
let param = h.param(0)
|
if let Some(param) = h.param(0) {
|
||||||
.and_then(|v| v.value().as_object())
|
if let Some(obj) = param.value().as_object() {
|
||||||
.ok_or::<RenderError>(RenderErrorReason::ParamNotFoundForIndex("", 0).into())?;
|
let output = serde_json::to_string(obj).unwrap();
|
||||||
let output = serde_json::to_string(param).unwrap();
|
|
||||||
out.write(&output)?;
|
out.write(&output)?;
|
||||||
|
} else if let Some(str) = param.value().as_str() {
|
||||||
|
out.write(str)?;
|
||||||
|
} else {
|
||||||
|
out.write("[unknown]")?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.write("undefined")?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
use bcrypt::BcryptError;
|
use bcrypt::BcryptError;
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
|
use rocket::form::Context;
|
||||||
use rocket::http::Status;
|
use rocket::http::Status;
|
||||||
use rocket::Request;
|
use rocket::{form, Request};
|
||||||
|
use rocket::form::error::Entity;
|
||||||
use rocket::response::Responder;
|
use rocket::response::Responder;
|
||||||
use rocket::serde::Serialize;
|
use rocket::serde::Serialize;
|
||||||
use rocket::serde::uuid::Uuid;
|
use rocket::serde::uuid::Uuid;
|
||||||
use sqlx::{query_as, FromRow};
|
use sqlx::{query_as, FromRow};
|
||||||
use crate::consts::ENCRYPTION_ROUNDS;
|
use crate::consts::ENCRYPTION_ROUNDS;
|
||||||
use crate::DB;
|
use crate::{LoginSessionData, SessionData, DB};
|
||||||
use crate::models::repo::RepoModel;
|
use crate::models::repo::RepoModel;
|
||||||
use crate::util::JsonErrorResponse;
|
use crate::util::JsonErrorResponse;
|
||||||
|
|
||||||
|
@ -37,6 +41,16 @@ pub enum UserAuthError {
|
||||||
PasswordInvalid,
|
PasswordInvalid,
|
||||||
EncryptionError(BcryptError),
|
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 {
|
impl UserAuthError {
|
||||||
fn get_err_code(&self) -> String {
|
fn get_err_code(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
|
@ -82,7 +96,23 @@ pub async fn get_user(pool: &DB, user_id: &str) -> Result<Option<UserModel>, any
|
||||||
.fetch_optional(pool)
|
.fetch_optional(pool)
|
||||||
.await.map_err(anyhow::Error::from)
|
.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> {
|
pub async fn validate_user(pool: &DB, email_or_usrname: &str, password: &str) -> Result<UserModel, UserAuthError> {
|
||||||
let user = query_as!(UserModelWithPassword,
|
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
|
"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 std::net::IpAddr;
|
||||||
use rocket::{get, post, FromForm, Route, State};
|
use log::debug;
|
||||||
use rocket::form::Form;
|
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_dyn_templates::{context, Template};
|
||||||
use rocket_session_store::Session;
|
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::{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")]
|
#[get("/login")]
|
||||||
pub async fn login(route: &Route, session: Session<'_, SessionData>) -> Template {
|
pub async fn login(route: &Route, session: Session<'_, SessionData>) -> Template {
|
||||||
let csrf_token = set_csrf(&session).await;
|
let csrf_token = set_csrf(&session).await;
|
||||||
Template::render("auth/login", context! {
|
Template::render("auth/login", context! {
|
||||||
route: route.uri.path(),
|
route: route.uri.path(),
|
||||||
csrf_token: csrf_token
|
csrf_token: csrf_token,
|
||||||
|
form: &Context::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(FromForm)]
|
#[derive(FromForm)]
|
||||||
|
#[derive(Debug)]
|
||||||
struct LoginForm<'r> {
|
struct LoginForm<'r> {
|
||||||
_csrf: &'r str,
|
_csrf: &'r str,
|
||||||
|
#[field(validate = len(1..))]
|
||||||
username: &'r str,
|
username: &'r str,
|
||||||
|
#[field(validate = len(1..))]
|
||||||
password: &'r str,
|
password: &'r str,
|
||||||
#[field(default = false)]
|
#[field(default = false)]
|
||||||
remember_me: bool
|
remember_me: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[post("/login", data = "<form>")]
|
#[post("/login", data = "<form>")]
|
||||||
pub async fn login_handler(
|
pub async fn login_handler(
|
||||||
pool: &State<DB>,
|
pool: &State<DB>,
|
||||||
route: &Route,
|
route: &Route,
|
||||||
ip_addr: IpAddr,
|
ip_addr: IpAddr,
|
||||||
session: Session<'_, SessionData>,
|
session: Session<'_, SessionData>,
|
||||||
form: Form<LoginForm<'_>>,
|
mut form: Form<Contextual<'_, LoginForm<'_>>>,
|
||||||
) -> Result<String, ResponseError> {
|
) -> Result<Redirect, Template> {
|
||||||
if let Ok(true) = validate_csrf(&session, &form._csrf).await {
|
validate_csrf_form(&mut form.context, &session).await;
|
||||||
let user = validate_user(pool, form.username, form.password).await?;
|
let user = validate_user_form(&mut form.context, &pool).await;
|
||||||
if let Ok(sess) = session.get().await.map(|s| s.unwrap_or_default()) {
|
if form.context.status() == Status::Ok {
|
||||||
|
if let Some(submission) = &form.value {
|
||||||
session.set(SessionData {
|
session.set(SessionData {
|
||||||
csrf_token: None,
|
csrf_token: None,
|
||||||
login: Some(LoginSessionData {
|
login: Some(LoginSessionData {
|
||||||
user: UserModel {
|
user: user.expect("failed to acquire user but no errors"), // if validate_user_form returned None, form had errors, this shouldnt run,
|
||||||
id: Default::default(),
|
|
||||||
username: "".to_string(),
|
|
||||||
email: "".to_string(),
|
|
||||||
created_at: Default::default(),
|
|
||||||
name: form.username.to_string(),
|
|
||||||
},
|
|
||||||
ip_address: ip_addr,
|
ip_address: ip_addr,
|
||||||
}),
|
}),
|
||||||
}).await.unwrap();
|
}).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")]
|
#[get("/register")]
|
||||||
|
|
|
@ -5,7 +5,9 @@ use rand::rngs::OsRng;
|
||||||
use rand::{rng, Rng, TryRngCore};
|
use rand::{rng, Rng, TryRngCore};
|
||||||
use rand::distr::Alphanumeric;
|
use rand::distr::Alphanumeric;
|
||||||
use rocket::http::{ContentType, Status};
|
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::fs::relative;
|
||||||
use rocket::response::Responder;
|
use rocket::response::Responder;
|
||||||
use rocket::serde::Serialize;
|
use rocket::serde::Serialize;
|
||||||
|
@ -41,19 +43,25 @@ pub async fn set_csrf(session: &Session<'_, SessionData>) -> String {
|
||||||
session.set(sess).await.unwrap();
|
session.set(sess).await.unwrap();
|
||||||
token
|
token
|
||||||
}
|
}
|
||||||
|
pub(crate) async fn validate_csrf_form(ctx: &mut Context<'_>, session: &Session<'_, SessionData>) -> bool {
|
||||||
pub async fn validate_csrf(session: &Session<'_, SessionData>, form_csrf_token: &str) -> Result<bool, SessionError> {
|
if let Some(form_token) = ctx.field_value("_csrf") {
|
||||||
if let Some(mut sess) = session.get().await? {
|
if let Some(mut sess) = session.get().await.unwrap() {
|
||||||
if let Some(sess_token) = sess.csrf_token {
|
if let Some(sess_token) = sess.csrf_token {
|
||||||
let success = sess_token == form_csrf_token;
|
let success = sess_token == form_token;
|
||||||
if success {
|
if success {
|
||||||
sess.csrf_token = None;
|
sess.csrf_token = None;
|
||||||
session.set(sess).await?;
|
session.set(sess).await.unwrap();
|
||||||
return Ok(true)
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(false)
|
} else {
|
||||||
|
ctx.push_error(form::Error::validation("_csrf token invalid").with_entity(Entity::Form));
|
||||||
|
return 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 {
|
pub fn gen_csrf_token() -> String {
|
||||||
|
|
|
@ -4,27 +4,42 @@
|
||||||
<h1 class="title is-1 has-text-centered">storage-app</h1>
|
<h1 class="title is-1 has-text-centered">storage-app</h1>
|
||||||
<div class="box is-radiusless">
|
<div class="box is-radiusless">
|
||||||
<h4 class="title is-4 has-text-centered">Login</h4>
|
<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">
|
<form method="post" action="/auth/login">
|
||||||
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
|
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Username / Email</label>
|
<label class="label">Username / Email</label>
|
||||||
<div class="control has-icons-left">
|
<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">
|
<span class="icon is-small is-left">
|
||||||
<i class="fas fa-user"></i>
|
<i class="fas fa-user"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Password</label>
|
<label class="label">Password</label>
|
||||||
<div class="control has-icons-left">
|
<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">
|
<span class="icon is-small is-left">
|
||||||
<i class="fas fa-key"></i>
|
<i class="fas fa-key"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue