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 get_register;
|
||||||
mod post_login;
|
|
||||||
mod post_register;
|
mod post_register;
|
||||||
|
|
||||||
use actix_web::web;
|
use actix_web::web;
|
||||||
|
|
||||||
pub fn init(cfg: &mut web::ServiceConfig) {
|
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::get_register::route);
|
||||||
cfg.service(self::post_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::patch::patch);
|
||||||
cfg.service(user::delete::delete);
|
cfg.service(user::delete::delete);
|
||||||
cfg.service(user::get_logout::get);
|
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::get_new::get);
|
||||||
cfg.service(events::post_new::post);
|
cfg.service(events::post_new::post);
|
||||||
|
@ -3,13 +3,13 @@ use actix_web::{Responder, HttpResponse, http::header::LOCATION};
|
|||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "login.html")]
|
#[template(path = "user/login.html")]
|
||||||
struct LoginTemplate {}
|
struct LoginTemplate {}
|
||||||
|
|
||||||
#[actix_web::get("/login")]
|
#[actix_web::get("/login")]
|
||||||
async fn route(user: Option<Identity>) -> impl Responder {
|
async fn get(user: Option<Identity>) -> impl Responder {
|
||||||
if let Some(_) = user {
|
if let Some(_) = user {
|
||||||
return HttpResponse::PermanentRedirect()
|
return HttpResponse::Found()
|
||||||
.insert_header((LOCATION, "/"))
|
.insert_header((LOCATION, "/"))
|
||||||
.finish();
|
.finish();
|
||||||
} else {
|
} else {
|
@ -6,3 +6,5 @@ pub mod post_edit;
|
|||||||
pub mod patch;
|
pub mod patch;
|
||||||
pub mod delete;
|
pub mod delete;
|
||||||
pub mod get_logout;
|
pub mod get_logout;
|
||||||
|
pub mod get_login;
|
||||||
|
pub mod post_login;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use actix_identity::Identity;
|
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 serde::Deserialize;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
@ -12,26 +12,25 @@ struct LoginForm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::post("/login")]
|
#[actix_web::post("/login")]
|
||||||
async fn route(
|
async fn post(
|
||||||
web::Form(form): web::Form<LoginForm>,
|
web::Form(form): web::Form<LoginForm>,
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
|
|
||||||
if let Ok(result) = User::read_for_login(pool.get_ref(), &form.email).await {
|
if let Ok(result) = User::read_for_login(pool.get_ref(), &form.email).await {
|
||||||
if let Some(user) = result {
|
if let Some(user) = result {
|
||||||
let hash = hash_plain_password_with_salt(&form.password, &user.salt).unwrap();
|
let hash = hash_plain_password_with_salt(&form.password, &user.salt).unwrap();
|
||||||
if hash == user.password {
|
if hash == user.password {
|
||||||
Identity::login(&request.extensions(), user.id.to_string()).unwrap();
|
Identity::login(&request.extensions(), user.id.to_string()).unwrap();
|
||||||
|
|
||||||
return HttpResponse::Found().insert_header((LOCATION, "/")).finish();
|
User::update_login_timestamp(pool.get_ref(), user.id).await.unwrap();
|
||||||
} else {
|
|
||||||
return HttpResponse::Unauthorized().body("Nutzername oder Passwort falsch.");
|
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)
|
exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
let (hash, salt) = generate_salt_and_hash_plain_password("admin")?;
|
let (hash, salt) = generate_salt_and_hash_plain_password(&password)?;
|
||||||
|
|
||||||
User::create(
|
User::create(
|
||||||
&pool,
|
&pool,
|
||||||
|
@ -108,7 +108,7 @@ impl User {
|
|||||||
lastLogin,
|
lastLogin,
|
||||||
receiveNotifications
|
receiveNotifications
|
||||||
FROM user_
|
FROM user_
|
||||||
WHERE email = $1;
|
WHERE email = $1 AND locked = FALSE;
|
||||||
"#,
|
"#,
|
||||||
email,
|
email,
|
||||||
)
|
)
|
||||||
@ -276,7 +276,7 @@ impl User {
|
|||||||
role: Option<Role>,
|
role: Option<Role>,
|
||||||
function: Option<Function>,
|
function: Option<Function>,
|
||||||
area_id: Option<i32>,
|
area_id: Option<i32>,
|
||||||
locked: Option<bool>
|
locked: Option<bool>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let mut query_builder = sqlx::QueryBuilder::new("UPDATE user_ SET ");
|
let mut query_builder = sqlx::QueryBuilder::new("UPDATE user_ SET ");
|
||||||
let mut separated = query_builder.separated(", ");
|
let mut separated = query_builder.separated(", ");
|
||||||
@ -315,7 +315,13 @@ impl User {
|
|||||||
query_builder.push_bind(id);
|
query_builder.push_bind(id);
|
||||||
query_builder.push(";");
|
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)
|
.execute(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -323,9 +329,10 @@ impl User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete(pool: &PgPool, id: i32) -> anyhow::Result<()> {
|
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)
|
.execute(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
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 charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Brass - Brasiwa Leipzig</title>
|
<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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body hx-ext="response-targets">
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<a class="navbar-item" href="/">
|
<a class="navbar-item" href="/">
|
||||||
BRASS
|
<img src="/static/brass.jpeg" height="240" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
|
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
|
||||||
|
@ -4,22 +4,24 @@
|
|||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1 class="title">Brass - Anmeldung</h1>
|
<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">
|
<div class="field">
|
||||||
<label class="label" for="email">E-Mail:</label>
|
<label class="label" for="email">E-Mail:</label>
|
||||||
<div class="control">
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="password">Passwort:</label>
|
<label class="label" for="password">Passwort:</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" placeholder="**********" name="password" type="password">
|
<input class="input" placeholder="**********" name="password" type="password" required>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
Loading…
x
Reference in New Issue
Block a user