Add language switching, fix logout

This commit is contained in:
Dietrich 2021-06-13 12:58:55 +02:00 committed by Franz Dietrich
parent fa924a8e8c
commit 0a23b786b0
16 changed files with 220 additions and 85 deletions

10
Cargo.lock generated
View File

@ -3450,6 +3450,8 @@ dependencies = [
"chrono",
"enum-map",
"serde",
"strum",
"strum_macros",
]
[[package]]
@ -3728,15 +3730,15 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "strum"
version = "0.20.0"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7318c509b5ba57f18533982607f24070a55d353e90d4cae30c467cdb2ad5ac5c"
checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2"
[[package]]
name = "strum_macros"
version = "0.20.1"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149"
checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec"
dependencies = [
"heck",
"proc-macro2 1.0.27",

View File

@ -20,8 +20,8 @@ fluent = "0.15"
seed = "0.8"
serde = "1.0"
unic-langid = "0.9"
strum_macros = "0.20"
strum = "0.20"
strum_macros = "0.21"
strum = "0.21"
enum-map = "1"
qrcode = "0.12"
image = "0.23"

View File

@ -2,7 +2,7 @@ use std::sync::Arc;
use fluent::{FluentArgs, FluentBundle, FluentResource};
use seed::log;
use strum_macros::{AsRefStr, Display, EnumIter, EnumString};
use shared::datatypes::Lang;
use unic_langid::LanguageIdentifier;
// A struct containing the functions and the current language to query the localized strings.
@ -22,10 +22,8 @@ impl I18n {
/// Create a new translator struct
#[must_use]
pub fn new(lang: Lang) -> Self {
Self {
lang,
ftl_bundle: Arc::new(lang.create_ftl_bundle()),
}
let ftl_bundle = Arc::new(Self::create_ftl_bundle(lang));
Self { lang, ftl_bundle }
}
/// Get the current language
@ -35,10 +33,9 @@ impl I18n {
}
/// Set the current language
pub fn set_lang(&mut self, lang: Lang) -> &Self {
pub fn set_lang(&mut self, lang: Lang) {
self.lang = lang;
self.ftl_bundle = Arc::new(lang.create_ftl_bundle());
self
self.ftl_bundle = Arc::new(Self::create_ftl_bundle(lang));
}
/// Get a localized string. Optionally with parameters provided in `args`.
@ -57,55 +54,44 @@ impl I18n {
}
}
/// An `enum` containing the available languages.
/// To add an additional language add it to this enum aswell as an appropriate file into the locales folder.
#[allow(clippy::upper_case_acronyms)]
#[derive(Debug, Copy, Clone, Display, EnumIter, EnumString, AsRefStr, Eq, PartialEq)]
pub enum Lang {
#[strum(serialize = "en-US")]
EnUS,
#[strum(serialize = "de-DE")]
DeDE,
}
impl Lang {
impl I18n {
/// Prettyprint the language name
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::EnUS => "English (US)",
Self::DeDE => "Deutsch (Deutschland)",
pub const fn label(&self) -> &'static str {
match self.lang {
Lang::EnUS => "English (US)",
Lang::DeDE => "Deutsch (Deutschland)",
}
}
/// include the fluent messages into the binary
#[must_use]
pub const fn ftl_messages(self) -> &'static str {
pub const fn ftl_messages(lang: Lang) -> &'static str {
macro_rules! include_ftl_messages {
( $lang_id:literal ) => {
include_str!(concat!("../../locales/", $lang_id, "/main.ftl"))
};
}
match self {
Self::EnUS => include_ftl_messages!("en"),
Self::DeDE => include_ftl_messages!("de"),
match lang {
Lang::EnUS => include_ftl_messages!("en"),
Lang::DeDE => include_ftl_messages!("de"),
}
}
#[must_use]
pub fn to_language_identifier(self) -> LanguageIdentifier {
self.as_ref()
pub fn language_identifier(lang: Lang) -> LanguageIdentifier {
lang.as_ref()
.parse()
.expect("parse Lang to LanguageIdentifier")
}
/// Create and initialize a fluent bundle.
#[must_use]
pub fn create_ftl_bundle(self) -> FluentBundle<FluentResource> {
let ftl_resource =
FluentResource::try_new(self.ftl_messages().to_owned()).expect("parse FTL messages");
pub fn create_ftl_bundle(lang: Lang) -> FluentBundle<FluentResource> {
let ftl_resource = FluentResource::try_new(Self::ftl_messages(lang).to_owned())
.expect("parse FTL messages");
let mut bundle = FluentBundle::new(vec![self.to_language_identifier()]);
let mut bundle = FluentBundle::new(vec![Self::language_identifier(lang)]);
bundle.add_resource(ftl_resource).expect("add FTL resource");
bundle
}

View File

@ -7,9 +7,10 @@ use pages::list_users;
use seed::window;
use seed::{attrs, button, div, input, label, log, prelude::*, App, Url, C};
use shared::apirequests::users::LoginUser;
use shared::datatypes::Lang;
use shared::datatypes::{Loadable, User};
use crate::i18n::{I18n, Lang};
use crate::i18n::I18n;
// ------ ------
// Init
@ -47,6 +48,17 @@ struct Model {
login_data: LoginUser,
}
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 => (),
}
}
}
#[derive(Default, Debug)]
struct LoginForm {
username: ElRef<web_sys::HtmlInputElement>,
@ -104,6 +116,18 @@ impl Page {
_other => Self::NotFound,
};
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)
});
log!("Page initialized");
result
}
@ -125,6 +149,8 @@ pub enum Msg {
Login,
UsernameChanged(String),
PasswordChanged(String),
SetLanguage(Lang),
LanguageChanged(Lang),
}
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
@ -163,6 +189,7 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
});
}
Msg::UserReceived(user) => {
model.set_lang(user.language);
model.user = Loadable::Data(Some(user));
model.page = Page::init(
model.location.current_url.clone(),
@ -175,6 +202,7 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
model.user = Loadable::Data(None);
logout(orders)
}
model.user = Loadable::Data(None);
}
Msg::Logout => {
model.user = Loadable::Data(None);
@ -183,7 +211,33 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
Msg::Login => login_user(model, orders),
Msg::UsernameChanged(s) => model.login_data.username = s,
Msg::PasswordChanged(s) => model.login_data.password = s,
Msg::SetLanguage(l) => {
change_language(l, orders);
}
Msg::LanguageChanged(l) => {
log!("Changed Language", &l);
model.set_lang(l);
}
}
}
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)
});
}
fn logout(orders: &mut impl Orders<Msg>) {

View File

@ -1,6 +1,6 @@
use fluent::fluent_args;
use seed::{a, attrs, div, li, nav, ol, prelude::*, Url};
use shared::datatypes::User;
use seed::{a, attrs, div, li, nav, ol, prelude::*, Url, C};
use shared::datatypes::{Lang, User};
use crate::{i18n::I18n, Msg};
@ -50,6 +50,12 @@ pub fn navigation(i18n: &I18n, base_url: &Url, user: &User) -> Node<Msg> {
],],
],
ol![
li![div![
C!("languageselector"),
t("language"),
a![ev(Ev::Click, |_| Msg::SetLanguage(Lang::DeDE)), "de"],
a![ev(Ev::Click, |_| Msg::SetLanguage(Lang::EnUS)), "en"]
]],
// The Welcome message
li![div![welcome]],
// The logout button

View File

@ -13,7 +13,7 @@ use shared::{
general::{EditMode, Message, Operation, Status},
links::{LinkDelta, LinkOverviewColumns, LinkRequestForm},
},
datatypes::{FullLink, Loadable},
datatypes::{FullLink, Lang, Loadable},
};
use crate::{get_host, i18n::I18n, unwrap_or_return};
@ -54,6 +54,12 @@ pub struct Model {
handle_timeout: Option<CmdHandle>, // Rendering qr-codes takes time... it is aborted when this handle is dropped and replaced.
}
impl Model {
pub fn set_lang(&mut self, l: Lang) {
self.i18n.set_lang(l);
}
}
#[derive(Debug, Clone)]
enum Dialog {
EditLink {

View File

@ -10,7 +10,7 @@ use shared::{
general::{EditMode, Status},
users::{UserDelta, UserOverviewColumns, UserRequestForm},
},
datatypes::User,
datatypes::{Lang, User},
};
/*
* init
@ -42,6 +42,12 @@ pub struct Model {
last_message: Option<Status>,
}
impl Model {
pub fn set_lang(&mut self, l: Lang) {
self.i18n.set_lang(l);
}
}
impl Model {
fn clean_dialogs(&mut self) {
self.last_message = None;

View File

@ -8,6 +8,7 @@ logout = Abmelden
login = Login
yes = Ja
no = Nein
language = Sprache:
not-found = Dieser Link existiert nicht, oder wurde gelöscht.

View File

@ -8,6 +8,7 @@ logout = Logout
login = Login
yes = Ja
no = Nein
language = Language:
not-found = This Link has not been found or has been deleted

View File

@ -234,6 +234,7 @@ pub async fn webservice(
// admin block
.service(
web::scope("/admin")
.route("/logout/", web::to(views::logout))
.service(
web::scope("/download")
.route("/png/{redirect_id}", web::get().to(views::download_png)),
@ -241,6 +242,8 @@ pub async fn webservice(
.service(
web::scope("/json")
.route("/list_links/", web::post().to(views::index_json))
.route("/get_language/", web::get().to(views::get_language))
.route("/change_language/", web::post().to(views::set_language))
.route(
"/create_link/",
web::post().to(views::process_create_link_json),

View File

@ -1,3 +1,5 @@
use std::str::FromStr;
use crate::{forms::LinkForm, Secret, ServerConfig, ServerError};
use argonautica::Hasher;
@ -7,7 +9,7 @@ use serde::{Deserialize, Serialize};
use shared::{
apirequests::links::LinkDelta,
datatypes::{Count, Link, User},
datatypes::{Count, Lang, Link, User},
};
use sqlx::Row;
use tracing::{error, info, instrument};
@ -22,7 +24,7 @@ pub trait UserDbOperations<T> {
async fn set_language(
self,
server_config: &ServerConfig,
new_language: &str,
new_language: Lang,
) -> Result<(), ServerError>;
async fn count_admins(server_config: &ServerConfig) -> Result<Count, ServerError>;
}
@ -40,7 +42,7 @@ impl UserDbOperations<Self> for User {
email: row.email,
password: Secret::new(row.password),
role: row.role,
language: row.language,
language: Lang::from_str(&row.language).expect("Should parse"),
});
user.map_err(ServerError::Database)
}
@ -63,7 +65,7 @@ impl UserDbOperations<Self> for User {
email: row.email,
password: Secret::new(row.password),
role: row.role,
language: row.language,
language: Lang::from_str(&row.language).expect("Should parse"),
});
user.map_err(ServerError::Database)
}
@ -81,7 +83,8 @@ impl UserDbOperations<Self> for User {
email: r.get("email"),
password: Secret::new(r.get("password")),
role: r.get("role"),
language: r.get("language"),
language: Lang::from_str(r.get("language"))
.expect("should parse correctly"),
})
.collect()
});
@ -124,11 +127,12 @@ impl UserDbOperations<Self> for User {
async fn set_language(
self,
server_config: &ServerConfig,
new_language: &str,
new_language: Lang,
) -> Result<(), ServerError> {
let lang_code = new_language.to_string();
sqlx::query!(
"UPDATE users SET language = ? where id = ?",
new_language,
lang_code,
self.id
)
.execute(&server_config.db_pool)

View File

@ -1,3 +1,5 @@
use std::str::FromStr;
use actix_identity::Identity;
use actix_web::web;
use enum_map::EnumMap;
@ -8,7 +10,7 @@ use shared::{
links::{LinkDelta, LinkOverviewColumns, LinkRequestForm},
users::{UserDelta, UserOverviewColumns, UserRequestForm},
},
datatypes::{Count, FullLink, Link, Secret, User},
datatypes::{Count, FullLink, Lang, Link, Secret, User},
};
use sqlx::Row;
use tracing::{info, instrument, warn};
@ -129,7 +131,7 @@ pub async fn list_all_allowed(
email: v.get("uemail"),
password: Secret::new("invalid".to_string()),
role: v.get("urole"),
language: v.get("ulang"),
language: Lang::from_str(v.get("ulang")).expect("Should parse"),
},
clicks: Count {
number: v.get("counter"), /* count is never None */
@ -242,7 +244,7 @@ pub async fn list_users(
email: v.get("email"),
password: Secret::new("".to_string()),
role: v.get("role"),
language: v.get("language"),
language: Lang::from_str(v.get("language")).expect("Should parse"),
})
.collect();
@ -591,24 +593,14 @@ pub async fn toggle_admin(
#[instrument(skip(id))]
pub async fn set_language(
id: &Identity,
lang_code: &str,
lang_code: Lang,
server_config: &ServerConfig,
) -> Result<(), ServerError> {
match lang_code {
"de" | "en" => match authenticate(id, server_config).await? {
match authenticate(id, server_config).await? {
Role::Admin { user } | Role::Regular { user } => {
user.set_language(server_config, lang_code).await
}
Role::Disabled | Role::NotAuthenticated => {
Err(ServerError::User("Not Allowed".to_owned()))
}
},
_ => {
warn!("An invalid language was selected!");
Err(ServerError::User(
"This language is not supported!".to_owned(),
))
}
Role::Disabled | Role::NotAuthenticated => Err(ServerError::User("Not Allowed".to_owned())),
}
}

View File

@ -14,10 +14,13 @@ use fluent_templates::LanguageIdentifier;
use image::{DynamicImage, ImageOutputFormat, Luma};
use qrcode::QrCode;
use queries::{authenticate, Role};
use shared::apirequests::{
use shared::{
apirequests::{
general::{Message, Status},
links::{LinkDelta, LinkRequestForm},
users::{LoginUser, UserDelta, UserRequestForm},
},
datatypes::Lang,
};
use tracing::{error, info, instrument, warn};
@ -38,7 +41,7 @@ fn redirect_builder(target: &str) -> HttpResponse {
}
#[instrument]
fn detect_language(request: &HttpRequest) -> Result<String, ServerError> {
fn detect_language(request: &HttpRequest) -> Result<Lang, ServerError> {
let requested = parse_accepted_languages(
request
.headers()
@ -49,7 +52,9 @@ fn detect_language(request: &HttpRequest) -> Result<String, ServerError> {
ServerError::User("Failed to convert Accept_language to str".to_owned())
})?,
);
info!("accepted languages: {:?}", requested);
let available = convert_vec_str_to_langids_lossy(&["de", "en"]);
info!("available languages: {:?}", available);
let default: LanguageIdentifier = "en"
.parse()
.map_err(|_| ServerError::User("Failed to parse a langid.".to_owned()))?;
@ -60,10 +65,18 @@ fn detect_language(request: &HttpRequest) -> Result<String, ServerError> {
Some(&default),
NegotiationStrategy::Filtering,
);
let languagecode = supported
.get(0)
.map_or("en".to_string(), std::string::ToString::to_string);
Ok(languagecode)
info!("supported languages: {:?}", supported);
if let Some(languagecode) = supported.get(0) {
info!("Supported Language: {}", languagecode);
Ok(languagecode
.to_string()
.parse()
.expect("Failed to parse 2 language"))
} else {
info!("Unsupported language using default!");
Ok("enEN".parse::<Lang>().unwrap())
}
}
#[instrument()]
@ -201,14 +214,35 @@ pub async fn toggle_admin(
)))
}
#[instrument(skip(id))]
pub async fn get_language(
id: Option<Identity>,
config: web::Data<crate::ServerConfig>,
req: HttpRequest,
) -> Result<HttpResponse, ServerError> {
if let Some(id) = id {
let user = authenticate(&id, &config).await?;
match user {
Role::NotAuthenticated | Role::Disabled => {
Ok(HttpResponse::Ok().json2(&detect_language(&req)?))
}
Role::Regular { user } | Role::Admin { user } => {
Ok(HttpResponse::Ok().json2(&user.language))
}
}
} else {
Ok(HttpResponse::Ok().json2(&detect_language(&req)?))
}
}
#[instrument(skip(id))]
pub async fn set_language(
data: web::Path<String>,
data: web::Json<Lang>,
config: web::Data<crate::ServerConfig>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
queries::set_language(&id, &data.0, &config).await?;
Ok(redirect_builder("/admin/index/"))
queries::set_language(&id, data.0, &config).await?;
Ok(HttpResponse::Ok().json2(&data.0))
}
#[instrument(skip(id))]
@ -259,6 +293,13 @@ pub async fn process_login_json(
}
}
#[instrument(skip(id))]
pub async fn logout(id: Identity) -> Result<HttpResponse, ServerError> {
info!("Logging out the user");
id.forget();
Ok(redirect_builder("/app/"))
}
#[instrument()]
pub async fn redirect(
config: web::Data<crate::ServerConfig>,

View File

@ -210,7 +210,14 @@ img.trashicon {
width: 0.5cm;
}
qrdownload {
.qrdownload {
display: flex;
vertical-align: middle;
}
.languageselector a {
height: 70%;
padding: 5px;
margin: 3px;
border-radius: 50%;
}

View File

@ -14,3 +14,5 @@ version = "0.3.1"
serde = "1.0"
chrono = {version = "0.4", features = ["serde"] }
enum-map = {version="1", features = ["serde"]}
strum_macros = "0.21"
strum = "0.21"

View File

@ -1,6 +1,7 @@
use std::ops::Deref;
use serde::{Deserialize, Serialize, Serializer};
use strum_macros::{AsRefStr, EnumIter, EnumString, ToString};
/// A generic list returntype containing the User and a Vec containing e.g. Links or Users
#[derive(Clone, Deserialize, Serialize)]
pub struct ListWithOwner<T> {
@ -23,7 +24,7 @@ pub struct User {
pub email: String,
pub password: Secret,
pub role: i64,
pub language: String,
pub language: Lang,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
@ -105,3 +106,26 @@ impl<T> Deref for Loadable<T> {
}
}
}
/// An `enum` containing the available languages.
/// To add an additional language add it to this enum aswell as an appropriate file into the locales folder.
#[allow(clippy::upper_case_acronyms)]
#[derive(
Debug,
Copy,
Clone,
EnumIter,
EnumString,
ToString,
AsRefStr,
Eq,
PartialEq,
Serialize,
Deserialize,
)]
pub enum Lang {
#[strum(serialize = "en-US", serialize = "en", serialize = "enUS")]
EnUS,
#[strum(serialize = "de-DE", serialize = "de", serialize = "deDE")]
DeDE,
}