Merge branch 'element-hq:main' into main
This commit is contained in:
@@ -1,14 +1,2 @@
|
||||
[test-groups]
|
||||
database = { max-threads = 1 }
|
||||
|
||||
[profile.default]
|
||||
retries = 1
|
||||
|
||||
# sqlx has a problem with nextest, as it uses a process-local semaphore to have
|
||||
# tests use different databases. This doesn't work with nextest, as it has a
|
||||
# process-per-test model, which is why we need to make sure only one test uses
|
||||
# the database at a time.
|
||||
# See https://github.com/launchbadge/sqlx/pull/3334
|
||||
[[profile.default.overrides]]
|
||||
filter = 'package(mas-handlers) or package(mas-storage-pg)'
|
||||
test-group = 'database'
|
||||
|
||||
2
.github/workflows/build.yaml
vendored
2
.github/workflows/build.yaml
vendored
@@ -340,7 +340,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4.3.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version-file: ./tools/syn2mas/.nvmrc
|
||||
|
||||
|
||||
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4.3.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4.3.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4.3.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
@@ -323,7 +323,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4.3.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version-file: ./tools/syn2mas/.nvmrc
|
||||
|
||||
|
||||
2
.github/workflows/docs.yaml
vendored
2
.github/workflows/docs.yaml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
tool: mdbook
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4.3.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
|
||||
2
.github/workflows/release-branch.yaml
vendored
2
.github/workflows/release-branch.yaml
vendored
@@ -48,7 +48,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4.3.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
|
||||
2
.github/workflows/translations-download.yaml
vendored
2
.github/workflows/translations-download.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4.3.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
|
||||
2
.github/workflows/translations-upload.yaml
vendored
2
.github/workflows/translations-upload.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4.3.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
|
||||
101
Cargo.lock
generated
101
Cargo.lock
generated
@@ -190,9 +190,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.97"
|
||||
version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
|
||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
@@ -1004,9 +1004,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.35"
|
||||
version = "4.5.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944"
|
||||
checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -1014,9 +1014,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.35"
|
||||
version = "4.5.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9"
|
||||
checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -3114,7 +3114,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-axum-utils"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"axum-extra",
|
||||
@@ -3147,7 +3147,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-cli"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -3218,7 +3218,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-config"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"camino",
|
||||
@@ -3248,7 +3248,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-data-model"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"chrono",
|
||||
@@ -3269,7 +3269,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-email"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"lettre",
|
||||
@@ -3280,7 +3280,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-handlers"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"aide",
|
||||
"anyhow",
|
||||
@@ -3357,7 +3357,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-http"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"headers",
|
||||
@@ -3378,7 +3378,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-i18n"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"icu_calendar",
|
||||
@@ -3400,7 +3400,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-i18n-scan"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"clap",
|
||||
@@ -3414,7 +3414,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-iana"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -3422,7 +3422,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-iana-codegen"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3438,7 +3438,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-jose"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"chrono",
|
||||
@@ -3468,7 +3468,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-keystore"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"base64ct",
|
||||
@@ -3496,7 +3496,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-listener"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -3520,7 +3520,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-matrix"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3530,7 +3530,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-matrix-synapse"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3547,7 +3547,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-oidc-client"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"assert_matches",
|
||||
"async-trait",
|
||||
@@ -3583,7 +3583,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-policy"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -3600,7 +3600,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-router"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"serde",
|
||||
@@ -3611,7 +3611,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-spa"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"serde",
|
||||
@@ -3620,7 +3620,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-storage"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -3642,7 +3642,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-storage-pg"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -3668,7 +3668,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-tasks"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3699,7 +3699,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-templates"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -3729,7 +3729,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-tower"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"http",
|
||||
"opentelemetry",
|
||||
@@ -3999,7 +3999,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oauth2-types"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"assert_matches",
|
||||
"base64ct",
|
||||
@@ -4724,9 +4724,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "psl"
|
||||
version = "2.1.99"
|
||||
version = "2.1.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89186bbd6ebdabc007b20bda4807abe8f4ad0636d0fb86be20f8cfce25b8f4b3"
|
||||
checksum = "70295efe3fd3db60e81f452e2eacc407b4e6c2e1ff7f763424ae6e16105cee26"
|
||||
dependencies = [
|
||||
"psl-types",
|
||||
]
|
||||
@@ -5861,9 +5861,9 @@ checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a"
|
||||
|
||||
[[package]]
|
||||
name = "sqlx"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f"
|
||||
checksum = "14e22987355fbf8cfb813a0cf8cd97b1b4ec834b94dbd759a9e8679d41fabe83"
|
||||
dependencies = [
|
||||
"sqlx-core",
|
||||
"sqlx-macros",
|
||||
@@ -5874,10 +5874,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-core"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0"
|
||||
checksum = "55c4720d7d4cd3d5b00f61d03751c685ad09c33ae8290c8a2c11335e0604300b"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"crc",
|
||||
@@ -5897,7 +5898,6 @@ dependencies = [
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -5913,9 +5913,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-macros"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310"
|
||||
checksum = "175147fcb75f353ac7675509bc58abb2cb291caf0fd24a3623b8f7e3eb0a754b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -5926,9 +5926,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-macros-core"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad"
|
||||
checksum = "1cde983058e53bfa75998e1982086c5efe3c370f3250bf0357e344fa3352e32b"
|
||||
dependencies = [
|
||||
"dotenvy",
|
||||
"either",
|
||||
@@ -5952,9 +5952,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-mysql"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233"
|
||||
checksum = "847d2e5393a4f39e47e4f36cab419709bc2b83cbe4223c60e86e1471655be333"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64 0.22.1",
|
||||
@@ -5996,9 +5996,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-postgres"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613"
|
||||
checksum = "cc35947a541b9e0a2e3d85da444f1c4137c13040267141b208395a0d0ca4659f"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64 0.22.1",
|
||||
@@ -6036,9 +6036,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-sqlite"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540"
|
||||
checksum = "6c48291dac4e5ed32da0927a0b981788be65674aeb62666d19873ab4289febde"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"chrono",
|
||||
@@ -6054,6 +6054,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_urlencoded",
|
||||
"sqlx-core",
|
||||
"thiserror 2.0.12",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
@@ -6148,7 +6149,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn2mas"
|
||||
version = "0.14.1"
|
||||
version = "0.15.0-rc.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
|
||||
64
Cargo.toml
64
Cargo.toml
@@ -4,7 +4,7 @@ members = ["crates/*"]
|
||||
resolver = "2"
|
||||
|
||||
# Updated in the CI with a `sed` command
|
||||
package.version = "0.14.1"
|
||||
package.version = "0.15.0-rc.0"
|
||||
package.license = "AGPL-3.0-only"
|
||||
package.authors = ["Element Backend Team"]
|
||||
package.edition = "2024"
|
||||
@@ -27,34 +27,34 @@ broken_intra_doc_links = "deny"
|
||||
[workspace.dependencies]
|
||||
|
||||
# Workspace crates
|
||||
mas-axum-utils = { path = "./crates/axum-utils/", version = "=0.14.1" }
|
||||
mas-cli = { path = "./crates/cli/", version = "=0.14.1" }
|
||||
mas-config = { path = "./crates/config/", version = "=0.14.1" }
|
||||
mas-data-model = { path = "./crates/data-model/", version = "=0.14.1" }
|
||||
mas-email = { path = "./crates/email/", version = "=0.14.1" }
|
||||
mas-graphql = { path = "./crates/graphql/", version = "=0.14.1" }
|
||||
mas-handlers = { path = "./crates/handlers/", version = "=0.14.1" }
|
||||
mas-http = { path = "./crates/http/", version = "=0.14.1" }
|
||||
mas-i18n = { path = "./crates/i18n/", version = "=0.14.1" }
|
||||
mas-i18n-scan = { path = "./crates/i18n-scan/", version = "=0.14.1" }
|
||||
mas-iana = { path = "./crates/iana/", version = "=0.14.1" }
|
||||
mas-iana-codegen = { path = "./crates/iana-codegen/", version = "=0.14.1" }
|
||||
mas-jose = { path = "./crates/jose/", version = "=0.14.1" }
|
||||
mas-keystore = { path = "./crates/keystore/", version = "=0.14.1" }
|
||||
mas-listener = { path = "./crates/listener/", version = "=0.14.1" }
|
||||
mas-matrix = { path = "./crates/matrix/", version = "=0.14.1" }
|
||||
mas-matrix-synapse = { path = "./crates/matrix-synapse/", version = "=0.14.1" }
|
||||
mas-oidc-client = { path = "./crates/oidc-client/", version = "=0.14.1" }
|
||||
mas-policy = { path = "./crates/policy/", version = "=0.14.1" }
|
||||
mas-router = { path = "./crates/router/", version = "=0.14.1" }
|
||||
mas-spa = { path = "./crates/spa/", version = "=0.14.1" }
|
||||
mas-storage = { path = "./crates/storage/", version = "=0.14.1" }
|
||||
mas-storage-pg = { path = "./crates/storage-pg/", version = "=0.14.1" }
|
||||
mas-tasks = { path = "./crates/tasks/", version = "=0.14.1" }
|
||||
mas-templates = { path = "./crates/templates/", version = "=0.14.1" }
|
||||
mas-tower = { path = "./crates/tower/", version = "=0.14.1" }
|
||||
oauth2-types = { path = "./crates/oauth2-types/", version = "=0.14.1" }
|
||||
syn2mas = { path = "./crates/syn2mas", version = "=0.14.1" }
|
||||
mas-axum-utils = { path = "./crates/axum-utils/", version = "=0.15.0-rc.0" }
|
||||
mas-cli = { path = "./crates/cli/", version = "=0.15.0-rc.0" }
|
||||
mas-config = { path = "./crates/config/", version = "=0.15.0-rc.0" }
|
||||
mas-data-model = { path = "./crates/data-model/", version = "=0.15.0-rc.0" }
|
||||
mas-email = { path = "./crates/email/", version = "=0.15.0-rc.0" }
|
||||
mas-graphql = { path = "./crates/graphql/", version = "=0.15.0-rc.0" }
|
||||
mas-handlers = { path = "./crates/handlers/", version = "=0.15.0-rc.0" }
|
||||
mas-http = { path = "./crates/http/", version = "=0.15.0-rc.0" }
|
||||
mas-i18n = { path = "./crates/i18n/", version = "=0.15.0-rc.0" }
|
||||
mas-i18n-scan = { path = "./crates/i18n-scan/", version = "=0.15.0-rc.0" }
|
||||
mas-iana = { path = "./crates/iana/", version = "=0.15.0-rc.0" }
|
||||
mas-iana-codegen = { path = "./crates/iana-codegen/", version = "=0.15.0-rc.0" }
|
||||
mas-jose = { path = "./crates/jose/", version = "=0.15.0-rc.0" }
|
||||
mas-keystore = { path = "./crates/keystore/", version = "=0.15.0-rc.0" }
|
||||
mas-listener = { path = "./crates/listener/", version = "=0.15.0-rc.0" }
|
||||
mas-matrix = { path = "./crates/matrix/", version = "=0.15.0-rc.0" }
|
||||
mas-matrix-synapse = { path = "./crates/matrix-synapse/", version = "=0.15.0-rc.0" }
|
||||
mas-oidc-client = { path = "./crates/oidc-client/", version = "=0.15.0-rc.0" }
|
||||
mas-policy = { path = "./crates/policy/", version = "=0.15.0-rc.0" }
|
||||
mas-router = { path = "./crates/router/", version = "=0.15.0-rc.0" }
|
||||
mas-spa = { path = "./crates/spa/", version = "=0.15.0-rc.0" }
|
||||
mas-storage = { path = "./crates/storage/", version = "=0.15.0-rc.0" }
|
||||
mas-storage-pg = { path = "./crates/storage-pg/", version = "=0.15.0-rc.0" }
|
||||
mas-tasks = { path = "./crates/tasks/", version = "=0.15.0-rc.0" }
|
||||
mas-templates = { path = "./crates/templates/", version = "=0.15.0-rc.0" }
|
||||
mas-tower = { path = "./crates/tower/", version = "=0.15.0-rc.0" }
|
||||
oauth2-types = { path = "./crates/oauth2-types/", version = "=0.15.0-rc.0" }
|
||||
syn2mas = { path = "./crates/syn2mas", version = "=0.15.0-rc.0" }
|
||||
|
||||
# OpenAPI schema generation and validation
|
||||
[workspace.dependencies.aide]
|
||||
@@ -80,7 +80,7 @@ version = "0.1.88"
|
||||
|
||||
# High-level error handling
|
||||
[workspace.dependencies.anyhow]
|
||||
version = "1.0.97"
|
||||
version = "1.0.98"
|
||||
|
||||
# HTTP router
|
||||
[workspace.dependencies.axum]
|
||||
@@ -119,7 +119,7 @@ features = ["serde", "clock"]
|
||||
|
||||
# CLI argument parsing
|
||||
[workspace.dependencies.clap]
|
||||
version = "4.5.35"
|
||||
version = "4.5.36"
|
||||
features = ["derive"]
|
||||
|
||||
# Cron expressions
|
||||
@@ -337,7 +337,7 @@ features = ["preserve_order"]
|
||||
|
||||
# SQL database support
|
||||
[workspace.dependencies.sqlx]
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
features = [
|
||||
"runtime-tokio",
|
||||
"tls-rustls-aws-lc-rs",
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use chrono::{DateTime, Utc};
|
||||
use mas_iana::oauth::PkceCodeChallengeMethod;
|
||||
use oauth2_types::{
|
||||
pkce::{CodeChallengeError, CodeChallengeMethodExt},
|
||||
@@ -158,11 +156,9 @@ pub struct AuthorizationGrant {
|
||||
pub scope: Scope,
|
||||
pub state: Option<String>,
|
||||
pub nonce: Option<String>,
|
||||
pub max_age: Option<NonZeroU32>,
|
||||
pub response_mode: ResponseMode,
|
||||
pub response_type_id_token: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub requires_consent: bool,
|
||||
pub login_hint: Option<String>,
|
||||
}
|
||||
|
||||
@@ -174,18 +170,7 @@ impl std::ops::Deref for AuthorizationGrant {
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_MAX_AGE: Duration = Duration::microseconds(3600 * 24 * 365 * 1000 * 1000);
|
||||
|
||||
impl AuthorizationGrant {
|
||||
#[must_use]
|
||||
pub fn max_auth_time(&self) -> DateTime<Utc> {
|
||||
let max_age = self
|
||||
.max_age
|
||||
.and_then(|x| Duration::try_seconds(x.get().into()))
|
||||
.unwrap_or(DEFAULT_MAX_AGE);
|
||||
self.created_at - max_age
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn parse_login_hint(&self, homeserver: &str) -> LoginHint {
|
||||
let Some(login_hint) = &self.login_hint else {
|
||||
@@ -274,11 +259,9 @@ impl AuthorizationGrant {
|
||||
scope: Scope::from_iter([OPENID, PROFILE]),
|
||||
state: Some(Alphanumeric.sample_string(rng, 10)),
|
||||
nonce: Some(Alphanumeric.sample_string(rng, 10)),
|
||||
max_age: None,
|
||||
response_mode: ResponseMode::Query,
|
||||
response_type_id_token: false,
|
||||
created_at: now,
|
||||
requires_consent: false,
|
||||
login_hint: Some(String::from("mxid:@example-user:example.com")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ hex.workspace = true
|
||||
governor.workspace = true
|
||||
indexmap.workspace = true
|
||||
pkcs8.workspace = true
|
||||
psl = "2.1.99"
|
||||
psl = "2.1.100"
|
||||
sha2.workspace = true
|
||||
time = "0.3.41"
|
||||
url.workspace = true
|
||||
|
||||
@@ -38,6 +38,7 @@ use opentelemetry_semantic_conventions::trace::{GRAPHQL_DOCUMENT, GRAPHQL_OPERAT
|
||||
use rand::{SeedableRng, thread_rng};
|
||||
use rand_chacha::ChaChaRng;
|
||||
use sqlx::PgPool;
|
||||
use state::has_session_ended;
|
||||
use tracing::{Instrument, info_span};
|
||||
use ulid::Ulid;
|
||||
|
||||
@@ -237,7 +238,7 @@ async fn get_requester(
|
||||
clock: &impl Clock,
|
||||
activity_tracker: &BoundActivityTracker,
|
||||
mut repo: BoxRepository,
|
||||
session_info: SessionInfo,
|
||||
session_info: &SessionInfo,
|
||||
user_agent: Option<String>,
|
||||
token: Option<&str>,
|
||||
) -> Result<Requester, RouteError> {
|
||||
@@ -328,13 +329,13 @@ pub async fn post(
|
||||
.as_ref()
|
||||
.map(|TypedHeader(Authorization(bearer))| bearer.token());
|
||||
let user_agent = user_agent.map(|TypedHeader(h)| h.to_string());
|
||||
let (session_info, _cookie_jar) = cookie_jar.session_info();
|
||||
let (session_info, mut cookie_jar) = cookie_jar.session_info();
|
||||
let requester = get_requester(
|
||||
undocumented_oauth2_access,
|
||||
&clock,
|
||||
&activity_tracker,
|
||||
repo,
|
||||
session_info,
|
||||
&session_info,
|
||||
user_agent,
|
||||
token,
|
||||
)
|
||||
@@ -352,7 +353,12 @@ pub async fn post(
|
||||
.data(requester); // XXX: this should probably return another error response?
|
||||
|
||||
let span = span_for_graphql_request(&request);
|
||||
let response = schema.execute(request).instrument(span).await;
|
||||
let mut response = schema.execute(request).instrument(span).await;
|
||||
|
||||
if has_session_ended(&mut response) {
|
||||
let session_info = session_info.mark_session_ended();
|
||||
cookie_jar = cookie_jar.update_session_info(&session_info);
|
||||
}
|
||||
|
||||
let cache_control = response
|
||||
.cache_control
|
||||
@@ -362,7 +368,7 @@ pub async fn post(
|
||||
|
||||
let headers = response.http_headers.clone();
|
||||
|
||||
Ok((headers, cache_control, Json(response)))
|
||||
Ok((headers, cache_control, cookie_jar, Json(response)))
|
||||
}
|
||||
|
||||
pub async fn get(
|
||||
@@ -382,13 +388,13 @@ pub async fn get(
|
||||
.as_ref()
|
||||
.map(|TypedHeader(Authorization(bearer))| bearer.token());
|
||||
let user_agent = user_agent.map(|TypedHeader(h)| h.to_string());
|
||||
let (session_info, _cookie_jar) = cookie_jar.session_info();
|
||||
let (session_info, mut cookie_jar) = cookie_jar.session_info();
|
||||
let requester = get_requester(
|
||||
undocumented_oauth2_access,
|
||||
&clock,
|
||||
&activity_tracker,
|
||||
repo,
|
||||
session_info,
|
||||
&session_info,
|
||||
user_agent,
|
||||
token,
|
||||
)
|
||||
@@ -398,7 +404,12 @@ pub async fn get(
|
||||
async_graphql::http::parse_query_string(&query.unwrap_or_default())?.data(requester);
|
||||
|
||||
let span = span_for_graphql_request(&request);
|
||||
let response = schema.execute(request).instrument(span).await;
|
||||
let mut response = schema.execute(request).instrument(span).await;
|
||||
|
||||
if has_session_ended(&mut response) {
|
||||
let session_info = session_info.mark_session_ended();
|
||||
cookie_jar = cookie_jar.update_session_info(&session_info);
|
||||
}
|
||||
|
||||
let cache_control = response
|
||||
.cache_control
|
||||
@@ -408,7 +419,7 @@ pub async fn get(
|
||||
|
||||
let headers = response.http_headers.clone();
|
||||
|
||||
Ok((headers, cache_control, Json(response)))
|
||||
Ok((headers, cache_control, cookie_jar, Json(response)))
|
||||
}
|
||||
|
||||
pub async fn playground() -> impl IntoResponse {
|
||||
|
||||
@@ -88,6 +88,15 @@ impl BrowserSessionMutations {
|
||||
|
||||
repo.save().await?;
|
||||
|
||||
// If we are ending the *current* session, we need to clear the session cookie
|
||||
// as well
|
||||
if requester
|
||||
.browser_session()
|
||||
.is_some_and(|s| s.id == session.id)
|
||||
{
|
||||
ctx.mark_session_ended();
|
||||
}
|
||||
|
||||
Ok(EndBrowserSessionPayload::Ended(Box::new(session)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
use async_graphql::{Response, ServerError};
|
||||
use mas_data_model::SiteConfig;
|
||||
use mas_matrix::HomeserverConnection;
|
||||
use mas_policy::Policy;
|
||||
@@ -12,6 +13,8 @@ use mas_storage::{BoxClock, BoxRepository, BoxRng, RepositoryError};
|
||||
|
||||
use crate::{Limiter, graphql::Requester, passwords::PasswordManager};
|
||||
|
||||
const CLEAR_SESSION_SENTINEL: &str = "__CLEAR_SESSION__";
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait State {
|
||||
async fn repository(&self) -> Result<BoxRepository, RepositoryError>;
|
||||
@@ -30,6 +33,8 @@ pub type BoxState = Box<dyn State + Send + Sync + 'static>;
|
||||
pub trait ContextExt {
|
||||
fn state(&self) -> &BoxState;
|
||||
|
||||
fn mark_session_ended(&self);
|
||||
|
||||
fn requester(&self) -> &Requester;
|
||||
}
|
||||
|
||||
@@ -38,7 +43,32 @@ impl ContextExt for async_graphql::Context<'_> {
|
||||
self.data_unchecked()
|
||||
}
|
||||
|
||||
fn mark_session_ended(&self) {
|
||||
// Add a sentinel to the error context, so that we can know that we need to
|
||||
// clear the session
|
||||
// XXX: this is a bit of a hack, but the only sane way to get infos from within
|
||||
// a mutation up to the HTTP handler
|
||||
self.add_error(ServerError::new(CLEAR_SESSION_SENTINEL, None));
|
||||
}
|
||||
|
||||
fn requester(&self) -> &Requester {
|
||||
self.data_unchecked()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the response contains a sentinel error indicating that the
|
||||
/// current cookie session has ended, and the session cookie should be cleared.
|
||||
///
|
||||
/// Also removes the sentinel error from the response.
|
||||
pub fn has_session_ended(response: &mut Response) -> bool {
|
||||
let errors = std::mem::take(&mut response.errors);
|
||||
let mut must_clear_session = false;
|
||||
for error in errors {
|
||||
if error.message == CLEAR_SESSION_SENTINEL {
|
||||
must_clear_session = true;
|
||||
} else {
|
||||
response.errors.push(error);
|
||||
}
|
||||
}
|
||||
must_clear_session
|
||||
}
|
||||
|
||||
@@ -371,10 +371,6 @@ where
|
||||
get(self::views::login::get).post(self::views::login::post),
|
||||
)
|
||||
.route(mas_router::Logout::route(), post(self::views::logout::post))
|
||||
.route(
|
||||
mas_router::Reauth::route(),
|
||||
get(self::views::reauth::get).post(self::views::reauth::post),
|
||||
)
|
||||
.route(
|
||||
mas_router::Register::route(),
|
||||
get(self::views::register::get),
|
||||
@@ -409,13 +405,10 @@ where
|
||||
mas_router::OAuth2AuthorizationEndpoint::route(),
|
||||
get(self::oauth2::authorization::get),
|
||||
)
|
||||
.route(
|
||||
mas_router::ContinueAuthorizationGrant::route(),
|
||||
get(self::oauth2::authorization::complete::get),
|
||||
)
|
||||
.route(
|
||||
mas_router::Consent::route(),
|
||||
get(self::oauth2::consent::get).post(self::oauth2::consent::post),
|
||||
get(self::oauth2::authorization::consent::get)
|
||||
.post(self::oauth2::authorization::consent::post),
|
||||
)
|
||||
.route(
|
||||
mas_router::CompatLoginSsoComplete::route(),
|
||||
|
||||
@@ -101,7 +101,7 @@ impl CallbackDestination {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn go<T: Serialize + Send + Sync>(
|
||||
pub fn go<T: Serialize + Send + Sync>(
|
||||
self,
|
||||
templates: &Templates,
|
||||
locale: &DataLocale,
|
||||
|
||||
@@ -1,309 +0,0 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
use axum_extra::TypedHeader;
|
||||
use hyper::StatusCode;
|
||||
use mas_axum_utils::{SessionInfoExt, cookies::CookieJar, csrf::CsrfExt, sentry::SentryEventID};
|
||||
use mas_data_model::{AuthorizationGrant, BrowserSession, Client, Device};
|
||||
use mas_keystore::Keystore;
|
||||
use mas_policy::{EvaluationResult, Policy};
|
||||
use mas_router::{PostAuthAction, UrlBuilder};
|
||||
use mas_storage::{
|
||||
BoxClock, BoxRepository, BoxRng, Clock, RepositoryAccess,
|
||||
oauth2::{OAuth2AuthorizationGrantRepository, OAuth2ClientRepository, OAuth2SessionRepository},
|
||||
user::BrowserSessionRepository,
|
||||
};
|
||||
use mas_templates::{PolicyViolationContext, TemplateContext, Templates};
|
||||
use oauth2_types::requests::AuthorizationResponse;
|
||||
use thiserror::Error;
|
||||
use tracing::warn;
|
||||
use ulid::Ulid;
|
||||
|
||||
use super::callback::CallbackDestination;
|
||||
use crate::{
|
||||
BoundActivityTracker, PreferredLanguage, impl_from_error_for_route, oauth2::generate_id_token,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RouteError {
|
||||
#[error(transparent)]
|
||||
Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
|
||||
|
||||
#[error("authorization grant was not found")]
|
||||
NotFound,
|
||||
|
||||
#[error("authorization grant is not in a pending state")]
|
||||
NotPending,
|
||||
|
||||
#[error("failed to load client")]
|
||||
NoSuchClient,
|
||||
}
|
||||
|
||||
impl IntoResponse for RouteError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
let event = sentry::capture_error(&self);
|
||||
// TODO: better error pages
|
||||
let response = match self {
|
||||
RouteError::NotFound => {
|
||||
(StatusCode::NOT_FOUND, "authorization grant was not found").into_response()
|
||||
}
|
||||
RouteError::NotPending => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
"authorization grant not in a pending state",
|
||||
)
|
||||
.into_response(),
|
||||
RouteError::Internal(_) | Self::NoSuchClient => {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
|
||||
}
|
||||
};
|
||||
|
||||
(SentryEventID::from(event), response).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl_from_error_for_route!(mas_storage::RepositoryError);
|
||||
impl_from_error_for_route!(mas_templates::TemplateError);
|
||||
impl_from_error_for_route!(mas_policy::LoadError);
|
||||
impl_from_error_for_route!(mas_policy::EvaluationError);
|
||||
impl_from_error_for_route!(super::callback::IntoCallbackDestinationError);
|
||||
impl_from_error_for_route!(super::callback::CallbackDestinationError);
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "handlers.oauth2.authorization_complete.get",
|
||||
fields(grant.id = %grant_id),
|
||||
skip_all,
|
||||
err,
|
||||
)]
|
||||
pub(crate) async fn get(
|
||||
mut rng: BoxRng,
|
||||
clock: BoxClock,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
State(url_builder): State<UrlBuilder>,
|
||||
State(key_store): State<Keystore>,
|
||||
policy: Policy,
|
||||
activity_tracker: BoundActivityTracker,
|
||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||
mut repo: BoxRepository,
|
||||
cookie_jar: CookieJar,
|
||||
Path(grant_id): Path<Ulid>,
|
||||
) -> Result<Response, RouteError> {
|
||||
let (session_info, cookie_jar) = cookie_jar.session_info();
|
||||
|
||||
let maybe_session = session_info.load_active_session(&mut repo).await?;
|
||||
|
||||
let user_agent = user_agent.map(|TypedHeader(ua)| ua.to_string());
|
||||
|
||||
let grant = repo
|
||||
.oauth2_authorization_grant()
|
||||
.lookup(grant_id)
|
||||
.await?
|
||||
.ok_or(RouteError::NotFound)?;
|
||||
|
||||
let callback_destination = CallbackDestination::try_from(&grant)?;
|
||||
let continue_grant = PostAuthAction::continue_grant(grant.id);
|
||||
|
||||
let Some(session) = maybe_session else {
|
||||
// If there is no session, redirect to the login screen, redirecting here after
|
||||
// logout
|
||||
return Ok((
|
||||
cookie_jar,
|
||||
url_builder.redirect(&mas_router::Login::and_then(continue_grant)),
|
||||
)
|
||||
.into_response());
|
||||
};
|
||||
|
||||
activity_tracker
|
||||
.record_browser_session(&clock, &session)
|
||||
.await;
|
||||
|
||||
let client = repo
|
||||
.oauth2_client()
|
||||
.lookup(grant.client_id)
|
||||
.await?
|
||||
.ok_or(RouteError::NoSuchClient)?;
|
||||
|
||||
match complete(
|
||||
&mut rng,
|
||||
&clock,
|
||||
&activity_tracker,
|
||||
user_agent,
|
||||
repo,
|
||||
key_store,
|
||||
policy,
|
||||
&url_builder,
|
||||
grant,
|
||||
&client,
|
||||
&session,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(params) => {
|
||||
let res = callback_destination.go(&templates, &locale, params).await?;
|
||||
Ok((cookie_jar, res).into_response())
|
||||
}
|
||||
Err(GrantCompletionError::RequiresReauth) => Ok((
|
||||
cookie_jar,
|
||||
url_builder.redirect(&mas_router::Reauth::and_then(continue_grant)),
|
||||
)
|
||||
.into_response()),
|
||||
Err(GrantCompletionError::RequiresConsent) => {
|
||||
let next = mas_router::Consent(grant_id);
|
||||
Ok((cookie_jar, url_builder.redirect(&next)).into_response())
|
||||
}
|
||||
Err(GrantCompletionError::PolicyViolation(grant, res)) => {
|
||||
warn!(violation = ?res, "Authorization grant for client {} denied by policy", client.id);
|
||||
|
||||
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
|
||||
let ctx = PolicyViolationContext::for_authorization_grant(grant, client)
|
||||
.with_session(session)
|
||||
.with_csrf(csrf_token.form_value())
|
||||
.with_language(locale);
|
||||
|
||||
let content = templates.render_policy_violation(&ctx)?;
|
||||
|
||||
Ok((cookie_jar, Html(content)).into_response())
|
||||
}
|
||||
Err(GrantCompletionError::NotPending) => Err(RouteError::NotPending),
|
||||
Err(GrantCompletionError::Internal(e)) => Err(RouteError::Internal(e)),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum GrantCompletionError {
|
||||
#[error(transparent)]
|
||||
Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
|
||||
|
||||
#[error("authorization grant is not in a pending state")]
|
||||
NotPending,
|
||||
|
||||
#[error("user needs to reauthenticate")]
|
||||
RequiresReauth,
|
||||
|
||||
#[error("client lacks consent")]
|
||||
RequiresConsent,
|
||||
|
||||
#[error("denied by the policy")]
|
||||
PolicyViolation(AuthorizationGrant, EvaluationResult),
|
||||
}
|
||||
|
||||
impl_from_error_for_route!(GrantCompletionError: mas_storage::RepositoryError);
|
||||
impl_from_error_for_route!(GrantCompletionError: super::callback::IntoCallbackDestinationError);
|
||||
impl_from_error_for_route!(GrantCompletionError: mas_policy::LoadError);
|
||||
impl_from_error_for_route!(GrantCompletionError: mas_policy::EvaluationError);
|
||||
impl_from_error_for_route!(GrantCompletionError: super::super::IdTokenSignatureError);
|
||||
|
||||
pub(crate) async fn complete(
|
||||
rng: &mut (impl rand::RngCore + rand::CryptoRng + Send),
|
||||
clock: &impl Clock,
|
||||
activity_tracker: &BoundActivityTracker,
|
||||
user_agent: Option<String>,
|
||||
mut repo: BoxRepository,
|
||||
key_store: Keystore,
|
||||
mut policy: Policy,
|
||||
url_builder: &UrlBuilder,
|
||||
grant: AuthorizationGrant,
|
||||
client: &Client,
|
||||
browser_session: &BrowserSession,
|
||||
) -> Result<AuthorizationResponse, GrantCompletionError> {
|
||||
// Verify that the grant is in a pending stage
|
||||
if !grant.stage.is_pending() {
|
||||
return Err(GrantCompletionError::NotPending);
|
||||
}
|
||||
|
||||
// Check if the authentication is fresh enough
|
||||
let authentication = repo
|
||||
.browser_session()
|
||||
.get_last_authentication(browser_session)
|
||||
.await?;
|
||||
let authentication = authentication.filter(|auth| auth.created_at > grant.max_auth_time());
|
||||
|
||||
let Some(valid_authentication) = authentication else {
|
||||
repo.save().await?;
|
||||
return Err(GrantCompletionError::RequiresReauth);
|
||||
};
|
||||
|
||||
// Run through the policy
|
||||
let res = policy
|
||||
.evaluate_authorization_grant(mas_policy::AuthorizationGrantInput {
|
||||
user: Some(&browser_session.user),
|
||||
client,
|
||||
scope: &grant.scope,
|
||||
grant_type: mas_policy::GrantType::AuthorizationCode,
|
||||
requester: mas_policy::Requester {
|
||||
ip_address: activity_tracker.ip(),
|
||||
user_agent,
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
|
||||
if !res.valid() {
|
||||
return Err(GrantCompletionError::PolicyViolation(grant, res));
|
||||
}
|
||||
|
||||
let current_consent = repo
|
||||
.oauth2_client()
|
||||
.get_consent_for_user(client, &browser_session.user)
|
||||
.await?;
|
||||
|
||||
let lacks_consent = grant
|
||||
.scope
|
||||
.difference(¤t_consent)
|
||||
.filter(|scope| Device::from_scope_token(scope).is_none())
|
||||
.any(|_| true);
|
||||
|
||||
// Check if the client lacks consent *or* if consent was explicitly asked
|
||||
if lacks_consent || grant.requires_consent {
|
||||
repo.save().await?;
|
||||
return Err(GrantCompletionError::RequiresConsent);
|
||||
}
|
||||
|
||||
// All good, let's start the session
|
||||
let session = repo
|
||||
.oauth2_session()
|
||||
.add_from_browser_session(rng, clock, client, browser_session, grant.scope.clone())
|
||||
.await?;
|
||||
|
||||
let grant = repo
|
||||
.oauth2_authorization_grant()
|
||||
.fulfill(clock, &session, grant)
|
||||
.await?;
|
||||
|
||||
// Yep! Let's complete the auth now
|
||||
let mut params = AuthorizationResponse::default();
|
||||
|
||||
// Did they request an ID token?
|
||||
if grant.response_type_id_token {
|
||||
params.id_token = Some(generate_id_token(
|
||||
rng,
|
||||
clock,
|
||||
url_builder,
|
||||
&key_store,
|
||||
client,
|
||||
Some(&grant),
|
||||
browser_session,
|
||||
None,
|
||||
Some(&valid_authentication),
|
||||
)?);
|
||||
}
|
||||
|
||||
// Did they request an auth code?
|
||||
if let Some(code) = grant.code {
|
||||
params.code = Some(code.code);
|
||||
}
|
||||
|
||||
repo.save().await?;
|
||||
|
||||
activity_tracker
|
||||
.record_oauth2_session(clock, &session)
|
||||
.await;
|
||||
|
||||
Ok(params)
|
||||
}
|
||||
@@ -15,7 +15,8 @@ use mas_axum_utils::{
|
||||
csrf::{CsrfExt, ProtectedForm},
|
||||
sentry::SentryEventID,
|
||||
};
|
||||
use mas_data_model::{AuthorizationGrantStage, Device};
|
||||
use mas_data_model::AuthorizationGrantStage;
|
||||
use mas_keystore::Keystore;
|
||||
use mas_policy::Policy;
|
||||
use mas_router::{PostAuthAction, UrlBuilder};
|
||||
use mas_storage::{
|
||||
@@ -23,11 +24,14 @@ use mas_storage::{
|
||||
oauth2::{OAuth2AuthorizationGrantRepository, OAuth2ClientRepository},
|
||||
};
|
||||
use mas_templates::{ConsentContext, PolicyViolationContext, TemplateContext, Templates};
|
||||
use oauth2_types::requests::AuthorizationResponse;
|
||||
use thiserror::Error;
|
||||
use ulid::Ulid;
|
||||
|
||||
use super::callback::CallbackDestination;
|
||||
use crate::{
|
||||
BoundActivityTracker, PreferredLanguage, impl_from_error_for_route,
|
||||
oauth2::generate_id_token,
|
||||
session::{SessionOrFallback, load_session_or_fallback},
|
||||
};
|
||||
|
||||
@@ -45,9 +49,6 @@ pub enum RouteError {
|
||||
#[error("Authorization grant already used")]
|
||||
GrantNotPending,
|
||||
|
||||
#[error("Policy violation")]
|
||||
PolicyViolation,
|
||||
|
||||
#[error("Failed to load client")]
|
||||
NoSuchClient,
|
||||
}
|
||||
@@ -57,20 +58,24 @@ impl_from_error_for_route!(mas_storage::RepositoryError);
|
||||
impl_from_error_for_route!(mas_policy::LoadError);
|
||||
impl_from_error_for_route!(mas_policy::EvaluationError);
|
||||
impl_from_error_for_route!(crate::session::SessionLoadError);
|
||||
impl_from_error_for_route!(crate::oauth2::IdTokenSignatureError);
|
||||
impl_from_error_for_route!(super::callback::IntoCallbackDestinationError);
|
||||
impl_from_error_for_route!(super::callback::CallbackDestinationError);
|
||||
|
||||
impl IntoResponse for RouteError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
let event_id = sentry::capture_error(&self);
|
||||
(
|
||||
SentryEventID::from(event_id),
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
SentryEventID::from(event_id),
|
||||
self.to_string(),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "handlers.oauth2.consent.get",
|
||||
name = "handlers.oauth2.authorization.consent.get",
|
||||
fields(grant.id = %grant_id),
|
||||
skip_all,
|
||||
err,
|
||||
@@ -142,17 +147,7 @@ pub(crate) async fn get(
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
|
||||
if res.valid() {
|
||||
let ctx = ConsentContext::new(grant, client)
|
||||
.with_session(session)
|
||||
.with_csrf(csrf_token.form_value())
|
||||
.with_language(locale);
|
||||
|
||||
let content = templates.render_consent(&ctx)?;
|
||||
|
||||
Ok((cookie_jar, Html(content)).into_response())
|
||||
} else {
|
||||
if !res.valid() {
|
||||
let ctx = PolicyViolationContext::for_authorization_grant(grant, client)
|
||||
.with_session(session)
|
||||
.with_csrf(csrf_token.form_value())
|
||||
@@ -160,12 +155,21 @@ pub(crate) async fn get(
|
||||
|
||||
let content = templates.render_policy_violation(&ctx)?;
|
||||
|
||||
Ok((cookie_jar, Html(content)).into_response())
|
||||
return Ok((cookie_jar, Html(content)).into_response());
|
||||
}
|
||||
|
||||
let ctx = ConsentContext::new(grant, client)
|
||||
.with_session(session)
|
||||
.with_csrf(csrf_token.form_value())
|
||||
.with_language(locale);
|
||||
|
||||
let content = templates.render_consent(&ctx)?;
|
||||
|
||||
Ok((cookie_jar, Html(content)).into_response())
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "handlers.oauth2.consent.post",
|
||||
name = "handlers.oauth2.authorization.consent.post",
|
||||
fields(grant.id = %grant_id),
|
||||
skip_all,
|
||||
err,
|
||||
@@ -175,6 +179,7 @@ pub(crate) async fn post(
|
||||
clock: BoxClock,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
State(key_store): State<Keystore>,
|
||||
mut policy: Policy,
|
||||
mut repo: BoxRepository,
|
||||
activity_tracker: BoundActivityTracker,
|
||||
@@ -199,6 +204,8 @@ pub(crate) async fn post(
|
||||
SessionOrFallback::Fallback { response } => return Ok(response),
|
||||
};
|
||||
|
||||
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
|
||||
|
||||
let user_agent = user_agent.map(|ua| ua.to_string());
|
||||
|
||||
let grant = repo
|
||||
@@ -206,15 +213,16 @@ pub(crate) async fn post(
|
||||
.lookup(grant_id)
|
||||
.await?
|
||||
.ok_or(RouteError::GrantNotFound)?;
|
||||
let next = PostAuthAction::continue_grant(grant_id);
|
||||
let callback_destination = CallbackDestination::try_from(&grant)?;
|
||||
|
||||
let Some(session) = maybe_session else {
|
||||
let Some(browser_session) = maybe_session else {
|
||||
let next = PostAuthAction::continue_grant(grant_id);
|
||||
let login = mas_router::Login::and_then(next);
|
||||
return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
|
||||
};
|
||||
|
||||
activity_tracker
|
||||
.record_browser_session(&clock, &session)
|
||||
.record_browser_session(&clock, &browser_session)
|
||||
.await;
|
||||
|
||||
let client = repo
|
||||
@@ -225,7 +233,7 @@ pub(crate) async fn post(
|
||||
|
||||
let res = policy
|
||||
.evaluate_authorization_grant(mas_policy::AuthorizationGrantInput {
|
||||
user: Some(&session.user),
|
||||
user: Some(&browser_session.user),
|
||||
client: &client,
|
||||
scope: &grant.scope,
|
||||
grant_type: mas_policy::GrantType::AuthorizationCode,
|
||||
@@ -237,32 +245,70 @@ pub(crate) async fn post(
|
||||
.await?;
|
||||
|
||||
if !res.valid() {
|
||||
return Err(RouteError::PolicyViolation);
|
||||
let ctx = PolicyViolationContext::for_authorization_grant(grant, client)
|
||||
.with_session(browser_session)
|
||||
.with_csrf(csrf_token.form_value())
|
||||
.with_language(locale);
|
||||
|
||||
let content = templates.render_policy_violation(&ctx)?;
|
||||
|
||||
return Ok((cookie_jar, Html(content)).into_response());
|
||||
}
|
||||
|
||||
// Do not consent for the "urn:matrix:org.matrix.msc2967.client:device:*" scope
|
||||
let scope_without_device = grant
|
||||
.scope
|
||||
.iter()
|
||||
.filter(|s| Device::from_scope_token(s).is_none())
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
repo.oauth2_client()
|
||||
.give_consent_for_user(
|
||||
// All good, let's start the session
|
||||
let session = repo
|
||||
.oauth2_session()
|
||||
.add_from_browser_session(
|
||||
&mut rng,
|
||||
&clock,
|
||||
&client,
|
||||
&session.user,
|
||||
&scope_without_device,
|
||||
&browser_session,
|
||||
grant.scope.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
repo.oauth2_authorization_grant()
|
||||
.give_consent(grant)
|
||||
let grant = repo
|
||||
.oauth2_authorization_grant()
|
||||
.fulfill(&clock, &session, grant)
|
||||
.await?;
|
||||
|
||||
let mut params = AuthorizationResponse::default();
|
||||
|
||||
// Did they request an ID token?
|
||||
if grant.response_type_id_token {
|
||||
// Fetch the last authentication
|
||||
let last_authentication = repo
|
||||
.browser_session()
|
||||
.get_last_authentication(&browser_session)
|
||||
.await?;
|
||||
|
||||
params.id_token = Some(generate_id_token(
|
||||
&mut rng,
|
||||
&clock,
|
||||
&url_builder,
|
||||
&key_store,
|
||||
&client,
|
||||
Some(&grant),
|
||||
&browser_session,
|
||||
None,
|
||||
last_authentication.as_ref(),
|
||||
)?);
|
||||
}
|
||||
|
||||
// Did they request an auth code?
|
||||
if let Some(code) = grant.code {
|
||||
params.code = Some(code.code);
|
||||
}
|
||||
|
||||
repo.save().await?;
|
||||
|
||||
Ok((cookie_jar, next.go_next(&url_builder)).into_response())
|
||||
activity_tracker
|
||||
.record_oauth2_session(&clock, &session)
|
||||
.await;
|
||||
|
||||
Ok((
|
||||
cookie_jar,
|
||||
callback_destination.go(&templates, &locale, params)?,
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
@@ -6,20 +6,17 @@
|
||||
|
||||
use axum::{
|
||||
extract::{Form, State},
|
||||
response::{Html, IntoResponse, Response},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use axum_extra::TypedHeader;
|
||||
use hyper::StatusCode;
|
||||
use mas_axum_utils::{SessionInfoExt, cookies::CookieJar, csrf::CsrfExt, sentry::SentryEventID};
|
||||
use mas_axum_utils::{SessionInfoExt, cookies::CookieJar, sentry::SentryEventID};
|
||||
use mas_data_model::{AuthorizationCode, Pkce};
|
||||
use mas_keystore::Keystore;
|
||||
use mas_policy::Policy;
|
||||
use mas_router::{PostAuthAction, UrlBuilder};
|
||||
use mas_storage::{
|
||||
BoxClock, BoxRepository, BoxRng,
|
||||
oauth2::{OAuth2AuthorizationGrantRepository, OAuth2ClientRepository},
|
||||
};
|
||||
use mas_templates::{PolicyViolationContext, TemplateContext, Templates};
|
||||
use mas_templates::Templates;
|
||||
use oauth2_types::{
|
||||
errors::{ClientError, ClientErrorCode},
|
||||
pkce,
|
||||
@@ -29,13 +26,12 @@ use oauth2_types::{
|
||||
use rand::{Rng, distributions::Alphanumeric};
|
||||
use serde::Deserialize;
|
||||
use thiserror::Error;
|
||||
use tracing::warn;
|
||||
|
||||
use self::{callback::CallbackDestination, complete::GrantCompletionError};
|
||||
use self::callback::CallbackDestination;
|
||||
use crate::{BoundActivityTracker, PreferredLanguage, impl_from_error_for_route};
|
||||
|
||||
mod callback;
|
||||
pub mod complete;
|
||||
pub(crate) mod consent;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RouteError {
|
||||
@@ -134,10 +130,7 @@ pub(crate) async fn get(
|
||||
clock: BoxClock,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
State(key_store): State<Keystore>,
|
||||
State(url_builder): State<UrlBuilder>,
|
||||
policy: Policy,
|
||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||
activity_tracker: BoundActivityTracker,
|
||||
mut repo: BoxRepository,
|
||||
cookie_jar: CookieJar,
|
||||
@@ -166,9 +159,6 @@ pub(crate) async fn get(
|
||||
|
||||
// Get the session info from the cookie
|
||||
let (session_info, cookie_jar) = cookie_jar.session_info();
|
||||
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
|
||||
|
||||
let user_agent = user_agent.map(|TypedHeader(ua)| ua.to_string());
|
||||
|
||||
// One day, we will have try blocks
|
||||
let res: Result<Response, RouteError> = ({
|
||||
@@ -182,80 +172,66 @@ pub(crate) async fn get(
|
||||
// Check if the request/request_uri/registration params are used. If so, reply
|
||||
// with the right error since we don't support them.
|
||||
if params.auth.request.is_some() {
|
||||
return Ok(callback_destination
|
||||
.go(
|
||||
&templates,
|
||||
&locale,
|
||||
ClientError::from(ClientErrorCode::RequestNotSupported),
|
||||
)
|
||||
.await?);
|
||||
return Ok(callback_destination.go(
|
||||
&templates,
|
||||
&locale,
|
||||
ClientError::from(ClientErrorCode::RequestNotSupported),
|
||||
)?);
|
||||
}
|
||||
|
||||
if params.auth.request_uri.is_some() {
|
||||
return Ok(callback_destination
|
||||
.go(
|
||||
&templates,
|
||||
&locale,
|
||||
ClientError::from(ClientErrorCode::RequestUriNotSupported),
|
||||
)
|
||||
.await?);
|
||||
return Ok(callback_destination.go(
|
||||
&templates,
|
||||
&locale,
|
||||
ClientError::from(ClientErrorCode::RequestUriNotSupported),
|
||||
)?);
|
||||
}
|
||||
|
||||
// Check if the client asked for a `token` response type, and bail out if it's
|
||||
// the case, since we don't support them
|
||||
if response_type.has_token() {
|
||||
return Ok(callback_destination
|
||||
.go(
|
||||
&templates,
|
||||
&locale,
|
||||
ClientError::from(ClientErrorCode::UnsupportedResponseType),
|
||||
)
|
||||
.await?);
|
||||
return Ok(callback_destination.go(
|
||||
&templates,
|
||||
&locale,
|
||||
ClientError::from(ClientErrorCode::UnsupportedResponseType),
|
||||
)?);
|
||||
}
|
||||
|
||||
// If the client asked for a `id_token` response type, we must check if it can
|
||||
// use the `implicit` grant type
|
||||
if response_type.has_id_token() && !client.grant_types.contains(&GrantType::Implicit) {
|
||||
return Ok(callback_destination
|
||||
.go(
|
||||
&templates,
|
||||
&locale,
|
||||
ClientError::from(ClientErrorCode::UnauthorizedClient),
|
||||
)
|
||||
.await?);
|
||||
return Ok(callback_destination.go(
|
||||
&templates,
|
||||
&locale,
|
||||
ClientError::from(ClientErrorCode::UnauthorizedClient),
|
||||
)?);
|
||||
}
|
||||
|
||||
if params.auth.registration.is_some() {
|
||||
return Ok(callback_destination
|
||||
.go(
|
||||
&templates,
|
||||
&locale,
|
||||
ClientError::from(ClientErrorCode::RegistrationNotSupported),
|
||||
)
|
||||
.await?);
|
||||
return Ok(callback_destination.go(
|
||||
&templates,
|
||||
&locale,
|
||||
ClientError::from(ClientErrorCode::RegistrationNotSupported),
|
||||
)?);
|
||||
}
|
||||
|
||||
// Fail early if prompt=none and there is no active session
|
||||
if prompt.contains(&Prompt::None) && maybe_session.is_none() {
|
||||
return Ok(callback_destination
|
||||
.go(
|
||||
&templates,
|
||||
&locale,
|
||||
ClientError::from(ClientErrorCode::LoginRequired),
|
||||
)
|
||||
.await?);
|
||||
// Fail early if prompt=none; we never let it go through
|
||||
if prompt.contains(&Prompt::None) {
|
||||
return Ok(callback_destination.go(
|
||||
&templates,
|
||||
&locale,
|
||||
ClientError::from(ClientErrorCode::LoginRequired),
|
||||
)?);
|
||||
}
|
||||
|
||||
let code: Option<AuthorizationCode> = if response_type.has_code() {
|
||||
// Check if it is allowed to use this grant type
|
||||
if !client.grant_types.contains(&GrantType::AuthorizationCode) {
|
||||
return Ok(callback_destination
|
||||
.go(
|
||||
&templates,
|
||||
&locale,
|
||||
ClientError::from(ClientErrorCode::UnauthorizedClient),
|
||||
)
|
||||
.await?);
|
||||
return Ok(callback_destination.go(
|
||||
&templates,
|
||||
&locale,
|
||||
ClientError::from(ClientErrorCode::UnauthorizedClient),
|
||||
)?);
|
||||
}
|
||||
|
||||
// 32 random alphanumeric characters, about 190bit of entropy
|
||||
@@ -275,20 +251,16 @@ pub(crate) async fn get(
|
||||
// If the request had PKCE params but no code asked, it should get back with an
|
||||
// error
|
||||
if params.pkce.is_some() {
|
||||
return Ok(callback_destination
|
||||
.go(
|
||||
&templates,
|
||||
&locale,
|
||||
ClientError::from(ClientErrorCode::InvalidRequest),
|
||||
)
|
||||
.await?);
|
||||
return Ok(callback_destination.go(
|
||||
&templates,
|
||||
&locale,
|
||||
ClientError::from(ClientErrorCode::InvalidRequest),
|
||||
)?);
|
||||
}
|
||||
|
||||
None
|
||||
};
|
||||
|
||||
let requires_consent = prompt.contains(&Prompt::Consent);
|
||||
|
||||
let grant = repo
|
||||
.oauth2_authorization_grant()
|
||||
.add(
|
||||
@@ -300,151 +272,43 @@ pub(crate) async fn get(
|
||||
code,
|
||||
params.auth.state.clone(),
|
||||
params.auth.nonce,
|
||||
params.auth.max_age,
|
||||
response_mode,
|
||||
response_type.has_id_token(),
|
||||
requires_consent,
|
||||
params.auth.login_hint,
|
||||
)
|
||||
.await?;
|
||||
let continue_grant = PostAuthAction::continue_grant(grant.id);
|
||||
|
||||
let res = match maybe_session {
|
||||
// Cases where there is no active session, redirect to the relevant page
|
||||
None if prompt.contains(&Prompt::None) => {
|
||||
// This case should already be handled earlier
|
||||
unreachable!();
|
||||
}
|
||||
None if prompt.contains(&Prompt::Create) => {
|
||||
// Client asked for a registration, show the registration prompt
|
||||
repo.save().await?;
|
||||
|
||||
url_builder.redirect(&mas_router::Register::and_then(continue_grant))
|
||||
url_builder
|
||||
.redirect(&mas_router::Register::and_then(continue_grant))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
None => {
|
||||
// Other cases where we don't have a session, ask for a login
|
||||
repo.save().await?;
|
||||
|
||||
url_builder.redirect(&mas_router::Login::and_then(continue_grant))
|
||||
url_builder
|
||||
.redirect(&mas_router::Login::and_then(continue_grant))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
// Special case when we already have a session but prompt=login|select_account
|
||||
Some(session)
|
||||
if prompt.contains(&Prompt::Login)
|
||||
|| prompt.contains(&Prompt::SelectAccount) =>
|
||||
{
|
||||
// TODO: better pages here
|
||||
Some(user_session) => {
|
||||
// TODO: better support for prompt=create when we have a session
|
||||
repo.save().await?;
|
||||
|
||||
activity_tracker.record_browser_session(&clock, &session).await;
|
||||
|
||||
url_builder.redirect(&mas_router::Reauth::and_then(continue_grant))
|
||||
activity_tracker
|
||||
.record_browser_session(&clock, &user_session)
|
||||
.await;
|
||||
url_builder
|
||||
.redirect(&mas_router::Consent(grant.id))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
// Else, we immediately try to complete the authorization grant
|
||||
Some(user_session) if prompt.contains(&Prompt::None) => {
|
||||
activity_tracker.record_browser_session(&clock, &user_session).await;
|
||||
|
||||
// With prompt=none, we should get back to the client immediately
|
||||
match self::complete::complete(
|
||||
&mut rng,
|
||||
&clock,
|
||||
&activity_tracker,
|
||||
user_agent,
|
||||
repo,
|
||||
key_store,
|
||||
policy,
|
||||
&url_builder,
|
||||
grant,
|
||||
&client,
|
||||
&user_session,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(params) => callback_destination.go(&templates, &locale, params).await?,
|
||||
Err(GrantCompletionError::RequiresConsent) => {
|
||||
callback_destination
|
||||
.go(
|
||||
&templates,
|
||||
&locale,
|
||||
ClientError::from(ClientErrorCode::ConsentRequired),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Err(GrantCompletionError::RequiresReauth) => {
|
||||
callback_destination
|
||||
.go(
|
||||
&templates,
|
||||
&locale,
|
||||
ClientError::from(ClientErrorCode::InteractionRequired),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Err(GrantCompletionError::PolicyViolation(_grant, _res)) => {
|
||||
callback_destination
|
||||
.go(&templates, &locale, ClientError::from(ClientErrorCode::AccessDenied))
|
||||
.await?
|
||||
}
|
||||
Err(GrantCompletionError::Internal(e)) => {
|
||||
return Err(RouteError::Internal(e))
|
||||
}
|
||||
Err(e @ GrantCompletionError::NotPending) => {
|
||||
// This should never happen
|
||||
return Err(RouteError::Internal(Box::new(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(user_session) => {
|
||||
activity_tracker.record_browser_session(&clock, &user_session).await;
|
||||
|
||||
let grant_id = grant.id;
|
||||
// Else, we show the relevant reauth/consent page if necessary
|
||||
match self::complete::complete(
|
||||
&mut rng,
|
||||
&clock,
|
||||
&activity_tracker,
|
||||
user_agent,
|
||||
repo,
|
||||
key_store,
|
||||
policy,
|
||||
&url_builder,
|
||||
grant,
|
||||
&client,
|
||||
&user_session,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(params) => callback_destination.go(&templates, &locale, params).await?,
|
||||
Err(GrantCompletionError::RequiresConsent) => {
|
||||
url_builder.redirect(&mas_router::Consent(grant_id)).into_response()
|
||||
}
|
||||
Err(GrantCompletionError::PolicyViolation(grant, res)) => {
|
||||
warn!(violation = ?res, "Authorization grant for client {} denied by policy", client.id);
|
||||
|
||||
let ctx = PolicyViolationContext::for_authorization_grant(grant, client)
|
||||
.with_session(user_session)
|
||||
.with_csrf(csrf_token.form_value())
|
||||
.with_language(locale);
|
||||
|
||||
let content = templates.render_policy_violation(&ctx)?;
|
||||
Html(content).into_response()
|
||||
}
|
||||
Err(GrantCompletionError::RequiresReauth) => {
|
||||
url_builder.redirect(&mas_router::Reauth::and_then(continue_grant))
|
||||
.into_response()
|
||||
}
|
||||
Err(GrantCompletionError::Internal(e)) => {
|
||||
return Err(RouteError::Internal(e))
|
||||
}
|
||||
Err(e @ GrantCompletionError::NotPending) => {
|
||||
// This should never happen
|
||||
return Err(RouteError::Internal(Box::new(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
@@ -456,13 +320,11 @@ pub(crate) async fn get(
|
||||
Ok(r) => r,
|
||||
Err(err) => {
|
||||
tracing::error!(%err);
|
||||
callback_destination
|
||||
.go(
|
||||
&templates,
|
||||
&locale,
|
||||
ClientError::from(ClientErrorCode::ServerError),
|
||||
)
|
||||
.await?
|
||||
callback_destination.go(
|
||||
&templates,
|
||||
&locale,
|
||||
ClientError::from(ClientErrorCode::ServerError),
|
||||
)?
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ pub(crate) async fn get(
|
||||
let request_uri_parameter_supported = Some(false);
|
||||
|
||||
let prompt_values_supported = Some({
|
||||
let mut v = vec![Prompt::None, Prompt::Login];
|
||||
let mut v = vec![Prompt::Login];
|
||||
// Advertise for prompt=create if password registration is enabled
|
||||
// TODO: we may want to be able to forward that to upstream providers if they
|
||||
// support it
|
||||
|
||||
@@ -23,7 +23,6 @@ use mas_storage::{Clock, RepositoryAccess};
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod authorization;
|
||||
pub mod consent;
|
||||
pub mod device;
|
||||
pub mod discovery;
|
||||
pub mod introspection;
|
||||
|
||||
@@ -978,10 +978,8 @@ mod tests {
|
||||
}),
|
||||
Some("state".to_owned()),
|
||||
Some("nonce".to_owned()),
|
||||
None,
|
||||
ResponseMode::Query,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
@@ -1079,10 +1077,8 @@ mod tests {
|
||||
}),
|
||||
Some("state".to_owned()),
|
||||
Some("nonce".to_owned()),
|
||||
None,
|
||||
ResponseMode::Query,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -8,7 +8,6 @@ pub mod app;
|
||||
pub mod index;
|
||||
pub mod login;
|
||||
pub mod logout;
|
||||
pub mod reauth;
|
||||
pub mod recovery;
|
||||
pub mod register;
|
||||
pub mod shared;
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{
|
||||
extract::{Form, Query, State},
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
use hyper::StatusCode;
|
||||
use mas_axum_utils::{
|
||||
FancyError, SessionInfoExt,
|
||||
cookies::CookieJar,
|
||||
csrf::{CsrfExt, ProtectedForm},
|
||||
};
|
||||
use mas_router::UrlBuilder;
|
||||
use mas_storage::{
|
||||
BoxClock, BoxRepository, BoxRng,
|
||||
user::{BrowserSessionRepository, UserPasswordRepository},
|
||||
};
|
||||
use mas_templates::{ReauthContext, TemplateContext, Templates};
|
||||
use serde::Deserialize;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use super::shared::OptionalPostAuthAction;
|
||||
use crate::{
|
||||
BoundActivityTracker, PreferredLanguage, SiteConfig,
|
||||
passwords::PasswordManager,
|
||||
session::{SessionOrFallback, load_session_or_fallback},
|
||||
};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub(crate) struct ReauthForm {
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "handlers.views.reauth.get", skip_all, err)]
|
||||
pub(crate) async fn get(
|
||||
mut rng: BoxRng,
|
||||
clock: BoxClock,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
State(url_builder): State<UrlBuilder>,
|
||||
State(site_config): State<SiteConfig>,
|
||||
activity_tracker: BoundActivityTracker,
|
||||
mut repo: BoxRepository,
|
||||
Query(query): Query<OptionalPostAuthAction>,
|
||||
cookie_jar: CookieJar,
|
||||
) -> Result<Response, FancyError> {
|
||||
if !site_config.password_login_enabled {
|
||||
// XXX: do something better here
|
||||
return Ok(url_builder
|
||||
.redirect(&mas_router::Account::default())
|
||||
.into_response());
|
||||
}
|
||||
|
||||
let (cookie_jar, maybe_session) = match load_session_or_fallback(
|
||||
cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
SessionOrFallback::MaybeSession {
|
||||
cookie_jar,
|
||||
maybe_session,
|
||||
..
|
||||
} => (cookie_jar, maybe_session),
|
||||
SessionOrFallback::Fallback { response } => return Ok(response),
|
||||
};
|
||||
|
||||
let Some(session) = maybe_session else {
|
||||
// If there is no session, redirect to the login screen, keeping the
|
||||
// PostAuthAction
|
||||
let login = mas_router::Login::from(query.post_auth_action);
|
||||
return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
|
||||
};
|
||||
|
||||
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
|
||||
|
||||
activity_tracker
|
||||
.record_browser_session(&clock, &session)
|
||||
.await;
|
||||
|
||||
let ctx = ReauthContext::default();
|
||||
let next = query.load_context(&mut repo).await?;
|
||||
let ctx = if let Some(next) = next {
|
||||
ctx.with_post_action(next)
|
||||
} else {
|
||||
ctx
|
||||
};
|
||||
let ctx = ctx
|
||||
.with_session(session)
|
||||
.with_csrf(csrf_token.form_value())
|
||||
.with_language(locale);
|
||||
|
||||
let content = templates.render_reauth(&ctx)?;
|
||||
|
||||
Ok((cookie_jar, Html(content)).into_response())
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "handlers.views.reauth.post", skip_all, err)]
|
||||
pub(crate) async fn post(
|
||||
mut rng: BoxRng,
|
||||
clock: BoxClock,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
State(password_manager): State<PasswordManager>,
|
||||
State(url_builder): State<UrlBuilder>,
|
||||
State(site_config): State<SiteConfig>,
|
||||
mut repo: BoxRepository,
|
||||
Query(query): Query<OptionalPostAuthAction>,
|
||||
cookie_jar: CookieJar,
|
||||
Form(form): Form<ProtectedForm<ReauthForm>>,
|
||||
) -> Result<Response, FancyError> {
|
||||
if !site_config.password_login_enabled {
|
||||
// XXX: do something better here
|
||||
return Ok(StatusCode::METHOD_NOT_ALLOWED.into_response());
|
||||
}
|
||||
|
||||
let form = cookie_jar.verify_form(&clock, form)?;
|
||||
|
||||
let (cookie_jar, maybe_session) = match load_session_or_fallback(
|
||||
cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
SessionOrFallback::MaybeSession {
|
||||
cookie_jar,
|
||||
maybe_session,
|
||||
..
|
||||
} => (cookie_jar, maybe_session),
|
||||
SessionOrFallback::Fallback { response } => return Ok(response),
|
||||
};
|
||||
|
||||
let Some(session) = maybe_session else {
|
||||
// If there is no session, redirect to the login screen, keeping the
|
||||
// PostAuthAction
|
||||
let login = mas_router::Login::from(query.post_auth_action);
|
||||
return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
|
||||
};
|
||||
|
||||
// Load the user password
|
||||
let user_password = repo
|
||||
.user_password()
|
||||
.active(&session.user)
|
||||
.await?
|
||||
.context("User has no password")?;
|
||||
|
||||
let password = Zeroizing::new(form.password.as_bytes().to_vec());
|
||||
|
||||
// TODO: recover from errors
|
||||
// Verify the password, and upgrade it on-the-fly if needed
|
||||
let new_password_hash = password_manager
|
||||
.verify_and_upgrade(
|
||||
&mut rng,
|
||||
user_password.version,
|
||||
password,
|
||||
user_password.hashed_password.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let user_password = if let Some((version, new_password_hash)) = new_password_hash {
|
||||
// Save the upgraded password
|
||||
repo.user_password()
|
||||
.add(
|
||||
&mut rng,
|
||||
&clock,
|
||||
&session.user,
|
||||
version,
|
||||
new_password_hash,
|
||||
Some(&user_password),
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
user_password
|
||||
};
|
||||
|
||||
// Mark the session as authenticated by the password
|
||||
repo.browser_session()
|
||||
.authenticate_with_password(&mut rng, &clock, &session, &user_password)
|
||||
.await?;
|
||||
|
||||
let cookie_jar = cookie_jar.set_session(&session);
|
||||
repo.save().await?;
|
||||
|
||||
let reply = query.go_next(&url_builder);
|
||||
Ok((cookie_jar, reply).into_response())
|
||||
}
|
||||
@@ -60,9 +60,7 @@ impl PostAuthAction {
|
||||
|
||||
pub fn go_next(&self, url_builder: &UrlBuilder) -> axum::response::Redirect {
|
||||
match self {
|
||||
Self::ContinueAuthorizationGrant { id } => {
|
||||
url_builder.redirect(&ContinueAuthorizationGrant(*id))
|
||||
}
|
||||
Self::ContinueAuthorizationGrant { id } => url_builder.redirect(&Consent(*id)),
|
||||
Self::ContinueDeviceCodeGrant { id } => {
|
||||
url_builder.redirect(&DeviceCodeConsent::new(*id))
|
||||
}
|
||||
@@ -255,66 +253,6 @@ impl SimpleRoute for Logout {
|
||||
const PATH: &'static str = "/logout";
|
||||
}
|
||||
|
||||
/// `GET|POST /reauth`
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct Reauth {
|
||||
post_auth_action: Option<PostAuthAction>,
|
||||
}
|
||||
|
||||
impl Reauth {
|
||||
#[must_use]
|
||||
pub fn and_then(action: PostAuthAction) -> Self {
|
||||
Self {
|
||||
post_auth_action: Some(action),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn and_continue_grant(data: Ulid) -> Self {
|
||||
Self {
|
||||
post_auth_action: Some(PostAuthAction::continue_grant(data)),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn and_continue_device_code_grant(data: Ulid) -> Self {
|
||||
Self {
|
||||
post_auth_action: Some(PostAuthAction::continue_device_code_grant(data)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the reauth's post auth action.
|
||||
#[must_use]
|
||||
pub fn post_auth_action(&self) -> Option<&PostAuthAction> {
|
||||
self.post_auth_action.as_ref()
|
||||
}
|
||||
|
||||
pub fn go_next(&self, url_builder: &UrlBuilder) -> axum::response::Redirect {
|
||||
match &self.post_auth_action {
|
||||
Some(action) => action.go_next(url_builder),
|
||||
None => url_builder.redirect(&Index),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Route for Reauth {
|
||||
type Query = PostAuthAction;
|
||||
|
||||
fn route() -> &'static str {
|
||||
"/reauth"
|
||||
}
|
||||
|
||||
fn query(&self) -> Option<&Self::Query> {
|
||||
self.post_auth_action.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<PostAuthAction>> for Reauth {
|
||||
fn from(post_auth_action: Option<PostAuthAction>) -> Self {
|
||||
Self { post_auth_action }
|
||||
}
|
||||
}
|
||||
|
||||
/// `POST /register`
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct Register {
|
||||
@@ -581,21 +519,6 @@ impl SimpleRoute for AccountPasswordChange {
|
||||
const PATH: &'static str = "/account/password/change";
|
||||
}
|
||||
|
||||
/// `GET /authorize/{grant_id}`
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContinueAuthorizationGrant(pub Ulid);
|
||||
|
||||
impl Route for ContinueAuthorizationGrant {
|
||||
type Query = ();
|
||||
fn route() -> &'static str {
|
||||
"/authorize/{grant_id}"
|
||||
}
|
||||
|
||||
fn path(&self) -> std::borrow::Cow<'static, str> {
|
||||
format!("/authorize/{}", self.0).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// `GET /consent/{grant_id}`
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Consent(pub Ulid);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT EXISTS(\n SELECT 1 FROM users WHERE username = $1\n ) AS \"exists!\"\n ",
|
||||
"query": "\n SELECT EXISTS(\n SELECT 1 FROM users WHERE LOWER(username) = LOWER($1)\n ) AS \"exists!\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -18,5 +18,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "94fd96446b237c87bd6bf741f3c42b37ee751b87b7fcc459602bdf8c46962443"
|
||||
"hash": "7f8335cc94347bc3a15afe7051658659347a1bf71dd62335df046708f19c967e"
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO oauth2_authorization_grants (\n oauth2_authorization_grant_id,\n oauth2_client_id,\n redirect_uri,\n scope,\n state,\n nonce,\n max_age,\n response_mode,\n code_challenge,\n code_challenge_method,\n response_type_code,\n response_type_id_token,\n authorization_code,\n requires_consent,\n login_hint,\n created_at\n )\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Int4",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "854cc8cd3c1fc3dbbdf4ce81b561aafadb0f4e98caeaba01597c6f62875ae691"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT oauth2_authorization_grant_id\n , created_at\n , cancelled_at\n , fulfilled_at\n , exchanged_at\n , scope\n , state\n , redirect_uri\n , response_mode\n , nonce\n , max_age\n , oauth2_client_id\n , authorization_code\n , response_type_code\n , response_type_id_token\n , code_challenge\n , code_challenge_method\n , requires_consent\n , login_hint\n , oauth2_session_id\n FROM\n oauth2_authorization_grants\n\n WHERE authorization_code = $1\n ",
|
||||
"query": "\n SELECT oauth2_authorization_grant_id\n , created_at\n , cancelled_at\n , fulfilled_at\n , exchanged_at\n , scope\n , state\n , redirect_uri\n , response_mode\n , nonce\n , oauth2_client_id\n , authorization_code\n , response_type_code\n , response_type_id_token\n , code_challenge\n , code_challenge_method\n , login_hint\n , oauth2_session_id\n FROM\n oauth2_authorization_grants\n\n WHERE authorization_code = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -55,51 +55,41 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "max_age",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "oauth2_client_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"ordinal": 11,
|
||||
"name": "authorization_code",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"ordinal": 12,
|
||||
"name": "response_type_code",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"ordinal": 13,
|
||||
"name": "response_type_id_token",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"ordinal": 14,
|
||||
"name": "code_challenge",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"ordinal": 15,
|
||||
"name": "code_challenge_method",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "requires_consent",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"ordinal": 16,
|
||||
"name": "login_hint",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"ordinal": 17,
|
||||
"name": "oauth2_session_id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
@@ -120,17 +110,15 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "1d9c478c7a5e3a672610376a290b9a1afaaa6fa2fb137f7307002f058b206dbd"
|
||||
"hash": "890516adeeaf2d6a4fe83a69e42e18b8817d1bb511e08ee761a031fa12d27251"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT scope_token\n FROM oauth2_consents\n WHERE user_id = $1 AND oauth2_client_id = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "scope_token",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "8b7297c263336d70c2b647212b16f7ae39bc5cb1572e3a2dcfcd67f196a1fa39"
|
||||
}
|
||||
27
crates/storage-pg/.sqlx/query-96208afd8b81e00f2700b0ded40c1eb529d321d0aa543be70f86ef96d0d8ff28.json
generated
Normal file
27
crates/storage-pg/.sqlx/query-96208afd8b81e00f2700b0ded40c1eb529d321d0aa543be70f86ef96d0d8ff28.json
generated
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO oauth2_authorization_grants (\n oauth2_authorization_grant_id,\n oauth2_client_id,\n redirect_uri,\n scope,\n state,\n nonce,\n response_mode,\n code_challenge,\n code_challenge_method,\n response_type_code,\n response_type_id_token,\n authorization_code,\n login_hint,\n created_at\n )\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "96208afd8b81e00f2700b0ded40c1eb529d321d0aa543be70f86ef96d0d8ff28"
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO oauth2_consents\n (oauth2_consent_id, user_id, oauth2_client_id, scope_token, created_at)\n SELECT id, $2, $3, scope_token, $5 FROM UNNEST($1::uuid[], $4::text[]) u(id, scope_token)\n ON CONFLICT (user_id, oauth2_client_id, scope_token) DO UPDATE SET refreshed_at = $5\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"UuidArray",
|
||||
"Uuid",
|
||||
"Uuid",
|
||||
"TextArray",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9a6c197ff4ad80217262d48f8792ce7e16bc5df0677c7cd4ecb4fdbc5ee86395"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT oauth2_authorization_grant_id\n , created_at\n , cancelled_at\n , fulfilled_at\n , exchanged_at\n , scope\n , state\n , redirect_uri\n , response_mode\n , nonce\n , max_age\n , oauth2_client_id\n , authorization_code\n , response_type_code\n , response_type_id_token\n , code_challenge\n , code_challenge_method\n , requires_consent\n , login_hint\n , oauth2_session_id\n FROM\n oauth2_authorization_grants\n\n WHERE oauth2_authorization_grant_id = $1\n ",
|
||||
"query": "\n SELECT oauth2_authorization_grant_id\n , created_at\n , cancelled_at\n , fulfilled_at\n , exchanged_at\n , scope\n , state\n , redirect_uri\n , response_mode\n , nonce\n , oauth2_client_id\n , authorization_code\n , response_type_code\n , response_type_id_token\n , code_challenge\n , code_challenge_method\n , login_hint\n , oauth2_session_id\n FROM\n oauth2_authorization_grants\n\n WHERE oauth2_authorization_grant_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -55,51 +55,41 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "max_age",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "oauth2_client_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"ordinal": 11,
|
||||
"name": "authorization_code",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"ordinal": 12,
|
||||
"name": "response_type_code",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"ordinal": 13,
|
||||
"name": "response_type_id_token",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"ordinal": 14,
|
||||
"name": "code_challenge",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"ordinal": 15,
|
||||
"name": "code_challenge_method",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "requires_consent",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"ordinal": 16,
|
||||
"name": "login_hint",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"ordinal": 17,
|
||||
"name": "oauth2_session_id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
@@ -120,17 +110,15 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "e0d3be7e741581430e3e4719c7e19596837234c94a398570bdac42652c2c4652"
|
||||
"hash": "bf6d1e3e3145438c988b1a47fc13f0168a63e278d8f8c947cb3ab65173f92ae4"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT user_id\n , username\n , created_at\n , locked_at\n , deactivated_at\n , can_request_admin\n FROM users\n WHERE username = $1\n ",
|
||||
"query": "\n SELECT user_id\n , username\n , created_at\n , locked_at\n , deactivated_at\n , can_request_admin\n FROM users\n WHERE LOWER(username) = LOWER($1)\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -48,5 +48,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "48213d718a256a12540c0aec595ca3e436be423f2d0c868700c6397745ed0455"
|
||||
"hash": "d2a4f5c01603463b78198529d295f7f121769ea5730d01c20c0ddbcdc79a5716"
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE oauth2_authorization_grants AS og\n SET\n requires_consent = 'f'\n WHERE\n og.oauth2_authorization_grant_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d83421d4a16f4ad084dd0db5abb56d3688851c36a48a50aa6104e8291e73630d"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
compat_access_tokens_session_fk
|
||||
ON compat_access_tokens (compat_session_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
compat_refresh_tokens_session_fk
|
||||
ON compat_refresh_tokens (compat_session_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
compat_refresh_tokens_access_token_fk
|
||||
ON compat_refresh_tokens (compat_access_token_id);
|
||||
@@ -0,0 +1,13 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
-- Including the `last_active_at` column lets us effeciently filter in-memory
|
||||
-- for those sessions without fetching the rows, and without including it in the
|
||||
-- index btree
|
||||
CREATE INDEX CONCURRENTLY
|
||||
compat_sessions_user_fk
|
||||
ON compat_sessions (user_id)
|
||||
INCLUDE (last_active_at);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
compat_sessions_user_session_fk
|
||||
ON compat_sessions (user_session_id);
|
||||
@@ -0,0 +1,8 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
-- Redundant with the `compat_sessions_user_fk`
|
||||
DROP INDEX IF EXISTS compat_sessions_user_id_last_active_at;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
compat_sso_logins_session_fk
|
||||
ON compat_sso_logins (compat_session_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
oauth2_access_tokens_session_fk
|
||||
ON oauth2_access_tokens (oauth2_session_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
oauth2_authorization_grants_session_fk
|
||||
ON oauth2_authorization_grants (oauth2_session_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
oauth2_authorization_grants_client_fk
|
||||
ON oauth2_authorization_grants (oauth2_client_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
oauth2_consents_client_fk
|
||||
ON oauth2_consents (oauth2_client_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
oauth2_consents_user_fk
|
||||
ON oauth2_consents (user_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
oauth2_device_code_grants_client_fk
|
||||
ON oauth2_device_code_grant (oauth2_client_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
oauth2_device_code_grants_session_fk
|
||||
ON oauth2_device_code_grant (oauth2_session_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
oauth2_device_code_grants_user_session_fk
|
||||
ON oauth2_device_code_grant (user_session_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
oauth2_refresh_tokens_session_fk
|
||||
ON oauth2_refresh_tokens (oauth2_session_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
oauth2_refresh_tokens_access_token_fk
|
||||
ON oauth2_refresh_tokens (oauth2_access_token_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
oauth2_refresh_tokens_next_refresh_token_fk
|
||||
ON oauth2_refresh_tokens (next_oauth2_refresh_token_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
oauth2_sessions_user_session_fk
|
||||
ON oauth2_sessions (user_session_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
oauth2_sessions_client_fk
|
||||
ON oauth2_sessions (oauth2_client_id);
|
||||
@@ -0,0 +1,13 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
-- Including the `last_active_at` column lets us effeciently filter in-memory
|
||||
-- for those sessions without fetching the rows, and without including it in the
|
||||
-- index btree
|
||||
CREATE INDEX CONCURRENTLY
|
||||
oauth2_sessions_user_fk
|
||||
ON oauth2_sessions (user_id)
|
||||
INCLUDE (last_active_at);
|
||||
@@ -0,0 +1,8 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
-- Redundant with the `oauth2_sessions_user_fk`
|
||||
DROP INDEX IF EXISTS oauth2_sessions_user_id_last_active_at;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
queue_jobs_started_by_fk
|
||||
ON queue_jobs (started_by);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
queue_jobs_next_attempt_fk
|
||||
ON queue_jobs (next_attempt_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
queue_jobs_schedule_name_fk
|
||||
ON queue_jobs (schedule_name);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
upstream_oauth_authorization_sessions_provider_fk
|
||||
ON upstream_oauth_authorization_sessions (upstream_oauth_provider_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
upstream_oauth_authorization_sessions_link_fk
|
||||
ON upstream_oauth_authorization_sessions (upstream_oauth_link_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
upstream_oauth_links_provider_fk
|
||||
ON upstream_oauth_links (upstream_oauth_provider_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
upstream_oauth_links_user_fk
|
||||
ON upstream_oauth_links (user_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
user_email_authentication_codes_authentication_fk
|
||||
ON user_email_authentication_codes (user_email_authentication_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
user_email_authentications_user_session_fk
|
||||
ON user_email_authentications (user_session_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
user_email_authentications_user_registration_fk
|
||||
ON user_email_authentications (user_registration_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
user_emails_user_fk
|
||||
ON user_emails (user_id);
|
||||
@@ -0,0 +1,10 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
-- This isn't a foreign key, but we really need that to be indexed
|
||||
CREATE INDEX CONCURRENTLY
|
||||
user_emails_email_idx
|
||||
ON user_emails (email);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
user_passwords_user_fk
|
||||
ON user_passwords (user_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
user_recovery_tickets_session_fk
|
||||
ON user_recovery_tickets (user_recovery_session_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
user_recovery_tickets_user_email_fk
|
||||
ON user_recovery_tickets (user_email_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
user_registrations_email_authentication_fk
|
||||
ON user_registrations (email_authentication_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
user_session_authentications_user_session_fk
|
||||
ON user_session_authentications (user_session_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
user_session_authentications_user_password_fk
|
||||
ON user_session_authentications (user_password_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
user_session_authentications_upstream_oauth_session_fk
|
||||
ON user_session_authentications (upstream_oauth_authorization_session_id);
|
||||
@@ -0,0 +1,13 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
-- Including the `last_active_at` column lets us effeciently filter in-memory
|
||||
-- for those sessions without fetching the rows, and without including it in the
|
||||
-- index btree
|
||||
CREATE INDEX CONCURRENTLY
|
||||
user_sessions_user_fk
|
||||
ON user_sessions (user_id)
|
||||
INCLUDE (last_active_at);
|
||||
@@ -0,0 +1,8 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
-- Redundant with the `user_sessions_user_fk`
|
||||
DROP INDEX IF EXISTS user_sessions_user_id_last_active_at;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY
|
||||
user_terms_user_fk
|
||||
ON user_terms (user_id);
|
||||
@@ -0,0 +1,11 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
-- We don't use this column anymore, but… it will still tank the performance on
|
||||
-- deletions of user_emails if we don't have it
|
||||
CREATE INDEX CONCURRENTLY
|
||||
users_primary_email_fk
|
||||
ON users (primary_user_email_id);
|
||||
@@ -0,0 +1,10 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
-- This isn't a foreign key, but we really need that to be indexed
|
||||
CREATE INDEX CONCURRENTLY
|
||||
user_recovery_tickets_ticket_idx
|
||||
ON user_recovery_tickets (ticket);
|
||||
@@ -0,0 +1,10 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
-- Create an index on the username column, lower-cased, so that we can lookup
|
||||
-- usernames in a case-insensitive manner.
|
||||
CREATE INDEX CONCURRENTLY users_lower_username_idx
|
||||
ON users (LOWER(username));
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
-- We stopped reading/writing to this column, but it's not nullable.
|
||||
-- So we need to add a default value, and drop it in the next release
|
||||
ALTER TABLE oauth2_authorization_grants
|
||||
ALTER COLUMN requires_consent SET DEFAULT false;
|
||||
@@ -4,8 +4,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use mas_data_model::{
|
||||
@@ -48,13 +46,11 @@ struct GrantLookup {
|
||||
nonce: Option<String>,
|
||||
redirect_uri: String,
|
||||
response_mode: String,
|
||||
max_age: Option<i32>,
|
||||
response_type_code: bool,
|
||||
response_type_id_token: bool,
|
||||
authorization_code: Option<String>,
|
||||
code_challenge: Option<String>,
|
||||
code_challenge_method: Option<String>,
|
||||
requires_consent: bool,
|
||||
login_hint: Option<String>,
|
||||
oauth2_client_id: Uuid,
|
||||
oauth2_session_id: Option<Uuid>,
|
||||
@@ -153,25 +149,6 @@ impl TryFrom<GrantLookup> for AuthorizationGrant {
|
||||
.source(e)
|
||||
})?;
|
||||
|
||||
let max_age = value
|
||||
.max_age
|
||||
.map(u32::try_from)
|
||||
.transpose()
|
||||
.map_err(|e| {
|
||||
DatabaseInconsistencyError::on("oauth2_authorization_grants")
|
||||
.column("max_age")
|
||||
.row(id)
|
||||
.source(e)
|
||||
})?
|
||||
.map(NonZeroU32::try_from)
|
||||
.transpose()
|
||||
.map_err(|e| {
|
||||
DatabaseInconsistencyError::on("oauth2_authorization_grants")
|
||||
.column("max_age")
|
||||
.row(id)
|
||||
.source(e)
|
||||
})?;
|
||||
|
||||
Ok(AuthorizationGrant {
|
||||
id,
|
||||
stage,
|
||||
@@ -180,12 +157,10 @@ impl TryFrom<GrantLookup> for AuthorizationGrant {
|
||||
scope,
|
||||
state: value.state,
|
||||
nonce: value.nonce,
|
||||
max_age,
|
||||
response_mode,
|
||||
redirect_uri,
|
||||
created_at: value.created_at,
|
||||
response_type_id_token: value.response_type_id_token,
|
||||
requires_consent: value.requires_consent,
|
||||
login_hint: value.login_hint,
|
||||
})
|
||||
}
|
||||
@@ -216,10 +191,8 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository
|
||||
code: Option<AuthorizationCode>,
|
||||
state: Option<String>,
|
||||
nonce: Option<String>,
|
||||
max_age: Option<NonZeroU32>,
|
||||
response_mode: ResponseMode,
|
||||
response_type_id_token: bool,
|
||||
requires_consent: bool,
|
||||
login_hint: Option<String>,
|
||||
) -> Result<AuthorizationGrant, Self::Error> {
|
||||
let code_challenge = code
|
||||
@@ -230,8 +203,6 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository
|
||||
.as_ref()
|
||||
.and_then(|c| c.pkce.as_ref())
|
||||
.map(|p| p.challenge_method.to_string());
|
||||
// TODO: this conversion is a bit ugly
|
||||
let max_age_i32 = max_age.map(|x| i32::try_from(u32::from(x)).unwrap_or(i32::MAX));
|
||||
let code_str = code.as_ref().map(|c| &c.code);
|
||||
|
||||
let created_at = clock.now();
|
||||
@@ -247,19 +218,17 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository
|
||||
scope,
|
||||
state,
|
||||
nonce,
|
||||
max_age,
|
||||
response_mode,
|
||||
code_challenge,
|
||||
code_challenge_method,
|
||||
response_type_code,
|
||||
response_type_id_token,
|
||||
authorization_code,
|
||||
requires_consent,
|
||||
login_hint,
|
||||
created_at
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||
"#,
|
||||
Uuid::from(id),
|
||||
Uuid::from(client.id),
|
||||
@@ -267,14 +236,12 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository
|
||||
scope.to_string(),
|
||||
state,
|
||||
nonce,
|
||||
max_age_i32,
|
||||
response_mode.to_string(),
|
||||
code_challenge,
|
||||
code_challenge_method,
|
||||
code.is_some(),
|
||||
response_type_id_token,
|
||||
code_str,
|
||||
requires_consent,
|
||||
login_hint,
|
||||
created_at,
|
||||
)
|
||||
@@ -291,11 +258,9 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository
|
||||
scope,
|
||||
state,
|
||||
nonce,
|
||||
max_age,
|
||||
response_mode,
|
||||
created_at,
|
||||
response_type_id_token,
|
||||
requires_consent,
|
||||
login_hint,
|
||||
})
|
||||
}
|
||||
@@ -323,14 +288,12 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository
|
||||
, redirect_uri
|
||||
, response_mode
|
||||
, nonce
|
||||
, max_age
|
||||
, oauth2_client_id
|
||||
, authorization_code
|
||||
, response_type_code
|
||||
, response_type_id_token
|
||||
, code_challenge
|
||||
, code_challenge_method
|
||||
, requires_consent
|
||||
, login_hint
|
||||
, oauth2_session_id
|
||||
FROM
|
||||
@@ -374,14 +337,12 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository
|
||||
, redirect_uri
|
||||
, response_mode
|
||||
, nonce
|
||||
, max_age
|
||||
, oauth2_client_id
|
||||
, authorization_code
|
||||
, response_type_code
|
||||
, response_type_id_token
|
||||
, code_challenge
|
||||
, code_challenge_method
|
||||
, requires_consent
|
||||
, login_hint
|
||||
, oauth2_session_id
|
||||
FROM
|
||||
@@ -480,37 +441,4 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository
|
||||
|
||||
Ok(grant)
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "db.oauth2_authorization_grant.give_consent",
|
||||
skip_all,
|
||||
fields(
|
||||
db.query.text,
|
||||
%grant.id,
|
||||
client.id = %grant.client_id,
|
||||
),
|
||||
err,
|
||||
)]
|
||||
async fn give_consent(
|
||||
&mut self,
|
||||
mut grant: AuthorizationGrant,
|
||||
) -> Result<AuthorizationGrant, Self::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE oauth2_authorization_grants AS og
|
||||
SET
|
||||
requires_consent = 'f'
|
||||
WHERE
|
||||
og.oauth2_authorization_grant_id = $1
|
||||
"#,
|
||||
Uuid::from(grant.id),
|
||||
)
|
||||
.traced()
|
||||
.execute(&mut *self.conn)
|
||||
.await?;
|
||||
|
||||
grant.requires_consent = false;
|
||||
|
||||
Ok(grant)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,20 +6,15 @@
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
str::FromStr,
|
||||
string::ToString,
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use mas_data_model::{Client, JwksOrJwksUri, User};
|
||||
use mas_data_model::{Client, JwksOrJwksUri};
|
||||
use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod};
|
||||
use mas_jose::jwk::PublicJsonWebKeySet;
|
||||
use mas_storage::{Clock, oauth2::OAuth2ClientRepository};
|
||||
use oauth2_types::{
|
||||
oidc::ApplicationType,
|
||||
requests::GrantType,
|
||||
scope::{Scope, ScopeToken},
|
||||
};
|
||||
use oauth2_types::{oidc::ApplicationType, requests::GrantType};
|
||||
use opentelemetry_semantic_conventions::attribute::DB_QUERY_TEXT;
|
||||
use rand::RngCore;
|
||||
use sqlx::PgConnection;
|
||||
@@ -698,97 +693,6 @@ impl OAuth2ClientRepository for PgOAuth2ClientRepository<'_> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "db.oauth2_client.get_consent_for_user",
|
||||
skip_all,
|
||||
fields(
|
||||
db.query.text,
|
||||
%user.id,
|
||||
%client.id,
|
||||
),
|
||||
err,
|
||||
)]
|
||||
async fn get_consent_for_user(
|
||||
&mut self,
|
||||
client: &Client,
|
||||
user: &User,
|
||||
) -> Result<Scope, Self::Error> {
|
||||
let scope_tokens: Vec<String> = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT scope_token
|
||||
FROM oauth2_consents
|
||||
WHERE user_id = $1 AND oauth2_client_id = $2
|
||||
"#,
|
||||
Uuid::from(user.id),
|
||||
Uuid::from(client.id),
|
||||
)
|
||||
.fetch_all(&mut *self.conn)
|
||||
.await?;
|
||||
|
||||
let scope: Result<Scope, _> = scope_tokens
|
||||
.into_iter()
|
||||
.map(|s| ScopeToken::from_str(&s))
|
||||
.collect();
|
||||
|
||||
let scope = scope.map_err(|e| {
|
||||
DatabaseInconsistencyError::on("oauth2_consents")
|
||||
.column("scope_token")
|
||||
.source(e)
|
||||
})?;
|
||||
|
||||
Ok(scope)
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "db.oauth2_client.give_consent_for_user",
|
||||
skip_all,
|
||||
fields(
|
||||
db.query.text,
|
||||
%user.id,
|
||||
%client.id,
|
||||
%scope,
|
||||
),
|
||||
err,
|
||||
)]
|
||||
async fn give_consent_for_user(
|
||||
&mut self,
|
||||
rng: &mut (dyn RngCore + Send),
|
||||
clock: &dyn Clock,
|
||||
client: &Client,
|
||||
user: &User,
|
||||
scope: &Scope,
|
||||
) -> Result<(), Self::Error> {
|
||||
let now = clock.now();
|
||||
let (tokens, ids): (Vec<String>, Vec<Uuid>) = scope
|
||||
.iter()
|
||||
.map(|token| {
|
||||
(
|
||||
token.to_string(),
|
||||
Uuid::from(Ulid::from_datetime_with_source(now.into(), rng)),
|
||||
)
|
||||
})
|
||||
.unzip();
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO oauth2_consents
|
||||
(oauth2_consent_id, user_id, oauth2_client_id, scope_token, created_at)
|
||||
SELECT id, $2, $3, scope_token, $5 FROM UNNEST($1::uuid[], $4::text[]) u(id, scope_token)
|
||||
ON CONFLICT (user_id, oauth2_client_id, scope_token) DO UPDATE SET refreshed_at = $5
|
||||
"#,
|
||||
&ids,
|
||||
Uuid::from(user.id),
|
||||
Uuid::from(client.id),
|
||||
&tokens,
|
||||
now,
|
||||
)
|
||||
.traced()
|
||||
.execute(&mut *self.conn)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "db.oauth2_client.delete_by_id",
|
||||
skip_all,
|
||||
|
||||
@@ -135,10 +135,8 @@ mod tests {
|
||||
}),
|
||||
Some("state".to_owned()),
|
||||
Some("nonce".to_owned()),
|
||||
None,
|
||||
ResponseMode::Query,
|
||||
true,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
@@ -175,29 +173,6 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Lookup the consent the user gave to the client
|
||||
let consent = repo
|
||||
.oauth2_client()
|
||||
.get_consent_for_user(&client, &user)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(consent.is_empty());
|
||||
|
||||
// Give consent to the client
|
||||
let scope = Scope::from_iter([OPENID]);
|
||||
repo.oauth2_client()
|
||||
.give_consent_for_user(&mut rng, &clock, &client, &user, &scope)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Lookup the consent the user gave to the client
|
||||
let consent = repo
|
||||
.oauth2_client()
|
||||
.get_consent_for_user(&client, &user)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(scope, consent);
|
||||
|
||||
// Lookup a non-existing session
|
||||
let session = repo.oauth2_session().lookup(Ulid::nil()).await.unwrap();
|
||||
assert_eq!(session, None);
|
||||
|
||||
@@ -165,6 +165,9 @@ impl UserRepository for PgUserRepository<'_> {
|
||||
err,
|
||||
)]
|
||||
async fn find_by_username(&mut self, username: &str) -> Result<Option<User>, Self::Error> {
|
||||
// We may have multiple users with the same username, but with a different
|
||||
// casing. In this case, we want to return the one which matches the exact
|
||||
// casing
|
||||
let res = sqlx::query_as!(
|
||||
UserLookup,
|
||||
r#"
|
||||
@@ -175,17 +178,30 @@ impl UserRepository for PgUserRepository<'_> {
|
||||
, deactivated_at
|
||||
, can_request_admin
|
||||
FROM users
|
||||
WHERE username = $1
|
||||
WHERE LOWER(username) = LOWER($1)
|
||||
"#,
|
||||
username,
|
||||
)
|
||||
.traced()
|
||||
.fetch_optional(&mut *self.conn)
|
||||
.fetch_all(&mut *self.conn)
|
||||
.await?;
|
||||
|
||||
let Some(res) = res else { return Ok(None) };
|
||||
|
||||
Ok(Some(res.into()))
|
||||
match &res[..] {
|
||||
// Happy path: there is only one user matching the username…
|
||||
[user] => Ok(Some(user.clone().into())),
|
||||
// …or none.
|
||||
[] => Ok(None),
|
||||
list => {
|
||||
// If there are multiple users with the same username, we want to
|
||||
// return the one which matches the exact casing
|
||||
if let Some(user) = list.iter().find(|user| user.username == username) {
|
||||
Ok(Some(user.clone().into()))
|
||||
} else {
|
||||
// If none match exactly, we prefer to return nothing
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
@@ -250,7 +266,7 @@ impl UserRepository for PgUserRepository<'_> {
|
||||
let exists = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM users WHERE username = $1
|
||||
SELECT 1 FROM users WHERE LOWER(username) = LOWER($1)
|
||||
) AS "exists!"
|
||||
"#,
|
||||
username
|
||||
|
||||
@@ -216,6 +216,50 @@ async fn test_user_repo(pool: PgPool) {
|
||||
repo.save().await.unwrap();
|
||||
}
|
||||
|
||||
/// Test [`UserRepository::find_by_username`] with different casings.
|
||||
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||
async fn test_user_repo_find_by_username(pool: PgPool) {
|
||||
let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
|
||||
let mut rng = ChaChaRng::seed_from_u64(42);
|
||||
let clock = MockClock::default();
|
||||
|
||||
let alice = repo
|
||||
.user()
|
||||
.add(&mut rng, &clock, "Alice".to_owned())
|
||||
.await
|
||||
.unwrap();
|
||||
let bob1 = repo
|
||||
.user()
|
||||
.add(&mut rng, &clock, "Bob".to_owned())
|
||||
.await
|
||||
.unwrap();
|
||||
let bob2 = repo
|
||||
.user()
|
||||
.add(&mut rng, &clock, "BOB".to_owned())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// This is fine, we can do a case-insensitive search
|
||||
assert_eq!(
|
||||
repo.user().find_by_username("alice").await.unwrap(),
|
||||
Some(alice)
|
||||
);
|
||||
|
||||
// In case there are multiple users with the same username, we should return the
|
||||
// one that matches the exact casing
|
||||
assert_eq!(
|
||||
repo.user().find_by_username("Bob").await.unwrap(),
|
||||
Some(bob1)
|
||||
);
|
||||
assert_eq!(
|
||||
repo.user().find_by_username("BOB").await.unwrap(),
|
||||
Some(bob2)
|
||||
);
|
||||
|
||||
// If none match, we should return None
|
||||
assert!(repo.user().find_by_username("bob").await.unwrap().is_none());
|
||||
}
|
||||
|
||||
/// Test the user email repository, by trying out most of its methods
|
||||
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||
async fn test_user_email_repo(pool: PgPool) {
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use mas_data_model::{AuthorizationCode, AuthorizationGrant, Client, Session};
|
||||
use oauth2_types::{requests::ResponseMode, scope::Scope};
|
||||
@@ -37,12 +35,9 @@ pub trait OAuth2AuthorizationGrantRepository: Send + Sync {
|
||||
/// `response_type` was requested
|
||||
/// * `state`: The state the client sent, if set
|
||||
/// * `nonce`: The nonce the client sent, if set
|
||||
/// * `max_age`: The maximum age since the user last authenticated, if asked
|
||||
/// by the client
|
||||
/// * `response_mode`: The response mode the client requested
|
||||
/// * `response_type_id_token`: Whether the `id_token` `response_type` was
|
||||
/// requested
|
||||
/// * `requires_consent`: Whether the client explicitly requested consent
|
||||
/// * `login_hint`: The login_hint the client sent, if set
|
||||
///
|
||||
/// # Errors
|
||||
@@ -59,10 +54,8 @@ pub trait OAuth2AuthorizationGrantRepository: Send + Sync {
|
||||
code: Option<AuthorizationCode>,
|
||||
state: Option<String>,
|
||||
nonce: Option<String>,
|
||||
max_age: Option<NonZeroU32>,
|
||||
response_mode: ResponseMode,
|
||||
response_type_id_token: bool,
|
||||
requires_consent: bool,
|
||||
login_hint: Option<String>,
|
||||
) -> Result<AuthorizationGrant, Self::Error>;
|
||||
|
||||
@@ -131,22 +124,6 @@ pub trait OAuth2AuthorizationGrantRepository: Send + Sync {
|
||||
clock: &dyn Clock,
|
||||
authorization_grant: AuthorizationGrant,
|
||||
) -> Result<AuthorizationGrant, Self::Error>;
|
||||
|
||||
/// Unset the `requires_consent` flag on an authorization grant
|
||||
///
|
||||
/// Returns the updated authorization grant
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `authorization_grant`: The authorization grant to update
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Self::Error`] if the underlying repository fails
|
||||
async fn give_consent(
|
||||
&mut self,
|
||||
authorization_grant: AuthorizationGrant,
|
||||
) -> Result<AuthorizationGrant, Self::Error>;
|
||||
}
|
||||
|
||||
repository_impl!(OAuth2AuthorizationGrantRepository:
|
||||
@@ -160,10 +137,8 @@ repository_impl!(OAuth2AuthorizationGrantRepository:
|
||||
code: Option<AuthorizationCode>,
|
||||
state: Option<String>,
|
||||
nonce: Option<String>,
|
||||
max_age: Option<NonZeroU32>,
|
||||
response_mode: ResponseMode,
|
||||
response_type_id_token: bool,
|
||||
requires_consent: bool,
|
||||
login_hint: Option<String>,
|
||||
) -> Result<AuthorizationGrant, Self::Error>;
|
||||
|
||||
@@ -184,9 +159,4 @@ repository_impl!(OAuth2AuthorizationGrantRepository:
|
||||
clock: &dyn Clock,
|
||||
authorization_grant: AuthorizationGrant,
|
||||
) -> Result<AuthorizationGrant, Self::Error>;
|
||||
|
||||
async fn give_consent(
|
||||
&mut self,
|
||||
authorization_grant: AuthorizationGrant,
|
||||
) -> Result<AuthorizationGrant, Self::Error>;
|
||||
);
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use mas_data_model::{Client, User};
|
||||
use mas_data_model::Client;
|
||||
use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod};
|
||||
use mas_jose::jwk::PublicJsonWebKeySet;
|
||||
use oauth2_types::{oidc::ApplicationType, requests::GrantType, scope::Scope};
|
||||
use oauth2_types::{oidc::ApplicationType, requests::GrantType};
|
||||
use rand_core::RngCore;
|
||||
use ulid::Ulid;
|
||||
use url::Url;
|
||||
@@ -171,45 +171,6 @@ pub trait OAuth2ClientRepository: Send + Sync {
|
||||
/// Returns [`Self::Error`] if the underlying repository fails
|
||||
async fn all_static(&mut self) -> Result<Vec<Client>, Self::Error>;
|
||||
|
||||
/// Get the list of scopes that the user has given consent for the given
|
||||
/// client
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `client`: The client to get the consent for
|
||||
/// * `user`: The user to get the consent for
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Self::Error`] if the underlying repository fails
|
||||
async fn get_consent_for_user(
|
||||
&mut self,
|
||||
client: &Client,
|
||||
user: &User,
|
||||
) -> Result<Scope, Self::Error>;
|
||||
|
||||
/// Give consent for a set of scopes for the given client and user
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `rng`: The random number generator to use
|
||||
/// * `clock`: The clock used to generate timestamps
|
||||
/// * `client`: The client to give the consent for
|
||||
/// * `user`: The user to give the consent for
|
||||
/// * `scope`: The scope to give consent for
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Self::Error`] if the underlying repository fails
|
||||
async fn give_consent_for_user(
|
||||
&mut self,
|
||||
rng: &mut (dyn RngCore + Send),
|
||||
clock: &dyn Clock,
|
||||
client: &Client,
|
||||
user: &User,
|
||||
scope: &Scope,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Delete a client
|
||||
///
|
||||
/// # Parameters
|
||||
@@ -288,19 +249,4 @@ repository_impl!(OAuth2ClientRepository:
|
||||
async fn delete(&mut self, client: Client) -> Result<(), Self::Error>;
|
||||
|
||||
async fn delete_by_id(&mut self, id: Ulid) -> Result<(), Self::Error>;
|
||||
|
||||
async fn get_consent_for_user(
|
||||
&mut self,
|
||||
client: &Client,
|
||||
user: &User,
|
||||
) -> Result<Scope, Self::Error>;
|
||||
|
||||
async fn give_consent_for_user(
|
||||
&mut self,
|
||||
rng: &mut (dyn RngCore + Send),
|
||||
clock: &dyn Clock,
|
||||
client: &Client,
|
||||
user: &User,
|
||||
scope: &Scope,
|
||||
) -> Result<(), Self::Error>;
|
||||
);
|
||||
|
||||
@@ -155,7 +155,7 @@ pub trait UserRepository: Send + Sync {
|
||||
/// Returns [`Self::Error`] if the underlying repository fails
|
||||
async fn lookup(&mut self, id: Ulid) -> Result<Option<User>, Self::Error>;
|
||||
|
||||
/// Find a [`User`] by its username
|
||||
/// Find a [`User`] by its username, in a case-insensitive manner
|
||||
///
|
||||
/// Returns `None` if no [`User`] was found
|
||||
///
|
||||
|
||||
@@ -381,7 +381,7 @@ impl FormField for LoginFormField {
|
||||
}
|
||||
}
|
||||
|
||||
/// Inner context used in login and reauth screens. See [`PostAuthContext`].
|
||||
/// Inner context used in login screen. See [`PostAuthContext`].
|
||||
#[derive(Serialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum PostAuthContextInner {
|
||||
@@ -420,7 +420,7 @@ pub enum PostAuthContextInner {
|
||||
ManageAccount,
|
||||
}
|
||||
|
||||
/// Context used in login and reauth screens, for the post-auth action to do
|
||||
/// Context used in login screen, for the post-auth action to do
|
||||
#[derive(Serialize)]
|
||||
pub struct PostAuthContext {
|
||||
/// The post auth action params from the URL
|
||||
@@ -734,59 +734,6 @@ impl PolicyViolationContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fields of the reauthentication form
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum ReauthFormField {
|
||||
/// The password field
|
||||
Password,
|
||||
}
|
||||
|
||||
impl FormField for ReauthFormField {
|
||||
fn keep(&self) -> bool {
|
||||
match self {
|
||||
Self::Password => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Context used by the `reauth.html` template
|
||||
#[derive(Serialize, Default)]
|
||||
pub struct ReauthContext {
|
||||
form: FormState<ReauthFormField>,
|
||||
next: Option<PostAuthContext>,
|
||||
}
|
||||
|
||||
impl TemplateContext for ReauthContext {
|
||||
fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
// TODO: samples with errors
|
||||
vec![ReauthContext {
|
||||
form: FormState::default(),
|
||||
next: None,
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
impl ReauthContext {
|
||||
/// Add an error on the reauthentication form
|
||||
#[must_use]
|
||||
pub fn with_form_state(self, form: FormState<ReauthFormField>) -> Self {
|
||||
Self { form, ..self }
|
||||
}
|
||||
|
||||
/// Add a post authentication action to the context
|
||||
#[must_use]
|
||||
pub fn with_post_action(self, next: PostAuthContext) -> Self {
|
||||
Self {
|
||||
next: Some(next),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Context used by the `sso.html` template
|
||||
#[derive(Serialize)]
|
||||
pub struct CompatSsoContext {
|
||||
|
||||
@@ -38,10 +38,10 @@ pub use self::{
|
||||
DeviceConsentContext, DeviceLinkContext, DeviceLinkFormField, EmailRecoveryContext,
|
||||
EmailVerificationContext, EmptyContext, ErrorContext, FormPostContext, IndexContext,
|
||||
LoginContext, LoginFormField, NotFoundContext, PasswordRegisterContext,
|
||||
PolicyViolationContext, PostAuthContext, PostAuthContextInner, ReauthContext,
|
||||
ReauthFormField, RecoveryExpiredContext, RecoveryFinishContext, RecoveryFinishFormField,
|
||||
RecoveryProgressContext, RecoveryStartContext, RecoveryStartFormField, RegisterContext,
|
||||
RegisterFormField, RegisterStepsDisplayNameContext, RegisterStepsDisplayNameFormField,
|
||||
PolicyViolationContext, PostAuthContext, PostAuthContextInner, RecoveryExpiredContext,
|
||||
RecoveryFinishContext, RecoveryFinishFormField, RecoveryProgressContext,
|
||||
RecoveryStartContext, RecoveryStartFormField, RegisterContext, RegisterFormField,
|
||||
RegisterStepsDisplayNameContext, RegisterStepsDisplayNameFormField,
|
||||
RegisterStepsEmailInUseContext, RegisterStepsVerifyEmailContext,
|
||||
RegisterStepsVerifyEmailFormField, SiteBranding, SiteConfigExt, SiteFeatures,
|
||||
TemplateContext, UpstreamExistingLinkContext, UpstreamRegister, UpstreamRegisterFormField,
|
||||
@@ -372,9 +372,6 @@ register_templates! {
|
||||
/// Render the account recovery disabled page
|
||||
pub fn render_recovery_disabled(WithLanguage<EmptyContext>) { "pages/recovery/disabled.html" }
|
||||
|
||||
/// Render the re-authentication form
|
||||
pub fn render_reauth(WithLanguage<WithCsrf<WithSession<ReauthContext>>>) { "pages/reauth.html" }
|
||||
|
||||
/// Render the form used by the form_post response mode
|
||||
pub fn render_form_post<T: Serialize>(WithLanguage<FormPostContext<T>>) { "form_post.html" }
|
||||
|
||||
@@ -456,7 +453,6 @@ impl Templates {
|
||||
check::render_recovery_expired(self, now, rng)?;
|
||||
check::render_recovery_consumed(self, now, rng)?;
|
||||
check::render_recovery_disabled(self, now, rng)?;
|
||||
check::render_reauth(self, now, rng)?;
|
||||
check::render_form_post::<EmptyContext>(self, now, rng)?;
|
||||
check::render_error(self, now, rng)?;
|
||||
check::render_email_verification_txt(self, now, rng)?;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* - Please do NOT serve this file on production.
|
||||
*/
|
||||
|
||||
const PACKAGE_VERSION = '2.7.3'
|
||||
const PACKAGE_VERSION = '2.7.4'
|
||||
const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f'
|
||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||
const activeClientIds = new Set()
|
||||
|
||||
@@ -87,8 +87,8 @@
|
||||
"title": "Vigane e-posti aadress"
|
||||
},
|
||||
"email_invalid_error": "Lisatud e-posti aadress on vigane",
|
||||
"incorrect_password_error": "Vale parool, proovige uuesti",
|
||||
"password_confirmation": "Selle e-posti aadressi lisamiseks kinnitage oma konto salasõnaga"
|
||||
"incorrect_password_error": "Vale salasõna, palun proovi uuesti",
|
||||
"password_confirmation": "Selle e-posti aadressi lisamiseks kinnita tegevus oma kasutajakonto salasõnaga"
|
||||
},
|
||||
"app_sessions_list": {
|
||||
"error": "Rakenduse sessioonide laadimine ei õnnestunud",
|
||||
@@ -327,8 +327,8 @@
|
||||
"delete_button_confirmation_modal": {
|
||||
"action": "Kustuta e-posti aadress",
|
||||
"body": "Kas kustutame selle e-posti aadressi?",
|
||||
"incorrect_password": "Vale parool, proovige uuesti",
|
||||
"password_confirmation": "Selle e-posti aadressi kustutamiseks kinnitage parooliga"
|
||||
"incorrect_password": "Vale salasõna, palun proovi uuesti",
|
||||
"password_confirmation": "Selle e-posti aadressi kustutamiseks kinnita tegevus oma salasõnaga"
|
||||
},
|
||||
"delete_button_title": "Eemalda e-posti aadress",
|
||||
"email": "E-posti aadress",
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
},
|
||||
"branding": {
|
||||
"privacy_policy": {
|
||||
"alt": "Посилання на політику конфіденційності сервісу",
|
||||
"link": "Політика конфіденційності"
|
||||
"alt": "Посилання на політику приватності служби",
|
||||
"link": "Політика приватності"
|
||||
},
|
||||
"terms_and_conditions": {
|
||||
"alt": "Посилання на умови надання послуг",
|
||||
"link": "Умови використання"
|
||||
"link": "Умови та положення"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
@@ -43,7 +43,7 @@
|
||||
"alert_description": "Цей обліковий запис буде стерто назавжди, і ви більше не матимете доступу до всіх своїх повідомлень.",
|
||||
"alert_title": "Ви можете втратити всі свої дані",
|
||||
"button": "Видалити обліковий запис",
|
||||
"dialog_description": "<text>Підтвердьте, що хочете видалити свій обліковий запис:</text>\n<profile />\n<list>\n<item>Ви не зможете повторно активувати свій обліковий запис</item>\n<item>Ви більше не зможете ввійти </item>\n<item>Ніхто не зможе повторно використовувати ваше ім'я користувача (MXID), включно з вами</item>\n<item>Ви вийдете з усіх кімнат та особистих розмов, в яких ви перебуваєте</item>\n<item>Вас буде вилучено з сервера ідентифікації, і ніхто не зможе знайти вас за вашою електронною поштою або номером телефону</item>\n</list>\n<text>Ваші старі повідомлення все одно будуть видимі людям, які їх отримали. Чи хотіли б ви сховати свої надіслані повідомлення від людей, які приєднуються до кімнат у майбутньому?</text>",
|
||||
"dialog_description": "<text>Підтвердьте, що хочете видалити свій обліковий запис:</text>\n<profile />\n<list>\n<item>Ви не зможете повторно активувати свій обліковий запис</item>\n<item>Ви більше не зможете ввійти </item>\n<item>Ніхто не зможе повторно використовувати ваше ім'я користувача (MXID), включно з вами</item>\n<item>Ви вийдете з усіх кімнат та особистих розмов, в яких ви перебуваєте</item>\n<item>Вас буде вилучено з сервера ідентифікації, і ніхто не зможе знайти вас за вашою електронною поштою або номером телефону</item>\n</list>\n<text>Ваші старі повідомлення все одно будуть видимі людям, які їх отримали. Чи хотіли б ви сховати свої надіслані повідомлення від людей, які приєднаються до кімнат у майбутньому?</text>",
|
||||
"dialog_title": "Видалити цей обліковий запис?",
|
||||
"erase_checkbox_label": "Так, сховати всі мої повідомлення від нових учасників",
|
||||
"incorrect_password": "Пароль неправильний, повторіть спробу",
|
||||
@@ -132,7 +132,7 @@
|
||||
"error": {
|
||||
"hideDetails": "Сховати подробиці",
|
||||
"showDetails": "Показати подробиці",
|
||||
"subtitle": "Сталася неочікувана помилка. Будь ласка спробуйте ще раз.",
|
||||
"subtitle": "Сталася неочікувана помилка. Повторіть спробу.",
|
||||
"title": "Щось пішло не так"
|
||||
},
|
||||
"error_boundary_title": "Щось пішло не так",
|
||||
@@ -155,7 +155,7 @@
|
||||
"not_logged_in_alert": "Ви не ввійшли в систему.",
|
||||
"oauth2_client_detail": {
|
||||
"details_title": "Інформація про клієнт",
|
||||
"id": "Ідентифікатор клієнта",
|
||||
"id": "ID клієнта",
|
||||
"name": "Ім'я",
|
||||
"policy": "Політика",
|
||||
"terms": "Умови надання послуг"
|
||||
@@ -173,8 +173,8 @@
|
||||
"failure": {
|
||||
"description": {
|
||||
"account_locked": "Ваш обліковий запис заблокований і не може бути відновлений на цей час. Якщо цього не очікується, зверніться до адміністратора сервера.",
|
||||
"expired_recovery_ticket": "Термін дії посилання для відновлення закінчився. Будь ласка, почніть процес відновлення облікового запису спочатку.",
|
||||
"invalid_new_password": "Вибраний вами новий пароль недійсний; він може не відповідати налаштованій політиці безпеки.",
|
||||
"expired_recovery_ticket": "Посилання для відновлення застаріло. Розпочніть процес відновлення облікового запису спочатку.",
|
||||
"invalid_new_password": "Обраний вами новий пароль неприпустимий; він може не відповідати налаштованій політиці безпеки.",
|
||||
"no_current_password": "У вас немає поточного пароля.",
|
||||
"no_such_recovery_ticket": "Посилання для відновлення недійсне. Якщо ви скопіювали посилання з електронної пошти для відновлення, перевірте, чи скопійовано повне посилання.",
|
||||
"password_changes_disabled": "Зміна пароля вимкнена.",
|
||||
@@ -203,7 +203,7 @@
|
||||
"expired": {
|
||||
"resend_email": "Повторно надіслати електронний лист",
|
||||
"subtitle": "Запит на новий електронний лист, який буде надіслано на адресу: {{email}}",
|
||||
"title": "Термін дії посилання для скидання пароля закінчився"
|
||||
"title": "Посилання для скидання пароля застаріло"
|
||||
},
|
||||
"subtitle": "Виберіть новий пароль для свого облікового запису.",
|
||||
"title": "Скидання пароля"
|
||||
@@ -218,15 +218,15 @@
|
||||
"4": "Дуже надійний пароль"
|
||||
},
|
||||
"suggestion": {
|
||||
"all_uppercase": "Використайте великі літери, але не всі.",
|
||||
"another_word": "Додайте більше слів, які є менш поширеними.",
|
||||
"all_uppercase": "Використайте великі букви, але не всі.",
|
||||
"another_word": "Додайте більше менш вживаних слів.",
|
||||
"associated_years": "Уникайте років, які пов'язані з вами.",
|
||||
"capitalization": "Використайте більше великих літер, не тільки першу.",
|
||||
"capitalization": "Використайте більше великих букв, не лише першу.",
|
||||
"dates": "Уникайте дат і років, які пов'язані з вами.",
|
||||
"l33t": "Уникайте передбачуваних замін букв, таких як «@» замість «a».",
|
||||
"longer_keyboard_pattern": "Використовуйте довші патерни клавіатури та змінюйте напрямок друку кілька разів.",
|
||||
"no_need": "Ви можете створювати надійні паролі без використання символів, цифр або великих літер.",
|
||||
"pwned": "Якщо ви використовуєте цей пароль деінде, вам слід змінити його.",
|
||||
"no_need": "Ви можете створювати надійні паролі не вживаючи символів, цифр або великих букв.",
|
||||
"pwned": "Якщо ви використовуєте цей пароль ще десь, вам слід змінити його.",
|
||||
"recent_years": "Уникайте останніх років.",
|
||||
"repeated": "Уникайте повторювання слів і символів.",
|
||||
"reverse_words": "Уникайте зворотного написання звичайних слів.",
|
||||
@@ -241,7 +241,7 @@
|
||||
"extended_repeat": "Повторювані шаблони символів, такі як \"abcabcabc\", легко вгадати.",
|
||||
"key_pattern": "Короткі послідовності клавіш легко вгадати.",
|
||||
"names_by_themselves": "Поодинокі імена або прізвища легко вгадати.",
|
||||
"pwned": "Ваш пароль було розкрито внаслідок витоку даних в Інтернеті.",
|
||||
"pwned": "Ваш пароль розкрито внаслідок витоку даних в інтернеті.",
|
||||
"recent_years": "Пароль із нещодавніми роками легко вгадати.",
|
||||
"sequences": "Поширені послідовності символів, такі як «abc», легко вгадати.",
|
||||
"similar_to_common": "Це схоже на часто використовуваний пароль.",
|
||||
@@ -249,7 +249,7 @@
|
||||
"straight_row": "Прямі послідовності клавіш на клавіатурі легко вгадати.",
|
||||
"top_hundred": "Це часто використовуваний пароль.",
|
||||
"top_ten": "Це широко використовуваний пароль.",
|
||||
"user_inputs": "Не повинно бути ніяких особистих даних або даних, пов'язаних зі сторінкою.",
|
||||
"user_inputs": "Не повинно бути жодних особистих даних або даних, пов'язаних зі сторінкою.",
|
||||
"word_by_itself": "Окремі слова легко вгадати."
|
||||
}
|
||||
},
|
||||
@@ -263,9 +263,9 @@
|
||||
"description": "Якщо ви не ввійшли в обліковий запис на інших пристроях і втратили ключ відновлення, вам потрібно буде скинути свою ідентичність, щоб продовжити користуватися застосунком.",
|
||||
"effect_list": {
|
||||
"negative_1": "Ви втратите наявну історію повідомлень",
|
||||
"negative_2": "Вам потрібно буде знову підтвердити всі наявні пристрої та контакти",
|
||||
"negative_2": "Вам потрібно буде знову верифікувати всі наявні пристрої та контакти",
|
||||
"neutral_1": "Ви втратите історію повідомлень, яка зберігається лише на сервері",
|
||||
"neutral_2": "Вам потрібно буде знову підтвердити всі наявні пристрої та контакти",
|
||||
"neutral_2": "Вам потрібно буде знову верифікувати всі наявні пристрої та контакти",
|
||||
"positive_1": "Ваші дані облікового запису, контакти, налаштування та список бесід будуть збережені"
|
||||
},
|
||||
"failure": {
|
||||
@@ -277,7 +277,7 @@
|
||||
"heading": "Скиньте свій обліковий запис, якщо не можете підтвердити його іншим способом",
|
||||
"start_reset": "Почати скидання",
|
||||
"success": {
|
||||
"description": "Скидання профілю було схвалено на наступні {{minutes}} хвилин. Ви можете закрити це вікно та повернутися до застосунку, щоб продовжити.",
|
||||
"description": "Скидання профілю схвалено на наступні {{minutes}} хвилин. Ви можете закрити це вікно та повернутися до застосунку, щоб продовжити.",
|
||||
"heading": "Облікові дані успішно скинуто. Поверніться до застосунку, щоб завершити процес.",
|
||||
"title": "Скидання криптоідентичності тимчасово дозволено"
|
||||
},
|
||||
@@ -287,7 +287,7 @@
|
||||
"label": "Вибрати сеанс"
|
||||
},
|
||||
"session": {
|
||||
"client_id_label": "Ідентифікатор клієнта",
|
||||
"client_id_label": "ID клієнта",
|
||||
"current": "Поточний",
|
||||
"current_badge": "Поточний",
|
||||
"device_id_label": "ID пристрою",
|
||||
@@ -306,7 +306,7 @@
|
||||
"unknown_browser": "Невідомий браузер",
|
||||
"unknown_device": "Невідомий пристрій",
|
||||
"uri_label": "Uri",
|
||||
"user_id_label": "Ідентифікатор користувача",
|
||||
"user_id_label": "ID користувача",
|
||||
"username_label": "Ім'я користувача"
|
||||
},
|
||||
"session_detail": {
|
||||
@@ -318,7 +318,7 @@
|
||||
},
|
||||
"unknown_route": "Невідомий роут {{route}}",
|
||||
"unverified_email_alert": {
|
||||
"button": "Перегляньте та перевірте",
|
||||
"button": "Переглянути та підтвердити",
|
||||
"text:one": "У вас є {{count}} непідтверджена адреса електронної пошти.",
|
||||
"text:few": "У вас є {{count}} непідтверджені адреси електронної пошти.",
|
||||
"text:many": "У вас є {{count}} непідтверджених адрес електронної пошти.",
|
||||
@@ -363,7 +363,7 @@
|
||||
"verify_email": {
|
||||
"code_expired_alert": {
|
||||
"description": "Термін дії коду закінчився. Будь ласка, надішліть запит на новий код.",
|
||||
"title": "Термін дії коду закінчився"
|
||||
"title": "Код застарів"
|
||||
},
|
||||
"code_field_error": "Код не розпізнано",
|
||||
"code_field_label": "6-значний код",
|
||||
|
||||
116
frontend/package-lock.json
generated
116
frontend/package-lock.json
generated
@@ -12,8 +12,8 @@
|
||||
"@fontsource/inter": "^5.2.5",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@tanstack/react-query": "^5.72.2",
|
||||
"@tanstack/react-router": "^1.115.2",
|
||||
"@tanstack/react-query": "^5.74.3",
|
||||
"@tanstack/react-router": "^1.116.0",
|
||||
"@vector-im/compound-design-tokens": "4.0.1",
|
||||
"@vector-im/compound-web": "^7.10.1",
|
||||
"@zxcvbn-ts/core": "^3.0.4",
|
||||
@@ -42,14 +42,14 @@
|
||||
"@storybook/react": "^8.6.12",
|
||||
"@storybook/react-vite": "^8.6.12",
|
||||
"@storybook/test": "^8.5.5",
|
||||
"@tanstack/react-query-devtools": "^5.72.2",
|
||||
"@tanstack/react-router-devtools": "^1.115.2",
|
||||
"@tanstack/router-plugin": "^1.115.2",
|
||||
"@tanstack/react-query-devtools": "^5.74.3",
|
||||
"@tanstack/react-router-devtools": "^1.116.0",
|
||||
"@tanstack/router-plugin": "^1.116.1",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/react": "19.1.0",
|
||||
"@types/node": "^22.14.1",
|
||||
"@types/react": "19.1.1",
|
||||
"@types/react-dom": "19.1.2",
|
||||
"@types/swagger-ui-dist": "^3.30.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
@@ -60,7 +60,7 @@
|
||||
"happy-dom": "^17.4.4",
|
||||
"i18next-parser": "^9.3.0",
|
||||
"knip": "^5.50.2",
|
||||
"msw": "^2.7.3",
|
||||
"msw": "^2.7.4",
|
||||
"msw-storybook-addon": "^2.0.4",
|
||||
"postcss": "^8.5.3",
|
||||
"postcss-import": "^16.1.0",
|
||||
@@ -5303,9 +5303,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.72.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.72.2.tgz",
|
||||
"integrity": "sha512-fxl9/0yk3mD/FwTmVEf1/H6N5B975H0luT+icKyX566w6uJG0x6o+Yl+I38wJRCaogiMkstByt+seXfDbWDAcA==",
|
||||
"version": "5.74.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.74.3.tgz",
|
||||
"integrity": "sha512-Mqk+5o3qTuAiZML248XpNH8r2cOzl15+LTbUsZQEwvSvn1GU4VQhvqzAbil36p+MBxpr/58oBSnRzhrBevDhfg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -5313,9 +5313,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-devtools": {
|
||||
"version": "5.72.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.72.2.tgz",
|
||||
"integrity": "sha512-mMKnGb+iOhVBcj6jaerCFRpg8pACStdG8hmUBHPtToeZzs4ctjBUL1FajqpVn2WaMxnq8Wya+P3Q5tPFNM9jQw==",
|
||||
"version": "5.73.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.73.3.tgz",
|
||||
"integrity": "sha512-hBQyYwsOuO7QOprK75NzfrWs/EQYjgFA0yykmcvsV62q0t6Ua97CU3sYgjHx0ZvxkXSOMkY24VRJ5uv9f5Ik4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -5324,12 +5324,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.72.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.72.2.tgz",
|
||||
"integrity": "sha512-SVNHzyBUYiis+XiCl+8yiPZmMYei2AKYY94wM/zpvB5l1jxqOo82FQTziSJ4pBi96jtYqvYrTMxWynmbQh3XKw==",
|
||||
"version": "5.74.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.74.3.tgz",
|
||||
"integrity": "sha512-QrycUn0wxjVPzITvQvOxFRdhlAwIoOQSuav7qWD4SWCoKCdLbyRZ2vji2GuBq/glaxbF4wBx3fqcYRDOt8KDTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.72.2"
|
||||
"@tanstack/query-core": "5.74.3"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -5340,32 +5340,32 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query-devtools": {
|
||||
"version": "5.72.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.72.2.tgz",
|
||||
"integrity": "sha512-n53qr9JdHCJTCUba6OvMhwiV2CcsckngOswKEE7nM5pQBa/fW9c43qw8omw1RPT2s+aC7MuwS8fHsWT8g+j6IQ==",
|
||||
"version": "5.74.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.74.3.tgz",
|
||||
"integrity": "sha512-H7TsOBB1fRCuuawrBzKMoIszqqILr2IN5oGLYMl7QG7ERJpMdc4hH8OwzBhVxJnmKeGwgtTQgcdKepfoJCWvFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-devtools": "5.72.2"
|
||||
"@tanstack/query-devtools": "5.73.3"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tanstack/react-query": "^5.72.2",
|
||||
"@tanstack/react-query": "^5.74.3",
|
||||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-router": {
|
||||
"version": "1.115.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.115.2.tgz",
|
||||
"integrity": "sha512-KWRtoDp1odMUUd0m7utTot3NsAxfb/W8UlPG5omtS0TCl8F+ErwurjS6Qn7rKg7q0CF8KcFDvhhBC1cXnOpoSQ==",
|
||||
"version": "1.116.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.116.0.tgz",
|
||||
"integrity": "sha512-ZBAg5Q6zJf0mnP9DYPiaaQ/wLDH2ujCMi/2RllpH86VUkdkyvQQzpAyKoiYJ891wh9OPgj6W6tPrzB4qy5FpRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/history": "1.115.0",
|
||||
"@tanstack/react-store": "^0.7.0",
|
||||
"@tanstack/router-core": "1.115.0",
|
||||
"@tanstack/router-core": "1.115.3",
|
||||
"jsesc": "^3.1.0",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"tiny-warning": "^1.0.3"
|
||||
@@ -5383,13 +5383,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-router-devtools": {
|
||||
"version": "1.115.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-router-devtools/-/react-router-devtools-1.115.2.tgz",
|
||||
"integrity": "sha512-g8lK8MXj9Mv0QKUNNC6QooUn9KJXcRZFQ0JiWUZNxeluTww43JFZ37zmD3fQugWRPOrcX9UaaJCjMaO/b+Sb6g==",
|
||||
"version": "1.116.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-router-devtools/-/react-router-devtools-1.116.0.tgz",
|
||||
"integrity": "sha512-PsJZWPjcmwZGe71kUvH4bI1ozkv1FgBuBEE0hTYlTCSJ3uG+qv3ndGEI+AiFyuF5OStrbfg0otW1OxeNq5vdGQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/router-devtools-core": "^1.115.0",
|
||||
"@tanstack/router-devtools-core": "^1.115.3",
|
||||
"solid-js": "^1.9.5"
|
||||
},
|
||||
"engines": {
|
||||
@@ -5400,7 +5400,7 @@
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tanstack/react-router": "^1.115.2",
|
||||
"@tanstack/react-router": "^1.116.0",
|
||||
"react": ">=18.0.0 || >=19.0.0",
|
||||
"react-dom": ">=18.0.0 || >=19.0.0"
|
||||
}
|
||||
@@ -5424,9 +5424,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/router-core": {
|
||||
"version": "1.115.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.115.0.tgz",
|
||||
"integrity": "sha512-5XgesPkppANSnR3lrzakjx5+Vx1q4azI1t+kG2ZFvcLG8iRiJ564bDB1W3X2PZQgfKD78jDO/uWAcJTHH4sXuw==",
|
||||
"version": "1.115.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.115.3.tgz",
|
||||
"integrity": "sha512-gynHs72LHVg05fuJTwZZYhDL4VNEAK0sXz7IqiBv7a3qsYeEmIZsGaFr9sVjTkuF1kbrFBdJd5JYutzBh9Uuhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/history": "1.115.0",
|
||||
@@ -5442,9 +5442,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/router-devtools-core": {
|
||||
"version": "1.115.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-devtools-core/-/router-devtools-core-1.115.0.tgz",
|
||||
"integrity": "sha512-s46V8bWxp4fWWjjDm7aGIYw8uPDXu8l1HkwGJwxkf1OQn1MdE7KRIVhGs/GM3Hp2KptQe4Gjomr7r1xrajuMhA==",
|
||||
"version": "1.115.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-devtools-core/-/router-devtools-core-1.115.3.tgz",
|
||||
"integrity": "sha512-VBdgw1qxeOD/6FlZ9gitrWPUKGW83CuAW31gf32E0dxL7sIXP+yEFyPlNsVlENan1oSaEuV8tjKkuq5s4MfaPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -5459,7 +5459,7 @@
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tanstack/router-core": "^1.115.0",
|
||||
"@tanstack/router-core": "^1.115.3",
|
||||
"csstype": "^3.0.10",
|
||||
"solid-js": ">=1.9.5",
|
||||
"tiny-invariant": "^1.3.3"
|
||||
@@ -5471,9 +5471,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/router-generator": {
|
||||
"version": "1.115.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.115.2.tgz",
|
||||
"integrity": "sha512-T77B6MnEdCPU9QFhjX/bhzaHKlKSo6n2MkIc78WrsnZ0Zx/zTtbzsGiLYyFZQ0tvB4/eazRrBh6YYY3qRwkGhg==",
|
||||
"version": "1.116.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.116.0.tgz",
|
||||
"integrity": "sha512-XhCp85zP87G2bpSXnosiP3fiMo8HMQD2mvWqFFTFKz87WocabQYGlfhmNYWmBwI50EuS7Ph9lwXsSkV0oKh0xw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -5490,7 +5490,7 @@
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tanstack/react-router": "^1.115.2"
|
||||
"@tanstack/react-router": "^1.116.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@tanstack/react-router": {
|
||||
@@ -5499,9 +5499,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/router-plugin": {
|
||||
"version": "1.115.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.115.2.tgz",
|
||||
"integrity": "sha512-81poBAU55nauRPddjbtRzGZwPy0/+SXIn6yRUXlMBQhnpMNlnsWbMyigV/iNm5F7SEUOI2u2Q79bt5Fvk2FNbA==",
|
||||
"version": "1.116.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.116.1.tgz",
|
||||
"integrity": "sha512-9A8DAyRejTzvkVOzgVPUY6l2aH7xOMEXSJJtV9GNbi4NtE6AXUCoFe3mtvYnHSzRqAUMCO0wnfVENCjXQoQYZw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -5511,8 +5511,8 @@
|
||||
"@babel/template": "^7.26.8",
|
||||
"@babel/traverse": "^7.26.8",
|
||||
"@babel/types": "^7.26.8",
|
||||
"@tanstack/router-core": "^1.115.0",
|
||||
"@tanstack/router-generator": "^1.115.2",
|
||||
"@tanstack/router-core": "^1.115.3",
|
||||
"@tanstack/router-generator": "^1.116.0",
|
||||
"@tanstack/router-utils": "^1.115.0",
|
||||
"@tanstack/virtual-file-routes": "^1.115.0",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
@@ -5532,7 +5532,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rsbuild/core": ">=1.0.2",
|
||||
"@tanstack/react-router": "^1.115.2",
|
||||
"@tanstack/react-router": "^1.116.0",
|
||||
"vite": ">=5.0.0 || >=6.0.0",
|
||||
"vite-plugin-solid": "^2.11.2",
|
||||
"webpack": ">=5.92.0"
|
||||
@@ -5819,9 +5819,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz",
|
||||
"integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==",
|
||||
"version": "22.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
|
||||
"integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -5829,9 +5829,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w==",
|
||||
"version": "19.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.1.tgz",
|
||||
"integrity": "sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -10528,9 +10528,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/msw": {
|
||||
"version": "2.7.3",
|
||||
"resolved": "https://registry.npmjs.org/msw/-/msw-2.7.3.tgz",
|
||||
"integrity": "sha512-+mycXv8l2fEAjFZ5sjrtjJDmm2ceKGjrNbBr1durRg6VkU9fNUE/gsmQ51hWbHqs+l35W1iM+ZsmOD9Fd6lspw==",
|
||||
"version": "2.7.4",
|
||||
"resolved": "https://registry.npmjs.org/msw/-/msw-2.7.4.tgz",
|
||||
"integrity": "sha512-A2kuMopOjAjNEYkn0AnB1uj+x7oBjLIunFk7Ud4icEnVWFf6iBekn8oXW4zIwcpfEdWP9sLqyVaHVzneWoGEww==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
"@fontsource/inter": "^5.2.5",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@tanstack/react-query": "^5.72.2",
|
||||
"@tanstack/react-router": "^1.115.2",
|
||||
"@tanstack/react-query": "^5.74.3",
|
||||
"@tanstack/react-router": "^1.116.0",
|
||||
"@vector-im/compound-design-tokens": "4.0.1",
|
||||
"@vector-im/compound-web": "^7.10.1",
|
||||
"@zxcvbn-ts/core": "^3.0.4",
|
||||
@@ -52,14 +52,14 @@
|
||||
"@storybook/react": "^8.6.12",
|
||||
"@storybook/react-vite": "^8.6.12",
|
||||
"@storybook/test": "^8.5.5",
|
||||
"@tanstack/react-query-devtools": "^5.72.2",
|
||||
"@tanstack/react-router-devtools": "^1.115.2",
|
||||
"@tanstack/router-plugin": "^1.115.2",
|
||||
"@tanstack/react-query-devtools": "^5.74.3",
|
||||
"@tanstack/react-router-devtools": "^1.116.0",
|
||||
"@tanstack/router-plugin": "^1.116.1",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/react": "19.1.0",
|
||||
"@types/node": "^22.14.1",
|
||||
"@types/react": "19.1.1",
|
||||
"@types/react-dom": "19.1.2",
|
||||
"@types/swagger-ui-dist": "^3.30.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
@@ -70,7 +70,7 @@
|
||||
"happy-dom": "^17.4.4",
|
||||
"i18next-parser": "^9.3.0",
|
||||
"knip": "^5.50.2",
|
||||
"msw": "^2.7.3",
|
||||
"msw": "^2.7.4",
|
||||
"msw-storybook-addon": "^2.0.4",
|
||||
"postcss": "^8.5.3",
|
||||
"postcss-import": "^16.1.0",
|
||||
|
||||
10
tools/syn2mas/package-lock.json
generated
10
tools/syn2mas/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@vector-im/syn2mas",
|
||||
"version": "0.14.1",
|
||||
"version": "0.15.0-rc.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@vector-im/syn2mas",
|
||||
"version": "0.14.1",
|
||||
"version": "0.15.0-rc.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"command-line-args": "^6.0.0",
|
||||
@@ -687,9 +687,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz",
|
||||
"integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==",
|
||||
"version": "22.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
|
||||
"integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vector-im/syn2mas",
|
||||
"version": "0.14.1",
|
||||
"version": "0.15.0-rc.0",
|
||||
"description": "A tool to migrate Synapse users and sessions to the Matrix Authentication Service",
|
||||
"license": "AGPL-3.0-only",
|
||||
"author": "Matrix.org",
|
||||
|
||||
@@ -151,7 +151,8 @@
|
||||
"headline": "Přihlaste se k odkazu"
|
||||
},
|
||||
"no_login_methods": "Nejsou k dispozici žádné metody přihlášení.",
|
||||
"separator": "Nebo"
|
||||
"separator": "Nebo",
|
||||
"username_or_email": "Uživatelské jméno nebo e-mail"
|
||||
},
|
||||
"navbar": {
|
||||
"my_account": "Můj účet",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user