Login for the wasm interface
This commit is contained in:
parent
e5d8e6c62f
commit
1aba33fb91
121
app/src/lib.rs
121
app/src/lib.rs
@ -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
|
||||
// ------ ------
|
||||
|
@ -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"),]]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>;
|
||||
|
@ -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(),
|
||||
})))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user