Merge remote-tracking branch 'origin/main' into quenting/upstream-oauth/skip-interactive
This commit is contained in:
12
.github/workflows/build.yaml
vendored
12
.github/workflows/build.yaml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# Need a full clone so that `git describe` reports the right version
|
||||
fetch-depth: 0
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- uses: ./.github/actions/build-frontend
|
||||
- uses: ./.github/actions/build-policies
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -276,7 +276,7 @@ jobs:
|
||||
|
||||
- name: Build and push
|
||||
id: bake
|
||||
uses: docker/bake-action@v6.9.0
|
||||
uses: docker/bake-action@v6.10.0
|
||||
with:
|
||||
files: |
|
||||
./docker-bake.hcl
|
||||
@@ -376,7 +376,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/scripts
|
||||
@@ -454,7 +454,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/scripts
|
||||
|
||||
22
.github/workflows/ci.yaml
vendored
22
.github/workflows/ci.yaml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- uses: ./.github/actions/build-policies
|
||||
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6.0.0
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6.0.0
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6.0.0
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
@@ -156,10 +156,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Run `cargo-deny`
|
||||
uses: EmbarkStudios/cargo-deny-action@v2.0.13
|
||||
uses: EmbarkStudios/cargo-deny-action@v2.0.14
|
||||
with:
|
||||
rust-version: stable
|
||||
|
||||
@@ -172,7 +172,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
run: |
|
||||
@@ -213,7 +213,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@1.89.0
|
||||
@@ -238,7 +238,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -291,7 +291,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
6
.github/workflows/coverage.yaml
vendored
6
.github/workflows/coverage.yaml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- uses: ./.github/actions/build-policies
|
||||
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- uses: ./.github/actions/build-frontend
|
||||
env:
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
2
.github/workflows/docs.yaml
vendored
2
.github/workflows/docs.yaml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
2
.github/workflows/merge-back.yaml
vendored
2
.github/workflows/merge-back.yaml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/scripts
|
||||
|
||||
6
.github/workflows/release-branch.yaml
vendored
6
.github/workflows/release-branch.yaml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
run: exit 1
|
||||
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6.0.0
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
needs: [tag, compute-version, localazy]
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/scripts
|
||||
|
||||
4
.github/workflows/release-bump.yaml
vendored
4
.github/workflows/release-bump.yaml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
run: exit 1
|
||||
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
needs: [tag, compute-version]
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/scripts
|
||||
|
||||
2
.github/workflows/tag.yaml
vendored
2
.github/workflows/tag.yaml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
4
.github/workflows/translations-download.yaml
vendored
4
.github/workflows/translations-download.yaml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
run: exit 1
|
||||
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6.0.0
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v7.0.8
|
||||
uses: peter-evans/create-pull-request@v7.0.9
|
||||
with:
|
||||
sign-commits: true
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||
|
||||
2
.github/workflows/translations-upload.yaml
vendored
2
.github/workflows/translations-upload.yaml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6.0.0
|
||||
|
||||
5
Cargo.lock
generated
5
Cargo.lock
generated
@@ -1089,9 +1089,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
|
||||
checksum = "db05ffb6856bf0ecdf6367558a76a0e8a77b1713044eb92845c692100ed50190"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
@@ -3723,6 +3723,7 @@ dependencies = [
|
||||
"mas-data-model",
|
||||
"mas-i18n",
|
||||
"mas-iana",
|
||||
"mas-policy",
|
||||
"mas-router",
|
||||
"mas-spa",
|
||||
"minijinja",
|
||||
|
||||
@@ -177,7 +177,7 @@ features = ["std"]
|
||||
|
||||
# Utility for converting between different cases
|
||||
[workspace.dependencies.convert_case]
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
|
||||
# CRC calculation
|
||||
[workspace.dependencies.crc]
|
||||
|
||||
@@ -145,6 +145,7 @@ pub async fn policy_factory_from_config(
|
||||
register: config.register_entrypoint.clone(),
|
||||
client_registration: config.client_registration_entrypoint.clone(),
|
||||
authorization_grant: config.authorization_grant_entrypoint.clone(),
|
||||
compat_login: config.compat_login_entrypoint.clone(),
|
||||
email: config.email_entrypoint.clone(),
|
||||
};
|
||||
|
||||
|
||||
@@ -62,6 +62,14 @@ fn is_default_password_entrypoint(value: &String) -> bool {
|
||||
*value == default_password_entrypoint()
|
||||
}
|
||||
|
||||
fn default_compat_login_entrypoint() -> String {
|
||||
"compat_login/violation".to_owned()
|
||||
}
|
||||
|
||||
fn is_default_compat_login_entrypoint(value: &String) -> bool {
|
||||
*value == default_compat_login_entrypoint()
|
||||
}
|
||||
|
||||
fn default_email_entrypoint() -> String {
|
||||
"email/violation".to_owned()
|
||||
}
|
||||
@@ -111,6 +119,13 @@ pub struct PolicyConfig {
|
||||
)]
|
||||
pub authorization_grant_entrypoint: String,
|
||||
|
||||
/// Entrypoint to use when evaluating compatibility logins
|
||||
#[serde(
|
||||
default = "default_compat_login_entrypoint",
|
||||
skip_serializing_if = "is_default_compat_login_entrypoint"
|
||||
)]
|
||||
pub compat_login_entrypoint: String,
|
||||
|
||||
/// Entrypoint to use when changing password
|
||||
#[serde(
|
||||
default = "default_password_entrypoint",
|
||||
@@ -137,6 +152,7 @@ impl Default for PolicyConfig {
|
||||
client_registration_entrypoint: default_client_registration_entrypoint(),
|
||||
register_entrypoint: default_register_entrypoint(),
|
||||
authorization_grant_entrypoint: default_authorization_grant_entrypoint(),
|
||||
compat_login_entrypoint: default_compat_login_entrypoint(),
|
||||
password_entrypoint: default_password_entrypoint(),
|
||||
email_entrypoint: default_email_entrypoint(),
|
||||
data: default_data(),
|
||||
|
||||
@@ -16,6 +16,7 @@ use mas_data_model::{
|
||||
User,
|
||||
};
|
||||
use mas_matrix::HomeserverConnection;
|
||||
use mas_policy::{Policy, Requester, ViolationCode, model::CompatLogin};
|
||||
use mas_storage::{
|
||||
BoxRepository, BoxRepositoryFactory, RepositoryAccess,
|
||||
compat::{
|
||||
@@ -37,6 +38,7 @@ use crate::{
|
||||
BoundActivityTracker, Limiter, METER, RequesterFingerprint, impl_from_error_for_route,
|
||||
passwords::{PasswordManager, PasswordVerificationResult},
|
||||
rate_limit::PasswordCheckLimitedError,
|
||||
session::count_user_sessions_for_limiting,
|
||||
};
|
||||
|
||||
static LOGIN_COUNTER: LazyLock<Counter<u64>> = LazyLock::new(|| {
|
||||
@@ -213,9 +215,16 @@ pub enum RouteError {
|
||||
|
||||
#[error("failed to provision device")]
|
||||
ProvisionDeviceFailed(#[source] anyhow::Error),
|
||||
|
||||
#[error("login rejected by policy")]
|
||||
PolicyRejected,
|
||||
|
||||
#[error("login rejected by policy (hard session limit reached)")]
|
||||
PolicyHardSessionLimitReached,
|
||||
}
|
||||
|
||||
impl_from_error_for_route!(mas_storage::RepositoryError);
|
||||
impl_from_error_for_route!(mas_policy::EvaluationError);
|
||||
|
||||
impl From<anyhow::Error> for RouteError {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
@@ -274,6 +283,16 @@ impl IntoResponse for RouteError {
|
||||
error: "User account has been locked",
|
||||
status: StatusCode::UNAUTHORIZED,
|
||||
},
|
||||
Self::PolicyRejected => MatrixError {
|
||||
errcode: "M_FORBIDDEN",
|
||||
error: "Login denied by the policy enforced by this service",
|
||||
status: StatusCode::FORBIDDEN,
|
||||
},
|
||||
Self::PolicyHardSessionLimitReached => MatrixError {
|
||||
errcode: "M_FORBIDDEN",
|
||||
error: "You have reached your hard device limit. Please visit your account page to sign some out.",
|
||||
status: StatusCode::FORBIDDEN,
|
||||
},
|
||||
};
|
||||
|
||||
(sentry_event_id, response).into_response()
|
||||
@@ -290,6 +309,7 @@ pub(crate) async fn post(
|
||||
State(homeserver): State<Arc<dyn HomeserverConnection>>,
|
||||
State(site_config): State<SiteConfig>,
|
||||
State(limiter): State<Limiter>,
|
||||
mut policy: Policy,
|
||||
requester: RequesterFingerprint,
|
||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||
MatrixJsonBody(input): MatrixJsonBody<RequestBody>,
|
||||
@@ -329,6 +349,11 @@ pub(crate) async fn post(
|
||||
&limiter,
|
||||
requester,
|
||||
&mut repo,
|
||||
&mut policy,
|
||||
Requester {
|
||||
ip_address: activity_tracker.ip(),
|
||||
user_agent: user_agent.clone(),
|
||||
},
|
||||
username,
|
||||
password,
|
||||
input.device_id, // TODO check for validity
|
||||
@@ -342,6 +367,11 @@ pub(crate) async fn post(
|
||||
&mut rng,
|
||||
&clock,
|
||||
&mut repo,
|
||||
&mut policy,
|
||||
Requester {
|
||||
ip_address: activity_tracker.ip(),
|
||||
user_agent: user_agent.clone(),
|
||||
},
|
||||
&token,
|
||||
input.device_id,
|
||||
input.initial_device_display_name,
|
||||
@@ -459,6 +489,8 @@ async fn token_login(
|
||||
rng: &mut (dyn RngCore + Send),
|
||||
clock: &dyn Clock,
|
||||
repo: &mut BoxRepository,
|
||||
policy: &mut Policy,
|
||||
requester: Requester,
|
||||
token: &str,
|
||||
requested_device_id: Option<String>,
|
||||
initial_device_display_name: Option<String>,
|
||||
@@ -544,10 +576,38 @@ async fn token_login(
|
||||
Device::generate(rng)
|
||||
};
|
||||
|
||||
repo.app_session()
|
||||
let session_replaced = repo
|
||||
.app_session()
|
||||
.finish_sessions_to_replace_device(clock, &browser_session.user, &device)
|
||||
.await?;
|
||||
|
||||
let session_counts = count_user_sessions_for_limiting(repo, &browser_session.user).await?;
|
||||
|
||||
let res = policy
|
||||
.evaluate_compat_login(mas_policy::CompatLoginInput {
|
||||
user: &browser_session.user,
|
||||
login: CompatLogin::Token,
|
||||
session_replaced,
|
||||
session_counts,
|
||||
requester,
|
||||
})
|
||||
.await?;
|
||||
if !res.valid() {
|
||||
// If the only violation is that we have too many sessions, then handle that
|
||||
// separately.
|
||||
// In the future, we intend to evict some sessions automatically instead. We
|
||||
// don't trigger this if there was some other violation anyway, since that means
|
||||
// that removing a session wouldn't actually unblock the login.
|
||||
if res.violations.len() == 1 {
|
||||
let violation = &res.violations[0];
|
||||
if violation.code == Some(ViolationCode::TooManySessions) {
|
||||
// The only violation is having reached the session limit.
|
||||
return Err(RouteError::PolicyHardSessionLimitReached);
|
||||
}
|
||||
}
|
||||
return Err(RouteError::PolicyRejected);
|
||||
}
|
||||
|
||||
// We first create the session in the database, commit the transaction, then
|
||||
// create it on the homeserver, scheduling a device sync job afterwards to
|
||||
// make sure we don't end up in an inconsistent state.
|
||||
@@ -578,6 +638,8 @@ async fn user_password_login(
|
||||
limiter: &Limiter,
|
||||
requester: RequesterFingerprint,
|
||||
repo: &mut BoxRepository,
|
||||
policy: &mut Policy,
|
||||
policy_requester: Requester,
|
||||
username: &str,
|
||||
password: String,
|
||||
requested_device_id: Option<String>,
|
||||
@@ -647,10 +709,38 @@ async fn user_password_login(
|
||||
Device::generate(&mut rng)
|
||||
};
|
||||
|
||||
repo.app_session()
|
||||
let session_replaced = repo
|
||||
.app_session()
|
||||
.finish_sessions_to_replace_device(clock, &user, &device)
|
||||
.await?;
|
||||
|
||||
let session_counts = count_user_sessions_for_limiting(repo, &user).await?;
|
||||
|
||||
let res = policy
|
||||
.evaluate_compat_login(mas_policy::CompatLoginInput {
|
||||
user: &user,
|
||||
login: CompatLogin::Password,
|
||||
session_replaced,
|
||||
session_counts,
|
||||
requester: policy_requester,
|
||||
})
|
||||
.await?;
|
||||
if !res.valid() {
|
||||
// If the only violation is that we have too many sessions, then handle that
|
||||
// separately.
|
||||
// In the future, we intend to evict some sessions automatically instead. We
|
||||
// don't trigger this if there was some other violation anyway, since that means
|
||||
// that removing a session wouldn't actually unblock the login.
|
||||
if res.violations.len() == 1 {
|
||||
let violation = &res.violations[0];
|
||||
if violation.code == Some(ViolationCode::TooManySessions) {
|
||||
// The only violation is having reached the session limit.
|
||||
return Err(RouteError::PolicyHardSessionLimitReached);
|
||||
}
|
||||
}
|
||||
return Err(RouteError::PolicyRejected);
|
||||
}
|
||||
|
||||
let session = repo
|
||||
.compat_session()
|
||||
.add(
|
||||
|
||||
@@ -11,23 +11,27 @@ use axum::{
|
||||
extract::{Form, Path, State},
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
};
|
||||
use axum_extra::extract::Query;
|
||||
use axum_extra::{TypedHeader, extract::Query};
|
||||
use chrono::Duration;
|
||||
use hyper::StatusCode;
|
||||
use mas_axum_utils::{
|
||||
InternalError,
|
||||
cookies::CookieJar,
|
||||
csrf::{CsrfExt, ProtectedForm},
|
||||
};
|
||||
use mas_data_model::{BoxClock, BoxRng, Clock};
|
||||
use mas_policy::{Policy, model::CompatLogin};
|
||||
use mas_router::{CompatLoginSsoAction, UrlBuilder};
|
||||
use mas_storage::{BoxRepository, RepositoryAccess, compat::CompatSsoLoginRepository};
|
||||
use mas_templates::{CompatSsoContext, ErrorContext, TemplateContext, Templates};
|
||||
use mas_templates::{
|
||||
CompatLoginPolicyViolationContext, CompatSsoContext, ErrorContext, TemplateContext, Templates,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{
|
||||
PreferredLanguage,
|
||||
session::{SessionOrFallback, load_session_or_fallback},
|
||||
BoundActivityTracker, PreferredLanguage,
|
||||
session::{SessionOrFallback, count_user_sessions_for_limiting, load_session_or_fallback},
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -56,10 +60,15 @@ pub async fn get(
|
||||
mut repo: BoxRepository,
|
||||
State(templates): State<Templates>,
|
||||
State(url_builder): State<UrlBuilder>,
|
||||
mut policy: Policy,
|
||||
activity_tracker: BoundActivityTracker,
|
||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||
cookie_jar: CookieJar,
|
||||
Path(id): Path<Ulid>,
|
||||
Query(params): Query<Params>,
|
||||
) -> Result<Response, InternalError> {
|
||||
let user_agent = user_agent.map(|ua| ua.to_string());
|
||||
|
||||
let (cookie_jar, maybe_session) = match load_session_or_fallback(
|
||||
cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
|
||||
)
|
||||
@@ -107,6 +116,35 @@ pub async fn get(
|
||||
return Ok((cookie_jar, Html(content)).into_response());
|
||||
}
|
||||
|
||||
let session_counts = count_user_sessions_for_limiting(&mut repo, &session.user).await?;
|
||||
|
||||
let res = policy
|
||||
.evaluate_compat_login(mas_policy::CompatLoginInput {
|
||||
user: &session.user,
|
||||
login: CompatLogin::Sso {
|
||||
redirect_uri: login.redirect_uri.to_string(),
|
||||
},
|
||||
// We don't know if there's going to be a replacement until we received the device ID,
|
||||
// which happens too late.
|
||||
session_replaced: false,
|
||||
session_counts,
|
||||
requester: mas_policy::Requester {
|
||||
ip_address: activity_tracker.ip(),
|
||||
user_agent,
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
if !res.valid() {
|
||||
let ctx = CompatLoginPolicyViolationContext::for_violations(res.violations)
|
||||
.with_session(session)
|
||||
.with_csrf(csrf_token.form_value())
|
||||
.with_language(locale);
|
||||
|
||||
let content = templates.render_compat_login_policy_violation(&ctx)?;
|
||||
|
||||
return Ok((StatusCode::FORBIDDEN, cookie_jar, Html(content)).into_response());
|
||||
}
|
||||
|
||||
let ctx = CompatSsoContext::new(login)
|
||||
.with_session(session)
|
||||
.with_csrf(csrf_token.form_value())
|
||||
@@ -129,11 +167,16 @@ pub async fn post(
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
State(url_builder): State<UrlBuilder>,
|
||||
mut policy: Policy,
|
||||
activity_tracker: BoundActivityTracker,
|
||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||
cookie_jar: CookieJar,
|
||||
Path(id): Path<Ulid>,
|
||||
Query(params): Query<Params>,
|
||||
Form(form): Form<ProtectedForm<()>>,
|
||||
) -> Result<Response, InternalError> {
|
||||
let user_agent = user_agent.map(|ua| ua.to_string());
|
||||
|
||||
let (cookie_jar, maybe_session) = match load_session_or_fallback(
|
||||
cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
|
||||
)
|
||||
@@ -200,6 +243,37 @@ pub async fn post(
|
||||
redirect_uri
|
||||
};
|
||||
|
||||
let session_counts = count_user_sessions_for_limiting(&mut repo, &session.user).await?;
|
||||
|
||||
let res = policy
|
||||
.evaluate_compat_login(mas_policy::CompatLoginInput {
|
||||
user: &session.user,
|
||||
login: CompatLogin::Sso {
|
||||
redirect_uri: login.redirect_uri.to_string(),
|
||||
},
|
||||
session_counts,
|
||||
// We don't know if there's going to be a replacement until we received the device ID,
|
||||
// which happens too late.
|
||||
session_replaced: false,
|
||||
requester: mas_policy::Requester {
|
||||
ip_address: activity_tracker.ip(),
|
||||
user_agent,
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
|
||||
if !res.valid() {
|
||||
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
|
||||
let ctx = CompatLoginPolicyViolationContext::for_violations(res.violations)
|
||||
.with_session(session)
|
||||
.with_csrf(csrf_token.form_value())
|
||||
.with_language(locale);
|
||||
|
||||
let content = templates.render_compat_login_policy_violation(&ctx)?;
|
||||
|
||||
return Ok((StatusCode::FORBIDDEN, cookie_jar, Html(content)).into_response());
|
||||
}
|
||||
|
||||
// Note that if the login is not Pending,
|
||||
// this fails and aborts the transaction.
|
||||
repo.compat_sso_login()
|
||||
|
||||
@@ -272,6 +272,7 @@ where
|
||||
BoxRepository: FromRequestParts<S>,
|
||||
BoxClock: FromRequestParts<S>,
|
||||
BoxRng: FromRequestParts<S>,
|
||||
Policy: FromRequestParts<S>,
|
||||
{
|
||||
// A sub-router for human-facing routes with error handling
|
||||
let human_router = Router::new()
|
||||
|
||||
@@ -82,6 +82,7 @@ pub(crate) async fn policy_factory(
|
||||
register: "register/violation".to_owned(),
|
||||
client_registration: "client_registration/violation".to_owned(),
|
||||
authorization_grant: "authorization_grant/violation".to_owned(),
|
||||
compat_login: "compat_login/violation".to_owned(),
|
||||
email: "email/violation".to_owned(),
|
||||
};
|
||||
|
||||
|
||||
@@ -629,7 +629,7 @@ pub(crate) async fn get(
|
||||
.await?;
|
||||
}
|
||||
|
||||
// We matched an existing user and the conflict resolution is link to the
|
||||
// We matched an existing user and the conflict resolution is to link to the
|
||||
// existing user *only if* there is no existing link on that user
|
||||
UpstreamOAuthProviderOnConflict::Set => {
|
||||
// Find existing links for this provider and user
|
||||
@@ -643,7 +643,7 @@ pub(crate) async fn get(
|
||||
upstream_oauth_provider.id = %provider.id,
|
||||
upstream_oauth_link.id = %link.id,
|
||||
user.id = %existing_user.id,
|
||||
"Upstream provider returned a localpart {localpart:?} which is already used by another user. That user already has a ({count}) link to this provider, which isn't allowed by the conflict resolution"
|
||||
"Upstream provider returned a localpart {localpart:?} matching an existing user who already has {count} link(s) to this provider, which isn't allowed by the conflict resolution"
|
||||
);
|
||||
|
||||
// TODO: translate
|
||||
@@ -651,7 +651,7 @@ pub(crate) async fn get(
|
||||
.with_code("User exists")
|
||||
.with_description(format!(
|
||||
r"Upstream account provider returned {localpart:?} as username,
|
||||
which is not linked to another existing upstream account.
|
||||
but this user already has an existing link to this provider.
|
||||
Your homeserver does not allow replacing upstream account links automatically."
|
||||
))
|
||||
.with_language(&locale);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use mas_policy::model::{
|
||||
AuthorizationGrantInput, ClientRegistrationInput, EmailInput, RegisterInput,
|
||||
AuthorizationGrantInput, ClientRegistrationInput, CompatLoginInput, EmailInput, RegisterInput,
|
||||
};
|
||||
use schemars::{JsonSchema, generate::SchemaSettings};
|
||||
|
||||
@@ -42,5 +42,6 @@ fn main() {
|
||||
write_schema::<RegisterInput>(output_root, "register_input.json");
|
||||
write_schema::<ClientRegistrationInput>(output_root, "client_registration_input.json");
|
||||
write_schema::<AuthorizationGrantInput>(output_root, "authorization_grant_input.json");
|
||||
write_schema::<CompatLoginInput>(output_root, "compat_login_input.json");
|
||||
write_schema::<EmailInput>(output_root, "email_input.json");
|
||||
}
|
||||
|
||||
@@ -19,8 +19,9 @@ use thiserror::Error;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt};
|
||||
|
||||
pub use self::model::{
|
||||
AuthorizationGrantInput, ClientRegistrationInput, Code as ViolationCode, EmailInput,
|
||||
EvaluationResult, GrantType, RegisterInput, RegistrationMethod, Requester, Violation,
|
||||
AuthorizationGrantInput, ClientRegistrationInput, Code as ViolationCode, CompatLoginInput,
|
||||
EmailInput, EvaluationResult, GrantType, RegisterInput, RegistrationMethod, Requester,
|
||||
Violation,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -72,15 +73,17 @@ pub struct Entrypoints {
|
||||
pub register: String,
|
||||
pub client_registration: String,
|
||||
pub authorization_grant: String,
|
||||
pub compat_login: String,
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
impl Entrypoints {
|
||||
fn all(&self) -> [&str; 4] {
|
||||
fn all(&self) -> [&str; 5] {
|
||||
[
|
||||
self.register.as_str(),
|
||||
self.client_registration.as_str(),
|
||||
self.authorization_grant.as_str(),
|
||||
self.compat_login.as_str(),
|
||||
self.email.as_str(),
|
||||
]
|
||||
}
|
||||
@@ -459,6 +462,30 @@ impl Policy {
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Evaluate the `compat_login` entrypoint.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the policy engine fails to evaluate the entrypoint.
|
||||
#[tracing::instrument(
|
||||
name = "policy.evaluate.compat_login",
|
||||
skip_all,
|
||||
fields(
|
||||
%input.user.id,
|
||||
),
|
||||
)]
|
||||
pub async fn evaluate_compat_login(
|
||||
&mut self,
|
||||
input: CompatLoginInput<'_>,
|
||||
) -> Result<EvaluationResult, EvaluationError> {
|
||||
let [res]: [EvaluationResult; 1] = self
|
||||
.instance
|
||||
.evaluate(&mut self.store, &self.entrypoints.compat_login, &input)
|
||||
.await?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -468,6 +495,16 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
fn make_entrypoints() -> Entrypoints {
|
||||
Entrypoints {
|
||||
register: "register/violation".to_owned(),
|
||||
client_registration: "client_registration/violation".to_owned(),
|
||||
authorization_grant: "authorization_grant/violation".to_owned(),
|
||||
compat_login: "compat_login/violation".to_owned(),
|
||||
email: "email/violation".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_register() {
|
||||
let data = Data::new("example.com".to_owned(), None).with_rest(serde_json::json!({
|
||||
@@ -484,14 +521,9 @@ mod tests {
|
||||
|
||||
let file = tokio::fs::File::open(path).await.unwrap();
|
||||
|
||||
let entrypoints = Entrypoints {
|
||||
register: "register/violation".to_owned(),
|
||||
client_registration: "client_registration/violation".to_owned(),
|
||||
authorization_grant: "authorization_grant/violation".to_owned(),
|
||||
email: "email/violation".to_owned(),
|
||||
};
|
||||
|
||||
let factory = PolicyFactory::load(file, data, entrypoints).await.unwrap();
|
||||
let factory = PolicyFactory::load(file, data, make_entrypoints())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut policy = factory.instantiate().await.unwrap();
|
||||
|
||||
@@ -551,14 +583,9 @@ mod tests {
|
||||
|
||||
let file = tokio::fs::File::open(path).await.unwrap();
|
||||
|
||||
let entrypoints = Entrypoints {
|
||||
register: "register/violation".to_owned(),
|
||||
client_registration: "client_registration/violation".to_owned(),
|
||||
authorization_grant: "authorization_grant/violation".to_owned(),
|
||||
email: "email/violation".to_owned(),
|
||||
};
|
||||
|
||||
let factory = PolicyFactory::load(file, data, entrypoints).await.unwrap();
|
||||
let factory = PolicyFactory::load(file, data, make_entrypoints())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut policy = factory.instantiate().await.unwrap();
|
||||
|
||||
@@ -620,14 +647,9 @@ mod tests {
|
||||
|
||||
let file = tokio::fs::File::open(path).await.unwrap();
|
||||
|
||||
let entrypoints = Entrypoints {
|
||||
register: "register/violation".to_owned(),
|
||||
client_registration: "client_registration/violation".to_owned(),
|
||||
authorization_grant: "authorization_grant/violation".to_owned(),
|
||||
email: "email/violation".to_owned(),
|
||||
};
|
||||
|
||||
let factory = PolicyFactory::load(file, data, entrypoints).await.unwrap();
|
||||
let factory = PolicyFactory::load(file, data, make_entrypoints())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// That is around 1 MB of JSON data. Each element is a 5-digit string, so 8
|
||||
// characters including the quotes and a comma.
|
||||
|
||||
@@ -17,7 +17,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A well-known policy code.
|
||||
#[derive(Deserialize, Debug, Clone, Copy, JsonSchema)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum Code {
|
||||
/// The username is too short.
|
||||
@@ -75,7 +75,7 @@ impl Code {
|
||||
}
|
||||
|
||||
/// A single violation of a policy.
|
||||
#[derive(Deserialize, Debug, JsonSchema)]
|
||||
#[derive(Serialize, Deserialize, Debug, JsonSchema)]
|
||||
pub struct Violation {
|
||||
pub msg: String,
|
||||
pub redirect_uri: Option<String>,
|
||||
@@ -187,6 +187,42 @@ pub struct AuthorizationGrantInput<'a> {
|
||||
pub requester: Requester,
|
||||
}
|
||||
|
||||
/// Input for the compatibility login policy.
|
||||
#[derive(Serialize, Debug, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct CompatLoginInput<'a> {
|
||||
#[schemars(with = "std::collections::HashMap<String, serde_json::Value>")]
|
||||
pub user: &'a User,
|
||||
|
||||
/// How many sessions the user has.
|
||||
pub session_counts: SessionCounts,
|
||||
|
||||
/// Whether a session will be replaced by this login
|
||||
pub session_replaced: bool,
|
||||
|
||||
/// What type of login is being performed.
|
||||
/// This also determines whether the login is interactive.
|
||||
pub login: CompatLogin,
|
||||
|
||||
pub requester: Requester,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, JsonSchema)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum CompatLogin {
|
||||
/// Used as the interactive part of SSO login.
|
||||
#[serde(rename = "m.login.sso")]
|
||||
Sso { redirect_uri: String },
|
||||
|
||||
/// Used as the final (non-interactive) stage of SSO login.
|
||||
#[serde(rename = "m.login.token")]
|
||||
Token,
|
||||
|
||||
/// Non-interactive password-over-the-API login.
|
||||
#[serde(rename = "m.login.password")]
|
||||
Password,
|
||||
}
|
||||
|
||||
/// Information about how many sessions the user has
|
||||
#[derive(Serialize, Debug, JsonSchema)]
|
||||
pub struct SessionCounts {
|
||||
|
||||
@@ -487,14 +487,15 @@ impl AppSessionRepository for PgAppSessionRepository<'_> {
|
||||
clock: &dyn Clock,
|
||||
user: &User,
|
||||
device: &Device,
|
||||
) -> Result<(), Self::Error> {
|
||||
) -> Result<bool, Self::Error> {
|
||||
let mut affected = false;
|
||||
// TODO need to invoke this from all the oauth2 login sites
|
||||
let span = tracing::info_span!(
|
||||
"db.app_session.finish_sessions_to_replace_device.compat_sessions",
|
||||
{ DB_QUERY_TEXT } = tracing::field::Empty,
|
||||
);
|
||||
let finished_at = clock.now();
|
||||
sqlx::query!(
|
||||
let compat_affected = sqlx::query!(
|
||||
"
|
||||
UPDATE compat_sessions SET finished_at = $3 WHERE user_id = $1 AND device_id = $2 AND finished_at IS NULL
|
||||
",
|
||||
@@ -505,7 +506,9 @@ impl AppSessionRepository for PgAppSessionRepository<'_> {
|
||||
.record(&span)
|
||||
.execute(&mut *self.conn)
|
||||
.instrument(span)
|
||||
.await?;
|
||||
.await?
|
||||
.rows_affected();
|
||||
affected |= compat_affected > 0;
|
||||
|
||||
if let Ok([stable_device_as_scope_token, unstable_device_as_scope_token]) =
|
||||
device.to_scope_token()
|
||||
@@ -514,7 +517,7 @@ impl AppSessionRepository for PgAppSessionRepository<'_> {
|
||||
"db.app_session.finish_sessions_to_replace_device.oauth2_sessions",
|
||||
{ DB_QUERY_TEXT } = tracing::field::Empty,
|
||||
);
|
||||
sqlx::query!(
|
||||
let oauth2_affected = sqlx::query!(
|
||||
"
|
||||
UPDATE oauth2_sessions
|
||||
SET finished_at = $4
|
||||
@@ -530,10 +533,12 @@ impl AppSessionRepository for PgAppSessionRepository<'_> {
|
||||
.record(&span)
|
||||
.execute(&mut *self.conn)
|
||||
.instrument(span)
|
||||
.await?;
|
||||
.await?
|
||||
.rows_affected();
|
||||
affected |= oauth2_affected > 0;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(affected)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -196,12 +196,14 @@ pub trait AppSessionRepository: Send + Sync {
|
||||
/// replacing a device).
|
||||
///
|
||||
/// Should be called *before* creating a new session for the device.
|
||||
///
|
||||
/// Returns true if a session was finished.
|
||||
async fn finish_sessions_to_replace_device(
|
||||
&mut self,
|
||||
clock: &dyn Clock,
|
||||
user: &User,
|
||||
device: &Device,
|
||||
) -> Result<(), Self::Error>;
|
||||
) -> Result<bool, Self::Error>;
|
||||
}
|
||||
|
||||
repository_impl!(AppSessionRepository:
|
||||
@@ -218,5 +220,5 @@ repository_impl!(AppSessionRepository:
|
||||
clock: &dyn Clock,
|
||||
user: &User,
|
||||
device: &Device,
|
||||
) -> Result<(), Self::Error>;
|
||||
) -> Result<bool, Self::Error>;
|
||||
);
|
||||
|
||||
@@ -41,6 +41,7 @@ oauth2-types.workspace = true
|
||||
mas-data-model.workspace = true
|
||||
mas-i18n.workspace = true
|
||||
mas-iana.workspace = true
|
||||
mas-policy.workspace = true
|
||||
mas-router.workspace = true
|
||||
mas-spa.workspace = true
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ use mas_data_model::{
|
||||
};
|
||||
use mas_i18n::DataLocale;
|
||||
use mas_iana::jose::JsonWebSignatureAlg;
|
||||
use mas_policy::{Violation, ViolationCode};
|
||||
use mas_router::{Account, GraphQL, PostAuthAction, UrlBuilder};
|
||||
use oauth2_types::scope::{OPENID, Scope};
|
||||
use rand::{
|
||||
@@ -860,6 +861,44 @@ impl PolicyViolationContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// Context used by the `compat_login_policy_violation.html` template
|
||||
#[derive(Serialize)]
|
||||
pub struct CompatLoginPolicyViolationContext {
|
||||
violations: Vec<Violation>,
|
||||
}
|
||||
|
||||
impl TemplateContext for CompatLoginPolicyViolationContext {
|
||||
fn sample<R: Rng>(
|
||||
_now: chrono::DateTime<Utc>,
|
||||
_rng: &mut R,
|
||||
_locales: &[DataLocale],
|
||||
) -> BTreeMap<SampleIdentifier, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
sample_list(vec![
|
||||
CompatLoginPolicyViolationContext { violations: vec![] },
|
||||
CompatLoginPolicyViolationContext {
|
||||
violations: vec![Violation {
|
||||
msg: "user has too many active sessions".to_owned(),
|
||||
redirect_uri: None,
|
||||
field: None,
|
||||
code: Some(ViolationCode::TooManySessions),
|
||||
}],
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl CompatLoginPolicyViolationContext {
|
||||
/// Constructs a context for the compatibility login policy violation page
|
||||
/// given the list of violations
|
||||
#[must_use]
|
||||
pub const fn for_violations(violations: Vec<Violation>) -> Self {
|
||||
Self { violations }
|
||||
}
|
||||
}
|
||||
|
||||
/// Context used by the `sso.html` template
|
||||
#[derive(Serialize)]
|
||||
pub struct CompatSsoContext {
|
||||
|
||||
@@ -37,14 +37,15 @@ mod macros;
|
||||
|
||||
pub use self::{
|
||||
context::{
|
||||
AccountInactiveContext, ApiDocContext, AppContext, CompatSsoContext, ConsentContext,
|
||||
DeviceConsentContext, DeviceLinkContext, DeviceLinkFormField, DeviceNameContext,
|
||||
EmailRecoveryContext, EmailVerificationContext, EmptyContext, ErrorContext,
|
||||
FormPostContext, IndexContext, LoginContext, LoginFormField, NotFoundContext,
|
||||
PasswordRegisterContext, PolicyViolationContext, PostAuthContext, PostAuthContextInner,
|
||||
RecoveryExpiredContext, RecoveryFinishContext, RecoveryFinishFormField,
|
||||
RecoveryProgressContext, RecoveryStartContext, RecoveryStartFormField, RegisterContext,
|
||||
RegisterFormField, RegisterStepsDisplayNameContext, RegisterStepsDisplayNameFormField,
|
||||
AccountInactiveContext, ApiDocContext, AppContext, CompatLoginPolicyViolationContext,
|
||||
CompatSsoContext, ConsentContext, DeviceConsentContext, DeviceLinkContext,
|
||||
DeviceLinkFormField, DeviceNameContext, EmailRecoveryContext, EmailVerificationContext,
|
||||
EmptyContext, ErrorContext, FormPostContext, IndexContext, LoginContext, LoginFormField,
|
||||
NotFoundContext, PasswordRegisterContext, PolicyViolationContext, PostAuthContext,
|
||||
PostAuthContextInner, RecoveryExpiredContext, RecoveryFinishContext,
|
||||
RecoveryFinishFormField, RecoveryProgressContext, RecoveryStartContext,
|
||||
RecoveryStartFormField, RegisterContext, RegisterFormField,
|
||||
RegisterStepsDisplayNameContext, RegisterStepsDisplayNameFormField,
|
||||
RegisterStepsEmailInUseContext, RegisterStepsRegistrationTokenContext,
|
||||
RegisterStepsRegistrationTokenFormField, RegisterStepsVerifyEmailContext,
|
||||
RegisterStepsVerifyEmailFormField, SiteBranding, SiteConfigExt, SiteFeatures,
|
||||
@@ -391,6 +392,9 @@ register_templates! {
|
||||
/// Render the policy violation page
|
||||
pub fn render_policy_violation(WithLanguage<WithCsrf<WithSession<PolicyViolationContext>>>) { "pages/policy_violation.html" }
|
||||
|
||||
/// Render the compatibility login policy violation page
|
||||
pub fn render_compat_login_policy_violation(WithLanguage<WithCsrf<WithSession<CompatLoginPolicyViolationContext>>>) { "pages/compat_login_policy_violation.html" }
|
||||
|
||||
/// Render the legacy SSO login consent page
|
||||
pub fn render_sso_login(WithLanguage<WithCsrf<WithSession<CompatSsoContext>>>) { "pages/sso.html" }
|
||||
|
||||
|
||||
@@ -1883,6 +1883,10 @@
|
||||
"description": "Entrypoint to use when evaluating authorization grants",
|
||||
"type": "string"
|
||||
},
|
||||
"compat_login_entrypoint": {
|
||||
"description": "Entrypoint to use when evaluating compatibility logins",
|
||||
"type": "string"
|
||||
},
|
||||
"password_entrypoint": {
|
||||
"description": "Entrypoint to use when changing password",
|
||||
"type": "string"
|
||||
|
||||
232
frontend/package-lock.json
generated
232
frontend/package-lock.json
generated
@@ -25,15 +25,15 @@
|
||||
"react-dom": "^19.2.0",
|
||||
"react-i18next": "^16.3.5",
|
||||
"swagger-ui-dist": "^5.29.5",
|
||||
"valibot": "^1.1.0",
|
||||
"valibot": "^1.2.0",
|
||||
"vaul": "^1.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.2",
|
||||
"@biomejs/biome": "^2.3.7",
|
||||
"@browser-logos/chrome": "^2.0.0",
|
||||
"@browser-logos/firefox": "^3.0.10",
|
||||
"@browser-logos/safari": "^2.1.0",
|
||||
"@graphql-codegen/cli": "^6.0.2",
|
||||
"@graphql-codegen/cli": "^6.1.0",
|
||||
"@graphql-codegen/client-preset": "^5.1.1",
|
||||
"@graphql-codegen/typescript-msw": "^3.0.1",
|
||||
"@storybook/addon-docs": "^10.0.8",
|
||||
@@ -57,7 +57,7 @@
|
||||
"i18next-cli": "^1.24.20",
|
||||
"knip": "^5.66.4",
|
||||
"msw": "^2.11.6",
|
||||
"msw-storybook-addon": "^2.0.5",
|
||||
"msw-storybook-addon": "^2.0.6",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-import": "^16.1.1",
|
||||
"postcss-nesting": "^13.0.2",
|
||||
@@ -163,6 +163,7 @@
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -1035,9 +1036,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/biome": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.2.tgz",
|
||||
"integrity": "sha512-8e9tzamuDycx7fdrcJ/F/GDZ8SYukc5ud6tDicjjFqURKYFSWMl0H0iXNXZEGmcmNUmABgGuHThPykcM41INgg==",
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.7.tgz",
|
||||
"integrity": "sha512-CTbAS/jNAiUc6rcq94BrTB8z83O9+BsgWj2sBCQg9rD6Wkh2gjfR87usjx0Ncx0zGXP1NKgT7JNglay5Zfs9jw==",
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"bin": {
|
||||
@@ -1051,20 +1052,20 @@
|
||||
"url": "https://opencollective.com/biome"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@biomejs/cli-darwin-arm64": "2.3.2",
|
||||
"@biomejs/cli-darwin-x64": "2.3.2",
|
||||
"@biomejs/cli-linux-arm64": "2.3.2",
|
||||
"@biomejs/cli-linux-arm64-musl": "2.3.2",
|
||||
"@biomejs/cli-linux-x64": "2.3.2",
|
||||
"@biomejs/cli-linux-x64-musl": "2.3.2",
|
||||
"@biomejs/cli-win32-arm64": "2.3.2",
|
||||
"@biomejs/cli-win32-x64": "2.3.2"
|
||||
"@biomejs/cli-darwin-arm64": "2.3.7",
|
||||
"@biomejs/cli-darwin-x64": "2.3.7",
|
||||
"@biomejs/cli-linux-arm64": "2.3.7",
|
||||
"@biomejs/cli-linux-arm64-musl": "2.3.7",
|
||||
"@biomejs/cli-linux-x64": "2.3.7",
|
||||
"@biomejs/cli-linux-x64-musl": "2.3.7",
|
||||
"@biomejs/cli-win32-arm64": "2.3.7",
|
||||
"@biomejs/cli-win32-x64": "2.3.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-darwin-arm64": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.2.tgz",
|
||||
"integrity": "sha512-4LECm4kc3If0JISai4c3KWQzukoUdpxy4fRzlrPcrdMSRFksR9ZoXK7JBcPuLBmd2SoT4/d7CQS33VnZpgBjew==",
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.7.tgz",
|
||||
"integrity": "sha512-LirkamEwzIUULhXcf2D5b+NatXKeqhOwilM+5eRkbrnr6daKz9rsBL0kNZ16Hcy4b8RFq22SG4tcLwM+yx/wFA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1079,9 +1080,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-darwin-x64": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.2.tgz",
|
||||
"integrity": "sha512-jNMnfwHT4N3wi+ypRfMTjLGnDmKYGzxVr1EYAPBcauRcDnICFXN81wD6wxJcSUrLynoyyYCdfW6vJHS/IAoTDA==",
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.7.tgz",
|
||||
"integrity": "sha512-Q4TO633kvrMQkKIV7wmf8HXwF0dhdTD9S458LGE24TYgBjSRbuhvio4D5eOQzirEYg6eqxfs53ga/rbdd8nBKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1096,9 +1097,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-linux-arm64": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.2.tgz",
|
||||
"integrity": "sha512-amnqvk+gWybbQleRRq8TMe0rIv7GHss8mFJEaGuEZYWg1Tw14YKOkeo8h6pf1c+d3qR+JU4iT9KXnBKGON4klw==",
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.7.tgz",
|
||||
"integrity": "sha512-inHOTdlstUBzgjDcx0ge71U4SVTbwAljmkfi3MC5WzsYCRhancqfeL+sa4Ke6v2ND53WIwCFD5hGsYExoI3EZQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1113,9 +1114,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.2.tgz",
|
||||
"integrity": "sha512-2Zz4usDG1GTTPQnliIeNx6eVGGP2ry5vE/v39nT73a3cKN6t5H5XxjcEoZZh62uVZvED7hXXikclvI64vZkYqw==",
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.7.tgz",
|
||||
"integrity": "sha512-/afy8lto4CB8scWfMdt+NoCZtatBUF62Tk3ilWH2w8ENd5spLhM77zKlFZEvsKJv9AFNHknMl03zO67CiklL2Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1130,9 +1131,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-linux-x64": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.2.tgz",
|
||||
"integrity": "sha512-8BG/vRAhFz1pmuyd24FQPhNeueLqPtwvZk6yblABY2gzL2H8fLQAF/Z2OPIc+BPIVPld+8cSiKY/KFh6k81xfA==",
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.7.tgz",
|
||||
"integrity": "sha512-fJMc3ZEuo/NaMYo5rvoWjdSS5/uVSW+HPRQujucpZqm2ZCq71b8MKJ9U4th9yrv2L5+5NjPF0nqqILCl8HY/fg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1147,9 +1148,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-linux-x64-musl": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.2.tgz",
|
||||
"integrity": "sha512-gzB19MpRdTuOuLtPpFBGrV3Lq424gHyq2lFj8wfX9tvLMLdmA/R9C7k/mqBp/spcbWuHeIEKgEs3RviOPcWGBA==",
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.7.tgz",
|
||||
"integrity": "sha512-CQUtgH1tIN6e5wiYSJqzSwJumHYolNtaj1dwZGCnZXm2PZU1jOJof9TsyiP3bXNDb+VOR7oo7ZvY01If0W3iFQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1164,9 +1165,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-win32-arm64": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.2.tgz",
|
||||
"integrity": "sha512-lCruqQlfWjhMlOdyf5pDHOxoNm4WoyY2vZ4YN33/nuZBRstVDuqPPjS0yBkbUlLEte11FbpW+wWSlfnZfSIZvg==",
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.7.tgz",
|
||||
"integrity": "sha512-aJAE8eCNyRpcfx2JJAtsPtISnELJ0H4xVVSwnxm13bzI8RwbXMyVtxy2r5DV1xT3WiSP+7LxORcApWw0LM8HiA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1181,9 +1182,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-win32-x64": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.2.tgz",
|
||||
"integrity": "sha512-6Ee9P26DTb4D8sN9nXxgbi9Dw5vSOfH98M7UlmkjKB2vtUbrRqCbZiNfryGiwnPIpd6YUoTl7rLVD2/x1CyEHQ==",
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.7.tgz",
|
||||
"integrity": "sha512-pulzUshqv9Ed//MiE8MOUeeEkbkSHVDVY5Cz5wVAnH1DUqliCQG3j6s1POaITTFqFfo7AVIx2sWdKpx/GS+Nqw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1846,6 +1847,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/inconsolata/-/inconsolata-5.2.8.tgz",
|
||||
"integrity": "sha512-lIZW+WOZYpUH91g9r6rYYhfTmptF3YPPM54ZOs8IYVeeL4SeiAu4tfj7mdr8llYEq31DLYgi6JtGIJa192gB0Q==",
|
||||
"license": "OFL-1.1",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
@@ -1855,6 +1857,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz",
|
||||
"integrity": "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==",
|
||||
"license": "OFL-1.1",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
@@ -1884,18 +1887,18 @@
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/@graphql-codegen/cli": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-6.0.2.tgz",
|
||||
"integrity": "sha512-W+0ime0xMrCyG77q+5xiPkkqPLuXJcTx0Zr9TTOxF4zIqWKVsuImS3qVxtpeTx+GRbb8VWv9IedWMtt91JGzQg==",
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-6.1.0.tgz",
|
||||
"integrity": "sha512-7w3Zq5IFONVOBcyOiP01Nv9WRxGS/TEaBCAb/ALYA3xHq95dqKCpoGnxt/Ut9R18jiS+aMgT0gc8Tr8sHy44jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/generator": "^7.18.13",
|
||||
"@babel/template": "^7.18.10",
|
||||
"@babel/types": "^7.18.13",
|
||||
"@graphql-codegen/client-preset": "^5.1.2",
|
||||
"@graphql-codegen/client-preset": "^5.2.0",
|
||||
"@graphql-codegen/core": "^5.0.0",
|
||||
"@graphql-codegen/plugin-helpers": "^6.0.0",
|
||||
"@graphql-codegen/plugin-helpers": "^6.1.0",
|
||||
"@graphql-tools/apollo-engine-loader": "^8.0.0",
|
||||
"@graphql-tools/code-file-loader": "^8.0.0",
|
||||
"@graphql-tools/git-loader": "^8.0.0",
|
||||
@@ -2144,21 +2147,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@graphql-codegen/client-preset": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-5.1.3.tgz",
|
||||
"integrity": "sha512-8nlKt8/gO/BovWahLb96taMssHKPibBfslry1ed9DIJtbOrceFYF3yNbFZuTHmI644C7ZvoYK93JkE3VzDlCyg==",
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-5.2.1.tgz",
|
||||
"integrity": "sha512-6qFjHQQUWrEH+MVvWs5sPUgme8X+Ivg3WfzaCESooRBQZ4/EnSFlXkPWUTbOKYLRUoMv4g6iTRcZQf6u1wtHZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.20.2",
|
||||
"@babel/template": "^7.20.7",
|
||||
"@graphql-codegen/add": "^6.0.0",
|
||||
"@graphql-codegen/gql-tag-operations": "5.0.5",
|
||||
"@graphql-codegen/plugin-helpers": "^6.0.0",
|
||||
"@graphql-codegen/typed-document-node": "^6.1.2",
|
||||
"@graphql-codegen/typescript": "^5.0.4",
|
||||
"@graphql-codegen/typescript-operations": "^5.0.4",
|
||||
"@graphql-codegen/visitor-plugin-common": "^6.1.2",
|
||||
"@graphql-codegen/gql-tag-operations": "5.1.1",
|
||||
"@graphql-codegen/plugin-helpers": "^6.1.0",
|
||||
"@graphql-codegen/typed-document-node": "^6.1.4",
|
||||
"@graphql-codegen/typescript": "^5.0.6",
|
||||
"@graphql-codegen/typescript-operations": "^5.0.6",
|
||||
"@graphql-codegen/visitor-plugin-common": "^6.2.1",
|
||||
"@graphql-tools/documents": "^1.0.0",
|
||||
"@graphql-tools/utils": "^10.0.0",
|
||||
"@graphql-typed-document-node/core": "3.2.0",
|
||||
@@ -2211,14 +2214,14 @@
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/@graphql-codegen/gql-tag-operations": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-5.0.5.tgz",
|
||||
"integrity": "sha512-DutUBwA3UMOB2AI6O1FDidYw7N0Br4d/ogGrYg6XOeeVuRYigc6i9wX4qiv4ofD34Ujfcfze0U2PI3ZOR33NKw==",
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-5.1.1.tgz",
|
||||
"integrity": "sha512-XewD0XxN2sgKieEIFeGWV5yT5X2aNy+eg+K8bHlUD7QfyrN2bi67rv/O5Edu7LVDOJR69uqVBp++18d742mn3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@graphql-codegen/plugin-helpers": "^6.0.0",
|
||||
"@graphql-codegen/visitor-plugin-common": "6.1.2",
|
||||
"@graphql-codegen/plugin-helpers": "^6.1.0",
|
||||
"@graphql-codegen/visitor-plugin-common": "6.2.1",
|
||||
"@graphql-tools/utils": "^10.0.0",
|
||||
"auto-bind": "~4.0.0",
|
||||
"tslib": "~2.6.0"
|
||||
@@ -2238,9 +2241,9 @@
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/@graphql-codegen/plugin-helpers": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-6.0.0.tgz",
|
||||
"integrity": "sha512-Z7P89vViJvQakRyMbq/JF2iPLruRFOwOB6IXsuSvV/BptuuEd7fsGPuEf8bdjjDxUY0pJZnFN8oC7jIQ8p9GKA==",
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-6.1.0.tgz",
|
||||
"integrity": "sha512-JJypehWTcty9kxKiqH7TQOetkGdOYjY78RHlI+23qB59cV2wxjFFVf8l7kmuXS4cpGVUNfIjFhVr7A1W7JMtdA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2291,14 +2294,14 @@
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/@graphql-codegen/typed-document-node": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-6.1.2.tgz",
|
||||
"integrity": "sha512-DelLv7BY8Sx0toyCiEsc46W3FtqipiiqhprUnGnSalfKnKVB8KUodXKaf70migy6hWyDl5d1OJOp5wrttuIy2Q==",
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-6.1.4.tgz",
|
||||
"integrity": "sha512-ITWsA+qvT7R64z7KmYHXfgyD5ff069FAGq/hpR0EWVfzXT4RW1Xn/3Biw7/jvwMGsS1BTjo8ZLSIMNM8KjE3GA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@graphql-codegen/plugin-helpers": "^6.0.0",
|
||||
"@graphql-codegen/visitor-plugin-common": "6.1.2",
|
||||
"@graphql-codegen/plugin-helpers": "^6.1.0",
|
||||
"@graphql-codegen/visitor-plugin-common": "6.2.1",
|
||||
"auto-bind": "~4.0.0",
|
||||
"change-case-all": "1.0.15",
|
||||
"tslib": "~2.6.0"
|
||||
@@ -2318,15 +2321,15 @@
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/@graphql-codegen/typescript": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-5.0.4.tgz",
|
||||
"integrity": "sha512-q6S8hX+aR4BzeGgolac4gp22rBnXbLhedmOwT1UBT9e3lGNmNpYC7WJUEzAPjWf6z1lRSNmojLlwEjTnffhKNA==",
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-5.0.6.tgz",
|
||||
"integrity": "sha512-rKW3wYInAnmO/DmKjhW3/KLMxUauUCZuMEPQmuoHChnwIuMjn5kVXCdArGyQqv+vVtFj55aS+sJLN4MPNNjSNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@graphql-codegen/plugin-helpers": "^6.0.0",
|
||||
"@graphql-codegen/plugin-helpers": "^6.1.0",
|
||||
"@graphql-codegen/schema-ast": "^5.0.0",
|
||||
"@graphql-codegen/visitor-plugin-common": "6.1.2",
|
||||
"@graphql-codegen/visitor-plugin-common": "6.2.1",
|
||||
"auto-bind": "~4.0.0",
|
||||
"tslib": "~2.6.0"
|
||||
},
|
||||
@@ -2655,15 +2658,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@graphql-codegen/typescript-operations": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-5.0.4.tgz",
|
||||
"integrity": "sha512-5Bu/BTmyNjdSfSLLBKjC0+4XWcY01uotVcnVIWIxxRdIHoRxnTW6PUkT5CoPHP5r/Uoo3OvIJxh+0LYSH5suwA==",
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-5.0.6.tgz",
|
||||
"integrity": "sha512-pkR/82qWO50OHWeV3BiDuVxNFxiJerpmNjFep71VlabADXiU3GIeSaDd6G9a1/SCniVTXZQk2ivCb0ZJiuwo1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@graphql-codegen/plugin-helpers": "^6.0.0",
|
||||
"@graphql-codegen/typescript": "^5.0.4",
|
||||
"@graphql-codegen/visitor-plugin-common": "6.1.2",
|
||||
"@graphql-codegen/plugin-helpers": "^6.1.0",
|
||||
"@graphql-codegen/typescript": "^5.0.6",
|
||||
"@graphql-codegen/visitor-plugin-common": "6.2.1",
|
||||
"auto-bind": "~4.0.0",
|
||||
"tslib": "~2.6.0"
|
||||
},
|
||||
@@ -2695,13 +2698,13 @@
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/@graphql-codegen/visitor-plugin-common": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-6.1.2.tgz",
|
||||
"integrity": "sha512-zYdrhJKgk8kqE1Xz5/m/Ua42zk+rIvYB/FHh3dE1AhZ6b1IDqgKjF3LnkT+K2qenf9EfT4yNjXd5CEKMeXfHyg==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-6.2.1.tgz",
|
||||
"integrity": "sha512-5QT1hCV3286mrmoIC7vlFXsTlwELMexhuFIkjh+oVGGL1E8hxkIPAU0kfH/lsPbQHKi8zKmic2pl3tAdyYxNyg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@graphql-codegen/plugin-helpers": "^6.0.0",
|
||||
"@graphql-codegen/plugin-helpers": "^6.1.0",
|
||||
"@graphql-tools/optimize": "^2.0.0",
|
||||
"@graphql-tools/relay-operation-optimizer": "^7.0.0",
|
||||
"@graphql-tools/utils": "^10.0.0",
|
||||
@@ -3215,14 +3218,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@graphql-tools/relay-operation-optimizer": {
|
||||
"version": "7.0.25",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.0.25.tgz",
|
||||
"integrity": "sha512-1S7qq9eyO6ygPNWX2lZd+oxbpl63OhnTTw8+t5OWprM2Tzws9HEosLUpsMR85z1gbezeKtUDt9a2bsSyu4MMFg==",
|
||||
"version": "7.0.26",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.0.26.tgz",
|
||||
"integrity": "sha512-cVdS2Hw4hg/WgPVV2wRIzZM975pW5k4vdih3hR4SvEDQVr6MmozmlTQSqzMyi9yg8LKTq540Oz3bYQa286yGmg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ardatan/relay-compiler": "^12.0.3",
|
||||
"@graphql-tools/utils": "^10.10.3",
|
||||
"@graphql-tools/utils": "^10.11.0",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3278,9 +3281,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@graphql-tools/utils": {
|
||||
"version": "10.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.10.3.tgz",
|
||||
"integrity": "sha512-2EdYiefeLLxsoeZTukSNZJ0E/Z5NnWBUGK2VJa0DQj1scDhVd93HeT1eW9TszJOYmIh3eWAKLv58ri/1XUmdsQ==",
|
||||
"version": "10.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.11.0.tgz",
|
||||
"integrity": "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -5808,6 +5811,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.10.tgz",
|
||||
"integrity": "sha512-BKLss9Y8PQ9IUjPYQiv3/Zmlx92uxffUOX8ZZNoQlCIZBJPT5M+GOMQj7xislvVQ6l1BstBjcX0XB/aHfFYVNw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.90.10"
|
||||
},
|
||||
@@ -5842,6 +5846,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.131.44.tgz",
|
||||
"integrity": "sha512-LREJfrl8lSedXHCRAAt0HvnHFP9ikAQWnVhYRM++B26w4ZYQBbLvgCT1BCDZVY7MR6rslcd4OfgpZMOyVhNzFg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@tanstack/history": "1.131.2",
|
||||
"@tanstack/react-store": "^0.7.0",
|
||||
@@ -5937,6 +5942,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.131.44.tgz",
|
||||
"integrity": "sha512-Npi9xB3GSYZhRW8+gPhP6bEbyx0vNc8ZNwsi0JapdiFpIiszgRJ57pesy/rklruv46gYQjLVA5KDOsuaCT/urA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@tanstack/history": "1.131.2",
|
||||
"@tanstack/store": "^0.7.0",
|
||||
@@ -6203,8 +6209,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
@@ -6295,6 +6300,7 @@
|
||||
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -6305,6 +6311,7 @@
|
||||
"integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -6315,6 +6322,7 @@
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@@ -6362,6 +6370,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@vector-im/compound-design-tokens/-/compound-design-tokens-6.4.0.tgz",
|
||||
"integrity": "sha512-93nYQZMgUt6apjCwwnMhMxN8VYQXN3GYOnwovwJjavImwsCGwI/e853BV/DstrWumYh6k5pZsP9e6AF+nz3SIQ==",
|
||||
"license": "SEE LICENSE IN README.md",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^17 || ^18 || ^19.0.0"
|
||||
@@ -7056,6 +7065,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.2",
|
||||
"caniuse-lite": "^1.0.30001741",
|
||||
@@ -7619,7 +7629,8 @@
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
@@ -7790,8 +7801,7 @@
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dot-case": {
|
||||
"version": "3.0.4",
|
||||
@@ -7892,6 +7902,7 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
@@ -8417,6 +8428,7 @@
|
||||
"integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
|
||||
}
|
||||
@@ -8628,6 +8640,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4"
|
||||
},
|
||||
@@ -9779,7 +9792,6 @@
|
||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
@@ -9986,6 +9998,7 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@inquirer/confirm": "^5.0.0",
|
||||
"@mswjs/interceptors": "^0.40.0",
|
||||
@@ -10025,9 +10038,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/msw-storybook-addon": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/msw-storybook-addon/-/msw-storybook-addon-2.0.5.tgz",
|
||||
"integrity": "sha512-uum2gtprDBoUb8GV/rPMwPytHmB8+AUr25BQUY0MpjYey5/ujaew2Edt+4oHiXpLTd0ThyMqmEvGy/sRpDV4lg==",
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/msw-storybook-addon/-/msw-storybook-addon-2.0.6.tgz",
|
||||
"integrity": "sha512-ExCwDbcJoM2V3iQU+fZNp+axVfNc7DWMRh4lyTXebDO8IbpUNYKGFUrA8UqaeWiRGKVuS7+fU+KXEa9b0OP6uA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -10706,6 +10719,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -10863,6 +10877,7 @@
|
||||
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
@@ -10900,7 +10915,6 @@
|
||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
@@ -10916,7 +10930,6 @@
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -10976,6 +10989,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -11017,6 +11031,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -11056,8 +11071,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.18.0",
|
||||
@@ -11473,6 +11487,7 @@
|
||||
"integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -11592,6 +11607,7 @@
|
||||
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz",
|
||||
"integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
@@ -11850,6 +11866,7 @@
|
||||
"integrity": "sha512-vQMufKKA9TxgoEDHJv3esrqUkjszuuRiDkThiHxENFPdQawHhm2Dei+iwNRwH5W671zTDy9iRT9P1KDjcU5Iyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
"@storybook/icons": "^1.6.0",
|
||||
@@ -12309,7 +12326,8 @@
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tiny-warning": {
|
||||
"version": "1.0.3",
|
||||
@@ -12512,6 +12530,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -12757,9 +12776,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/valibot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/valibot/-/valibot-1.1.0.tgz",
|
||||
"integrity": "sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz",
|
||||
"integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5"
|
||||
@@ -12789,6 +12808,7 @@
|
||||
"integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -12936,6 +12956,7 @@
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
@@ -13154,6 +13175,7 @@
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
|
||||
@@ -35,15 +35,15 @@
|
||||
"react-dom": "^19.2.0",
|
||||
"react-i18next": "^16.3.5",
|
||||
"swagger-ui-dist": "^5.29.5",
|
||||
"valibot": "^1.1.0",
|
||||
"valibot": "^1.2.0",
|
||||
"vaul": "^1.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.2",
|
||||
"@biomejs/biome": "^2.3.7",
|
||||
"@browser-logos/chrome": "^2.0.0",
|
||||
"@browser-logos/firefox": "^3.0.10",
|
||||
"@browser-logos/safari": "^2.1.0",
|
||||
"@graphql-codegen/cli": "^6.0.2",
|
||||
"@graphql-codegen/cli": "^6.1.0",
|
||||
"@graphql-codegen/client-preset": "^5.1.1",
|
||||
"@graphql-codegen/typescript-msw": "^3.0.1",
|
||||
"@storybook/addon-docs": "^10.0.8",
|
||||
@@ -67,7 +67,7 @@
|
||||
"i18next-cli": "^1.24.20",
|
||||
"knip": "^5.66.4",
|
||||
"msw": "^2.11.6",
|
||||
"msw-storybook-addon": "^2.0.5",
|
||||
"msw-storybook-addon": "^2.0.6",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-import": "^16.1.1",
|
||||
"postcss-nesting": "^13.0.2",
|
||||
|
||||
@@ -16,6 +16,7 @@ INPUTS := \
|
||||
client_registration/client_registration.rego \
|
||||
register/register.rego \
|
||||
authorization_grant/authorization_grant.rego \
|
||||
compat_login/compat_login.rego \
|
||||
email/email.rego
|
||||
|
||||
ifeq ($(DOCKER), 1)
|
||||
@@ -38,6 +39,7 @@ policy.wasm: $(INPUTS)
|
||||
-e "client_registration/violation" \
|
||||
-e "register/violation" \
|
||||
-e "authorization_grant/violation" \
|
||||
-e "compat_login/violation" \
|
||||
-e "email/violation" \
|
||||
$^
|
||||
tar xzf bundle.tar.gz /policy.wasm
|
||||
|
||||
74
policies/compat_login/compat_login.rego
Normal file
74
policies/compat_login/compat_login.rego
Normal file
@@ -0,0 +1,74 @@
|
||||
# Copyright 2025 Element Creations Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
# METADATA
|
||||
# schemas:
|
||||
# - input: schema["compat_login_input"]
|
||||
package compat_login
|
||||
|
||||
import rego.v1
|
||||
|
||||
import data.common
|
||||
|
||||
default allow := false
|
||||
|
||||
allow if {
|
||||
count(violation) == 0
|
||||
}
|
||||
|
||||
violation contains {"msg": sprintf(
|
||||
"Requester [%s] isn't allowed to do this action",
|
||||
[common.format_requester(input.requester)],
|
||||
)} if {
|
||||
common.requester_banned(input.requester, data.requester)
|
||||
}
|
||||
|
||||
violation contains {
|
||||
"code": "too-many-sessions",
|
||||
"msg": "user has too many active sessions (soft limit)",
|
||||
} if {
|
||||
# Only apply if session limits are enabled in the config
|
||||
data.session_limit != null
|
||||
|
||||
# This is a web-based interactive login
|
||||
is_interactive
|
||||
|
||||
# Only apply if this login doesn't replace a session
|
||||
# (As then this login is not actually increasing the number of devices)
|
||||
not input.session_replaced
|
||||
|
||||
# For web-based 'compat SSO' login, a violation occurs when the soft limit has already been
|
||||
# reached or exceeded.
|
||||
# We use the soft limit because the user will be able to interactively remove
|
||||
# sessions to return under the limit.
|
||||
data.session_limit.soft_limit <= input.session_counts.total
|
||||
}
|
||||
|
||||
violation contains {
|
||||
"code": "too-many-sessions",
|
||||
"msg": "user has too many active sessions (hard limit)",
|
||||
} if {
|
||||
# Only apply if session limits are enabled in the config
|
||||
data.session_limit != null
|
||||
|
||||
# This is not a web-based interactive login
|
||||
not is_interactive
|
||||
|
||||
# Only apply if this login doesn't replace a session
|
||||
# (As then this login is not actually increasing the number of devices)
|
||||
not input.session_replaced
|
||||
|
||||
# For `m.login.password` login, a violation occurs when the hard limit has already been
|
||||
# reached or exceeded.
|
||||
# We don't use the soft limit because the user won't be able to interactively remove
|
||||
# sessions to return under the limit.
|
||||
data.session_limit.hard_limit <= input.session_counts.total
|
||||
}
|
||||
|
||||
is_interactive if {
|
||||
# Only `m.login.sso` (the interactive web form) is interactive;
|
||||
# `m.login.password` and `m.login.token` (including the finalisation of an SSO login) are not
|
||||
input.login.type == "m.login.sso"
|
||||
}
|
||||
99
policies/compat_login/compat_login_test.rego
Normal file
99
policies/compat_login/compat_login_test.rego
Normal file
@@ -0,0 +1,99 @@
|
||||
# Copyright 2025 Element Creations Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
package compat_login_test
|
||||
|
||||
import data.compat_login
|
||||
import rego.v1
|
||||
|
||||
user := {"username": "john"}
|
||||
|
||||
# Tests session limiting when using (the interactive part of) `m.login.sso`
|
||||
test_session_limiting_sso if {
|
||||
compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 1}
|
||||
with input.login as {"type": "m.login.sso"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
|
||||
compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 31}
|
||||
with input.login as {"type": "m.login.sso"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
|
||||
not compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 32}
|
||||
with input.login as {"type": "m.login.sso"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
|
||||
not compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 42}
|
||||
with input.login as {"type": "m.login.sso"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
|
||||
not compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 65}
|
||||
with input.login as {"type": "m.login.sso"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
|
||||
# No limit configured
|
||||
compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 1}
|
||||
with input.login as {"type": "m.login.sso"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as null
|
||||
}
|
||||
|
||||
# Test session limiting when using `m.login.password`
|
||||
test_session_limiting_password if {
|
||||
compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 1}
|
||||
with input.login as {"type": "m.login.password"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
|
||||
compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 63}
|
||||
with input.login as {"type": "m.login.password"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
|
||||
not compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 64}
|
||||
with input.login as {"type": "m.login.password"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
|
||||
not compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 65}
|
||||
with input.login as {"type": "m.login.password"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
|
||||
# No limit configured
|
||||
compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 1}
|
||||
with input.login as {"type": "m.login.password"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as null
|
||||
}
|
||||
|
||||
test_no_session_limiting_upon_replacement if {
|
||||
not compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 65}
|
||||
with input.login as {"type": "m.login.password"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
|
||||
not compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 65}
|
||||
with input.login as {"type": "m.login.sso"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
}
|
||||
144
policies/schema/compat_login_input.json
Normal file
144
policies/schema/compat_login_input.json
Normal file
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "CompatLoginInput",
|
||||
"description": "Input for the compatibility login policy.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"session_counts": {
|
||||
"description": "How many sessions the user has.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SessionCounts"
|
||||
}
|
||||
]
|
||||
},
|
||||
"session_replaced": {
|
||||
"description": "Whether a session will be replaced by this login",
|
||||
"type": "boolean"
|
||||
},
|
||||
"login": {
|
||||
"description": "What type of login is being performed.\n This also determines whether the login is interactive.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CompatLogin"
|
||||
}
|
||||
]
|
||||
},
|
||||
"requester": {
|
||||
"$ref": "#/definitions/Requester"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"user",
|
||||
"session_counts",
|
||||
"session_replaced",
|
||||
"login",
|
||||
"requester"
|
||||
],
|
||||
"definitions": {
|
||||
"SessionCounts": {
|
||||
"description": "Information about how many sessions the user has",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"total": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"oauth2": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"compat": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"personal": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"total",
|
||||
"oauth2",
|
||||
"compat",
|
||||
"personal"
|
||||
]
|
||||
},
|
||||
"CompatLogin": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Used as the interactive part of SSO login.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "m.login.sso"
|
||||
},
|
||||
"redirect_uri": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"redirect_uri"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Used as the final (non-interactive) stage of SSO login.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "m.login.token"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Non-interactive password-over-the-API login.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "m.login.password"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"Requester": {
|
||||
"description": "Identity of the requester",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ip_address": {
|
||||
"description": "IP address of the entity making the request",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"format": "ip"
|
||||
},
|
||||
"user_agent": {
|
||||
"description": "User agent of the entity making the request",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
templates/pages/compat_login_policy_violation.html
Normal file
31
templates/pages/compat_login_policy_violation.html
Normal file
@@ -0,0 +1,31 @@
|
||||
{#
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
-#}
|
||||
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<header class="page-heading">
|
||||
<div class="icon invalid">
|
||||
{{ icon.error_solid() }}
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<h1 class="title">{{ _("mas.policy_violation.heading") }}</h1>
|
||||
<p class="text">{{ _("mas.policy_violation.description") }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="flex flex-col gap-10">
|
||||
<div class="flex gap-1 justify-center items-center">
|
||||
<p class="cpd-text-secondary cpd-text-body-md-regular">
|
||||
{{ _("mas.policy_violation.logged_as", username=current_session.user.username) }}
|
||||
</p>
|
||||
|
||||
{{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token, post_logout_action=action, as_link=True) }}
|
||||
</div>
|
||||
</main>
|
||||
{% endblock content %}
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"sign_out": "Sign out",
|
||||
"@sign_out": {
|
||||
"context": "pages/account/logged_out.html:22:28-48, pages/consent.html:65:28-48, pages/device_consent.html:136:30-50, pages/index.html:28:28-48, pages/policy_violation.html:38:28-48, pages/sso.html:45:28-48, pages/upstream_oauth2/link_mismatch.html:24:24-44, pages/upstream_oauth2/suggest_link.html:32:26-46"
|
||||
"context": "pages/account/logged_out.html:22:28-48, pages/compat_login_policy_violation.html:28:28-48, pages/consent.html:65:28-48, pages/device_consent.html:136:30-50, pages/index.html:28:28-48, pages/policy_violation.html:38:28-48, pages/sso.html:45:28-48, pages/upstream_oauth2/link_mismatch.html:24:24-44, pages/upstream_oauth2/suggest_link.html:32:26-46"
|
||||
},
|
||||
"skip": "Skip",
|
||||
"@skip": {
|
||||
@@ -496,17 +496,17 @@
|
||||
"policy_violation": {
|
||||
"description": "This might be because of the client which authored the request, the currently logged in user, or the request itself.",
|
||||
"@description": {
|
||||
"context": "pages/policy_violation.html:19:25-62",
|
||||
"context": "pages/compat_login_policy_violation.html:18:25-62, pages/policy_violation.html:19:25-62",
|
||||
"description": "Displayed when an authorization request is denied by the policy"
|
||||
},
|
||||
"heading": "The authorization request was denied by the policy enforced by this service",
|
||||
"@heading": {
|
||||
"context": "pages/policy_violation.html:18:27-60",
|
||||
"context": "pages/compat_login_policy_violation.html:17:27-60, pages/policy_violation.html:18:27-60",
|
||||
"description": "Displayed when an authorization request is denied by the policy"
|
||||
},
|
||||
"logged_as": "Logged as <span class=\"font-semibold\">%(username)s</span>",
|
||||
"@logged_as": {
|
||||
"context": "pages/policy_violation.html:35:11-86"
|
||||
"context": "pages/compat_login_policy_violation.html:25:11-86, pages/policy_violation.html:35:11-86"
|
||||
}
|
||||
},
|
||||
"recovery": {
|
||||
|
||||
Reference in New Issue
Block a user