auth: wip: sign in page and form, need to finish

This commit is contained in:
Philippe Loctaux 2023-03-11 00:38:13 +01:00
parent d790d2ff29
commit 1695eca466
9 changed files with 179 additions and 6 deletions

View file

@ -13,6 +13,7 @@ pub enum Page {
Error(Error), Error(Error),
Setup, Setup,
Homepage(Homepage), Homepage(Homepage),
SignIn(SignIn),
} }
impl Page { impl Page {
@ -22,6 +23,7 @@ impl Page {
Page::Error(_) => "pages/error", Page::Error(_) => "pages/error",
Page::Setup => "pages/setup", Page::Setup => "pages/setup",
Page::Homepage(_) => "pages/homepage", Page::Homepage(_) => "pages/homepage",
Page::SignIn(_) => "pages/auth/sign_in",
} }
} }
@ -31,6 +33,7 @@ impl Page {
Page::Error(_) => "Error", Page::Error(_) => "Error",
Page::Setup => "Setup", Page::Setup => "Setup",
Page::Homepage(_) => "Home", Page::Homepage(_) => "Home",
Page::SignIn(_) => "Sign in",
} }
} }
@ -40,6 +43,7 @@ impl Page {
Page::Error(_) => None, Page::Error(_) => None,
Page::Setup => None, Page::Setup => None,
Page::Homepage(_) => Some(Item::Home.into()), Page::Homepage(_) => Some(Item::Home.into()),
Page::SignIn(_) => None,
} }
} }
@ -49,6 +53,7 @@ impl Page {
Page::Error(error) => Box::new(error), Page::Error(error) => Box::new(error),
Page::Setup => Box::new(()), Page::Setup => Box::new(()),
Page::Homepage(homepage) => Box::new(homepage), Page::Homepage(homepage) => Box::new(homepage),
Page::SignIn(sig_in) => Box::new(sig_in),
} }
} }
} }

View file

@ -1,2 +1,3 @@
pub use crate::error::content::*; pub use crate::error::content::*;
pub use crate::routes::auth::content::*;
pub use crate::routes::root::content::*; pub use crate::routes::root::content::*;

View file

@ -1,5 +1,6 @@
use rocket::{Build, Rocket}; use rocket::{Build, Rocket};
pub mod auth;
pub mod root; pub mod root;
pub mod setup; pub mod setup;
@ -32,4 +33,5 @@ pub fn routes(rocket_builder: Rocket<Build>) -> Rocket<Build> {
.mount("/", root::routes()) .mount("/", root::routes())
// Setup // Setup
.mount("/setup", setup::routes()) .mount("/setup", setup::routes())
.mount("/auth", auth::routes())
} }

View file

@ -0,0 +1,95 @@
use super::prelude::*;
use rocket::{get, post};
use settings::Settings;
use users::User;
pub fn routes() -> Vec<Route> {
routes![sign_in_page, sign_in]
}
pub mod content {
use rocket::serde::Serialize;
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
#[derive(Clone)]
pub struct SignIn {
pub business_name: String,
}
}
// TODO: default page when signed in -> redirect to home
#[get("/sign_in", rank = 2)]
async fn sign_in_page(
mut db: Connection<Database>,
flash: Option<FlashMessage<'_>>,
) -> Result<Template> {
// Define content
let content = content::SignIn {
business_name: Settings::get(&mut *db).await?.business_name().into(),
};
Ok(flash
.map(|flash| Page::with_flash(Page::SignIn(content.clone()), flash))
.unwrap_or_else(|| Page::SignIn(content).into()))
}
#[derive(Debug, FromForm)]
struct SignIn<'r> {
pub login: &'r str,
pub password: &'r str,
}
fn flash(message: String) -> Flash<Redirect> {
Flash::new(
Redirect::to(uri!("/auth/sign_in")),
FlashKind::Danger,
message,
)
}
fn invalid_credentials(login: &str) -> Flash<Redirect> {
flash(format!("Invalid credentials for {login}"))
}
fn user_archived(login: &str) -> Flash<Redirect> {
flash(format!("User {login} is archived"))
}
#[post("/sign_in", data = "<form>")]
async fn sign_in(
form: Form<SignIn<'_>>,
mut db: Connection<Database>,
) -> Result<Either<Redirect, Flash<Redirect>>> {
let form = form.into_inner();
let mut transaction = db.begin().await?;
// Get user
let Some(user) = User::get_by_login(&mut transaction, form.login).await? else {
return Ok(Either::Right(invalid_credentials(form.login)));
};
// Check if user is archived
if user.is_archived() {
return Ok(Either::Right(user_archived(form.login)));
}
// Get password (can't use Password struct directly because of non-async `compare`)
let password = match user.password_hashed() {
Some(password_hashed) => Password::from_hash(password_hashed),
None => return Ok(Either::Right(invalid_credentials(form.login))),
};
// Verify password
let password_input = form.password.to_string();
if !task::spawn_blocking(move || password.compare(&password_input)).await?? {
return Ok(Either::Right(invalid_credentials(form.login)));
}
// TODO: get ip
// TODO: refresh token + jwt
Ok(Either::Left(Redirect::to(uri!("/"))))
}

View file

@ -21,7 +21,7 @@ async fn setup(flash: Option<FlashMessage<'_>>) -> Template {
} }
#[derive(Debug, FromForm)] #[derive(Debug, FromForm)]
pub struct CreateFirstAccount<'r> { struct CreateFirstAccount<'r> {
pub username: &'r str, pub username: &'r str,
pub password: &'r str, pub password: &'r str,
pub url: &'r str, pub url: &'r str,
@ -40,7 +40,7 @@ async fn create_first_account(
Ok(url) => url, Ok(url) => url,
Err(e) => { Err(e) => {
return Ok(Either::Right(Flash::new( return Ok(Either::Right(Flash::new(
Redirect::to(uri!(self::setup)), Redirect::to(uri!("/setup")),
FlashKind::Danger, FlashKind::Danger,
e.to_string(), e.to_string(),
))); )));

View file

@ -0,0 +1,55 @@
{% extends "base" %}
{% block page %}
<body class=" d-flex flex-column">
<script src="/js/demo-theme.min.js"></script>
<div>
<div class="container container-tight py-4">
<div class="text-center mb-4">
{% include "utils/logo" %}
</div>
{% if flash %}
<div class="alert alert-{{flash.0}}" role="alert">
<h4 class="alert-title">Failed to sign in</h4>
<div class="text-muted">{{ flash.1 }}</div>
</div>
{% endif %}
<div class="card card-md">
<div class="card-body">
<div class="text-center mb-2">
<h2 class="h2">Sign in</h2>
<p class="text-muted">With your {{ business_name }} account</p>
</div>
<form action="/auth/sign_in" method="post" autocomplete="off" novalidate>
<div class="mb-3">
<label class="form-label">Login</label>
<input name="login" type="text" class="form-control" placeholder="Email or username"
autocomplete="off">
</div>
<div class="mb-2">
<label class="form-label">Password</label>
<div class="input-group input-group-flat">
<input name="password" type="password" class="form-control" placeholder="Your password"
autocomplete="off">
</div>
</div>
<div class="form-footer">
<button type="submit" class="btn btn-primary w-100">Sign in</button>
</div>
</form>
</div>
</div>
<div class="text-center text-muted mt-3">
<a href="./sign-up.html" tabindex="-1">Reset password</a>
</div>
{% include "shell/footer" %}
</div>
</div>
<!-- Libs JS -->
<!-- Tabler Core -->
<script src="/js/tabler.min.js" defer></script>
<script src="/js/demo.min.js" defer></script>
</body>
{% endblock page %}

View file

@ -3,7 +3,7 @@
{% block page %} {% block page %}
<body class=" d-flex flex-column"> <body class=" d-flex flex-column">
<script src="/js/demo-theme.min.js"></script> <script src="/js/demo-theme.min.js"></script>
<div class=""> <div>
<div class="container container-tight py-4"> <div class="container container-tight py-4">
<div class="text-center mb-4"> <div class="text-center mb-4">
{% include "utils/logo" %} {% include "utils/logo" %}
@ -28,12 +28,14 @@
<div class="card-body"> <div class="card-body">
<div class="mb-3"> <div class="mb-3">
<label class="form-label required" for="username">Username</label> <label class="form-label required" for="username">Username</label>
<input name="username" id="username" type="text" placeholder="Enter a username" class="form-control" required> <input name="username" id="username" type="text" placeholder="Enter a username"
class="form-control" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label required" for="password">Password</label> <label class="form-label required" for="password">Password</label>
<div class="input-group input-group-flat"> <div class="input-group input-group-flat">
<input name="password" id="password" type="password" placeholder="Enter password" class="form-control" autocomplete="off" required> <input name="password" id="password" type="password" placeholder="Enter password"
class="form-control" autocomplete="off" required>
</div> </div>
</div> </div>
</div> </div>
@ -43,7 +45,8 @@
<div class="card-body"> <div class="card-body">
<div class="mb-3"> <div class="mb-3">
<label class="form-label required" for="url">Base URL</label> <label class="form-label required" for="url">Base URL</label>
<input name="url" id="url" type="url" placeholder="https://example.com" class="form-control" required> <input name="url" id="url" type="url" placeholder="https://example.com" class="form-control"
required>
</div> </div>
</div> </div>
</div> </div>

View file

@ -20,4 +20,7 @@ impl Settings {
pub fn business_logo(&self) -> &[u8] { pub fn business_logo(&self) -> &[u8] {
self.business_logo.as_slice() self.business_logo.as_slice()
} }
pub fn business_name(&self) -> &str {
&self.business_name
}
} }

View file

@ -20,3 +20,12 @@ pub struct User {
paper_key: Option<String>, paper_key: Option<String>,
is_archived: bool, is_archived: bool,
} }
impl User {
pub fn is_archived(&self) -> bool {
self.is_archived
}
pub fn password_hashed(&self) -> Option<&str> {
self.password.as_deref()
}
}