auth: wip: sign in page and form, need to finish
This commit is contained in:
parent
d790d2ff29
commit
1695eca466
9 changed files with 179 additions and 6 deletions
|
|
@ -13,6 +13,7 @@ pub enum Page {
|
|||
Error(Error),
|
||||
Setup,
|
||||
Homepage(Homepage),
|
||||
SignIn(SignIn),
|
||||
}
|
||||
|
||||
impl Page {
|
||||
|
|
@ -22,6 +23,7 @@ impl Page {
|
|||
Page::Error(_) => "pages/error",
|
||||
Page::Setup => "pages/setup",
|
||||
Page::Homepage(_) => "pages/homepage",
|
||||
Page::SignIn(_) => "pages/auth/sign_in",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -31,6 +33,7 @@ impl Page {
|
|||
Page::Error(_) => "Error",
|
||||
Page::Setup => "Setup",
|
||||
Page::Homepage(_) => "Home",
|
||||
Page::SignIn(_) => "Sign in",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -40,6 +43,7 @@ impl Page {
|
|||
Page::Error(_) => None,
|
||||
Page::Setup => None,
|
||||
Page::Homepage(_) => Some(Item::Home.into()),
|
||||
Page::SignIn(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -49,6 +53,7 @@ impl Page {
|
|||
Page::Error(error) => Box::new(error),
|
||||
Page::Setup => Box::new(()),
|
||||
Page::Homepage(homepage) => Box::new(homepage),
|
||||
Page::SignIn(sig_in) => Box::new(sig_in),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
pub use crate::error::content::*;
|
||||
pub use crate::routes::auth::content::*;
|
||||
pub use crate::routes::root::content::*;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use rocket::{Build, Rocket};
|
||||
|
||||
pub mod auth;
|
||||
pub mod root;
|
||||
pub mod setup;
|
||||
|
||||
|
|
@ -32,4 +33,5 @@ pub fn routes(rocket_builder: Rocket<Build>) -> Rocket<Build> {
|
|||
.mount("/", root::routes())
|
||||
// Setup
|
||||
.mount("/setup", setup::routes())
|
||||
.mount("/auth", auth::routes())
|
||||
}
|
||||
|
|
|
|||
95
crates/ezidam/src/routes/auth.rs
Normal file
95
crates/ezidam/src/routes/auth.rs
Normal 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!("/"))))
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ async fn setup(flash: Option<FlashMessage<'_>>) -> Template {
|
|||
}
|
||||
|
||||
#[derive(Debug, FromForm)]
|
||||
pub struct CreateFirstAccount<'r> {
|
||||
struct CreateFirstAccount<'r> {
|
||||
pub username: &'r str,
|
||||
pub password: &'r str,
|
||||
pub url: &'r str,
|
||||
|
|
@ -40,7 +40,7 @@ async fn create_first_account(
|
|||
Ok(url) => url,
|
||||
Err(e) => {
|
||||
return Ok(Either::Right(Flash::new(
|
||||
Redirect::to(uri!(self::setup)),
|
||||
Redirect::to(uri!("/setup")),
|
||||
FlashKind::Danger,
|
||||
e.to_string(),
|
||||
)));
|
||||
|
|
|
|||
55
crates/ezidam/templates/pages/auth/sign_in.html.tera
Normal file
55
crates/ezidam/templates/pages/auth/sign_in.html.tera
Normal 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 %}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
{% block page %}
|
||||
<body class=" d-flex flex-column">
|
||||
<script src="/js/demo-theme.min.js"></script>
|
||||
<div class="">
|
||||
<div>
|
||||
<div class="container container-tight py-4">
|
||||
<div class="text-center mb-4">
|
||||
{% include "utils/logo" %}
|
||||
|
|
@ -28,12 +28,14 @@
|
|||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<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 class="mb-3">
|
||||
<label class="form-label required" for="password">Password</label>
|
||||
<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>
|
||||
|
|
@ -43,7 +45,8 @@
|
|||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -20,4 +20,7 @@ impl Settings {
|
|||
pub fn business_logo(&self) -> &[u8] {
|
||||
self.business_logo.as_slice()
|
||||
}
|
||||
pub fn business_name(&self) -> &str {
|
||||
&self.business_name
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,3 +20,12 @@ pub struct User {
|
|||
paper_key: Option<String>,
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue