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_links;
|
||||||
use pages::list_users;
|
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 seed::{div, log, prelude::*, App, Url, C};
|
||||||
|
use shared::apirequests::users::LoginUser;
|
||||||
use shared::datatypes::User;
|
use shared::datatypes::User;
|
||||||
|
|
||||||
use crate::i18n::{I18n, Lang};
|
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()),
|
page: Page::init(url, orders, lang.clone()),
|
||||||
i18n: lang,
|
i18n: lang,
|
||||||
user: None,
|
user: None,
|
||||||
|
login_form: LoginForm::default(),
|
||||||
|
login_data: LoginUser::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,6 +48,14 @@ struct Model {
|
|||||||
page: Page,
|
page: Page,
|
||||||
i18n: i18n::I18n,
|
i18n: i18n::I18n,
|
||||||
user: Option<User>,
|
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)]
|
#[derive(Debug)]
|
||||||
@ -83,6 +98,10 @@ pub enum Msg {
|
|||||||
GetLoggedUser,
|
GetLoggedUser,
|
||||||
UserReceived(User),
|
UserReceived(User),
|
||||||
NoMessage,
|
NoMessage,
|
||||||
|
NotAuthenticated,
|
||||||
|
Login,
|
||||||
|
UsernameChanged(String),
|
||||||
|
PasswordChanged(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
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::NoMessage => (),
|
||||||
Msg::GetLoggedUser => {
|
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 {
|
orders.perform_cmd(async {
|
||||||
let response = fetch(
|
// create request
|
||||||
|
let request = unwrap_or_return!(
|
||||||
Request::new("/admin/json/get_logged_user/")
|
Request::new("/admin/json/get_logged_user/")
|
||||||
.method(Method::Post)
|
.method(Method::Post)
|
||||||
.json(&())
|
.json(&()),
|
||||||
.expect("serialization failed"),
|
Msg::NotAuthenticated
|
||||||
)
|
);
|
||||||
.await
|
// perform and get response
|
||||||
.expect("HTTP request failed");
|
let response = unwrap_or_return!(fetch(request).await, Msg::NotAuthenticated);
|
||||||
|
// validate response status
|
||||||
let user: User = response
|
let response = unwrap_or_return!(response.check_status(), Msg::NotAuthenticated);
|
||||||
.check_status() // ensure we've got 2xx status
|
let user: User = unwrap_or_return!(response.json().await, Msg::NotAuthenticated);
|
||||||
.expect("status check failed")
|
|
||||||
.json()
|
|
||||||
.await
|
|
||||||
.expect("deserialization failed");
|
|
||||||
|
|
||||||
Msg::UserReceived(user)
|
Msg::UserReceived(user)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Msg::UserReceived(user) => model.user = Some(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> {
|
pub struct Urls<'a> {
|
||||||
base_url: std::borrow::Cow<'a, Url>,
|
base_url: std::borrow::Cow<'a, Url>,
|
||||||
}
|
}
|
||||||
@ -188,8 +238,14 @@ impl<'a> Urls<'a> {
|
|||||||
fn view(model: &Model) -> Node<Msg> {
|
fn view(model: &Model) -> Node<Msg> {
|
||||||
div![
|
div![
|
||||||
C!["page"],
|
C!["page"],
|
||||||
navigation::navigation(&model.i18n, &model.base_url, &model.user),
|
if let Some(user) = &model.user {
|
||||||
view_content(&model.page, &model.base_url),
|
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
|
// Start
|
||||||
// ------ ------
|
// ------ ------
|
||||||
|
@ -8,18 +8,14 @@ use crate::{i18n::I18n, Msg};
|
|||||||
///
|
///
|
||||||
/// The menu options are translated using the i18n module.
|
/// The menu options are translated using the i18n module.
|
||||||
#[must_use]
|
#[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.
|
// A shortcut for translating strings.
|
||||||
let t = move |key: &str| i18n.translate(key, None);
|
let t = move |key: &str| i18n.translate(key, None);
|
||||||
// Translate the wellcome message
|
// Translate the wellcome message
|
||||||
let welcome = if let Some(user) = user {
|
let welcome = i18n.translate(
|
||||||
i18n.translate(
|
|
||||||
"welcome-user",
|
"welcome-user",
|
||||||
Some(&fluent_args![ "username" => user.username.clone()]),
|
Some(&fluent_args![ "username" => user.username.clone()]),
|
||||||
)
|
);
|
||||||
} else {
|
|
||||||
t("welcome")
|
|
||||||
};
|
|
||||||
nav![
|
nav![
|
||||||
ol![
|
ol![
|
||||||
// A button for the homepage, the list of URLs
|
// 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
|
// The Welcome message
|
||||||
li![div![welcome]],
|
li![div![welcome]],
|
||||||
// The logout button
|
// The logout button
|
||||||
li![a![
|
li![a![ev(Ev::Click, |_| Msg::NotAuthenticated), t("logout"),]]
|
||||||
attrs! {At::Href => "/admin/logout/"},
|
|
||||||
t("logout"),
|
|
||||||
]]
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -281,7 +281,7 @@ pub async fn webservice(
|
|||||||
.wrap(IdentityService::new(
|
.wrap(IdentityService::new(
|
||||||
CookieIdentityPolicy::new(&[0; 32])
|
CookieIdentityPolicy::new(&[0; 32])
|
||||||
.name("auth-cookie")
|
.name("auth-cookie")
|
||||||
.secure(false),
|
.secure(true),
|
||||||
))
|
))
|
||||||
.data(tera.clone())
|
.data(tera.clone())
|
||||||
.service(actix_web_static_files::ResourceFiles::new(
|
.service(actix_web_static_files::ResourceFiles::new(
|
||||||
@ -377,6 +377,10 @@ pub async fn webservice(
|
|||||||
.route(
|
.route(
|
||||||
"/get_logged_user/",
|
"/get_logged_user/",
|
||||||
web::post().to(views::get_logged_user_json),
|
web::post().to(views::get_logged_user_json),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/login_user/",
|
||||||
|
web::post().to(views::process_login_json),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
// login to the admin area
|
// 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]
|
#[async_trait]
|
||||||
pub trait LinkDbOperations<T> {
|
pub trait LinkDbOperations<T> {
|
||||||
async fn get_link_by_code(code: &str, server_config: &ServerConfig) -> Result<T, ServerError>;
|
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::{
|
use shared::apirequests::{
|
||||||
general::{Message, Status},
|
general::{Message, Status},
|
||||||
links::{LinkDelta, LinkRequestForm},
|
links::{LinkDelta, LinkRequestForm},
|
||||||
users::{UserDelta, UserRequestForm},
|
users::{LoginUser, UserDelta, UserRequestForm},
|
||||||
};
|
};
|
||||||
use tera::{Context, Tera};
|
use tera::{Context, Tera};
|
||||||
use tracing::{error, info, instrument, warn};
|
use tracing::{error, info, instrument, warn};
|
||||||
|
|
||||||
use crate::forms::LinkForm;
|
use crate::forms::LinkForm;
|
||||||
use crate::models::{LoginUser, NewUser};
|
use crate::models::NewUser;
|
||||||
use crate::queries;
|
use crate::queries;
|
||||||
use crate::ServerError;
|
use crate::ServerError;
|
||||||
|
|
||||||
@ -74,7 +74,6 @@ pub async fn wasm_app(config: web::Data<crate::ServerConfig>) -> Result<HttpResp
|
|||||||
Ok(HttpResponse::Ok().body(
|
Ok(HttpResponse::Ok().body(
|
||||||
r#"<!DOCTYPE html>
|
r#"<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
<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">
|
<link rel="stylesheet" href="/static/admin.css">
|
||||||
<title>Server integration example</title>
|
<title>Server integration example</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<section id="app"><div class="lds-ellipsis">Loading: <div></div><div></div><div></div><div></div></div></section>
|
<section id="app"><div class="lds-ellipsis">Loading: <div></div><div></div><div></div><div></div></div></section>
|
||||||
<script type="module">
|
<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');
|
init('/app/pkg/package_bg.wasm');
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>"#,
|
</html>"#,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@ -178,7 +175,7 @@ pub async fn get_logged_user_json(
|
|||||||
let user = authenticate(&id, &config).await?;
|
let user = authenticate(&id, &config).await?;
|
||||||
match user {
|
match user {
|
||||||
Role::NotAuthenticated | Role::Disabled => {
|
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)),
|
Role::Regular { user } | Role::Admin { user } => Ok(HttpResponse::Ok().json2(&user)),
|
||||||
}
|
}
|
||||||
@ -484,8 +481,58 @@ pub async fn process_login(
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
info!("Failed to login: {}", 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/"))
|
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;
|
background-color: #eae9ea;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.login div {
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
div.login input {
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.center table p {
|
.center table p {
|
||||||
font-size: x-small;
|
font-size: x-small;
|
||||||
margin: 0;
|
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.
|
/// The Struct that is responsible for creating and editing users.
|
||||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct UserDelta {
|
pub struct UserDelta {
|
||||||
|
Loading…
Reference in New Issue
Block a user