diff --git a/scripts/devenv/mock_relying_party.toml.template b/scripts/devenv/mock_relying_party.toml.template
index dedd74a07..9b881c4f7 100644
--- a/scripts/devenv/mock_relying_party.toml.template
+++ b/scripts/devenv/mock_relying_party.toml.template
@@ -15,19 +15,19 @@ sha256 = "${WALLET_WEB_SHA256}"
docType = "com.example.pid"
nameSpaces = { "com.example.pid" = { bsn = true } }
-[[usecases.xyz_bank.items_requests]]
+[[usecases.online_marketplace.items_requests]]
docType = "com.example.pid"
-nameSpaces = { "com.example.pid" = { given_name = true, family_name = true, birth_date = true, bsn = true } }
+nameSpaces = { "com.example.pid" = { given_name = true, family_name = true, birth_date = true } }
-[[usecases.xyz_bank.items_requests]]
+[[usecases.online_marketplace.items_requests]]
docType = "com.example.address"
nameSpaces = { "com.example.address" = { resident_street = true, resident_house_number = true, resident_postal_code = true } }
-[[usecases.online_marketplace.items_requests]]
+[[usecases.xyz_bank.items_requests]]
docType = "com.example.pid"
-nameSpaces = { "com.example.pid" = { given_name = true, family_name = true, birth_date = true } }
+nameSpaces = { "com.example.pid" = { given_name = true, family_name = true, birth_date = true, bsn = true } }
-[[usecases.online_marketplace.items_requests]]
+[[usecases.xyz_bank.items_requests]]
docType = "com.example.address"
nameSpaces = { "com.example.address" = { resident_street = true, resident_house_number = true, resident_postal_code = true } }
diff --git a/scripts/devenv/mrp_verification_server.toml.template b/scripts/devenv/mrp_verification_server.toml.template
index 6b3c34010..9fe1a252e 100644
--- a/scripts/devenv/mrp_verification_server.toml.template
+++ b/scripts/devenv/mrp_verification_server.toml.template
@@ -22,14 +22,14 @@ ephemeral_id_secret = "${MRP_VERIFICATION_SERVER_EPHEMERAL_ID_SECRET}"
certificate = "${MOCK_RELYING_PARTY_CRT_MIJN_AMSTERDAM}"
private_key = "${MOCK_RELYING_PARTY_KEY_MIJN_AMSTERDAM}"
-[verifier.usecases.xyz_bank]
-certificate = "${MOCK_RELYING_PARTY_CRT_XYZ_BANK}"
-private_key = "${MOCK_RELYING_PARTY_KEY_XYZ_BANK}"
-
[verifier.usecases.online_marketplace]
certificate = "${MOCK_RELYING_PARTY_CRT_ONLINE_MARKETPLACE}"
private_key = "${MOCK_RELYING_PARTY_KEY_ONLINE_MARKETPLACE}"
+[verifier.usecases.xyz_bank]
+certificate = "${MOCK_RELYING_PARTY_CRT_XYZ_BANK}"
+private_key = "${MOCK_RELYING_PARTY_KEY_XYZ_BANK}"
+
[verifier.usecases.monkey_bike]
certificate = "${MOCK_RELYING_PARTY_CRT_MONKEY_BIKE}"
private_key = "${MOCK_RELYING_PARTY_KEY_MONKEY_BIKE}"
diff --git a/scripts/setup-devenv.sh b/scripts/setup-devenv.sh
index ec47a2c0d..965c3e1e6 100755
--- a/scripts/setup-devenv.sh
+++ b/scripts/setup-devenv.sh
@@ -221,13 +221,6 @@ export MOCK_RELYING_PARTY_KEY_MIJN_AMSTERDAM
MOCK_RELYING_PARTY_CRT_MIJN_AMSTERDAM=$(< "${TARGET_DIR}/mock_relying_party/mijn_amsterdam.crt.der" ${BASE64})
export MOCK_RELYING_PARTY_CRT_MIJN_AMSTERDAM
-# Generate relying party key and cert
-generate_mock_relying_party_key_pair xyz_bank
-MOCK_RELYING_PARTY_KEY_XYZ_BANK=$(< "${TARGET_DIR}/mock_relying_party/xyz_bank.key.der" ${BASE64})
-export MOCK_RELYING_PARTY_KEY_XYZ_BANK
-MOCK_RELYING_PARTY_CRT_XYZ_BANK=$(< "${TARGET_DIR}/mock_relying_party/xyz_bank.crt.der" ${BASE64})
-export MOCK_RELYING_PARTY_CRT_XYZ_BANK
-
# Generate relying party key and cert
generate_mock_relying_party_key_pair online_marketplace
MOCK_RELYING_PARTY_KEY_ONLINE_MARKETPLACE=$(< "${TARGET_DIR}/mock_relying_party/online_marketplace.key.der" ${BASE64})
@@ -235,6 +228,13 @@ export MOCK_RELYING_PARTY_KEY_ONLINE_MARKETPLACE
MOCK_RELYING_PARTY_CRT_ONLINE_MARKETPLACE=$(< "${TARGET_DIR}/mock_relying_party/online_marketplace.crt.der" ${BASE64})
export MOCK_RELYING_PARTY_CRT_ONLINE_MARKETPLACE
+# Generate relying party key and cert
+generate_mock_relying_party_key_pair xyz_bank
+MOCK_RELYING_PARTY_KEY_XYZ_BANK=$(< "${TARGET_DIR}/mock_relying_party/xyz_bank.key.der" ${BASE64})
+export MOCK_RELYING_PARTY_KEY_XYZ_BANK
+MOCK_RELYING_PARTY_CRT_XYZ_BANK=$(< "${TARGET_DIR}/mock_relying_party/xyz_bank.crt.der" ${BASE64})
+export MOCK_RELYING_PARTY_CRT_XYZ_BANK
+
# Generate relying party key and cert
generate_mock_relying_party_key_pair monkey_bike
MOCK_RELYING_PARTY_KEY_MONKEY_BIKE=$(< "${TARGET_DIR}/mock_relying_party/monkey_bike.key.der" ${BASE64})
diff --git a/wallet_core/Cargo.lock b/wallet_core/Cargo.lock
index c054de6ba..d67f61b3d 100644
--- a/wallet_core/Cargo.lock
+++ b/wallet_core/Cargo.lock
@@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 3
+[[package]]
+name = "accept-language"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f27d075294830fcab6f66e320dab524bc6d048f4a151698e153205559113772"
+
[[package]]
name = "addr2line"
version = "0.22.0"
@@ -798,10 +804,13 @@ version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be"
dependencies = [
+ "indexmap 2.2.6",
"lazy_static",
"nom",
"pathdiff",
+ "ron",
"serde",
+ "serde_json",
"toml 0.8.14",
]
@@ -2415,6 +2424,7 @@ dependencies = [
name = "mock_relying_party"
version = "0.1.0"
dependencies = [
+ "accept-language",
"anyhow",
"askama",
"axum",
@@ -2422,9 +2432,11 @@ dependencies = [
"config",
"futures",
"http",
+ "indexmap 2.2.6",
"nl_wallet_mdoc",
"nutype",
"reqwest",
+ "rstest",
"sentry",
"serde",
"serde_json",
@@ -3352,6 +3364,19 @@ dependencies = [
"windows-sys 0.52.0",
]
+[[package]]
+name = "ron"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
+dependencies = [
+ "base64 0.21.7",
+ "bitflags 2.6.0",
+ "indexmap 2.2.6",
+ "serde",
+ "serde_derive",
+]
+
[[package]]
name = "rsa"
version = "0.9.6"
@@ -4799,6 +4824,7 @@ version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335"
dependencies = [
+ "indexmap 2.2.6",
"serde",
"serde_spanned",
"toml_datetime",
diff --git a/wallet_core/Cargo.toml b/wallet_core/Cargo.toml
index 3fce73339..5118b5a98 100644
--- a/wallet_core/Cargo.toml
+++ b/wallet_core/Cargo.toml
@@ -37,6 +37,7 @@ rust-version = "1.80"
async_fn_in_trait = "allow"
[workspace.dependencies]
+accept-language = "3.1.0"
aes-gcm = "0.10.3"
android_logger = { version = "0.14.1", default-features = false }
anyhow = "1.0.66"
diff --git a/wallet_core/mock_relying_party/Cargo.toml b/wallet_core/mock_relying_party/Cargo.toml
index 9106154dd..c2e32d56b 100644
--- a/wallet_core/mock_relying_party/Cargo.toml
+++ b/wallet_core/mock_relying_party/Cargo.toml
@@ -15,13 +15,15 @@ doctest = false
allow_http_return_url = ["wallet_server/allow_http_return_url"]
[dependencies]
+accept-language.workspace = true
anyhow.workspace = true
askama.workspace = true
axum = { workspace = true, features = ["http1", "query", "tokio", "tower-log", "tracing"] }
base64.workspace = true
-config = { workspace = true, features = ["toml"] }
+config = { workspace = true, features = ["preserve_order", "toml"] }
futures = { workspace = true, features = ["std"] }
http.workspace = true
+indexmap.workspace = true
nutype = { workspace = true, features = ["serde"] }
reqwest = { workspace = true, features = ["rustls-tls-webpki-roots"] }
sentry = { workspace = true, features = [
@@ -58,3 +60,6 @@ url = { workspace = true, features = ["serde"] }
nl_wallet_mdoc.path = "../mdoc"
wallet_common = { path = "../wallet_common", features = ["sentry"] }
wallet_server = { path = "../wallet_server", features = ["disclosure"] }
+
+[dev-dependencies]
+rstest.workspace = true
diff --git a/wallet_core/mock_relying_party/assets/css/demo_bar.css b/wallet_core/mock_relying_party/assets/css/demo_bar.css
index 7adcce5ee..6e8c5d5a8 100644
--- a/wallet_core/mock_relying_party/assets/css/demo_bar.css
+++ b/wallet_core/mock_relying_party/assets/css/demo_bar.css
@@ -1,53 +1,157 @@
aside {
- order: -1;
+ order: -1;
- width: 100vw;
- display: flex;
- flex-direction: row;
- justify-content: center;
- align-items: center;
- gap: 16px;
- padding: 16px;
- background: #f2f1fe;
- color: #152a62;
+ width: 100vw;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px;
+ gap: 16px;
+ color: #152a62;
+}
+
+aside:has(.text) {
+ background: #f2f1fe;
}
@media screen and (min-width: 500px) {
- aside {
- padding: 8px 24px;
- }
+ aside {
+ padding: 8px 24px 8px 94px; /* 24 + 16 + 54 */
+ }
}
aside b {
- font-weight: 700;
+ font-weight: 700;
}
aside a {
- color: #383ede;
+ color: #383ede;
}
aside a:hover {
- color: #3237c4;
- text-decoration: none;
+ color: #3237c4;
+ text-decoration: none;
+}
+
+aside .demo-bar {
+ flex-grow: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 16px;
}
-aside::before {
- content: " ";
- background: url("../non-free/images/nl-wallet.svg") no-repeat center center / cover;
- width: 40px;
- height: 40px;
+aside .demo-bar::before {
+ content: " ";
+ background: url("../non-free/images/nl-wallet.svg") no-repeat center center / cover;
+ width: 40px;
+ height: 40px;
}
aside .text {
- display: flex;
- flex-direction: column;
- justify-content: center;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
}
@media screen and (min-width: 500px) {
- aside .text {
- flex-direction: row;
- align-items: center;
- gap: 8px;
- }
+ aside {
+ justify-content: right;
+ }
+
+ aside .demo-bar {
+ flex-grow: 1;
+ }
+
+ aside .text {
+ flex-direction: row;
+ align-items: center;
+ gap: 8px;
+ }
+}
+
+.lang-selector {
+ position: relative;
+}
+
+.lang-selector label[for="lang_toggle"] {
+ display: flex;
+ align-items: center;
+
+ padding: 4px 8px;
+ gap: 4px;
+ border-radius: 2px;
+
+ background: #fcfcfc;
+ color: #383ede;
+ font-weight: 700;
+ text-transform: uppercase;
+ line-height: 1.25;
+
+ user-select: none;
+}
+
+.lang-selector label[for="lang_toggle"]:hover {
+ background-color: #f1f1f1;
+ cursor: pointer;
+}
+
+.lang-selector label[for="lang_toggle"]::after {
+ content: " ";
+ background: url("../non-free/images/down.svg") no-repeat center center / contain;
+
+ width: 16px;
+ height: 16px;
+}
+
+#lang_toggle {
+ display: none;
+}
+
+#lang_toggle:checked + .lang-modal {
+ display: block;
+}
+
+.lang-selector .lang-modal {
+ position: absolute;
+ right: 0;
+ z-index: 1;
+
+ margin-top: 2px;
+
+ background: #fcfcfc;
+ box-shadow: 0px 4px 40px 0px #00000029;
+ border-radius: 2px;
+
+ overflow: hidden;
+
+ display: none;
+}
+
+.lang-selector .lang-modal button {
+ display: flex;
+ padding: 12px 24px 12px 12px;
+ gap: 12px;
+ color: #152a62;
+ width: 100%;
+}
+
+.lang-selector .lang-modal button:not(:disabled):hover {
+ background-color: #f1f1f1;
+ cursor: pointer;
+}
+
+.lang-selector .lang-modal button::before {
+ content: " ";
+ width: 24px;
+ height: 24px;
+}
+
+.lang-selector .lang-modal button:disabled::before {
+ content: " ";
+ background-color: #152a62;
+ mask: url("../non-free/images/checkmark.svg") no-repeat center center / contain;
+ width: 24px;
+ height: 24px;
}
diff --git a/wallet_core/mock_relying_party/assets/css/nav.css b/wallet_core/mock_relying_party/assets/css/nav.css
index 6b049ce23..cdc3031e9 100644
--- a/wallet_core/mock_relying_party/assets/css/nav.css
+++ b/wallet_core/mock_relying_party/assets/css/nav.css
@@ -1,126 +1,132 @@
:root {
- font-family: "RO Sans", sans-serif;
- font-feature-settings:
- "clig" off,
- "liga" off;
- font-size: 16px;
- line-height: 1.5;
+ font-family: "RO Sans", sans-serif;
+ font-feature-settings:
+ "clig" off,
+ "liga" off;
+ font-size: 16px;
+ line-height: 1.5;
}
body {
- background: #faf9fb;
- display: flex;
- flex-direction: column;
- align-items: center;
+ background: #faf9fb;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
}
main {
- display: flex;
- flex-direction: column;
- align-items: center;
- margin: 0 16px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin: 0 16px;
}
main > * {
- max-width: 500px;
+ max-width: 500px;
}
aside {
- /* TODO this should become the translations bar */
- height: 60px;
+ justify-content: right;
+ padding: 24px 16px 8px 16px;
+}
+
+@media only screen and (min-width: 577px) {
+ aside {
+ padding: 24px;
+ }
}
header {
- content: " ";
- background: url("../non-free/images/nl-wallet.svg") no-repeat center center / cover;
- width: 64px;
- height: 64px;
+ content: " ";
+ background: url("../non-free/images/nl-wallet.svg") no-repeat center center / cover;
+ width: 64px;
+ height: 64px;
}
main section {
- display: flex;
- flex-direction: column;
- text-align: center;
- gap: 8px;
- margin: 40px 0;
+ display: flex;
+ flex-direction: column;
+ text-align: center;
+ gap: 8px;
+ margin: 40px 0;
}
main section h1 {
- color: #152a62;
- font-size: 30px;
- font-weight: 700;
- line-height: 44px;
+ color: #152a62;
+ font-size: 30px;
+ font-weight: 700;
+ line-height: 44px;
}
main section p {
- color: #30293d;
- letter-spacing: 0.5px;
+ color: #30293d;
+ letter-spacing: 0.5px;
}
main section a {
- color: #383ede;
+ color: #383ede;
}
main section a:hover {
- color: #3237c4;
- text-decoration: none;
+ color: #3237c4;
+ text-decoration: none;
}
nav {
- display: flex;
- flex-direction: column;
- gap: 32px;
- width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+ width: 100%;
}
nav a {
- display: flex;
- text-align: center;
- gap: 16px;
- height: 100%;
- text-decoration: none;
+ display: flex;
+ text-align: center;
+ gap: 16px;
+ height: 100%;
+ text-decoration: none;
}
nav a span {
- flex-grow: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #383ede;
- color: white;
- font-weight: 700;
- letter-spacing: 1px;
- border-radius: 12px;
- padding: 16px 24px;
- line-height: 20px;
+ flex-grow: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #383ede;
+ color: white;
+ font-weight: 700;
+ letter-spacing: 1px;
+ border-radius: 12px;
+ padding: 16px 24px;
+ line-height: 20px;
}
nav a:hover span {
- color: white;
- background: #3237c4;
+ color: white;
+ background: #3237c4;
}
nav a::before {
- content: " ";
- background-color: black;
- border-radius: 9.75px;
- width: 52px;
- height: 52px;
+ content: " ";
+ background-color: black;
+ border-radius: 9.75px;
+ width: 52px;
+ height: 52px;
}
/* use case icons */
#mijn_amsterdam::before {
- background: url("../non-free/images/mijn_amsterdam.svg") no-repeat center center / cover;
+ background: url("../non-free/images/mijn_amsterdam.svg") no-repeat center center / cover;
}
#online_marketplace::before {
- background: url("../images/online_marketplace.svg") no-repeat center center / cover;
+ background: url("../images/online_marketplace.svg") no-repeat center center / cover;
}
#xyz_bank::before {
- background: url("../images/xyz_bank.svg") no-repeat center center / cover;
+ background: url("../images/xyz_bank.svg") no-repeat center center / cover;
}
#monkey_bike::before {
- background: url("../images/monkey_bike.svg") no-repeat center center / cover;
+ background: url("../images/monkey_bike.svg") no-repeat center center / cover;
}
diff --git a/wallet_core/mock_relying_party/assets/index.html b/wallet_core/mock_relying_party/assets/index.html
deleted file mode 100644
index 62cf6ffc3..000000000
--- a/wallet_core/mock_relying_party/assets/index.html
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
-
-
-
-
- NL Wallet demo
-
-
-
-
-
-
-
- NL Wallet demo
-
- Deze voorbeelden zijn fictief en dienen alleen ter illustratie. Volg de ontwikkelingen op
- edi.pleio.nl.
-
-
-
-
-
-
diff --git a/wallet_core/mock_relying_party/assets/non-free/images/down.svg b/wallet_core/mock_relying_party/assets/non-free/images/down.svg
new file mode 100644
index 000000000..2b79d995a
--- /dev/null
+++ b/wallet_core/mock_relying_party/assets/non-free/images/down.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/wallet_core/mock_relying_party/assets/non-free/images/nl-wallet-border.svg.bak b/wallet_core/mock_relying_party/assets/non-free/images/nl-wallet-border.svg.bak
new file mode 100644
index 000000000..b0d3c4e3b
--- /dev/null
+++ b/wallet_core/mock_relying_party/assets/non-free/images/nl-wallet-border.svg.bak
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/wallet_core/mock_relying_party/assets/usecase.js b/wallet_core/mock_relying_party/assets/usecase.js
index 5f678c5f0..1c7c2b0b5 100644
--- a/wallet_core/mock_relying_party/assets/usecase.js
+++ b/wallet_core/mock_relying_party/assets/usecase.js
@@ -5,10 +5,15 @@ for (const button of wallet_buttons) {
const session_token = e.detail[0]
const session_type = e.detail[1]
const usecase = button.attributes.getNamedItem("usecase").value
+ const lang = button.attributes.getNamedItem("lang")
+ ? button.attributes.getNamedItem("lang").value
+ : "nl"
// this only works for cross_device without a configured return URL
if (session_type === "cross_device") {
- window.location.assign("../" + usecase + "/return?session_token=" + session_token)
+ window.location.assign(
+ "../" + usecase + "/return?session_token=" + session_token + "&lang=" + lang,
+ )
}
}
}
diff --git a/wallet_core/mock_relying_party/src/app.rs b/wallet_core/mock_relying_party/src/app.rs
index 6f6a07df1..5f3d337dc 100644
--- a/wallet_core/mock_relying_party/src/app.rs
+++ b/wallet_core/mock_relying_party/src/app.rs
@@ -1,5 +1,4 @@
use std::{
- collections::HashMap,
env,
path::PathBuf,
result::Result as StdResult,
@@ -8,7 +7,8 @@ use std::{
use askama::Template;
use axum::{
- extract::{Path, Query, Request, State},
+ async_trait,
+ extract::{FromRequestParts, Path, Query, Request, State},
handler::HandlerWithoutStateExt,
http::{Method, StatusCode},
middleware::{self, Next},
@@ -17,8 +17,14 @@ use axum::{
Json, Router,
};
use base64::prelude::*;
-use http::{header::CACHE_CONTROL, HeaderValue};
+use http::{
+ header::{ACCEPT_LANGUAGE, CACHE_CONTROL},
+ request::Parts,
+ HeaderMap, HeaderValue,
+};
+use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
+use strum::IntoEnumIterator;
use tower::ServiceBuilder;
use tower_http::{
cors::{Any, CorsLayer},
@@ -35,6 +41,7 @@ use crate::{
askama_axum,
client::WalletServerClient,
settings::{Origin, ReturnUrlMode, Settings, Usecase, WalletWeb},
+ translations::{Words, TRANSLATIONS},
};
#[derive(Debug)]
@@ -61,7 +68,7 @@ struct ApplicationState {
client: WalletServerClient,
public_wallet_server_url: BaseUrl,
public_url: BaseUrl,
- usecases: HashMap,
+ usecases: IndexMap,
wallet_web: WalletWeb,
}
@@ -111,6 +118,7 @@ pub fn create_router(settings: Settings) -> Router {
let root_dir = env::var("CARGO_MANIFEST_DIR").map(PathBuf::from).unwrap_or_default();
let mut app = Router::new()
+ .route("/", get(index))
.route("/sessions", post(create_session))
.route("/:usecase/", get(usecase))
.route(&format!("/:usecase/{}", RETURN_URL_SEGMENT), get(disclosed_attributes))
@@ -131,7 +139,7 @@ pub fn create_router(settings: Settings) -> Router {
app
}
-#[derive(Deserialize, Serialize)]
+#[derive(Serialize, Deserialize)]
struct SessionOptions {
usecase: String,
}
@@ -142,31 +150,59 @@ struct SessionResponse {
session_token: SessionToken,
}
-#[derive(Template, Serialize)]
-#[template(path = "disclosed/attributes.askama", escape = "html", ext = "html")]
-struct DisclosureTemplate<'a> {
- usecase: &'a str,
- attributes: DisclosedAttributes,
+#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, strum::Display, strum::EnumIter)]
+pub enum Language {
+ #[default]
+ #[serde(rename = "nl")]
+ #[strum(to_string = "nl")]
+ Nl,
+ #[serde(rename = "en")]
+ #[strum(to_string = "en")]
+ En,
}
-#[derive(Template, Serialize)]
-#[template(path = "usecase/usecase.askama", escape = "html", ext = "html")]
-struct UsecaseTemplate<'a> {
- usecase: &'a str,
- usecase_js_sha256: &'a str,
- wallet_web_filename: &'a str,
- wallet_web_sha256: &'a str,
- error: Option<&'a str>,
+impl Language {
+ fn parse(s: &str) -> Option {
+ match s.split('-').next() {
+ Some("en") => Some(Language::En),
+ Some("nl") => Some(Language::Nl),
+ _ => None,
+ }
+ }
+
+ fn match_accept_language(headers: &HeaderMap) -> Option {
+ let accept_language = headers.get(ACCEPT_LANGUAGE)?;
+ let languages = accept_language::parse(accept_language.to_str().ok()?);
+
+ // applies function to the elements of iterator and returns the first non-None result
+ languages.into_iter().find_map(|l| Language::parse(&l))
+ }
}
#[derive(Debug, Serialize, Deserialize)]
-pub struct DisclosedAttributesParams {
- pub nonce: Option,
- pub session_token: SessionToken,
+pub struct LanguageParam {
+ pub lang: Language,
+}
+
+#[async_trait]
+impl FromRequestParts for Language
+where
+ S: Send + Sync,
+{
+ type Rejection = std::convert::Infallible;
+
+ async fn from_request_parts(parts: &mut Parts, state: &S) -> std::result::Result {
+ let lang = Query::::from_request_parts(parts, state)
+ .await
+ .map(|l| l.lang)
+ .unwrap_or(Language::match_accept_language(&parts.headers).unwrap_or_default());
+ Ok(lang)
+ }
}
async fn create_session(
State(state): State>,
+ language: Language,
Json(options): Json,
) -> Result> {
let usecase = state
@@ -174,24 +210,26 @@ async fn create_session(
.get(&options.usecase)
.ok_or(anyhow::Error::msg("usecase not found"))?;
+ let return_url_template = match usecase.return_url {
+ ReturnUrlMode::None => None,
+ _ => Some(
+ format!(
+ "{}/{}?session_token={{session_token}}&lang={}",
+ state.public_url.join(&options.usecase),
+ RETURN_URL_SEGMENT,
+ language,
+ )
+ .parse()
+ .expect("should always be a valid ReturnUrlTemplate"),
+ ),
+ };
+
let session_token = state
.client
.start(
options.usecase.clone(),
usecase.items_requests.clone(),
- if usecase.return_url == ReturnUrlMode::None {
- None
- } else {
- Some(
- format!(
- "{}/{}?session_token={{session_token}}",
- state.public_url.join(&options.usecase),
- RETURN_URL_SEGMENT
- )
- .parse()
- .expect("should always be a valid ReturnUrlTemplate"),
- )
- },
+ return_url_template,
)
.await?;
@@ -204,29 +242,105 @@ async fn create_session(
Ok(result.into())
}
+struct BaseTemplate<'a> {
+ session_token: Option,
+ nonce: Option,
+ selected_lang: Language,
+ trans: &'a Words<'a>,
+ available_languages: &'a Vec,
+}
+
+#[derive(Template)]
+#[template(path = "index.askama", escape = "html", ext = "html")]
+struct IndexTemplate<'a> {
+ usecases: &'a [&'a str],
+ base: BaseTemplate<'a>,
+}
+
+async fn index(State(state): State>, language: Language) -> Result {
+ let result = IndexTemplate {
+ usecases: &state.usecases.keys().map(|s| s.as_str()).collect::>(),
+ base: BaseTemplate {
+ session_token: None,
+ nonce: None,
+ selected_lang: language,
+ trans: &TRANSLATIONS[language],
+ available_languages: &Language::iter().collect(),
+ },
+ };
+
+ Ok(askama_axum::into_response(&result))
+}
+
+#[derive(Template)]
+#[template(path = "usecase/usecase.askama", escape = "html", ext = "html")]
+struct UsecaseTemplate<'a> {
+ usecase: &'a str,
+ start_url: Url,
+ usecase_js_sha256: &'a str,
+ wallet_web_filename: &'a str,
+ wallet_web_sha256: &'a str,
+ base: BaseTemplate<'a>,
+}
+
static USECASE_JS_SHA256: LazyLock =
LazyLock::new(|| BASE64_STANDARD.encode(sha256(include_bytes!("../assets/usecase.js"))));
-async fn usecase(State(state): State>, Path(usecase): Path) -> Result {
+fn format_start_url(public_url: &BaseUrl, lang: Language) -> Url {
+ let mut start_url = public_url.join("/sessions");
+ start_url.set_query(Some(
+ serde_urlencoded::to_string(LanguageParam { lang }).unwrap().as_str(),
+ ));
+ start_url
+}
+
+async fn usecase(
+ State(state): State>,
+ Path(usecase): Path,
+ language: Language,
+) -> Result {
if !state.usecases.contains_key(&usecase) {
return Ok(StatusCode::NOT_FOUND.into_response());
}
+ let start_url = format_start_url(&state.public_url, language);
let result = UsecaseTemplate {
usecase: &usecase,
+ start_url,
usecase_js_sha256: &USECASE_JS_SHA256,
wallet_web_filename: &state.wallet_web.filename.to_string_lossy(),
wallet_web_sha256: &state.wallet_web.sha256,
- error: None,
+ base: BaseTemplate {
+ session_token: None,
+ nonce: None,
+ selected_lang: language,
+ trans: &TRANSLATIONS[language],
+ available_languages: &Language::iter().collect(),
+ },
};
Ok(askama_axum::into_response(&result))
}
+#[derive(Debug, Serialize, Deserialize)]
+pub struct DisclosedAttributesParams {
+ pub nonce: Option,
+ pub session_token: SessionToken,
+}
+
+#[derive(Template)]
+#[template(path = "disclosed/attributes.askama", escape = "html", ext = "html")]
+struct DisclosedAttributesTemplate<'a> {
+ usecase: &'a str,
+ attributes: DisclosedAttributes,
+ base: BaseTemplate<'a>,
+}
+
async fn disclosed_attributes(
State(state): State>,
Path(usecase): Path,
Query(params): Query,
+ language: Language,
) -> Result {
if !state.usecases.contains_key(&usecase) {
return Ok(StatusCode::NOT_FOUND.into_response());
@@ -234,25 +348,36 @@ async fn disclosed_attributes(
let attributes = state
.client
- .disclosed_attributes(params.session_token, params.nonce)
+ .disclosed_attributes(params.session_token.clone(), params.nonce.clone())
.await;
+ let start_url = format_start_url(&state.public_url, language);
+ let base = BaseTemplate {
+ session_token: Some(params.session_token),
+ nonce: params.nonce,
+ selected_lang: language,
+ trans: &TRANSLATIONS[language],
+ available_languages: &Language::iter().collect(),
+ };
+
match attributes {
Ok(attributes) => {
- let result = DisclosureTemplate {
+ let result = DisclosedAttributesTemplate {
usecase: &usecase,
attributes,
+ base,
};
Ok(askama_axum::into_response(&result))
}
Err(err) => {
- let err = err.to_string();
+ warn!("Error getting disclosed attributes: {err}");
let result = UsecaseTemplate {
usecase: &usecase,
+ start_url,
usecase_js_sha256: &USECASE_JS_SHA256,
wallet_web_filename: &state.wallet_web.filename.to_string_lossy(),
wallet_web_sha256: &state.wallet_web.sha256,
- error: Some(&err),
+ base,
};
Ok(askama_axum::into_response(&result))
}
@@ -276,3 +401,20 @@ mod filters {
Ok(format!("attribute '{name}' cannot be found"))
}
}
+
+#[cfg(test)]
+mod test {
+ use rstest::rstest;
+
+ use super::*;
+
+ #[rstest]
+ #[case("en", Some(Language::En))]
+ #[case("nl", Some(Language::Nl))]
+ #[case("123", None)]
+ #[case("en-GB", Some(Language::En))]
+ #[case("nl-NL", Some(Language::Nl))]
+ fn test_parse_language(#[case] s: &str, #[case] expected: Option) {
+ assert_eq!(Language::parse(s), expected);
+ }
+}
diff --git a/wallet_core/mock_relying_party/src/lib.rs b/wallet_core/mock_relying_party/src/lib.rs
index 3334a2c61..1a16bb231 100644
--- a/wallet_core/mock_relying_party/src/lib.rs
+++ b/wallet_core/mock_relying_party/src/lib.rs
@@ -2,6 +2,7 @@ pub mod app;
pub mod client;
pub mod server;
pub mod settings;
+pub mod translations;
// workaround for: https://github.com/djc/askama/issues/810#issuecomment-1494522435
pub mod askama_axum;
diff --git a/wallet_core/mock_relying_party/src/settings.rs b/wallet_core/mock_relying_party/src/settings.rs
index f7262cb8a..04a92b3f9 100644
--- a/wallet_core/mock_relying_party/src/settings.rs
+++ b/wallet_core/mock_relying_party/src/settings.rs
@@ -1,7 +1,8 @@
-use std::{collections::HashMap, env, net::IpAddr, path::PathBuf};
+use std::{env, net::IpAddr, path::PathBuf};
use config::{Config, ConfigError, Environment, File};
use http::{header::InvalidHeaderValue, HeaderValue};
+use indexmap::IndexMap;
use nutype::nutype;
use serde::{Deserialize, Serialize};
use url::Url;
@@ -19,7 +20,7 @@ pub struct Settings {
#[serde(default)]
pub allow_origins: Vec,
pub wallet_web: WalletWeb,
- pub usecases: HashMap,
+ pub usecases: IndexMap,
pub sentry: Option,
}
diff --git a/wallet_core/mock_relying_party/src/translations.rs b/wallet_core/mock_relying_party/src/translations.rs
new file mode 100644
index 000000000..7c3ebe7e1
--- /dev/null
+++ b/wallet_core/mock_relying_party/src/translations.rs
@@ -0,0 +1,187 @@
+use std::ops::Index;
+
+use crate::app::Language;
+
+pub struct Translations<'a> {
+ en: Words<'a>,
+ nl: Words<'a>,
+}
+
+impl<'a> Index for Translations<'a> {
+ type Output = Words<'a>;
+
+ fn index(&self, lang: Language) -> &Self::Output {
+ match lang {
+ Language::Nl => &self.nl,
+ Language::En => &self.en,
+ }
+ }
+}
+
+pub const TRANSLATIONS: Translations = Translations {
+ en: Words {
+ en: "English",
+ nl: "Nederlands",
+ index_title: "NL Wallet demo",
+ index_intro: "These examples are fictional and for illustration purposes only. Follow the developments at",
+ index_intro_link: "edi.pleio.nl",
+ amsterdam_index: "Log in municipality",
+ marketplace_index: "Log in webshop",
+ xyz_index: "Open bank account",
+ monkeybike_index: "Create account",
+ demo_bar_text: "NL Wallet demo",
+ demo_see_other: "View other",
+ demo_see_examples: "examples",
+ demo_follow_development: "Follow the developments at",
+ continue_with_nl_wallet: "Continue with NL Wallet",
+ continue_with_google: "Continue with Google",
+ continue_with_email: "Continue with email",
+ login_with_nl_wallet: "Login with NL Wallet",
+ login_with_digid: "Login with DigiD",
+ use_nl_wallet: "Use NL Wallet",
+ choose_another_method: "Choose another method",
+ amsterdam_title: "Municipality of Amsterdam",
+ amsterdam_failed: "Login failed",
+ amsterdam_try_again: "Try again",
+ amsterdam_login: "Login to Mijn Amsterdam",
+ amsterdam_subtitle: "For individuals and sole proprietors",
+ amsterdam_nl_wallet_digid: "You need either the NL Wallet app or DigiD.",
+ amsterdam_profile_name: "Account",
+ amsterdam_success: "Success",
+ amsterdam_logged_in: "You are logged in.",
+ amsterdam_welcome: "Welcome to Mijn Amsterdam",
+ amsterdam_subtitle_disclosed: "Personal online services for Amsterdam residents",
+ monkeybike_title: "MonkeyBike",
+ monkeybike_login: "Log in",
+ marketplace_title: "Marktplek",
+ marketplace_login: "Sign up or log in",
+ login_failed_try_again: "Login failed. Try again.",
+ click_continue: "By clicking \"Continue\", you agree to the",
+ terms_and_conditions: "Terms and Conditions",
+ and_the: "and the",
+ privacy_policy: "Privacy Policy",
+ xyz_title: "XYZ Bank",
+ xyz_open_account: "Open bank account",
+ xyz_identify_yourself: "Step 1. Identify yourself",
+ xyz_failed_try_again: "Identification failed. Try again.",
+ xyz_success: "Identification successful",
+ welcome: "Welcome",
+ search_product: "Search product...",
+ search_by_topic: "Search by topic...",
+ next: "Next",
+ },
+ nl: Words {
+ en: "English",
+ nl: "Nederlands",
+ index_title: "NL Wallet demo",
+ index_intro: "Deze voorbeelden zijn fictief en dienen alleen ter illustratie. Volg de ontwikkelingen op",
+ index_intro_link: "edi.pleio.nl",
+ amsterdam_index: "Inloggen gemeente",
+ marketplace_index: "Inloggen webshop",
+ xyz_index: "Bankrekening openen",
+ monkeybike_index: "Account aanmaken",
+ demo_bar_text: "NL Wallet demo",
+ demo_see_other: "Bekijk andere",
+ demo_see_examples: "voorbeelden",
+ demo_follow_development: "Volg de ontwikkelingen op",
+ continue_with_nl_wallet: "Verder met NL Wallet",
+ continue_with_google: "Verder met Google",
+ continue_with_email: "Verder met email",
+ login_with_nl_wallet: "Inloggen met NL Wallet",
+ login_with_digid: "Inloggen met DigiD",
+ use_nl_wallet: "Gebruik NL Wallet",
+ choose_another_method: "Kies een ander middel",
+ amsterdam_title: "Gemeente Amsterdam",
+ amsterdam_failed: "Inloggen mislukt",
+ amsterdam_try_again: "Probeer het opnieuw",
+ amsterdam_login: "Inloggen op Mijn Amsterdam",
+ amsterdam_subtitle: "Voor particulieren en eenmanszaken",
+ amsterdam_nl_wallet_digid: "U heeft de NL Wallet app of DigiD nodig.",
+ amsterdam_profile_name: "Account",
+ amsterdam_success: "Gelukt",
+ amsterdam_logged_in: "Je bent ingelogd.",
+ amsterdam_welcome: "Welkom in Mijn Amsterdam",
+ amsterdam_subtitle_disclosed: "Persoonlijke online dienstverlening voor de Amsterdammer",
+ monkeybike_title: "MonkeyBike",
+ monkeybike_login: "Meld je aan",
+ marketplace_title: "Marktplek",
+ marketplace_login: "Meld je aan of log in",
+ login_failed_try_again: "Inloggen mislukt. Probeer het opnieuw.",
+ click_continue: "Door op \"Verder\" te klikken, ga je akkoord met de",
+ terms_and_conditions: "Algemene Voorwaarden",
+ and_the: "en het",
+ privacy_policy: "Privacybeleid",
+ xyz_title: "XYZ Bank",
+ xyz_open_account: "Bankrekening openen",
+ xyz_identify_yourself: "Stap 1. Identificeer uzelf",
+ xyz_failed_try_again: "Identificatie mislukt. Probeer het opnieuw.",
+ xyz_success: "Identificatie gelukt",
+ welcome: "Welkom",
+ search_product: "Zoek product...",
+ search_by_topic: "Zoek op onderwerp...",
+ next: "Volgende",
+ },
+};
+
+pub struct Words<'a> {
+ en: &'a str,
+ nl: &'a str,
+ pub index_title: &'a str,
+ pub index_intro: &'a str,
+ pub index_intro_link: &'a str,
+ pub amsterdam_index: &'a str,
+ pub monkeybike_index: &'a str,
+ pub marketplace_index: &'a str,
+ pub xyz_index: &'a str,
+ pub demo_bar_text: &'a str,
+ pub demo_see_other: &'a str,
+ pub demo_see_examples: &'a str,
+ pub demo_follow_development: &'a str,
+ pub continue_with_nl_wallet: &'a str,
+ pub continue_with_google: &'a str,
+ pub continue_with_email: &'a str,
+ pub login_with_nl_wallet: &'a str,
+ pub login_with_digid: &'a str,
+ pub use_nl_wallet: &'a str,
+ pub choose_another_method: &'a str,
+ pub amsterdam_title: &'a str,
+ pub amsterdam_failed: &'a str,
+ pub amsterdam_try_again: &'a str,
+ pub amsterdam_login: &'a str,
+ pub amsterdam_subtitle: &'a str,
+ pub amsterdam_nl_wallet_digid: &'a str,
+ pub amsterdam_profile_name: &'a str,
+ pub amsterdam_success: &'a str,
+ pub amsterdam_logged_in: &'a str,
+ pub amsterdam_welcome: &'a str,
+ pub amsterdam_subtitle_disclosed: &'a str,
+ pub monkeybike_title: &'a str,
+ pub monkeybike_login: &'a str,
+ pub marketplace_title: &'a str,
+ pub marketplace_login: &'a str,
+ pub login_failed_try_again: &'a str,
+ pub click_continue: &'a str,
+ pub terms_and_conditions: &'a str,
+ pub and_the: &'a str,
+ pub privacy_policy: &'a str,
+ pub xyz_title: &'a str,
+ pub xyz_open_account: &'a str,
+ pub xyz_identify_yourself: &'a str,
+ pub xyz_failed_try_again: &'a str,
+ pub xyz_success: &'a str,
+ pub welcome: &'a str,
+ pub search_product: &'a str,
+ pub search_by_topic: &'a str,
+ pub next: &'a str,
+}
+
+impl<'a> Index for Words<'a> {
+ type Output = &'a str;
+
+ fn index(&self, lang: Language) -> &Self::Output {
+ match lang {
+ Language::Nl => &self.nl,
+ Language::En => &self.en,
+ }
+ }
+}
diff --git a/wallet_core/mock_relying_party/templates/components/demo_bar.askama b/wallet_core/mock_relying_party/templates/components/demo_bar.askama
index 30be1b18c..b0036e5f4 100644
--- a/wallet_core/mock_relying_party/templates/components/demo_bar.askama
+++ b/wallet_core/mock_relying_party/templates/components/demo_bar.askama
@@ -1,11 +1,36 @@
-{% macro demo_bar(text, url, url_text) %}
+{% macro demo_bar(text, url, url_text, selected_lang, trans, available_languages, session_token, nonce) %}