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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
use fluent::fluent_args; use fluent::fluent_args;
use seed::{a, attrs, div, li, nav, ol, prelude::*, Url}; use seed::{a, attrs, div, li, nav, ol, prelude::*, Url, C};
use shared::datatypes::User; use shared::datatypes::{Lang, User};
use crate::{i18n::I18n, Msg}; use crate::{i18n::I18n, Msg};
@ -50,6 +50,12 @@ pub fn navigation(i18n: &I18n, base_url: &Url, user: &User) -> Node<Msg> {
],], ],],
], ],
ol![ 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 // The Welcome message
li![div![welcome]], li![div![welcome]],
// The logout button // The logout button

View File

@ -13,7 +13,7 @@ use shared::{
general::{EditMode, Message, Operation, Status}, general::{EditMode, Message, Operation, Status},
links::{LinkDelta, LinkOverviewColumns, LinkRequestForm}, links::{LinkDelta, LinkOverviewColumns, LinkRequestForm},
}, },
datatypes::{FullLink, Loadable}, datatypes::{FullLink, Lang, Loadable},
}; };
use crate::{get_host, i18n::I18n, unwrap_or_return}; 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. 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)] #[derive(Debug, Clone)]
enum Dialog { enum Dialog {
EditLink { EditLink {

View File

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

View File

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

View File

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

View File

@ -234,6 +234,7 @@ pub async fn webservice(
// admin block // admin block
.service( .service(
web::scope("/admin") web::scope("/admin")
.route("/logout/", web::to(views::logout))
.service( .service(
web::scope("/download") web::scope("/download")
.route("/png/{redirect_id}", web::get().to(views::download_png)), .route("/png/{redirect_id}", web::get().to(views::download_png)),
@ -241,6 +242,8 @@ pub async fn webservice(
.service( .service(
web::scope("/json") web::scope("/json")
.route("/list_links/", web::post().to(views::index_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( .route(
"/create_link/", "/create_link/",
web::post().to(views::process_create_link_json), 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 crate::{forms::LinkForm, Secret, ServerConfig, ServerError};
use argonautica::Hasher; use argonautica::Hasher;
@ -7,7 +9,7 @@ use serde::{Deserialize, Serialize};
use shared::{ use shared::{
apirequests::links::LinkDelta, apirequests::links::LinkDelta,
datatypes::{Count, Link, User}, datatypes::{Count, Lang, Link, User},
}; };
use sqlx::Row; use sqlx::Row;
use tracing::{error, info, instrument}; use tracing::{error, info, instrument};
@ -22,7 +24,7 @@ pub trait UserDbOperations<T> {
async fn set_language( async fn set_language(
self, self,
server_config: &ServerConfig, server_config: &ServerConfig,
new_language: &str, new_language: Lang,
) -> Result<(), ServerError>; ) -> Result<(), ServerError>;
async fn count_admins(server_config: &ServerConfig) -> Result<Count, ServerError>; async fn count_admins(server_config: &ServerConfig) -> Result<Count, ServerError>;
} }
@ -40,7 +42,7 @@ impl UserDbOperations<Self> for User {
email: row.email, email: row.email,
password: Secret::new(row.password), password: Secret::new(row.password),
role: row.role, role: row.role,
language: row.language, language: Lang::from_str(&row.language).expect("Should parse"),
}); });
user.map_err(ServerError::Database) user.map_err(ServerError::Database)
} }
@ -63,7 +65,7 @@ impl UserDbOperations<Self> for User {
email: row.email, email: row.email,
password: Secret::new(row.password), password: Secret::new(row.password),
role: row.role, role: row.role,
language: row.language, language: Lang::from_str(&row.language).expect("Should parse"),
}); });
user.map_err(ServerError::Database) user.map_err(ServerError::Database)
} }
@ -81,7 +83,8 @@ impl UserDbOperations<Self> for User {
email: r.get("email"), email: r.get("email"),
password: Secret::new(r.get("password")), password: Secret::new(r.get("password")),
role: r.get("role"), role: r.get("role"),
language: r.get("language"), language: Lang::from_str(r.get("language"))
.expect("should parse correctly"),
}) })
.collect() .collect()
}); });
@ -124,11 +127,12 @@ impl UserDbOperations<Self> for User {
async fn set_language( async fn set_language(
self, self,
server_config: &ServerConfig, server_config: &ServerConfig,
new_language: &str, new_language: Lang,
) -> Result<(), ServerError> { ) -> Result<(), ServerError> {
let lang_code = new_language.to_string();
sqlx::query!( sqlx::query!(
"UPDATE users SET language = ? where id = ?", "UPDATE users SET language = ? where id = ?",
new_language, lang_code,
self.id self.id
) )
.execute(&server_config.db_pool) .execute(&server_config.db_pool)

View File

@ -1,3 +1,5 @@
use std::str::FromStr;
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::web; use actix_web::web;
use enum_map::EnumMap; use enum_map::EnumMap;
@ -8,7 +10,7 @@ use shared::{
links::{LinkDelta, LinkOverviewColumns, LinkRequestForm}, links::{LinkDelta, LinkOverviewColumns, LinkRequestForm},
users::{UserDelta, UserOverviewColumns, UserRequestForm}, users::{UserDelta, UserOverviewColumns, UserRequestForm},
}, },
datatypes::{Count, FullLink, Link, Secret, User}, datatypes::{Count, FullLink, Lang, Link, Secret, User},
}; };
use sqlx::Row; use sqlx::Row;
use tracing::{info, instrument, warn}; use tracing::{info, instrument, warn};
@ -129,7 +131,7 @@ pub async fn list_all_allowed(
email: v.get("uemail"), email: v.get("uemail"),
password: Secret::new("invalid".to_string()), password: Secret::new("invalid".to_string()),
role: v.get("urole"), role: v.get("urole"),
language: v.get("ulang"), language: Lang::from_str(v.get("ulang")).expect("Should parse"),
}, },
clicks: Count { clicks: Count {
number: v.get("counter"), /* count is never None */ number: v.get("counter"), /* count is never None */
@ -242,7 +244,7 @@ pub async fn list_users(
email: v.get("email"), email: v.get("email"),
password: Secret::new("".to_string()), password: Secret::new("".to_string()),
role: v.get("role"), role: v.get("role"),
language: v.get("language"), language: Lang::from_str(v.get("language")).expect("Should parse"),
}) })
.collect(); .collect();
@ -591,24 +593,14 @@ pub async fn toggle_admin(
#[instrument(skip(id))] #[instrument(skip(id))]
pub async fn set_language( pub async fn set_language(
id: &Identity, id: &Identity,
lang_code: &str, lang_code: Lang,
server_config: &ServerConfig, server_config: &ServerConfig,
) -> Result<(), ServerError> { ) -> Result<(), ServerError> {
match lang_code { match authenticate(id, server_config).await? {
"de" | "en" => match authenticate(id, server_config).await? { Role::Admin { user } | Role::Regular { user } => {
Role::Admin { user } | Role::Regular { user } => { user.set_language(server_config, lang_code).await
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 image::{DynamicImage, ImageOutputFormat, Luma};
use qrcode::QrCode; use qrcode::QrCode;
use queries::{authenticate, Role}; use queries::{authenticate, Role};
use shared::apirequests::{ use shared::{
general::{Message, Status}, apirequests::{
links::{LinkDelta, LinkRequestForm}, general::{Message, Status},
users::{LoginUser, UserDelta, UserRequestForm}, links::{LinkDelta, LinkRequestForm},
users::{LoginUser, UserDelta, UserRequestForm},
},
datatypes::Lang,
}; };
use tracing::{error, info, instrument, warn}; use tracing::{error, info, instrument, warn};
@ -38,7 +41,7 @@ fn redirect_builder(target: &str) -> HttpResponse {
} }
#[instrument] #[instrument]
fn detect_language(request: &HttpRequest) -> Result<String, ServerError> { fn detect_language(request: &HttpRequest) -> Result<Lang, ServerError> {
let requested = parse_accepted_languages( let requested = parse_accepted_languages(
request request
.headers() .headers()
@ -49,7 +52,9 @@ fn detect_language(request: &HttpRequest) -> Result<String, ServerError> {
ServerError::User("Failed to convert Accept_language to str".to_owned()) 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"]); let available = convert_vec_str_to_langids_lossy(&["de", "en"]);
info!("available languages: {:?}", available);
let default: LanguageIdentifier = "en" let default: LanguageIdentifier = "en"
.parse() .parse()
.map_err(|_| ServerError::User("Failed to parse a langid.".to_owned()))?; .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), Some(&default),
NegotiationStrategy::Filtering, NegotiationStrategy::Filtering,
); );
let languagecode = supported info!("supported languages: {:?}", supported);
.get(0)
.map_or("en".to_string(), std::string::ToString::to_string); if let Some(languagecode) = supported.get(0) {
Ok(languagecode) 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()] #[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))] #[instrument(skip(id))]
pub async fn set_language( pub async fn set_language(
data: web::Path<String>, data: web::Json<Lang>,
config: web::Data<crate::ServerConfig>, config: web::Data<crate::ServerConfig>,
id: Identity, id: Identity,
) -> Result<HttpResponse, ServerError> { ) -> Result<HttpResponse, ServerError> {
queries::set_language(&id, &data.0, &config).await?; queries::set_language(&id, data.0, &config).await?;
Ok(redirect_builder("/admin/index/")) Ok(HttpResponse::Ok().json2(&data.0))
} }
#[instrument(skip(id))] #[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()] #[instrument()]
pub async fn redirect( pub async fn redirect(
config: web::Data<crate::ServerConfig>, config: web::Data<crate::ServerConfig>,

View File

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

View File

@ -13,4 +13,6 @@ version = "0.3.1"
[dependencies] [dependencies]
serde = "1.0" serde = "1.0"
chrono = {version = "0.4", features = ["serde"] } chrono = {version = "0.4", features = ["serde"] }
enum-map = {version="1", 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 std::ops::Deref;
use serde::{Deserialize, Serialize, Serializer}; 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 /// A generic list returntype containing the User and a Vec containing e.g. Links or Users
#[derive(Clone, Deserialize, Serialize)] #[derive(Clone, Deserialize, Serialize)]
pub struct ListWithOwner<T> { pub struct ListWithOwner<T> {
@ -23,7 +24,7 @@ pub struct User {
pub email: String, pub email: String,
pub password: Secret, pub password: Secret,
pub role: i64, pub role: i64,
pub language: String, pub language: Lang,
} }
#[derive(Serialize, Deserialize, Clone, Debug)] #[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,
}