feat: introduce htmx for login page
This commit is contained in:
parent
76b0d70a01
commit
564d1b5946
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
@ -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;
|
||||
|
@ -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.");
|
||||
}
|
@ -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,
|
||||
|
@ -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
BIN
static/brass.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
11851
static/bulma.css
vendored
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
4
static/bulma.min.css
vendored
4
static/bulma.min.css
vendored
File diff suppressed because one or more lines are too long
1
static/htmx.min.js
vendored
Normal file
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
129
static/response-targets.js
Normal 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
|
||||
}
|
||||
}
|
||||
})
|
||||
})()
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
Loading…
x
Reference in New Issue
Block a user