2021-06-16 14:29:24 +02:00
|
|
|
//! The admin interface of pslink. It communicates with the server mostly via https and json.
|
2021-05-04 11:29:36 +02:00
|
|
|
pub mod i18n;
|
|
|
|
pub mod navigation;
|
|
|
|
pub mod pages;
|
|
|
|
|
|
|
|
use pages::list_links;
|
|
|
|
use pages::list_users;
|
2021-05-31 06:55:56 +02:00
|
|
|
use seed::window;
|
2021-05-30 14:33:23 +02:00
|
|
|
use seed::{attrs, button, div, input, label, log, prelude::*, App, Url, C};
|
2021-05-26 20:10:01 +02:00
|
|
|
use shared::apirequests::users::LoginUser;
|
2021-06-13 12:58:55 +02:00
|
|
|
use shared::datatypes::Lang;
|
2021-05-30 14:33:23 +02:00
|
|
|
use shared::datatypes::{Loadable, User};
|
2021-05-04 11:29:36 +02:00
|
|
|
|
2021-06-13 12:58:55 +02:00
|
|
|
use crate::i18n::I18n;
|
2021-05-04 11:29:36 +02:00
|
|
|
|
|
|
|
// ------ ------
|
|
|
|
// Init
|
|
|
|
// ------ ------
|
|
|
|
|
|
|
|
fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
|
|
|
|
orders.subscribe(Msg::UrlChanged);
|
2021-05-20 11:40:44 +02:00
|
|
|
orders.send_msg(Msg::GetLoggedUser);
|
2021-05-04 11:29:36 +02:00
|
|
|
|
2021-05-25 20:13:56 +02:00
|
|
|
let lang = I18n::new(Lang::EnUS);
|
2021-05-04 11:29:36 +02:00
|
|
|
|
|
|
|
Model {
|
|
|
|
index: 0,
|
2021-05-31 06:55:56 +02:00
|
|
|
location: Location::new(url.clone()),
|
2021-05-04 11:29:36 +02:00
|
|
|
page: Page::init(url, orders, lang.clone()),
|
|
|
|
i18n: lang,
|
2021-05-27 07:26:39 +02:00
|
|
|
user: Loadable::Data(None),
|
2021-05-26 20:10:01 +02:00
|
|
|
login_form: LoginForm::default(),
|
|
|
|
login_data: LoginUser::default(),
|
2021-05-04 11:29:36 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ------ ------
|
|
|
|
// Model
|
|
|
|
// ------ ------
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
struct Model {
|
|
|
|
index: usize,
|
2021-05-31 06:55:56 +02:00
|
|
|
location: Location,
|
2021-05-04 11:29:36 +02:00
|
|
|
page: Page,
|
|
|
|
i18n: i18n::I18n,
|
2021-05-27 07:26:39 +02:00
|
|
|
user: Loadable<User>,
|
2021-05-26 20:10:01 +02:00
|
|
|
login_form: LoginForm,
|
|
|
|
login_data: LoginUser,
|
|
|
|
}
|
|
|
|
|
2021-06-13 12:58:55 +02:00
|
|
|
impl Model {
|
|
|
|
fn set_lang(&mut self, l: Lang) {
|
|
|
|
self.i18n.set_lang(l);
|
|
|
|
match &mut self.page {
|
|
|
|
Page::Home(ref mut m) => m.set_lang(l),
|
|
|
|
Page::ListUsers(ref mut m) => m.set_lang(l),
|
|
|
|
Page::NotFound => (),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-26 20:10:01 +02:00
|
|
|
#[derive(Default, Debug)]
|
|
|
|
struct LoginForm {
|
|
|
|
username: ElRef<web_sys::HtmlInputElement>,
|
|
|
|
password: ElRef<web_sys::HtmlInputElement>,
|
2021-05-04 11:29:36 +02:00
|
|
|
}
|
|
|
|
|
2021-05-31 06:55:56 +02:00
|
|
|
#[derive(Debug)]
|
|
|
|
struct Location {
|
|
|
|
host: String,
|
|
|
|
base_url: Url,
|
|
|
|
current_url: Url,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Location {
|
|
|
|
fn new(url: Url) -> Self {
|
|
|
|
let host = get_host();
|
|
|
|
Self {
|
|
|
|
host,
|
|
|
|
base_url: Url::new().add_path_part("app"),
|
|
|
|
current_url: url,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
pub fn get_host() -> String {
|
|
|
|
window()
|
|
|
|
.location()
|
|
|
|
.host()
|
|
|
|
.expect("Failed to extract the host of the url")
|
|
|
|
}
|
|
|
|
|
2021-05-04 11:29:36 +02:00
|
|
|
#[derive(Debug)]
|
|
|
|
enum Page {
|
|
|
|
Home(pages::list_links::Model),
|
|
|
|
ListUsers(pages::list_users::Model),
|
|
|
|
NotFound,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Page {
|
|
|
|
fn init(mut url: Url, orders: &mut impl Orders<Msg>, i18n: I18n) -> Self {
|
2021-05-31 06:55:56 +02:00
|
|
|
log!(&url);
|
2021-05-04 11:29:36 +02:00
|
|
|
url.next_path_part();
|
2021-05-14 18:28:33 +02:00
|
|
|
let result = match url.next_path_part() {
|
|
|
|
None | Some("list_links") => Self::Home(pages::list_links::init(
|
2021-05-04 11:29:36 +02:00
|
|
|
url,
|
2021-05-15 19:20:18 +02:00
|
|
|
&mut orders.proxy(Msg::ListLinks),
|
2021-05-04 11:29:36 +02:00
|
|
|
i18n,
|
|
|
|
)),
|
2021-05-14 18:28:33 +02:00
|
|
|
Some("list_users") => Self::ListUsers(pages::list_users::init(
|
2021-05-04 11:29:36 +02:00
|
|
|
url,
|
2021-05-15 19:20:18 +02:00
|
|
|
&mut orders.proxy(Msg::ListUsers),
|
2021-05-04 11:29:36 +02:00
|
|
|
i18n,
|
|
|
|
)),
|
|
|
|
_other => Self::NotFound,
|
|
|
|
};
|
|
|
|
|
2021-06-13 12:58:55 +02:00
|
|
|
orders.perform_cmd(async {
|
|
|
|
// create request
|
|
|
|
let request = Request::new("/admin/json/get_language/");
|
|
|
|
// perform and get response
|
|
|
|
let response = unwrap_or_return!(fetch(request).await, Msg::NoMessage);
|
|
|
|
// validate response status
|
|
|
|
let response = unwrap_or_return!(response.check_status(), Msg::NoMessage);
|
|
|
|
let lang: Lang = unwrap_or_return!(response.json().await, Msg::NoMessage);
|
|
|
|
|
|
|
|
Msg::LanguageChanged(lang)
|
|
|
|
});
|
|
|
|
|
2021-05-04 11:29:36 +02:00
|
|
|
log!("Page initialized");
|
|
|
|
result
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ------ ------
|
|
|
|
// Update
|
|
|
|
// ------ ------
|
|
|
|
#[derive(Clone)]
|
|
|
|
pub enum Msg {
|
|
|
|
UrlChanged(subs::UrlChanged),
|
2021-05-15 19:20:18 +02:00
|
|
|
ListLinks(list_links::Msg),
|
|
|
|
ListUsers(list_users::Msg),
|
2021-05-20 11:40:44 +02:00
|
|
|
GetLoggedUser,
|
|
|
|
UserReceived(User),
|
2021-05-04 11:29:36 +02:00
|
|
|
NoMessage,
|
2021-05-26 20:10:01 +02:00
|
|
|
NotAuthenticated,
|
2021-05-30 14:33:23 +02:00
|
|
|
Logout,
|
2021-05-26 20:10:01 +02:00
|
|
|
Login,
|
|
|
|
UsernameChanged(String),
|
|
|
|
PasswordChanged(String),
|
2021-06-13 12:58:55 +02:00
|
|
|
SetLanguage(Lang),
|
|
|
|
LanguageChanged(Lang),
|
2021-05-04 11:29:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|
|
|
match msg {
|
|
|
|
Msg::UrlChanged(url) => {
|
|
|
|
model.page = Page::init(url.0, orders, model.i18n.clone());
|
|
|
|
}
|
2021-05-15 19:20:18 +02:00
|
|
|
Msg::ListLinks(msg) => {
|
2021-05-04 11:29:36 +02:00
|
|
|
if let Page::Home(model) = &mut model.page {
|
2021-05-15 19:20:18 +02:00
|
|
|
list_links::update(msg, model, &mut orders.proxy(Msg::ListLinks))
|
2021-05-04 11:29:36 +02:00
|
|
|
}
|
|
|
|
}
|
2021-05-15 19:20:18 +02:00
|
|
|
Msg::ListUsers(msg) => {
|
2021-05-04 11:29:36 +02:00
|
|
|
if let Page::ListUsers(model) = &mut model.page {
|
2021-05-15 19:20:18 +02:00
|
|
|
list_users::update(msg, model, &mut orders.proxy(Msg::ListUsers))
|
2021-05-04 11:29:36 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Msg::NoMessage => (),
|
2021-05-20 11:40:44 +02:00
|
|
|
Msg::GetLoggedUser => {
|
2021-05-30 14:33:23 +02:00
|
|
|
model.user = Loadable::Loading;
|
2021-05-20 11:40:44 +02:00
|
|
|
orders.perform_cmd(async {
|
2021-05-26 20:10:01 +02:00
|
|
|
// create request
|
|
|
|
let request = unwrap_or_return!(
|
2021-05-20 11:40:44 +02:00
|
|
|
Request::new("/admin/json/get_logged_user/")
|
|
|
|
.method(Method::Post)
|
2021-05-26 20:10:01 +02:00
|
|
|
.json(&()),
|
2021-05-30 14:33:23 +02:00
|
|
|
Msg::Logout
|
2021-05-26 20:10:01 +02:00
|
|
|
);
|
|
|
|
// perform and get response
|
2021-05-30 14:33:23 +02:00
|
|
|
let response = unwrap_or_return!(fetch(request).await, Msg::Logout);
|
2021-05-26 20:10:01 +02:00
|
|
|
// validate response status
|
2021-05-30 14:33:23 +02:00
|
|
|
let response = unwrap_or_return!(response.check_status(), Msg::Logout);
|
|
|
|
let user: User = unwrap_or_return!(response.json().await, Msg::Logout);
|
2021-05-20 11:40:44 +02:00
|
|
|
|
|
|
|
Msg::UserReceived(user)
|
|
|
|
});
|
|
|
|
}
|
2021-05-30 14:33:23 +02:00
|
|
|
Msg::UserReceived(user) => {
|
2021-06-13 12:58:55 +02:00
|
|
|
model.set_lang(user.language);
|
2021-05-30 14:33:23 +02:00
|
|
|
model.user = Loadable::Data(Some(user));
|
2021-05-31 06:55:56 +02:00
|
|
|
model.page = Page::init(
|
|
|
|
model.location.current_url.clone(),
|
|
|
|
orders,
|
|
|
|
model.i18n.clone(),
|
|
|
|
);
|
2021-05-30 14:33:23 +02:00
|
|
|
}
|
2021-05-28 17:22:27 +02:00
|
|
|
Msg::NotAuthenticated => {
|
|
|
|
if model.user.is_some() {
|
|
|
|
model.user = Loadable::Data(None);
|
|
|
|
logout(orders)
|
|
|
|
}
|
2021-06-13 12:58:55 +02:00
|
|
|
model.user = Loadable::Data(None);
|
2021-05-28 17:22:27 +02:00
|
|
|
}
|
2021-05-30 14:33:23 +02:00
|
|
|
Msg::Logout => {
|
|
|
|
model.user = Loadable::Data(None);
|
|
|
|
logout(orders)
|
|
|
|
}
|
2021-05-28 17:22:27 +02:00
|
|
|
Msg::Login => login_user(model, orders),
|
2021-05-26 20:10:01 +02:00
|
|
|
Msg::UsernameChanged(s) => model.login_data.username = s,
|
|
|
|
Msg::PasswordChanged(s) => model.login_data.password = s,
|
2021-06-13 12:58:55 +02:00
|
|
|
Msg::SetLanguage(l) => {
|
|
|
|
change_language(l, orders);
|
|
|
|
}
|
|
|
|
Msg::LanguageChanged(l) => {
|
|
|
|
log!("Changed Language", &l);
|
|
|
|
model.set_lang(l);
|
|
|
|
}
|
2021-05-04 11:29:36 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-13 12:58:55 +02:00
|
|
|
fn change_language(l: Lang, orders: &mut impl Orders<Msg>) {
|
|
|
|
orders.perform_cmd(async move {
|
|
|
|
// create request
|
|
|
|
let request = unwrap_or_return!(
|
|
|
|
Request::new("/admin/json/change_language/")
|
|
|
|
.method(Method::Post)
|
|
|
|
.json(&l),
|
|
|
|
Msg::NoMessage
|
|
|
|
);
|
|
|
|
// perform and get response
|
|
|
|
let response = unwrap_or_return!(fetch(request).await, Msg::NoMessage);
|
|
|
|
// validate response status
|
|
|
|
let response = unwrap_or_return!(response.check_status(), Msg::NoMessage);
|
|
|
|
let l: Lang = unwrap_or_return!(response.json().await, Msg::NoMessage);
|
|
|
|
|
|
|
|
Msg::LanguageChanged(l)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-05-26 20:10:01 +02:00
|
|
|
fn logout(orders: &mut impl Orders<Msg>) {
|
2021-05-28 17:22:27 +02:00
|
|
|
orders.perform_cmd(async {
|
|
|
|
let request = Request::new("/admin/logout/");
|
|
|
|
unwrap_or_return!(fetch(request).await, Msg::GetLoggedUser);
|
|
|
|
Msg::NotAuthenticated
|
|
|
|
});
|
2021-05-26 20:10:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
fn login_user(model: &mut Model, orders: &mut impl Orders<Msg>) {
|
2021-05-30 14:33:23 +02:00
|
|
|
model.user = Loadable::Loading;
|
2021-05-26 20:10:01 +02:00
|
|
|
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)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-05-04 11:29:36 +02:00
|
|
|
pub struct Urls<'a> {
|
|
|
|
base_url: std::borrow::Cow<'a, Url>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> Urls<'a> {
|
|
|
|
/// Create a new `Urls` instance.
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
|
|
|
/// ```rust,no_run
|
|
|
|
/// Urls::new(base_url).home()
|
|
|
|
/// ```
|
|
|
|
pub fn new(base_url: impl Into<std::borrow::Cow<'a, Url>>) -> Self {
|
|
|
|
Self {
|
|
|
|
base_url: base_url.into(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Return base `Url`. If `base_url` isn't owned, it will be cloned.
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
|
|
|
/// ```rust,no_run
|
|
|
|
/// pub fn admin_urls(self) -> page::admin::Urls<'a> {
|
|
|
|
/// page::admin::Urls::new(self.base_url().add_path_part(ADMIN))
|
|
|
|
/// }
|
|
|
|
/// ```
|
|
|
|
#[must_use]
|
|
|
|
pub fn base_url(self) -> Url {
|
|
|
|
self.base_url.into_owned()
|
|
|
|
}
|
|
|
|
#[must_use]
|
|
|
|
pub fn home(self) -> Url {
|
|
|
|
self.base_url()
|
|
|
|
}
|
|
|
|
#[must_use]
|
|
|
|
pub fn list_links(self) -> Url {
|
|
|
|
self.base_url().add_path_part("list_links")
|
|
|
|
}
|
|
|
|
#[must_use]
|
2021-05-20 11:40:44 +02:00
|
|
|
pub fn create_link(self) -> Url {
|
|
|
|
self.list_links().add_path_part("create_link")
|
|
|
|
}
|
|
|
|
#[must_use]
|
2021-05-04 11:29:36 +02:00
|
|
|
pub fn list_users(self) -> Url {
|
|
|
|
self.base_url().add_path_part("list_users")
|
|
|
|
}
|
2021-05-14 18:28:33 +02:00
|
|
|
#[must_use]
|
|
|
|
pub fn create_user(self) -> Url {
|
|
|
|
self.list_users().add_path_part("create_user")
|
|
|
|
}
|
2021-05-04 11:29:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// ------ ------
|
|
|
|
// View
|
|
|
|
// ------ ------
|
|
|
|
|
2021-05-25 14:58:25 +02:00
|
|
|
/// Render the menu and the subpages.
|
2021-05-04 11:29:36 +02:00
|
|
|
fn view(model: &Model) -> Node<Msg> {
|
|
|
|
div![
|
|
|
|
C!["page"],
|
2021-05-30 14:33:23 +02:00
|
|
|
match model.user {
|
|
|
|
Loadable::Data(Some(ref user)) => div![
|
2021-05-31 06:55:56 +02:00
|
|
|
navigation::navigation(&model.i18n, &model.location.base_url, user),
|
2021-06-15 12:59:56 +02:00
|
|
|
view_content(&model.page, &model.location.base_url, user)
|
2021-05-30 14:33:23 +02:00
|
|
|
],
|
|
|
|
Loadable::Data(None) => view_login(&model.i18n, model),
|
|
|
|
Loadable::Loading => div![C!("lds-ellipsis"), div!(), div!(), div!(), div!()],
|
2021-05-26 20:10:01 +02:00
|
|
|
}
|
2021-05-04 11:29:36 +02:00
|
|
|
]
|
|
|
|
}
|
2021-05-25 14:58:25 +02:00
|
|
|
|
|
|
|
/// Render the subpages.
|
2021-06-15 12:59:56 +02:00
|
|
|
fn view_content(page: &Page, url: &Url, user: &User) -> Node<Msg> {
|
2021-05-04 11:29:36 +02:00
|
|
|
div![
|
|
|
|
C!["container"],
|
|
|
|
match page {
|
2021-06-15 12:59:56 +02:00
|
|
|
Page::Home(model) => pages::list_links::view(model, user).map_msg(Msg::ListLinks),
|
|
|
|
Page::ListUsers(model) => pages::list_users::view(model, user).map_msg(Msg::ListUsers),
|
2021-05-04 11:29:36 +02:00
|
|
|
Page::NotFound => div![div![url.to_string()], "Page not found!"],
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2021-05-26 20:10:01 +02:00
|
|
|
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)]
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2021-05-04 11:29:36 +02:00
|
|
|
// ------ ------
|
|
|
|
// Start
|
|
|
|
// ------ ------
|
|
|
|
#[wasm_bindgen(start)]
|
|
|
|
pub fn main() {
|
|
|
|
App::start("app", init, update, view);
|
|
|
|
}
|