feat: introduce htmx for login page

This commit is contained in:
Max Hohlfeld 2024-05-12 20:04:49 +02:00
parent 76b0d70a01
commit 564d1b5946
16 changed files with 172 additions and 11882 deletions

View File

@ -1,13 +1,9 @@
mod get_login;
mod get_register;
mod post_login;
mod post_register;
use actix_web::web;
pub fn init(cfg: &mut web::ServiceConfig) {
cfg.service(self::get_login::route);
cfg.service(self::post_login::route);
cfg.service(self::get_register::route);
cfg.service(self::post_register::route);
}

View File

@ -30,6 +30,8 @@ pub fn init(cfg: &mut ServiceConfig) {
cfg.service(user::patch::patch);
cfg.service(user::delete::delete);
cfg.service(user::get_logout::get);
cfg.service(user::get_login::get);
cfg.service(user::post_login::post);
cfg.service(events::get_new::get);
cfg.service(events::post_new::post);

View File

@ -3,13 +3,13 @@ use actix_web::{Responder, HttpResponse, http::header::LOCATION};
use askama::Template;
#[derive(Template)]
#[template(path = "login.html")]
#[template(path = "user/login.html")]
struct LoginTemplate {}
#[actix_web::get("/login")]
async fn route(user: Option<Identity>) -> impl Responder {
async fn get(user: Option<Identity>) -> impl Responder {
if let Some(_) = user {
return HttpResponse::PermanentRedirect()
return HttpResponse::Found()
.insert_header((LOCATION, "/"))
.finish();
} else {

View File

@ -6,3 +6,5 @@ pub mod post_edit;
pub mod patch;
pub mod delete;
pub mod get_logout;
pub mod get_login;
pub mod post_login;

View File

@ -1,5 +1,5 @@
use actix_identity::Identity;
use actix_web::{http::header::LOCATION, web, HttpMessage, HttpRequest, HttpResponse, Responder};
use actix_web::{web, HttpMessage, HttpRequest, HttpResponse, Responder};
use serde::Deserialize;
use sqlx::PgPool;
@ -12,26 +12,25 @@ struct LoginForm {
}
#[actix_web::post("/login")]
async fn route(
async fn post(
web::Form(form): web::Form<LoginForm>,
request: HttpRequest,
pool: web::Data<PgPool>,
) -> impl Responder {
if let Ok(result) = User::read_for_login(pool.get_ref(), &form.email).await {
if let Some(user) = result {
let hash = hash_plain_password_with_salt(&form.password, &user.salt).unwrap();
if hash == user.password {
Identity::login(&request.extensions(), user.id.to_string()).unwrap();
return HttpResponse::Found().insert_header((LOCATION, "/")).finish();
} else {
return HttpResponse::Unauthorized().body("Nutzername oder Passwort falsch.");
User::update_login_timestamp(pool.get_ref(), user.id).await.unwrap();
return HttpResponse::Found()
.insert_header(("HX-LOCATION", "/"))
.finish();
}
} else {
return HttpResponse::Unauthorized().body("Nutzername oder Passwort falsch.");
}
} else {
return HttpResponse::Unauthorized().body("Nutzername oder Passwort falsch.");
}
return HttpResponse::BadRequest().body("E-Mail oder Passwort falsch.");
}

View File

@ -92,7 +92,7 @@ async fn main() -> anyhow::Result<()> {
exit(1)
}
let (hash, salt) = generate_salt_and_hash_plain_password("admin")?;
let (hash, salt) = generate_salt_and_hash_plain_password(&password)?;
User::create(
&pool,

View File

@ -108,7 +108,7 @@ impl User {
lastLogin,
receiveNotifications
FROM user_
WHERE email = $1;
WHERE email = $1 AND locked = FALSE;
"#,
email,
)
@ -276,7 +276,7 @@ impl User {
role: Option<Role>,
function: Option<Function>,
area_id: Option<i32>,
locked: Option<bool>
locked: Option<bool>,
) -> anyhow::Result<()> {
let mut query_builder = sqlx::QueryBuilder::new("UPDATE user_ SET ");
let mut separated = query_builder.separated(", ");
@ -315,7 +315,13 @@ impl User {
query_builder.push_bind(id);
query_builder.push(";");
query_builder.build()
query_builder.build().execute(pool).await?;
Ok(())
}
pub async fn update_login_timestamp(pool: &PgPool, id: i32) -> anyhow::Result<()> {
sqlx::query!("UPDATE user_ SET lastLogin = NOW() WHERE id = $1;", id)
.execute(pool)
.await?;
@ -323,9 +329,10 @@ impl User {
}
pub async fn delete(pool: &PgPool, id: i32) -> anyhow::Result<()> {
sqlx::query!("DELETE FROM user_ WHERE id = $1", id)
sqlx::query!("DELETE FROM user_ WHERE id = $1;", id)
.execute(pool)
.await?;
Ok(())
}
}

BIN
static/brass.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

11851
static/bulma.css vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
static/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

129
static/response-targets.js Normal file
View File

@ -0,0 +1,129 @@
(function() {
/** @type {import("../htmx").HtmxInternalApi} */
var api
var attrPrefix = 'hx-target-'
// IE11 doesn't support string.startsWith
function startsWith(str, prefix) {
return str.substring(0, prefix.length) === prefix
}
/**
* @param {HTMLElement} elt
* @param {number} respCode
* @returns {HTMLElement | null}
*/
function getRespCodeTarget(elt, respCodeNumber) {
if (!elt || !respCodeNumber) return null
var respCode = respCodeNumber.toString()
// '*' is the original syntax, as the obvious character for a wildcard.
// The 'x' alternative was added for maximum compatibility with HTML
// templating engines, due to ambiguity around which characters are
// supported in HTML attributes.
//
// Start with the most specific possible attribute and generalize from
// there.
var attrPossibilities = [
respCode,
respCode.substr(0, 2) + '*',
respCode.substr(0, 2) + 'x',
respCode.substr(0, 1) + '*',
respCode.substr(0, 1) + 'x',
respCode.substr(0, 1) + '**',
respCode.substr(0, 1) + 'xx',
'*',
'x',
'***',
'xxx'
]
if (startsWith(respCode, '4') || startsWith(respCode, '5')) {
attrPossibilities.push('error')
}
for (var i = 0; i < attrPossibilities.length; i++) {
var attr = attrPrefix + attrPossibilities[i]
var attrValue = api.getClosestAttributeValue(elt, attr)
if (attrValue) {
if (attrValue === 'this') {
return api.findThisElement(elt, attr)
} else {
return api.querySelectorExt(elt, attrValue)
}
}
}
return null
}
/** @param {Event} evt */
function handleErrorFlag(evt) {
if (evt.detail.isError) {
if (htmx.config.responseTargetUnsetsError) {
evt.detail.isError = false
}
} else if (htmx.config.responseTargetSetsError) {
evt.detail.isError = true
}
}
htmx.defineExtension('response-targets', {
/** @param {import("../htmx").HtmxInternalApi} apiRef */
init: function(apiRef) {
api = apiRef
if (htmx.config.responseTargetUnsetsError === undefined) {
htmx.config.responseTargetUnsetsError = true
}
if (htmx.config.responseTargetSetsError === undefined) {
htmx.config.responseTargetSetsError = false
}
if (htmx.config.responseTargetPrefersExisting === undefined) {
htmx.config.responseTargetPrefersExisting = false
}
if (htmx.config.responseTargetPrefersRetargetHeader === undefined) {
htmx.config.responseTargetPrefersRetargetHeader = true
}
},
/**
* @param {string} name
* @param {Event} evt
*/
onEvent: function(name, evt) {
if (name === 'htmx:beforeSwap' &&
evt.detail.xhr &&
evt.detail.xhr.status !== 200) {
if (evt.detail.target) {
if (htmx.config.responseTargetPrefersExisting) {
evt.detail.shouldSwap = true
handleErrorFlag(evt)
return true
}
if (htmx.config.responseTargetPrefersRetargetHeader &&
evt.detail.xhr.getAllResponseHeaders().match(/HX-Retarget:/i)) {
evt.detail.shouldSwap = true
handleErrorFlag(evt)
return true
}
}
if (!evt.detail.requestConfig) {
return true
}
var target = getRespCodeTarget(evt.detail.requestConfig.elt, evt.detail.xhr.status)
if (target) {
handleErrorFlag(evt)
evt.detail.shouldSwap = true
evt.detail.target = target
}
return true
}
}
})
})()

View File

@ -5,10 +5,12 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Brass - Brasiwa Leipzig</title>
<link rel="stylesheet" href="/static/bulma.css">
<link rel="stylesheet" href="/static/bulma.min.css">
<script src="/static/htmx.min.js"></script>
<script src="/static/response-targets.js"></script>
</head>
<body>
<body hx-ext="response-targets">
{% block body %}
{% endblock %}
</body>

View File

@ -4,7 +4,7 @@
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/">
BRASS
<img src="/static/brass.jpeg" height="240" />
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">

View File

@ -4,22 +4,24 @@
<section class="section">
<div class="container">
<h1 class="title">Brass - Anmeldung</h1>
<form class="box">
<form class="box" hx-post="/login" hx-target-400="#error-message" hx-on:change="document.getElementById('error-message').innerHTML = ''">
<div class="field">
<label class="label" for="email">E-Mail:</label>
<div class="control">
<input class="input" placeholder="max.mustermann@example.com" name="email" type="text">
<input class="input" placeholder="max.mustermann@example.com" name="email" type="text" required>
</div>
</div>
<div class="field">
<label class="label" for="password">Passwort:</label>
<div class="control">
<input class="input" placeholder="**********" name="password" type="password">
<input class="input" placeholder="**********" name="password" type="password" required>
</div>
</div>
<input class="button is-primary" type="submit" value="Anmelden" formmethod="post">
<div id="error-message" class="mb-3 help is-danger"></div>
<input class="button is-primary" type="submit" value="Anmelden">
</form>
</div>
</section>