Login for the wasm interface

This commit is contained in:
Franz Dietrich 2021-05-26 20:10:01 +02:00
parent e5d8e6c62f
commit 1aba33fb91
7 changed files with 187 additions and 44 deletions

View File

@ -4,7 +4,12 @@ pub mod pages;
use pages::list_links;
use pages::list_users;
use seed::attrs;
use seed::button;
use seed::input;
use seed::label;
use seed::{div, log, prelude::*, App, Url, C};
use shared::apirequests::users::LoginUser;
use shared::datatypes::User;
use crate::i18n::{I18n, Lang};
@ -27,6 +32,8 @@ fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
page: Page::init(url, orders, lang.clone()),
i18n: lang,
user: None,
login_form: LoginForm::default(),
login_data: LoginUser::default(),
}
}
@ -41,6 +48,14 @@ struct Model {
page: Page,
i18n: i18n::I18n,
user: Option<User>,
login_form: LoginForm,
login_data: LoginUser,
}
#[derive(Default, Debug)]
struct LoginForm {
username: ElRef<web_sys::HtmlInputElement>,
password: ElRef<web_sys::HtmlInputElement>,
}
#[derive(Debug)]
@ -83,6 +98,10 @@ pub enum Msg {
GetLoggedUser,
UserReceived(User),
NoMessage,
NotAuthenticated,
Login,
UsernameChanged(String),
PasswordChanged(String),
}
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
@ -102,31 +121,62 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}
Msg::NoMessage => (),
Msg::GetLoggedUser => {
orders.skip(); // No need to rerender/ complicated way to move into the closure
orders.skip(); // No need to rerender
orders.perform_cmd(async {
let response = fetch(
// create request
let request = unwrap_or_return!(
Request::new("/admin/json/get_logged_user/")
.method(Method::Post)
.json(&())
.expect("serialization failed"),
)
.await
.expect("HTTP request failed");
let user: User = response
.check_status() // ensure we've got 2xx status
.expect("status check failed")
.json()
.await
.expect("deserialization failed");
.json(&()),
Msg::NotAuthenticated
);
// perform and get response
let response = unwrap_or_return!(fetch(request).await, Msg::NotAuthenticated);
// validate response status
let response = unwrap_or_return!(response.check_status(), Msg::NotAuthenticated);
let user: User = unwrap_or_return!(response.json().await, Msg::NotAuthenticated);
Msg::UserReceived(user)
});
}
Msg::UserReceived(user) => model.user = Some(user),
Msg::NotAuthenticated => {if model.user.is_some() {model.user = None; logout(orders)}},
Msg::Login => {login_user(model, orders)}
Msg::UsernameChanged(s) => model.login_data.username = s,
Msg::PasswordChanged(s) => model.login_data.password = s,
}
}
fn logout(orders: &mut impl Orders<Msg>) {
orders.perform_cmd(async {let request = Request::new("/admin/logout/");
unwrap_or_return!(fetch(request).await, Msg::GetLoggedUser);
Msg::NotAuthenticated});
}
fn login_user(model: &mut Model, orders: &mut impl Orders<Msg>) {
orders.skip(); // No need to rerender
let data = model.login_data.clone();
orders.perform_cmd(async {
let data = data;
// create request
let request = unwrap_or_return!(
Request::new("/admin/json/login_user/")
.method(Method::Post)
.json(&data),
Msg::NotAuthenticated
);
// perform and get response
let response = unwrap_or_return!(fetch(request).await, Msg::NotAuthenticated);
// validate response status
let response = unwrap_or_return!(response.check_status(), Msg::NotAuthenticated);
let user: User = unwrap_or_return!(response.json().await, Msg::NotAuthenticated);
Msg::UserReceived(user)
});
}
pub struct Urls<'a> {
base_url: std::borrow::Cow<'a, Url>,
}
@ -188,8 +238,14 @@ impl<'a> Urls<'a> {
fn view(model: &Model) -> Node<Msg> {
div![
C!["page"],
navigation::navigation(&model.i18n, &model.base_url, &model.user),
view_content(&model.page, &model.base_url),
if let Some(user) = &model.user {
div![
navigation::navigation(&model.i18n, &model.base_url, user),
view_content(&model.page, &model.base_url)
]
} else {
view_login(&model.i18n, &model)
}
]
}
@ -205,6 +261,39 @@ fn view_content(page: &Page, url: &Url) -> Node<Msg> {
]
}
fn view_login(lang: &I18n, model: &Model) -> Node<Msg> {
let t = move |key: &str| lang.translate(key, None);
div![
C!["center", "login"],
div![
label![t("username")],
input![
input_ev(Ev::Input, |s| { Msg::UsernameChanged(s) }),
attrs![
At::Type => "text",
At::Placeholder => t("username"),
At::Name => "username",
At::Value => model.login_data.username],
el_ref(&model.login_form.username)
]
],
div![
label![t("password")],
input![
input_ev(Ev::Input, |s| { Msg::PasswordChanged(s) }),
attrs![
At::Type => "password",
At::Placeholder => t("password"),
At::Name => "password",
At::Value => model.login_data.password],
el_ref(&model.login_form.password)
]
],
button![t("login"), ev(Ev::Click, |_| Msg::Login)]
]
}
// ------ ------
// Start
// ------ ------

View File

@ -8,18 +8,14 @@ use crate::{i18n::I18n, Msg};
///
/// The menu options are translated using the i18n module.
#[must_use]
pub fn navigation(i18n: &I18n, base_url: &Url, user: &Option<User>) -> Node<Msg> {
pub fn navigation(i18n: &I18n, base_url: &Url, user: &User) -> Node<Msg> {
// A shortcut for translating strings.
let t = move |key: &str| i18n.translate(key, None);
// Translate the wellcome message
let welcome = if let Some(user) = user {
i18n.translate(
let welcome = i18n.translate(
"welcome-user",
Some(&fluent_args![ "username" => user.username.clone()]),
)
} else {
t("welcome")
};
);
nav![
ol![
// A button for the homepage, the list of URLs
@ -57,10 +53,7 @@ pub fn navigation(i18n: &I18n, base_url: &Url, user: &Option<User>) -> Node<Msg>
// The Welcome message
li![div![welcome]],
// The logout button
li![a![
attrs! {At::Href => "/admin/logout/"},
t("logout"),
]]
li![a![ev(Ev::Click, |_| Msg::NotAuthenticated), t("logout"),]]
]
]
}

View File

@ -281,7 +281,7 @@ pub async fn webservice(
.wrap(IdentityService::new(
CookieIdentityPolicy::new(&[0; 32])
.name("auth-cookie")
.secure(false),
.secure(true),
))
.data(tera.clone())
.service(actix_web_static_files::ResourceFiles::new(
@ -377,6 +377,10 @@ pub async fn webservice(
.route(
"/get_logged_user/",
web::post().to(views::get_logged_user_json),
)
.route(
"/login_user/",
web::post().to(views::process_login_json),
),
)
// login to the admin area

View File

@ -213,12 +213,6 @@ impl NewUser {
}
}
#[derive(Debug, Deserialize)]
pub struct LoginUser {
pub username: String,
pub password: String,
}
#[async_trait]
pub trait LinkDbOperations<T> {
async fn get_link_by_code(code: &str, server_config: &ServerConfig) -> Result<T, ServerError>;

View File

@ -17,13 +17,13 @@ use queries::{authenticate, Role};
use shared::apirequests::{
general::{Message, Status},
links::{LinkDelta, LinkRequestForm},
users::{UserDelta, UserRequestForm},
users::{LoginUser, UserDelta, UserRequestForm},
};
use tera::{Context, Tera};
use tracing::{error, info, instrument, warn};
use crate::forms::LinkForm;
use crate::models::{LoginUser, NewUser};
use crate::models::NewUser;
use crate::queries;
use crate::ServerError;
@ -74,7 +74,6 @@ pub async fn wasm_app(config: web::Data<crate::ServerConfig>) -> Result<HttpResp
Ok(HttpResponse::Ok().body(
r#"<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
@ -84,7 +83,6 @@ pub async fn wasm_app(config: web::Data<crate::ServerConfig>) -> Result<HttpResp
<link rel="stylesheet" href="/static/admin.css">
<title>Server integration example</title>
</head>
<body>
<section id="app"><div class="lds-ellipsis">Loading: <div></div><div></div><div></div><div></div></div></section>
<script type="module">
@ -92,7 +90,6 @@ pub async fn wasm_app(config: web::Data<crate::ServerConfig>) -> Result<HttpResp
init('/app/pkg/package_bg.wasm');
</script>
</body>
</html>"#,
))
}
@ -178,7 +175,7 @@ pub async fn get_logged_user_json(
let user = authenticate(&id, &config).await?;
match user {
Role::NotAuthenticated | Role::Disabled => {
Err(ServerError::User("User not logged in!".to_string()))
Ok(HttpResponse::Unauthorized().finish())
}
Role::Regular { user } | Role::Admin { user } => Ok(HttpResponse::Ok().json2(&user)),
}
@ -484,8 +481,58 @@ pub async fn process_login(
}
Err(e) => {
info!("Failed to login: {}", e);
Ok(HttpResponse::Unauthorized().json2(&Status::Error(Message {
message: "Failed to Login".to_string(),
})))
}
}
}
#[instrument(skip(id))]
pub async fn process_login_json(
data: web::Json<LoginUser>,
config: web::Data<crate::ServerConfig>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
// query the username to see if a user by that name exists.
let user = queries::get_user_by_name(&data.username, &config).await;
match user {
Ok(u) => {
// get the password hash
if let Some(hash) = &u.password.secret {
// get the servers secret
let secret = &config.secret;
// validate the secret
let valid = Verifier::default()
.with_hash(hash)
.with_password(&data.password)
.with_secret_key(secret.secret.as_ref().expect("No secret available"))
.verify()?;
// login the user
if valid {
info!("Log-in of user: {}", &u.username);
let session_token = u.username.clone();
id.remember(session_token);
Ok(HttpResponse::Ok().json2(&u))
} else {
info!("Invalid password for user: {}", &u.username);
Ok(redirect_builder("/admin/login/"))
}
} else {
// should fail earlier if secret is missing.
Ok(HttpResponse::Unauthorized().json2(&Status::Error(Message {
message: "Failed to Login".to_string(),
})))
}
}
Err(e) => {
info!("Failed to login: {}", e);
Ok(HttpResponse::Unauthorized().json2(&Status::Error(Message {
message: "Failed to Login".to_string(),
})))
}
}
}

View File

@ -35,6 +35,16 @@ form {
background-color: #eae9ea;
}
div.login div {
width: 100%;
height: 60px;
display: table;
}
div.login input {
padding: 15px;
margin-bottom: 20px;
}
.center table p {
font-size: x-small;
margin: 0;

View File

@ -25,6 +25,12 @@ impl Default for UserRequestForm {
}
}
#[derive(Debug, Deserialize, Default, Serialize, Clone)]
pub struct LoginUser {
pub username: String,
pub password: String,
}
/// The Struct that is responsible for creating and editing users.
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct UserDelta {