mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-08-25 02:36:06 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7926c798c6 | ||
|
|
d5ed420dd7 | ||
|
|
92e609d367 | ||
|
|
546d342136 | ||
|
|
c680f3c419 | ||
|
|
806cb4b8c5 |
+52
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n d.controller_did,\n u.handle as \"handle?\",\n d.granted_scopes,\n d.granted_at,\n true as \"is_active!\",\n u.did IS NOT NULL as \"is_local!\"\n FROM account_delegations d\n LEFT JOIN users u ON u.did = d.controller_did\n WHERE d.delegated_did = $1\n AND d.revoked_at IS NULL\n AND (u.did IS NULL OR (u.deactivated_at IS NULL AND u.takedown_ref IS NULL))\n ORDER BY d.granted_at DESC\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "controller_did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "handle?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "granted_scopes",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "granted_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "is_active!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "is_local!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "d8e646324c93b375cceccea533ddd880225931f29ce4d8c5184197fecce25fa7"
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n d.controller_did,\n u.handle as \"handle?\",\n d.granted_scopes,\n d.granted_at,\n CASE WHEN u.did IS NOT NULL\n THEN u.deactivated_at IS NULL AND u.takedown_ref IS NULL\n ELSE true\n END as \"is_active!\",\n u.did IS NOT NULL as \"is_local!\"\n FROM account_delegations d\n LEFT JOIN users u ON u.did = d.controller_did\n WHERE d.delegated_did = $1 AND d.revoked_at IS NULL\n ORDER BY d.granted_at DESC\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "controller_did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "handle?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "granted_scopes",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "granted_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "is_active!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "is_local!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "dd6021dd12823e042b011b2c1507736a46c0dcf0eb94cb41de58f8ca0b3a2f08"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT COUNT(*) as \"count!\"\n FROM account_delegations d\n LEFT JOIN users u ON u.did = d.controller_did\n WHERE d.delegated_did = $1\n AND d.revoked_at IS NULL\n AND (u.did IS NULL OR (u.deactivated_at IS NULL AND u.takedown_ref IS NULL))\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "ff2ffeb1ea1c1375ff0edc4c9ce2f3cdeb92e2b0a72405ea0441b0340b177b96"
|
||||
}
|
||||
Generated
+142
-19
@@ -6092,9 +6092,57 @@ dependencies = [
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-api"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"backon",
|
||||
"base32",
|
||||
"base64 0.22.1",
|
||||
"bcrypt",
|
||||
"bs58",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"cid",
|
||||
"ed25519-dalek",
|
||||
"futures",
|
||||
"hex",
|
||||
"http 1.4.0",
|
||||
"infer",
|
||||
"ipld-core",
|
||||
"jacquard-common",
|
||||
"jacquard-repo",
|
||||
"k256",
|
||||
"multibase",
|
||||
"multihash",
|
||||
"rand 0.8.5",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_ipld_dagcbor",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"subtle",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tranquil-config",
|
||||
"tranquil-db",
|
||||
"tranquil-db-traits",
|
||||
"tranquil-lexicon",
|
||||
"tranquil-pds",
|
||||
"tranquil-scopes",
|
||||
"tranquil-types",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
"webauthn-rs",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-auth"
|
||||
version = "0.3.1"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base32",
|
||||
@@ -6117,7 +6165,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-cache"
|
||||
version = "0.3.1"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
@@ -6131,7 +6179,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-comms"
|
||||
version = "0.3.1"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
@@ -6146,7 +6194,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-config"
|
||||
version = "0.3.1"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"confique",
|
||||
"serde",
|
||||
@@ -6154,7 +6202,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-crypto"
|
||||
version = "0.3.1"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"base64 0.22.1",
|
||||
@@ -6170,7 +6218,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-db"
|
||||
version = "0.3.1"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -6187,7 +6235,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-db-traits"
|
||||
version = "0.3.1"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
@@ -6203,7 +6251,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-infra"
|
||||
version = "0.3.1"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
@@ -6214,7 +6262,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-lexicon"
|
||||
version = "0.3.1"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"hickory-resolver",
|
||||
@@ -6232,7 +6280,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-oauth"
|
||||
version = "0.3.1"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -6253,9 +6301,42 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-oauth-server"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
"bcrypt",
|
||||
"chrono",
|
||||
"cid",
|
||||
"hmac",
|
||||
"http 1.4.0",
|
||||
"jacquard-common",
|
||||
"jacquard-repo",
|
||||
"k256",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sha2",
|
||||
"subtle",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tranquil-api",
|
||||
"tranquil-config",
|
||||
"tranquil-crypto",
|
||||
"tranquil-db-traits",
|
||||
"tranquil-pds",
|
||||
"tranquil-types",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
"webauthn-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-pds"
|
||||
version = "0.3.1"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
@@ -6272,9 +6353,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"ciborium",
|
||||
"cid",
|
||||
"clap",
|
||||
"ctor",
|
||||
"dotenvy",
|
||||
"ed25519-dalek",
|
||||
"futures",
|
||||
"futures-util",
|
||||
@@ -6319,7 +6398,7 @@ dependencies = [
|
||||
"tower-http",
|
||||
"tower-layer",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tranquil-api",
|
||||
"tranquil-auth",
|
||||
"tranquil-cache",
|
||||
"tranquil-comms",
|
||||
@@ -6329,10 +6408,12 @@ dependencies = [
|
||||
"tranquil-db-traits",
|
||||
"tranquil-lexicon",
|
||||
"tranquil-oauth",
|
||||
"tranquil-oauth-server",
|
||||
"tranquil-repo",
|
||||
"tranquil-ripple",
|
||||
"tranquil-scopes",
|
||||
"tranquil-storage",
|
||||
"tranquil-sync",
|
||||
"tranquil-types",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
@@ -6343,7 +6424,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-repo"
|
||||
version = "0.3.1"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cid",
|
||||
@@ -6355,7 +6436,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-ripple"
|
||||
version = "0.3.1"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"backon",
|
||||
@@ -6380,7 +6461,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-scopes"
|
||||
version = "0.3.1"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"futures",
|
||||
@@ -6394,9 +6475,29 @@ dependencies = [
|
||||
"urlencoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-server"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"clap",
|
||||
"dotenvy",
|
||||
"ed25519-dalek",
|
||||
"hex",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tranquil-api",
|
||||
"tranquil-config",
|
||||
"tranquil-oauth-server",
|
||||
"tranquil-pds",
|
||||
"tranquil-sync",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-storage"
|
||||
version = "0.3.1"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"aws-config",
|
||||
@@ -6411,9 +6512,31 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-sync"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"cid",
|
||||
"futures",
|
||||
"ipld-core",
|
||||
"jacquard-repo",
|
||||
"serde",
|
||||
"serde_ipld_dagcbor",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tranquil-config",
|
||||
"tranquil-db-traits",
|
||||
"tranquil-pds",
|
||||
"tranquil-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tranquil-types"
|
||||
version = "0.3.1"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"cid",
|
||||
|
||||
+10
-1
@@ -16,11 +16,15 @@ members = [
|
||||
"crates/tranquil-db-traits",
|
||||
"crates/tranquil-db",
|
||||
"crates/tranquil-pds",
|
||||
"crates/tranquil-server",
|
||||
"crates/tranquil-sync",
|
||||
"crates/tranquil-oauth-server",
|
||||
"crates/tranquil-api",
|
||||
"crates/tranquil-lexicon",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.0"
|
||||
version = "0.4.3"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
@@ -40,6 +44,11 @@ tranquil-db-traits = { path = "crates/tranquil-db-traits" }
|
||||
tranquil-db = { path = "crates/tranquil-db" }
|
||||
tranquil-ripple = { path = "crates/tranquil-ripple" }
|
||||
tranquil-lexicon = { path = "crates/tranquil-lexicon" }
|
||||
tranquil-pds = { path = "crates/tranquil-pds" }
|
||||
tranquil-server = { path = "crates/tranquil-server" }
|
||||
tranquil-sync = { path = "crates/tranquil-sync" }
|
||||
tranquil-oauth-server = { path = "crates/tranquil-oauth-server" }
|
||||
tranquil-api = { path = "crates/tranquil-api" }
|
||||
|
||||
unicode-segmentation = "1"
|
||||
|
||||
|
||||
+25
-5
@@ -4,21 +4,41 @@ COPY frontend/ ./
|
||||
RUN deno task build
|
||||
|
||||
FROM rust:1.92-alpine AS builder
|
||||
RUN apk add --no-cache ca-certificates musl-dev pkgconfig openssl-dev openssl-libs-static
|
||||
RUN apk add --no-cache ca-certificates musl-dev pkgconfig openssl-dev openssl-libs-static mold clang
|
||||
ENV RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=mold"
|
||||
WORKDIR /app
|
||||
ARG SLIM="false"
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY crates ./crates
|
||||
COPY .sqlx ./.sqlx
|
||||
COPY crates/tranquil-types ./crates/tranquil-types
|
||||
COPY crates/tranquil-crypto ./crates/tranquil-crypto
|
||||
COPY crates/tranquil-scopes ./crates/tranquil-scopes
|
||||
COPY crates/tranquil-config ./crates/tranquil-config
|
||||
COPY crates/tranquil-repo ./crates/tranquil-repo
|
||||
COPY crates/tranquil-lexicon ./crates/tranquil-lexicon
|
||||
COPY crates/tranquil-oauth ./crates/tranquil-oauth
|
||||
COPY crates/tranquil-db-traits ./crates/tranquil-db-traits
|
||||
COPY crates/tranquil-infra ./crates/tranquil-infra
|
||||
COPY crates/tranquil-auth ./crates/tranquil-auth
|
||||
COPY crates/tranquil-comms ./crates/tranquil-comms
|
||||
COPY crates/tranquil-db ./crates/tranquil-db
|
||||
COPY crates/tranquil-ripple ./crates/tranquil-ripple
|
||||
COPY crates/tranquil-storage ./crates/tranquil-storage
|
||||
COPY crates/tranquil-cache ./crates/tranquil-cache
|
||||
COPY crates/tranquil-pds ./crates/tranquil-pds
|
||||
COPY crates/tranquil-sync ./crates/tranquil-sync
|
||||
COPY crates/tranquil-api ./crates/tranquil-api
|
||||
COPY crates/tranquil-oauth-server ./crates/tranquil-oauth-server
|
||||
COPY crates/tranquil-server ./crates/tranquil-server
|
||||
COPY migrations ./crates/tranquil-pds/migrations
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/app/target \
|
||||
if [ "$SLIM" = "true" ]; then \
|
||||
SQLX_OFFLINE=true cargo build --release -p tranquil-pds --no-default-features; \
|
||||
SQLX_OFFLINE=true cargo build --release -p tranquil-server --no-default-features; \
|
||||
else \
|
||||
SQLX_OFFLINE=true cargo build --release -p tranquil-pds; \
|
||||
SQLX_OFFLINE=true cargo build --release -p tranquil-server; \
|
||||
fi && \
|
||||
cp target/release/tranquil-pds /tmp/tranquil-pds
|
||||
cp target/release/tranquil-server /tmp/tranquil-pds
|
||||
|
||||
FROM alpine:3.23 AS signal-cli
|
||||
RUN apk add --no-cache curl tar
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
[package]
|
||||
name = "tranquil-api"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tranquil-pds = { workspace = true }
|
||||
tranquil-types = { workspace = true }
|
||||
tranquil-config = { workspace = true }
|
||||
tranquil-db = { workspace = true }
|
||||
tranquil-db-traits = { workspace = true }
|
||||
tranquil-lexicon = { workspace = true, features = ["resolve"] }
|
||||
tranquil-scopes = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
axum = { workspace = true }
|
||||
backon = { workspace = true }
|
||||
base32 = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
bcrypt = { workspace = true }
|
||||
bs58 = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
cid = { workspace = true }
|
||||
ed25519-dalek = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
http = { workspace = true }
|
||||
infer = { workspace = true }
|
||||
ipld-core = { workspace = true }
|
||||
jacquard-common = { workspace = true }
|
||||
jacquard-repo = { workspace = true }
|
||||
k256 = { workspace = true }
|
||||
multibase = { workspace = true }
|
||||
multihash = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_ipld_dagcbor = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
subtle = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
urlencoding = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
webauthn-rs = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::{Auth, NotTakendown, Permissive};
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::auth::{Auth, NotTakendown, Permissive};
|
||||
use tranquil_pds::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
+7
-7
@@ -1,8 +1,8 @@
|
||||
use crate::api::EmptyResponse;
|
||||
use crate::api::error::{ApiError, DbResultExt};
|
||||
use crate::auth::{Admin, Auth};
|
||||
use crate::state::AppState;
|
||||
use crate::types::Did;
|
||||
use tranquil_pds::api::EmptyResponse;
|
||||
use tranquil_pds::api::error::{ApiError, DbResultExt};
|
||||
use tranquil_pds::auth::{Admin, Auth};
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::Did;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -36,7 +36,7 @@ pub async fn delete_account(
|
||||
.await
|
||||
.log_db_err("deleting account")?;
|
||||
|
||||
if let Err(e) = crate::api::repo::record::sequence_account_event(
|
||||
if let Err(e) = tranquil_pds::repo_ops::sequence_account_event(
|
||||
&state,
|
||||
did,
|
||||
tranquil_db_traits::AccountStatus::Deleted,
|
||||
@@ -50,7 +50,7 @@ pub async fn delete_account(
|
||||
}
|
||||
let _ = state
|
||||
.cache
|
||||
.delete(&crate::cache_keys::handle_key(&handle))
|
||||
.delete(&tranquil_pds::cache_keys::handle_key(&handle))
|
||||
.await;
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
}
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
use crate::api::error::{ApiError, DbResultExt};
|
||||
use crate::auth::{Admin, Auth};
|
||||
use crate::state::AppState;
|
||||
use crate::types::Did;
|
||||
use tranquil_pds::api::error::{ApiError, DbResultExt};
|
||||
use tranquil_pds::auth::{Admin, Auth};
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::Did;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
use crate::api::error::{ApiError, DbResultExt};
|
||||
use crate::auth::{Admin, Auth};
|
||||
use crate::state::AppState;
|
||||
use crate::types::{Did, Handle};
|
||||
use tranquil_pds::api::error::{ApiError, DbResultExt};
|
||||
use tranquil_pds::auth::{Admin, Auth};
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::{Did, Handle};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Query, RawQuery, State},
|
||||
@@ -196,7 +196,7 @@ pub async fn get_account_infos(
|
||||
_auth: Auth<Admin>,
|
||||
RawQuery(raw_query): RawQuery,
|
||||
) -> Result<Response, ApiError> {
|
||||
let dids: Vec<String> = crate::util::parse_repeated_query_param(raw_query.as_deref(), "dids")
|
||||
let dids: Vec<String> = tranquil_pds::util::parse_repeated_query_param(raw_query.as_deref(), "dids")
|
||||
.into_iter()
|
||||
.filter(|d| !d.is_empty())
|
||||
.collect();
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
use crate::api::error::{ApiError, DbResultExt};
|
||||
use crate::auth::{Admin, Auth};
|
||||
use crate::state::AppState;
|
||||
use crate::types::{Did, Handle};
|
||||
use tranquil_pds::api::error::{ApiError, DbResultExt};
|
||||
use tranquil_pds::auth::{Admin, Auth};
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::{Did, Handle};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Query, State},
|
||||
+9
-9
@@ -1,8 +1,8 @@
|
||||
use crate::api::EmptyResponse;
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::{Admin, Auth};
|
||||
use crate::state::AppState;
|
||||
use crate::types::{Did, Handle, PlainPassword};
|
||||
use tranquil_pds::api::EmptyResponse;
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::auth::{Admin, Auth};
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::{Did, Handle, PlainPassword};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -101,14 +101,14 @@ pub async fn update_account_handle(
|
||||
if let Some(old) = old_handle {
|
||||
let _ = state
|
||||
.cache
|
||||
.delete(&crate::cache_keys::handle_key(&old))
|
||||
.delete(&tranquil_pds::cache_keys::handle_key(&old))
|
||||
.await;
|
||||
}
|
||||
let _ = state
|
||||
.cache
|
||||
.delete(&crate::cache_keys::handle_key(&handle))
|
||||
.delete(&tranquil_pds::cache_keys::handle_key(&handle))
|
||||
.await;
|
||||
if let Err(e) = crate::api::repo::record::sequence_identity_event(
|
||||
if let Err(e) = tranquil_pds::repo_ops::sequence_identity_event(
|
||||
&state,
|
||||
did,
|
||||
Some(&handle_for_check),
|
||||
@@ -121,7 +121,7 @@ pub async fn update_account_handle(
|
||||
);
|
||||
}
|
||||
if let Err(e) =
|
||||
crate::api::identity::did::update_plc_handle(&state, did, &handle_for_check).await
|
||||
crate::identity::did::update_plc_handle(&state, did, &handle_for_check).await
|
||||
{
|
||||
warn!("Failed to update PLC handle for admin handle update: {}", e);
|
||||
}
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
use crate::api::error::{ApiError, DbResultExt};
|
||||
use crate::auth::{Admin, Auth};
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::api::error::{ApiError, DbResultExt};
|
||||
use tranquil_pds::auth::{Admin, Auth};
|
||||
use tranquil_pds::state::AppState;
|
||||
use axum::{Json, extract::State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{error, warn};
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
use crate::api::EmptyResponse;
|
||||
use crate::api::error::{ApiError, DbResultExt};
|
||||
use crate::auth::{Admin, Auth};
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::api::EmptyResponse;
|
||||
use tranquil_pds::api::error::{ApiError, DbResultExt};
|
||||
use tranquil_pds::auth::{Admin, Auth};
|
||||
use tranquil_pds::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Query, State},
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::{Admin, Auth};
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::auth::{Admin, Auth};
|
||||
use tranquil_pds::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
+7
-7
@@ -1,7 +1,7 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::{Admin, Auth};
|
||||
use crate::state::AppState;
|
||||
use crate::types::{CidLink, Did};
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::auth::{Admin, Auth};
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::{CidLink, Did};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Query, State},
|
||||
@@ -215,7 +215,7 @@ pub async fn update_subject_status(
|
||||
tranquil_db_traits::AccountStatus::Active
|
||||
};
|
||||
if let Err(e) =
|
||||
crate::api::repo::record::sequence_account_event(&state, &did, status).await
|
||||
tranquil_pds::repo_ops::sequence_account_event(&state, &did, status).await
|
||||
{
|
||||
warn!("Failed to sequence account event for takedown: {}", e);
|
||||
}
|
||||
@@ -227,7 +227,7 @@ pub async fn update_subject_status(
|
||||
tranquil_db_traits::AccountStatus::Active
|
||||
};
|
||||
if let Err(e) =
|
||||
crate::api::repo::record::sequence_account_event(&state, &did, status).await
|
||||
tranquil_pds::repo_ops::sequence_account_event(&state, &did, status).await
|
||||
{
|
||||
warn!("Failed to sequence account event for deactivation: {}", e);
|
||||
}
|
||||
@@ -235,7 +235,7 @@ pub async fn update_subject_status(
|
||||
if let Ok(Some(handle)) = state.user_repo.get_handle_by_did(&did).await {
|
||||
let _ = state
|
||||
.cache
|
||||
.delete(&crate::cache_keys::handle_key(&handle))
|
||||
.delete(&tranquil_pds::cache_keys::handle_key(&handle))
|
||||
.await;
|
||||
}
|
||||
return Ok((
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
use crate::auth::{AccountRequirement, extract_auth_token_from_header, validate_token_with_dpop};
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::auth::{AccountRequirement, extract_auth_token_from_header, validate_token_with_dpop};
|
||||
use tranquil_pds::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -33,13 +33,13 @@ pub async fn get_age_assurance_state() -> Response {
|
||||
}
|
||||
|
||||
async fn get_account_created_at(state: &AppState, headers: &HeaderMap) -> Option<String> {
|
||||
let auth_header = crate::util::get_header_str(headers, http::header::AUTHORIZATION);
|
||||
let auth_header = tranquil_pds::util::get_header_str(headers, http::header::AUTHORIZATION);
|
||||
tracing::debug!(?auth_header, "age assurance: extracting token");
|
||||
|
||||
let extracted = extract_auth_token_from_header(auth_header)?;
|
||||
tracing::debug!("age assurance: got token, validating");
|
||||
|
||||
let dpop_proof = crate::util::get_header_str(headers, crate::util::HEADER_DPOP);
|
||||
let dpop_proof = tranquil_pds::util::get_header_str(headers, tranquil_pds::util::HEADER_DPOP);
|
||||
let http_uri = "/";
|
||||
|
||||
let auth_user = match validate_token_with_dpop(
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::api::{EmptyResponse, EnabledResponse};
|
||||
use crate::auth::{Active, Auth};
|
||||
use crate::scheduled::generate_full_backup;
|
||||
use crate::state::AppState;
|
||||
use crate::storage::{BackupStorage, backup_retention_count};
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::api::{EmptyResponse, EnabledResponse};
|
||||
use tranquil_pds::auth::{Active, Auth};
|
||||
use tranquil_pds::scheduled::generate_full_backup;
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::storage::{BackupStorage, backup_retention_count};
|
||||
use anyhow::Context;
|
||||
use axum::{
|
||||
Json,
|
||||
@@ -39,7 +39,7 @@ pub struct ListBackupsOutput {
|
||||
pub async fn list_backups(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, crate::api::error::ApiError> {
|
||||
) -> Result<Response, tranquil_pds::api::error::ApiError> {
|
||||
let (user_id, backup_enabled) = match state.backup_repo.get_user_backup_status(&auth.did).await
|
||||
{
|
||||
Ok(Some(status)) => status,
|
||||
@@ -91,7 +91,7 @@ pub async fn get_backup(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
Query(query): Query<GetBackupQuery>,
|
||||
) -> Result<Response, crate::api::error::ApiError> {
|
||||
) -> Result<Response, tranquil_pds::api::error::ApiError> {
|
||||
let backup_id = match uuid::Uuid::parse_str(&query.id) {
|
||||
Ok(id) => id,
|
||||
Err(_) => {
|
||||
@@ -157,7 +157,7 @@ pub struct CreateBackupOutput {
|
||||
pub async fn create_backup(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, crate::api::error::ApiError> {
|
||||
) -> Result<Response, tranquil_pds::api::error::ApiError> {
|
||||
let backup_storage = match state.backup_storage.as_ref() {
|
||||
Some(storage) => storage,
|
||||
None => {
|
||||
@@ -213,7 +213,7 @@ pub async fn create_backup(
|
||||
}
|
||||
};
|
||||
|
||||
let block_count = crate::scheduled::count_car_blocks(&car_bytes);
|
||||
let block_count = tranquil_pds::scheduled::count_car_blocks(&car_bytes);
|
||||
let size_bytes = i64::try_from(car_bytes.len()).unwrap_or(i64::MAX);
|
||||
|
||||
let storage_key = match backup_storage
|
||||
@@ -327,7 +327,7 @@ pub async fn delete_backup(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
Query(query): Query<DeleteBackupQuery>,
|
||||
) -> Result<Response, crate::api::error::ApiError> {
|
||||
) -> Result<Response, tranquil_pds::api::error::ApiError> {
|
||||
let backup_id = match uuid::Uuid::parse_str(&query.id) {
|
||||
Ok(id) => id,
|
||||
Err(_) => {
|
||||
@@ -384,7 +384,7 @@ pub async fn set_backup_enabled(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<SetBackupEnabledInput>,
|
||||
) -> Result<Response, crate::api::error::ApiError> {
|
||||
) -> Result<Response, tranquil_pds::api::error::ApiError> {
|
||||
let deactivated_at = match state
|
||||
.backup_repo
|
||||
.get_user_deactivated_status(&auth.did)
|
||||
@@ -423,7 +423,7 @@ pub async fn set_backup_enabled(
|
||||
pub async fn export_blobs(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, crate::api::error::ApiError> {
|
||||
) -> Result<Response, tranquil_pds::api::error::ApiError> {
|
||||
let user_id = match state.backup_repo.get_user_id_by_did(&auth.did).await {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => {
|
||||
+128
-259
@@ -1,41 +1,23 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::api::repo::record::utils::create_signed_commit;
|
||||
use crate::auth::{Active, Auth};
|
||||
use crate::delegation::{
|
||||
use crate::identity::provision::{create_plc_did, init_genesis_repo};
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::auth::{Active, Auth};
|
||||
use tranquil_pds::delegation::{
|
||||
DelegationActionType, SCOPE_PRESETS, ValidatedDelegationScope, verify_can_add_controllers,
|
||||
verify_can_be_controller, verify_can_control_accounts,
|
||||
verify_can_control_accounts,
|
||||
};
|
||||
use crate::rate_limit::{AccountCreationLimit, RateLimited};
|
||||
use crate::state::AppState;
|
||||
use crate::types::{Did, Handle};
|
||||
use tranquil_pds::rate_limit::{AccountCreationLimit, RateLimited};
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::{Did, Handle};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Query, State},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use jacquard_common::types::{integer::LimitedU32, string::Tid};
|
||||
use jacquard_repo::{mst::Mst, storage::BlockStore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ControllerInfo {
|
||||
pub did: Did,
|
||||
pub handle: Handle,
|
||||
pub granted_scopes: String,
|
||||
pub granted_at: chrono::DateTime<chrono::Utc>,
|
||||
pub is_active: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ListControllersResponse {
|
||||
pub controllers: Vec<ControllerInfo>,
|
||||
}
|
||||
|
||||
pub async fn list_controllers(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
@@ -54,19 +36,23 @@ pub async fn list_controllers(
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(ListControllersResponse {
|
||||
controllers: controllers
|
||||
.into_iter()
|
||||
.map(|c| ControllerInfo {
|
||||
did: c.did,
|
||||
handle: c.handle,
|
||||
granted_scopes: c.granted_scopes.into_string(),
|
||||
granted_at: c.granted_at,
|
||||
is_active: c.is_active,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.into_response())
|
||||
let resolve_futures = controllers.into_iter().map(|mut c| {
|
||||
let did_resolver = state.did_resolver.clone();
|
||||
async move {
|
||||
if c.handle.is_none() {
|
||||
c.handle = did_resolver
|
||||
.resolve_did_document(c.did.as_str())
|
||||
.await
|
||||
.and_then(|doc| tranquil_types::did_doc::extract_handle(&doc))
|
||||
.map(|h| h.into());
|
||||
}
|
||||
c
|
||||
}
|
||||
});
|
||||
|
||||
let controllers = futures::future::join_all(resolve_futures).await;
|
||||
|
||||
Ok(Json(serde_json::json!({ "controllers": controllers })).into_response())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -80,16 +66,39 @@ pub async fn add_controller(
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<AddControllerInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let controller_exists = state
|
||||
.user_repo
|
||||
.get_by_did(&input.controller_did)
|
||||
let resolved = tranquil_pds::delegation::resolve_identity(&state, &input.controller_did)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some();
|
||||
.ok_or(ApiError::ControllerNotFound)?;
|
||||
|
||||
if !controller_exists {
|
||||
return Ok(ApiError::ControllerNotFound.into_response());
|
||||
if !resolved.is_local {
|
||||
if let Some(ref pds_url) = resolved.pds_url {
|
||||
if !pds_url.starts_with("https://") {
|
||||
return Ok(
|
||||
ApiError::InvalidDelegation("Controller PDS must use HTTPS".into())
|
||||
.into_response(),
|
||||
);
|
||||
}
|
||||
match state
|
||||
.cross_pds_oauth
|
||||
.check_remote_is_delegated(pds_url, input.controller_did.as_str())
|
||||
.await
|
||||
{
|
||||
Some(true) => {
|
||||
return Ok(ApiError::InvalidDelegation(
|
||||
"Cannot add a delegated account from another PDS as a controller".into(),
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
Some(false) => {}
|
||||
None => {
|
||||
warn!(
|
||||
controller = %input.controller_did,
|
||||
pds = %pds_url,
|
||||
"Could not verify remote controller delegation status"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let can_add = match verify_can_add_controllers(&state, &auth).await {
|
||||
@@ -97,16 +106,19 @@ pub async fn add_controller(
|
||||
Err(response) => return Ok(response),
|
||||
};
|
||||
|
||||
let can_be_controller = match verify_can_be_controller(&state, &input.controller_did).await {
|
||||
Ok(proof) => proof,
|
||||
Err(response) => return Ok(response),
|
||||
};
|
||||
if resolved.is_local {
|
||||
if state.delegation_repo.is_delegated_account(&input.controller_did).await.unwrap_or(false) {
|
||||
return Ok(ApiError::InvalidDelegation(
|
||||
"Cannot add a controlled account as a controller".into(),
|
||||
).into_response());
|
||||
}
|
||||
}
|
||||
|
||||
match state
|
||||
.delegation_repo
|
||||
.create_delegation(
|
||||
can_add.did(),
|
||||
can_be_controller.did(),
|
||||
&input.controller_did,
|
||||
&input.granted_scopes,
|
||||
can_add.did(),
|
||||
)
|
||||
@@ -118,10 +130,11 @@ pub async fn add_controller(
|
||||
.log_delegation_action(
|
||||
can_add.did(),
|
||||
can_add.did(),
|
||||
Some(can_be_controller.did()),
|
||||
Some(&input.controller_did),
|
||||
DelegationActionType::GrantCreated,
|
||||
Some(serde_json::json!({
|
||||
"granted_scopes": input.granted_scopes.as_str()
|
||||
"granted_scopes": input.granted_scopes.as_str(),
|
||||
"is_local": resolved.is_local
|
||||
})),
|
||||
None,
|
||||
None,
|
||||
@@ -256,20 +269,6 @@ pub async fn update_controller_scopes(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DelegatedAccountInfo {
|
||||
pub did: Did,
|
||||
pub handle: Handle,
|
||||
pub granted_scopes: String,
|
||||
pub granted_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ListControlledAccountsResponse {
|
||||
pub accounts: Vec<DelegatedAccountInfo>,
|
||||
}
|
||||
|
||||
pub async fn list_controlled_accounts(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
@@ -289,18 +288,7 @@ pub async fn list_controlled_accounts(
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(ListControlledAccountsResponse {
|
||||
accounts: accounts
|
||||
.into_iter()
|
||||
.map(|a| DelegatedAccountInfo {
|
||||
did: a.did,
|
||||
handle: a.handle,
|
||||
granted_scopes: a.granted_scopes.into_string(),
|
||||
granted_at: a.granted_at,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.into_response())
|
||||
Ok(Json(serde_json::json!({ "accounts": accounts })).into_response())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -315,24 +303,6 @@ fn default_limit() -> i64 {
|
||||
50
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuditLogEntry {
|
||||
pub id: String,
|
||||
pub delegated_did: Did,
|
||||
pub actor_did: Did,
|
||||
pub controller_did: Option<Did>,
|
||||
pub action_type: String,
|
||||
pub action_details: Option<serde_json::Value>,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct GetAuditLogResponse {
|
||||
pub entries: Vec<AuditLogEntry>,
|
||||
pub total: i64,
|
||||
}
|
||||
|
||||
pub async fn get_audit_log(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
@@ -361,50 +331,11 @@ pub async fn get_audit_log(
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(Json(GetAuditLogResponse {
|
||||
entries: entries
|
||||
.into_iter()
|
||||
.map(|e| AuditLogEntry {
|
||||
id: e.id.to_string(),
|
||||
delegated_did: e.delegated_did,
|
||||
actor_did: e.actor_did,
|
||||
controller_did: e.controller_did,
|
||||
action_type: format!("{:?}", e.action_type),
|
||||
action_details: e.action_details,
|
||||
created_at: e.created_at,
|
||||
})
|
||||
.collect(),
|
||||
total,
|
||||
})
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ScopePresetInfo {
|
||||
pub name: &'static str,
|
||||
pub label: &'static str,
|
||||
pub description: &'static str,
|
||||
pub scopes: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct GetScopePresetsResponse {
|
||||
pub presets: Vec<ScopePresetInfo>,
|
||||
Ok(Json(serde_json::json!({ "entries": entries, "total": total })).into_response())
|
||||
}
|
||||
|
||||
pub async fn get_scope_presets() -> Response {
|
||||
Json(GetScopePresetsResponse {
|
||||
presets: SCOPE_PRESETS
|
||||
.iter()
|
||||
.map(|p| ScopePresetInfo {
|
||||
name: p.name,
|
||||
label: p.label,
|
||||
description: p.description,
|
||||
scopes: p.scopes,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.into_response()
|
||||
Json(serde_json::json!({ "presets": SCOPE_PRESETS })).into_response()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -434,29 +365,11 @@ pub async fn create_delegated_account(
|
||||
Err(response) => return Ok(response),
|
||||
};
|
||||
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let available_domains = tranquil_config::get().server.available_user_domain_list();
|
||||
let matched_domain = available_domains
|
||||
.iter()
|
||||
.filter(|d| input.handle.ends_with(&format!(".{}", d)))
|
||||
.max_by_key(|d| d.len());
|
||||
|
||||
let handle = if !input.handle.contains('.') || matched_domain.is_some() {
|
||||
let handle_to_validate = match matched_domain {
|
||||
Some(domain) => input
|
||||
.handle
|
||||
.strip_suffix(&format!(".{}", domain))
|
||||
.unwrap_or(&input.handle),
|
||||
None => &input.handle,
|
||||
};
|
||||
match crate::api::validation::validate_short_handle(handle_to_validate) {
|
||||
Ok(h) => format!("{}.{}", h, matched_domain.unwrap_or(&available_domains[0])),
|
||||
Err(e) => {
|
||||
return Ok(ApiError::InvalidRequest(e.to_string()).into_response());
|
||||
}
|
||||
let handle = match tranquil_pds::api::validation::resolve_handle_input(&input.handle) {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
return Ok(ApiError::InvalidRequest(e.to_string()).into_response());
|
||||
}
|
||||
} else {
|
||||
input.handle.to_lowercase()
|
||||
};
|
||||
|
||||
let email = input
|
||||
@@ -465,7 +378,7 @@ pub async fn create_delegated_account(
|
||||
.map(|e| e.trim().to_string())
|
||||
.filter(|e| !e.is_empty());
|
||||
if let Some(ref email) = email
|
||||
&& !crate::api::validation::is_valid_email(email)
|
||||
&& !tranquil_pds::api::validation::is_valid_email(email)
|
||||
{
|
||||
return Ok(ApiError::InvalidEmail.into_response());
|
||||
}
|
||||
@@ -483,96 +396,15 @@ pub async fn create_delegated_account(
|
||||
None
|
||||
};
|
||||
|
||||
use k256::ecdsa::SigningKey;
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
let pds_endpoint = format!("https://{}", hostname);
|
||||
let secret_key = k256::SecretKey::random(&mut OsRng);
|
||||
let secret_key_bytes = secret_key.to_bytes().to_vec();
|
||||
|
||||
let signing_key = match SigningKey::from_slice(&secret_key_bytes) {
|
||||
Ok(k) => k,
|
||||
Err(e) => {
|
||||
error!("Error creating signing key: {:?}", e);
|
||||
return Ok(ApiError::InternalError(None).into_response());
|
||||
}
|
||||
};
|
||||
|
||||
let rotation_key = tranquil_config::get()
|
||||
.secrets
|
||||
.plc_rotation_key
|
||||
.clone()
|
||||
.unwrap_or_else(|| crate::plc::signing_key_to_did_key(&signing_key));
|
||||
|
||||
let genesis_result = match crate::plc::create_genesis_operation(
|
||||
&signing_key,
|
||||
&rotation_key,
|
||||
&handle,
|
||||
&pds_endpoint,
|
||||
) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!("Error creating PLC genesis operation: {:?}", e);
|
||||
return Ok(
|
||||
ApiError::InternalError(Some("Failed to create PLC operation".into()))
|
||||
.into_response(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let plc_client = crate::plc::PlcClient::with_cache(None, Some(state.cache.clone()));
|
||||
if let Err(e) = plc_client
|
||||
.send_operation(&genesis_result.did, &genesis_result.signed_operation)
|
||||
.await
|
||||
{
|
||||
error!("Failed to submit PLC genesis operation: {:?}", e);
|
||||
return Ok(ApiError::UpstreamErrorMsg(format!(
|
||||
"Failed to register DID with PLC directory: {}",
|
||||
e
|
||||
))
|
||||
.into_response());
|
||||
}
|
||||
|
||||
let did: Did = genesis_result
|
||||
.did
|
||||
.parse()
|
||||
.map_err(|_| ApiError::InternalError(Some("PLC genesis returned invalid DID".into())))?;
|
||||
let plc = create_plc_did(&state, &handle).await.map_err(|e| {
|
||||
tracing::error!("PLC DID creation failed: {:?}", e);
|
||||
e
|
||||
})?;
|
||||
let did = plc.did;
|
||||
let handle: Handle = handle.parse().map_err(|_| ApiError::InvalidHandle(None))?;
|
||||
info!(did = %did, handle = %handle, controller = %can_control.did(), "Created DID for delegated account");
|
||||
|
||||
let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => {
|
||||
error!("Error encrypting signing key: {:?}", e);
|
||||
return Ok(ApiError::InternalError(None).into_response());
|
||||
}
|
||||
};
|
||||
|
||||
let mst = Mst::new(Arc::new(state.block_store.clone()));
|
||||
let mst_root = match mst.persist().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Error persisting MST: {:?}", e);
|
||||
return Ok(ApiError::InternalError(None).into_response());
|
||||
}
|
||||
};
|
||||
let rev = Tid::now(LimitedU32::MIN);
|
||||
let (commit_bytes, _sig) =
|
||||
match create_signed_commit(&did, mst_root, rev.as_ref(), None, &signing_key) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
error!("Error creating genesis commit: {:?}", e);
|
||||
return Ok(ApiError::InternalError(None).into_response());
|
||||
}
|
||||
};
|
||||
let commit_cid: cid::Cid = match state.block_store.put(&commit_bytes).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Error saving genesis commit: {:?}", e);
|
||||
return Ok(ApiError::InternalError(None).into_response());
|
||||
}
|
||||
};
|
||||
let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()];
|
||||
let repo = init_genesis_repo(&state, &did, &plc.signing_key, &plc.signing_key_bytes).await?;
|
||||
|
||||
let create_input = tranquil_db_traits::CreateDelegatedAccountInput {
|
||||
handle: handle.clone(),
|
||||
@@ -580,11 +412,11 @@ pub async fn create_delegated_account(
|
||||
did: did.clone(),
|
||||
controller_did: can_control.did().clone(),
|
||||
controller_scopes: input.controller_scopes.as_str().to_string(),
|
||||
encrypted_key_bytes,
|
||||
encryption_version: crate::config::ENCRYPTION_VERSION,
|
||||
commit_cid: commit_cid.to_string(),
|
||||
repo_rev: rev.as_ref().to_string(),
|
||||
genesis_block_cids,
|
||||
encrypted_key_bytes: repo.encrypted_key_bytes,
|
||||
encryption_version: tranquil_pds::config::ENCRYPTION_VERSION,
|
||||
commit_cid: repo.commit_cid.to_string(),
|
||||
repo_rev: repo.repo_rev.clone(),
|
||||
genesis_block_cids: repo.genesis_block_cids,
|
||||
invite_code: input.invite_code.clone(),
|
||||
};
|
||||
|
||||
@@ -616,11 +448,11 @@ pub async fn create_delegated_account(
|
||||
}
|
||||
|
||||
if let Err(e) =
|
||||
crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle)).await
|
||||
tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle)).await
|
||||
{
|
||||
warn!("Failed to sequence identity event for {}: {}", did, e);
|
||||
}
|
||||
if let Err(e) = crate::api::repo::record::sequence_account_event(
|
||||
if let Err(e) = tranquil_pds::repo_ops::sequence_account_event(
|
||||
&state,
|
||||
&did,
|
||||
tranquil_db_traits::AccountStatus::Active,
|
||||
@@ -634,11 +466,11 @@ pub async fn create_delegated_account(
|
||||
"$type": "app.bsky.actor.profile",
|
||||
"displayName": handle
|
||||
});
|
||||
if let Err(e) = crate::api::repo::record::create_record_internal(
|
||||
if let Err(e) = tranquil_pds::repo_ops::create_record_internal(
|
||||
&state,
|
||||
&did,
|
||||
&crate::types::PROFILE_COLLECTION,
|
||||
&crate::types::PROFILE_RKEY,
|
||||
&tranquil_pds::types::PROFILE_COLLECTION,
|
||||
&tranquil_pds::types::PROFILE_RKEY,
|
||||
&profile_record,
|
||||
)
|
||||
.await
|
||||
@@ -666,3 +498,40 @@ pub async fn create_delegated_account(
|
||||
|
||||
Ok(Json(CreateDelegatedAccountResponse { did, handle }).into_response())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ResolveControllerParams {
|
||||
pub identifier: String,
|
||||
}
|
||||
|
||||
pub async fn resolve_controller(
|
||||
State(state): State<AppState>,
|
||||
Query(params): Query<ResolveControllerParams>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let identifier = params.identifier.trim().trim_start_matches('@');
|
||||
|
||||
let did: Did = if identifier.starts_with("did:") {
|
||||
identifier.parse().map_err(|_| ApiError::ControllerNotFound)?
|
||||
} else {
|
||||
let local_handle: Option<Handle> = identifier.parse().ok();
|
||||
let local_user = match local_handle {
|
||||
Some(ref h) => state.user_repo.get_by_handle(h).await.ok().flatten(),
|
||||
None => None,
|
||||
};
|
||||
match local_user {
|
||||
Some(user) => user.did,
|
||||
None => tranquil_pds::handle::resolve_handle(identifier)
|
||||
.await
|
||||
.map_err(|_| ApiError::ControllerNotFound)?
|
||||
.parse()
|
||||
.map_err(|_| ApiError::ControllerNotFound)?,
|
||||
}
|
||||
};
|
||||
|
||||
let resolved = tranquil_pds::delegation::resolve_identity(&state, &did)
|
||||
.await
|
||||
.ok_or(ApiError::ControllerNotFound)?;
|
||||
|
||||
Ok(Json(resolved).into_response())
|
||||
}
|
||||
|
||||
+3
-3
@@ -10,9 +10,9 @@ use serde_json::json;
|
||||
use tracing::{debug, info, warn};
|
||||
use tranquil_types::Handle;
|
||||
|
||||
use crate::comms::comms_repo;
|
||||
use crate::state::AppState;
|
||||
use crate::util::discord_public_key;
|
||||
use tranquil_pds::comms::comms_repo;
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::util::discord_public_key;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Interaction {
|
||||
+61
-175
@@ -1,12 +1,10 @@
|
||||
use super::did::verify_did_web;
|
||||
use crate::api::error::ApiError;
|
||||
use crate::api::repo::record::utils::create_signed_commit;
|
||||
use crate::auth::{ServiceTokenVerifier, extract_auth_token_from_header, is_service_token};
|
||||
use crate::plc::{PlcClient, create_genesis_operation, signing_key_to_did_key};
|
||||
use crate::rate_limit::{AccountCreationLimit, RateLimited};
|
||||
use crate::state::AppState;
|
||||
use crate::types::{Did, Handle, PlainPassword};
|
||||
use crate::validation::validate_password;
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::auth::{ServiceTokenVerifier, extract_auth_token_from_header, is_service_token};
|
||||
use tranquil_pds::rate_limit::{AccountCreationLimit, RateLimited};
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::{Did, Handle, PlainPassword};
|
||||
use tranquil_pds::validation::validate_password;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -14,13 +12,10 @@ use axum::{
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use bcrypt::{DEFAULT_COST, hash};
|
||||
use jacquard_common::types::{integer::LimitedU32, string::Tid};
|
||||
use jacquard_repo::{mst::Mst, storage::BlockStore};
|
||||
use k256::{SecretKey, ecdsa::SigningKey};
|
||||
use rand::rngs::OsRng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -73,7 +68,7 @@ pub async fn create_account(
|
||||
}
|
||||
|
||||
let migration_auth = if let Some(extracted) = extract_auth_token_from_header(
|
||||
crate::util::get_header_str(&headers, http::header::AUTHORIZATION),
|
||||
tranquil_pds::util::get_header_str(&headers, http::header::AUTHORIZATION),
|
||||
) {
|
||||
let token = extracted.token;
|
||||
if is_service_token(&token) {
|
||||
@@ -141,31 +136,9 @@ pub async fn create_account(
|
||||
}
|
||||
|
||||
let cfg = tranquil_config::get();
|
||||
let available_domains = cfg.server.available_user_domain_list();
|
||||
let matched_domain = available_domains
|
||||
.iter()
|
||||
.filter(|d| input.handle.ends_with(&format!(".{}", d)))
|
||||
.max_by_key(|d| d.len());
|
||||
|
||||
let validated_short_handle = if !input.handle.contains('.') || matched_domain.is_some() {
|
||||
let handle_to_validate = match matched_domain {
|
||||
Some(domain) => input
|
||||
.handle
|
||||
.strip_suffix(&format!(".{}", domain))
|
||||
.unwrap_or(&input.handle),
|
||||
None => &input.handle,
|
||||
};
|
||||
match crate::api::validation::validate_short_handle(handle_to_validate) {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
return ApiError::from(e).into_response();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match crate::api::validation::validate_full_domain_handle(&input.handle) {
|
||||
Ok(h) => h,
|
||||
Err(e) => return ApiError::from(e).into_response(),
|
||||
}
|
||||
let handle = match tranquil_pds::api::validation::resolve_handle_input(&input.handle) {
|
||||
Ok(h) => h,
|
||||
Err(e) => return ApiError::from(e).into_response(),
|
||||
};
|
||||
let email: Option<String> = input
|
||||
.email
|
||||
@@ -173,7 +146,7 @@ pub async fn create_account(
|
||||
.map(|e| e.trim().to_string())
|
||||
.filter(|e| !e.is_empty());
|
||||
if let Some(ref email) = email
|
||||
&& !crate::api::validation::is_valid_email(email)
|
||||
&& !tranquil_pds::api::validation::is_valid_email(email)
|
||||
{
|
||||
return ApiError::InvalidEmail.into_response();
|
||||
}
|
||||
@@ -191,7 +164,7 @@ pub async fn create_account(
|
||||
tranquil_db_traits::CommsChannel::Discord => match &input.discord_username {
|
||||
Some(username) if !username.trim().is_empty() => {
|
||||
let clean = username.trim().to_lowercase();
|
||||
if !crate::api::validation::is_valid_discord_username(&clean) {
|
||||
if !tranquil_pds::api::validation::is_valid_discord_username(&clean) {
|
||||
return ApiError::InvalidRequest(
|
||||
"Invalid Discord username. Must be 2-32 lowercase characters (letters, numbers, underscores, periods)".into(),
|
||||
).into_response();
|
||||
@@ -203,7 +176,7 @@ pub async fn create_account(
|
||||
tranquil_db_traits::CommsChannel::Telegram => match &input.telegram_username {
|
||||
Some(username) if !username.trim().is_empty() => {
|
||||
let clean = username.trim().trim_start_matches('@');
|
||||
if !crate::api::validation::is_valid_telegram_username(clean) {
|
||||
if !tranquil_pds::api::validation::is_valid_telegram_username(clean) {
|
||||
return ApiError::InvalidRequest(
|
||||
"Invalid Telegram username. Must be 5-32 characters, alphanumeric or underscore".into(),
|
||||
).into_response();
|
||||
@@ -221,12 +194,6 @@ pub async fn create_account(
|
||||
})
|
||||
};
|
||||
let hostname = &cfg.server.hostname;
|
||||
let pds_endpoint = format!("https://{}", hostname);
|
||||
let handle = match matched_domain {
|
||||
Some(domain) => format!("{}.{}", validated_short_handle, domain),
|
||||
None if input.handle.contains('.') => validated_short_handle.clone(),
|
||||
None => format!("{}.{}", validated_short_handle, &available_domains[0]),
|
||||
};
|
||||
let (secret_key_bytes, reserved_key_id): (Vec<u8>, Option<uuid::Uuid>) =
|
||||
if let Some(signing_key_did) = &input.signing_key {
|
||||
match state
|
||||
@@ -257,7 +224,7 @@ pub async fn create_account(
|
||||
let did_type = input.did_type.as_deref().unwrap_or("plc");
|
||||
let did = match did_type {
|
||||
"web" => {
|
||||
if !crate::api::server::meta::is_self_hosted_did_web_enabled() {
|
||||
if !tranquil_pds::util::is_self_hosted_did_web_enabled() {
|
||||
return ApiError::SelfHostedDidWebDisabled.into_response();
|
||||
}
|
||||
let encoded_handle = handle.replace(':', "%3A");
|
||||
@@ -308,76 +275,17 @@ pub async fn create_account(
|
||||
)
|
||||
.into_response();
|
||||
} else {
|
||||
let rotation_key = tranquil_config::get()
|
||||
.secrets
|
||||
.plc_rotation_key
|
||||
.clone()
|
||||
.unwrap_or_else(|| signing_key_to_did_key(&signing_key));
|
||||
let genesis_result = match create_genesis_operation(
|
||||
&signing_key,
|
||||
&rotation_key,
|
||||
&handle,
|
||||
&pds_endpoint,
|
||||
) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!("Error creating PLC genesis operation: {:?}", e);
|
||||
return ApiError::InternalError(Some(
|
||||
"Failed to create PLC operation".into(),
|
||||
))
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let plc_client = PlcClient::with_cache(None, Some(state.cache.clone()));
|
||||
if let Err(e) = plc_client
|
||||
.send_operation(&genesis_result.did, &genesis_result.signed_operation)
|
||||
.await
|
||||
match super::provision::submit_plc_genesis(&state, &signing_key, &handle).await
|
||||
{
|
||||
error!("Failed to submit PLC genesis operation: {:?}", e);
|
||||
return ApiError::UpstreamErrorMsg(format!(
|
||||
"Failed to register DID with PLC directory: {}",
|
||||
e
|
||||
))
|
||||
.into_response();
|
||||
Ok(did) => did,
|
||||
Err(e) => return e.into_response(),
|
||||
}
|
||||
info!(did = %genesis_result.did, "Successfully registered DID with PLC directory");
|
||||
genesis_result.did
|
||||
}
|
||||
} else {
|
||||
let rotation_key = tranquil_config::get()
|
||||
.secrets
|
||||
.plc_rotation_key
|
||||
.clone()
|
||||
.unwrap_or_else(|| signing_key_to_did_key(&signing_key));
|
||||
let genesis_result = match create_genesis_operation(
|
||||
&signing_key,
|
||||
&rotation_key,
|
||||
&handle,
|
||||
&pds_endpoint,
|
||||
) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!("Error creating PLC genesis operation: {:?}", e);
|
||||
return ApiError::InternalError(Some(
|
||||
"Failed to create PLC operation".into(),
|
||||
))
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let plc_client = PlcClient::with_cache(None, Some(state.cache.clone()));
|
||||
if let Err(e) = plc_client
|
||||
.send_operation(&genesis_result.did, &genesis_result.signed_operation)
|
||||
.await
|
||||
{
|
||||
error!("Failed to submit PLC genesis operation: {:?}", e);
|
||||
return ApiError::UpstreamErrorMsg(format!(
|
||||
"Failed to register DID with PLC directory: {}",
|
||||
e
|
||||
))
|
||||
.into_response();
|
||||
match super::provision::submit_plc_genesis(&state, &signing_key, &handle).await {
|
||||
Ok(did) => did,
|
||||
Err(e) => return e.into_response(),
|
||||
}
|
||||
info!(did = %genesis_result.did, "Successfully registered DID with PLC directory");
|
||||
genesis_result.did
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -408,7 +316,7 @@ pub async fn create_account(
|
||||
.await
|
||||
{
|
||||
Ok(Some(key_info)) => {
|
||||
match crate::config::decrypt_key(
|
||||
match tranquil_pds::config::decrypt_key(
|
||||
&key_info.key_bytes,
|
||||
key_info.encryption_version,
|
||||
) {
|
||||
@@ -428,14 +336,14 @@ pub async fn create_account(
|
||||
}
|
||||
};
|
||||
let access_meta =
|
||||
match crate::auth::create_access_token_with_metadata(&did, &secret_key_bytes) {
|
||||
match tranquil_pds::auth::create_access_token_with_metadata(&did, &secret_key_bytes) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
error!("Error creating access token: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let refresh_meta = match crate::auth::create_refresh_token_with_metadata(
|
||||
let refresh_meta = match tranquil_pds::auth::create_refresh_token_with_metadata(
|
||||
&did,
|
||||
&secret_key_bytes,
|
||||
) {
|
||||
@@ -453,7 +361,7 @@ pub async fn create_account(
|
||||
refresh_expires_at: refresh_meta.expires_at,
|
||||
login_type: tranquil_db_traits::LoginType::Modern,
|
||||
mfa_verified: false,
|
||||
scope: None,
|
||||
scope: Some("transition:generic transition:chat.bsky".to_string()),
|
||||
controller_did: None,
|
||||
app_password_name: None,
|
||||
};
|
||||
@@ -463,12 +371,12 @@ pub async fn create_account(
|
||||
}
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let verification_required = if let Some(ref user_email) = email {
|
||||
let token = crate::auth::verification_token::generate_migration_token(
|
||||
let token = tranquil_pds::auth::verification_token::generate_migration_token(
|
||||
&did_typed, user_email,
|
||||
);
|
||||
let formatted_token =
|
||||
crate::auth::verification_token::format_token_for_display(&token);
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_migration_verification(
|
||||
tranquil_pds::auth::verification_token::format_token_for_display(&token);
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_migration_verification(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
reactivated.user_id,
|
||||
@@ -590,45 +498,23 @@ pub async fn create_account(
|
||||
None
|
||||
};
|
||||
|
||||
let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) {
|
||||
Ok(enc) => enc,
|
||||
Err(e) => {
|
||||
error!("Error encrypting user key: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let mst = Mst::new(Arc::new(state.block_store.clone()));
|
||||
let mst_root = match mst.persist().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Error persisting MST: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let rev = Tid::now(LimitedU32::MIN);
|
||||
let did_for_commit: Did = match did.parse() {
|
||||
Ok(d) => d,
|
||||
Err(_) => return ApiError::InternalError(Some("Invalid DID".into())).into_response(),
|
||||
};
|
||||
let (commit_bytes, _sig) =
|
||||
match create_signed_commit(&did_for_commit, mst_root, rev.as_ref(), None, &signing_key) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
error!("Error creating genesis commit: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let commit_cid = match state.block_store.put(&commit_bytes).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Error saving genesis commit: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
let repo = match super::provision::init_genesis_repo(
|
||||
&state,
|
||||
&did_for_commit,
|
||||
&signing_key,
|
||||
&secret_key_bytes,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(e) => return e.into_response(),
|
||||
};
|
||||
let commit_cid_str = commit_cid.to_string();
|
||||
let rev_str = rev.as_ref().to_string();
|
||||
let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()];
|
||||
let commit_cid_str = repo.commit_cid.to_string();
|
||||
let rev_str = repo.repo_rev.clone();
|
||||
|
||||
let birthdate_pref = if tranquil_config::get().server.age_assurance_override {
|
||||
Some(json!({
|
||||
@@ -665,12 +551,12 @@ pub async fn create_account(
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_lowercase()),
|
||||
deactivated_at,
|
||||
encrypted_key_bytes,
|
||||
encryption_version: crate::config::ENCRYPTION_VERSION,
|
||||
encrypted_key_bytes: repo.encrypted_key_bytes,
|
||||
encryption_version: tranquil_pds::config::ENCRYPTION_VERSION,
|
||||
reserved_key_id,
|
||||
commit_cid: commit_cid_str.clone(),
|
||||
repo_rev: rev_str.clone(),
|
||||
genesis_block_cids,
|
||||
genesis_block_cids: repo.genesis_block_cids,
|
||||
invite_code: if is_bootstrap {
|
||||
None
|
||||
} else {
|
||||
@@ -697,7 +583,7 @@ pub async fn create_account(
|
||||
};
|
||||
let user_id = create_result.user_id;
|
||||
if !is_migration && !is_did_web_byod {
|
||||
if let Err(e) = crate::api::repo::record::sequence_identity_event(
|
||||
if let Err(e) = tranquil_pds::repo_ops::sequence_identity_event(
|
||||
&state,
|
||||
&did_for_commit,
|
||||
Some(&handle_typed),
|
||||
@@ -706,7 +592,7 @@ pub async fn create_account(
|
||||
{
|
||||
warn!("Failed to sequence identity event for {}: {}", did, e);
|
||||
}
|
||||
if let Err(e) = crate::api::repo::record::sequence_account_event(
|
||||
if let Err(e) = tranquil_pds::repo_ops::sequence_account_event(
|
||||
&state,
|
||||
&did_for_commit,
|
||||
tranquil_db_traits::AccountStatus::Active,
|
||||
@@ -715,22 +601,22 @@ pub async fn create_account(
|
||||
{
|
||||
warn!("Failed to sequence account event for {}: {}", did, e);
|
||||
}
|
||||
if let Err(e) = crate::api::repo::record::sequence_genesis_commit(
|
||||
if let Err(e) = tranquil_pds::repo_ops::sequence_genesis_commit(
|
||||
&state,
|
||||
&did_for_commit,
|
||||
&commit_cid,
|
||||
&mst_root,
|
||||
&repo.commit_cid,
|
||||
&repo.mst_root_cid,
|
||||
&rev_str,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!("Failed to sequence commit event for {}: {}", did, e);
|
||||
}
|
||||
if let Err(e) = crate::api::repo::record::sequence_sync_event(
|
||||
if let Err(e) = tranquil_pds::repo_ops::sequence_sync_event(
|
||||
&state,
|
||||
&did_for_commit,
|
||||
&commit_cid_str,
|
||||
Some(rev.as_ref()),
|
||||
Some(&rev_str),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -740,11 +626,11 @@ pub async fn create_account(
|
||||
"$type": "app.bsky.actor.profile",
|
||||
"displayName": input.handle
|
||||
});
|
||||
if let Err(e) = crate::api::repo::record::create_record_internal(
|
||||
if let Err(e) = tranquil_pds::repo_ops::create_record_internal(
|
||||
&state,
|
||||
&did_for_commit,
|
||||
&crate::types::PROFILE_COLLECTION,
|
||||
&crate::types::PROFILE_RKEY,
|
||||
&tranquil_pds::types::PROFILE_COLLECTION,
|
||||
&tranquil_pds::types::PROFILE_RKEY,
|
||||
&profile_record,
|
||||
)
|
||||
.await
|
||||
@@ -755,14 +641,14 @@ pub async fn create_account(
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if !is_migration {
|
||||
if let Some(ref recipient) = verification_recipient {
|
||||
let verification_token = crate::auth::verification_token::generate_signup_token(
|
||||
let verification_token = tranquil_pds::auth::verification_token::generate_signup_token(
|
||||
&did_for_commit,
|
||||
verification_channel,
|
||||
recipient,
|
||||
);
|
||||
let formatted_token =
|
||||
crate::auth::verification_token::format_token_for_display(&verification_token);
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_signup_verification(
|
||||
tranquil_pds::auth::verification_token::format_token_for_display(&verification_token);
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_signup_verification(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
user_id,
|
||||
@@ -781,9 +667,9 @@ pub async fn create_account(
|
||||
}
|
||||
} else if let Some(ref user_email) = email {
|
||||
let token =
|
||||
crate::auth::verification_token::generate_migration_token(&did_for_commit, user_email);
|
||||
let formatted_token = crate::auth::verification_token::format_token_for_display(&token);
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_migration_verification(
|
||||
tranquil_pds::auth::verification_token::generate_migration_token(&did_for_commit, user_email);
|
||||
let formatted_token = tranquil_pds::auth::verification_token::format_token_for_display(&token);
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_migration_verification(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
user_id,
|
||||
@@ -797,7 +683,7 @@ pub async fn create_account(
|
||||
}
|
||||
}
|
||||
|
||||
let access_meta = match crate::auth::create_access_token_with_metadata(&did, &secret_key_bytes)
|
||||
let access_meta = match tranquil_pds::auth::create_access_token_with_metadata(&did, &secret_key_bytes)
|
||||
{
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
@@ -806,7 +692,7 @@ pub async fn create_account(
|
||||
}
|
||||
};
|
||||
let refresh_meta =
|
||||
match crate::auth::create_refresh_token_with_metadata(&did, &secret_key_bytes) {
|
||||
match tranquil_pds::auth::create_refresh_token_with_metadata(&did, &secret_key_bytes) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
error!("createAccount: Error creating refresh token: {:?}", e);
|
||||
@@ -821,7 +707,7 @@ pub async fn create_account(
|
||||
refresh_expires_at: refresh_meta.expires_at,
|
||||
login_type: tranquil_db_traits::LoginType::Modern,
|
||||
mfa_verified: false,
|
||||
scope: None,
|
||||
scope: Some("transition:generic transition:chat.bsky".to_string()),
|
||||
controller_did: None,
|
||||
app_password_name: None,
|
||||
};
|
||||
+37
-37
@@ -1,12 +1,12 @@
|
||||
use crate::api::{ApiError, DidResponse, EmptyResponse};
|
||||
use crate::auth::{Auth, NotTakendown};
|
||||
use crate::plc::signing_key_to_did_key;
|
||||
use crate::rate_limit::{
|
||||
use tranquil_pds::api::{ApiError, DidResponse, EmptyResponse};
|
||||
use tranquil_pds::auth::{Auth, NotTakendown};
|
||||
use tranquil_pds::plc::signing_key_to_did_key;
|
||||
use tranquil_pds::rate_limit::{
|
||||
HandleUpdateDailyLimit, HandleUpdateLimit, check_user_rate_limit_with_message,
|
||||
};
|
||||
use crate::state::AppState;
|
||||
use crate::types::Handle;
|
||||
use crate::util::get_header_str;
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::Handle;
|
||||
use tranquil_pds::util::get_header_str;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, Query, State},
|
||||
@@ -42,7 +42,7 @@ pub async fn resolve_handle(
|
||||
if handle_str.is_empty() {
|
||||
return ApiError::InvalidRequest("handle is required".into()).into_response();
|
||||
}
|
||||
let cache_key = crate::cache_keys::handle_key(handle_str);
|
||||
let cache_key = tranquil_pds::cache_keys::handle_key(handle_str);
|
||||
if let Some(did) = state.cache.get(&cache_key).await {
|
||||
return DidResponse::response(did).into_response();
|
||||
}
|
||||
@@ -61,7 +61,7 @@ pub async fn resolve_handle(
|
||||
.await;
|
||||
DidResponse::response(row.did).into_response()
|
||||
}
|
||||
Ok(None) => match crate::handle::resolve_handle(handle.as_str()).await {
|
||||
Ok(None) => match tranquil_pds::handle::resolve_handle(handle.as_str()).await {
|
||||
Ok(did) => {
|
||||
let _ = state
|
||||
.cache
|
||||
@@ -148,7 +148,7 @@ pub async fn well_known_did(State(state): State<AppState>, headers: HeaderMap) -
|
||||
"id": did,
|
||||
"service": [{
|
||||
"id": "#atproto_pds",
|
||||
"type": crate::plc::ServiceType::Pds.as_str(),
|
||||
"type": tranquil_pds::plc::ServiceType::Pds.as_str(),
|
||||
"serviceEndpoint": format!("https://{}", hostname)
|
||||
}]
|
||||
}))
|
||||
@@ -158,7 +158,7 @@ pub async fn well_known_did(State(state): State<AppState>, headers: HeaderMap) -
|
||||
async fn serve_handle_did_doc(state: &AppState, handle: &str, hostname: &str) -> Response {
|
||||
let encoded_handle = handle.replace(':', "%3A");
|
||||
let expected_did = format!("did:web:{}", encoded_handle);
|
||||
let expected_did_typed: crate::types::Did = match expected_did.parse() {
|
||||
let expected_did_typed: tranquil_pds::types::Did = match expected_did.parse() {
|
||||
Ok(d) => d,
|
||||
Err(_) => return ApiError::InvalidRequest("Invalid DID format".into()).into_response(),
|
||||
};
|
||||
@@ -216,7 +216,7 @@ async fn serve_handle_did_doc(state: &AppState, handle: &str, hostname: &str) ->
|
||||
})).collect::<Vec<_>>(),
|
||||
"service": [{
|
||||
"id": "#atproto_pds",
|
||||
"type": crate::plc::ServiceType::Pds.as_str(),
|
||||
"type": tranquil_pds::plc::ServiceType::Pds.as_str(),
|
||||
"serviceEndpoint": service_endpoint
|
||||
}]
|
||||
}))
|
||||
@@ -229,7 +229,7 @@ async fn serve_handle_did_doc(state: &AppState, handle: &str, hostname: &str) ->
|
||||
Err(_) => return ApiError::InternalError(None).into_response(),
|
||||
};
|
||||
let key_bytes: Vec<u8> =
|
||||
match crate::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) {
|
||||
match tranquil_pds::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) {
|
||||
Ok(k) => k,
|
||||
Err(_) => {
|
||||
return ApiError::InternalError(None).into_response();
|
||||
@@ -269,7 +269,7 @@ async fn serve_handle_did_doc(state: &AppState, handle: &str, hostname: &str) ->
|
||||
}],
|
||||
"service": [{
|
||||
"id": "#atproto_pds",
|
||||
"type": crate::plc::ServiceType::Pds.as_str(),
|
||||
"type": tranquil_pds::plc::ServiceType::Pds.as_str(),
|
||||
"serviceEndpoint": service_endpoint
|
||||
}]
|
||||
}))
|
||||
@@ -351,7 +351,7 @@ pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<Stri
|
||||
})).collect::<Vec<_>>(),
|
||||
"service": [{
|
||||
"id": "#atproto_pds",
|
||||
"type": crate::plc::ServiceType::Pds.as_str(),
|
||||
"type": tranquil_pds::plc::ServiceType::Pds.as_str(),
|
||||
"serviceEndpoint": service_endpoint
|
||||
}]
|
||||
}))
|
||||
@@ -364,7 +364,7 @@ pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<Stri
|
||||
Err(_) => return ApiError::InternalError(None).into_response(),
|
||||
};
|
||||
let key_bytes: Vec<u8> =
|
||||
match crate::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) {
|
||||
match tranquil_pds::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) {
|
||||
Ok(k) => k,
|
||||
Err(_) => {
|
||||
return ApiError::InternalError(None).into_response();
|
||||
@@ -404,7 +404,7 @@ pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<Stri
|
||||
}],
|
||||
"service": [{
|
||||
"id": "#atproto_pds",
|
||||
"type": crate::plc::ServiceType::Pds.as_str(),
|
||||
"type": tranquil_pds::plc::ServiceType::Pds.as_str(),
|
||||
"serviceEndpoint": service_endpoint
|
||||
}]
|
||||
}))
|
||||
@@ -480,7 +480,7 @@ pub async fn verify_did_web(
|
||||
let path = parts[3..].join("/");
|
||||
format!("{}://{}/{}/did.json", scheme, domain, path)
|
||||
};
|
||||
let client = crate::api::proxy_client::did_resolution_client();
|
||||
let client = tranquil_pds::api::proxy_client::did_resolution_client();
|
||||
let resp = client
|
||||
.get(&url)
|
||||
.send()
|
||||
@@ -503,7 +503,7 @@ pub async fn verify_did_web(
|
||||
))?;
|
||||
let pds_endpoint = format!("https://{}", hostname);
|
||||
let has_valid_service = services.iter().any(|s| {
|
||||
s["type"] == crate::plc::ServiceType::Pds.as_str() && s["serviceEndpoint"] == pds_endpoint
|
||||
s["type"] == tranquil_pds::plc::ServiceType::Pds.as_str() && s["serviceEndpoint"] == pds_endpoint
|
||||
});
|
||||
if !has_valid_service {
|
||||
return Err(DidWebVerifyError::PdsNotListed(pds_endpoint));
|
||||
@@ -600,7 +600,7 @@ pub async fn get_recommended_did_credentials(
|
||||
verification_methods: VerificationMethods { atproto: did_key },
|
||||
services: Services {
|
||||
atproto_pds: AtprotoPds {
|
||||
service_type: crate::plc::ServiceType::Pds.as_str().to_string(),
|
||||
service_type: tranquil_pds::plc::ServiceType::Pds.as_str().to_string(),
|
||||
endpoint: pds_endpoint,
|
||||
},
|
||||
},
|
||||
@@ -619,10 +619,10 @@ pub async fn update_handle(
|
||||
auth: Auth<NotTakendown>,
|
||||
Json(input): Json<UpdateHandleInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
if let Err(e) = crate::auth::scope_check::check_identity_scope(
|
||||
if let Err(e) = tranquil_pds::auth::scope_check::check_identity_scope(
|
||||
&auth.auth_source,
|
||||
auth.scope.as_deref(),
|
||||
crate::oauth::scopes::IdentityAttr::Handle,
|
||||
tranquil_pds::oauth::scopes::IdentityAttr::Handle,
|
||||
) {
|
||||
return Ok(e);
|
||||
}
|
||||
@@ -672,7 +672,7 @@ pub async fn update_handle(
|
||||
"Handle segment cannot start or end with hyphen".into(),
|
||||
)));
|
||||
}
|
||||
if crate::moderation::has_explicit_slur(&new_handle) {
|
||||
if tranquil_pds::moderation::has_explicit_slur(&new_handle) {
|
||||
return Err(ApiError::InvalidHandle(Some(
|
||||
"Inappropriate language in handle".into(),
|
||||
)));
|
||||
@@ -704,7 +704,7 @@ pub async fn update_handle(
|
||||
Err(_) => return Err(ApiError::InvalidHandle(None)),
|
||||
};
|
||||
if let Err(e) =
|
||||
crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle_typed))
|
||||
tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle_typed))
|
||||
.await
|
||||
{
|
||||
warn!("Failed to sequence identity event for handle update: {}", e);
|
||||
@@ -730,19 +730,19 @@ pub async fn update_handle(
|
||||
Err(_) => return Err(ApiError::InvalidHandle(None)),
|
||||
};
|
||||
if let Err(e) =
|
||||
crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle_typed))
|
||||
tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle_typed))
|
||||
.await
|
||||
{
|
||||
warn!("Failed to sequence identity event for handle update: {}", e);
|
||||
}
|
||||
return Ok(EmptyResponse::ok().into_response());
|
||||
}
|
||||
match crate::handle::verify_handle_ownership(&new_handle, &did).await {
|
||||
match tranquil_pds::handle::verify_handle_ownership(&new_handle, &did).await {
|
||||
Ok(()) => {}
|
||||
Err(crate::handle::HandleResolutionError::NotFound) => {
|
||||
Err(tranquil_pds::handle::HandleResolutionError::NotFound) => {
|
||||
return Err(ApiError::HandleNotAvailable(None));
|
||||
}
|
||||
Err(crate::handle::HandleResolutionError::DidMismatch { expected, actual }) => {
|
||||
Err(tranquil_pds::handle::HandleResolutionError::DidMismatch { expected, actual }) => {
|
||||
return Err(ApiError::HandleNotAvailable(Some(format!(
|
||||
"Handle points to different DID. Expected {}, got {}",
|
||||
expected, actual
|
||||
@@ -781,15 +781,15 @@ pub async fn update_handle(
|
||||
if !current_handle.is_empty() {
|
||||
let _ = state
|
||||
.cache
|
||||
.delete(&crate::cache_keys::handle_key(¤t_handle))
|
||||
.delete(&tranquil_pds::cache_keys::handle_key(¤t_handle))
|
||||
.await;
|
||||
}
|
||||
let _ = state
|
||||
.cache
|
||||
.delete(&crate::cache_keys::handle_key(&handle))
|
||||
.delete(&tranquil_pds::cache_keys::handle_key(&handle))
|
||||
.await;
|
||||
if let Err(e) =
|
||||
crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle_typed)).await
|
||||
tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle_typed)).await
|
||||
{
|
||||
warn!("Failed to sequence identity event for handle update: {}", e);
|
||||
}
|
||||
@@ -801,7 +801,7 @@ pub async fn update_handle(
|
||||
|
||||
pub async fn update_plc_handle(
|
||||
state: &AppState,
|
||||
did: &crate::types::Did,
|
||||
did: &tranquil_pds::types::Did,
|
||||
new_handle: &Handle,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
if !did.as_str().starts_with("did:plc:") {
|
||||
@@ -811,20 +811,20 @@ pub async fn update_plc_handle(
|
||||
Some(r) => r,
|
||||
None => return Ok(()),
|
||||
};
|
||||
let key_bytes = crate::config::decrypt_key(&user_row.key_bytes, user_row.encryption_version)?;
|
||||
let key_bytes = tranquil_pds::config::decrypt_key(&user_row.key_bytes, user_row.encryption_version)?;
|
||||
let signing_key = k256::ecdsa::SigningKey::from_slice(&key_bytes)?;
|
||||
let plc_client = crate::plc::PlcClient::with_cache(None, Some(state.cache.clone()));
|
||||
let plc_client = state.plc_client();
|
||||
let last_op = plc_client.get_last_op(did).await?;
|
||||
let new_also_known_as = vec![format!("at://{}", new_handle)];
|
||||
let update_op =
|
||||
crate::plc::create_update_op(&last_op, None, None, Some(new_also_known_as), None)?;
|
||||
let signed_op = crate::plc::sign_operation(&update_op, &signing_key)?;
|
||||
tranquil_pds::plc::create_update_op(&last_op, None, None, Some(new_also_known_as), None)?;
|
||||
let signed_op = tranquil_pds::plc::sign_operation(&update_op, &signing_key)?;
|
||||
plc_client.send_operation(did, &signed_op).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn well_known_atproto_did(State(state): State<AppState>, headers: HeaderMap) -> Response {
|
||||
let host = match crate::util::get_header_str(&headers, http::header::HOST) {
|
||||
let host = match tranquil_pds::util::get_header_str(&headers, http::header::HOST) {
|
||||
Some(h) => h,
|
||||
None => return (StatusCode::BAD_REQUEST, "Missing host header").into_response(),
|
||||
};
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
use crate::rate_limit::{HandleVerificationLimit, RateLimited};
|
||||
use crate::types::{Did, Handle};
|
||||
use tranquil_pds::rate_limit::{HandleVerificationLimit, RateLimited};
|
||||
use tranquil_pds::types::{Did, Handle};
|
||||
use axum::{
|
||||
Json,
|
||||
response::{IntoResponse, Response},
|
||||
@@ -29,7 +29,7 @@ pub async fn verify_handle_ownership(
|
||||
let handle_str = input.handle.as_str();
|
||||
let did_str = input.did.as_str();
|
||||
|
||||
let dns_mismatch = match crate::handle::resolve_handle_dns(handle_str).await {
|
||||
let dns_mismatch = match tranquil_pds::handle::resolve_handle_dns(handle_str).await {
|
||||
Ok(did) if did == did_str => {
|
||||
return Json(VerifyHandleOwnershipOutput {
|
||||
verified: true,
|
||||
@@ -45,7 +45,7 @@ pub async fn verify_handle_ownership(
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
match crate::handle::resolve_handle_http(handle_str).await {
|
||||
match tranquil_pds::handle::resolve_handle_http(handle_str).await {
|
||||
Ok(did) if did == did_str => Json(VerifyHandleOwnershipOutput {
|
||||
verified: true,
|
||||
method: Some("http".to_string()),
|
||||
@@ -2,6 +2,7 @@ pub mod account;
|
||||
pub mod did;
|
||||
pub mod handle;
|
||||
pub mod plc;
|
||||
pub mod provision;
|
||||
|
||||
pub use account::create_account;
|
||||
pub use did::{
|
||||
+8
-8
@@ -1,7 +1,7 @@
|
||||
use crate::api::EmptyResponse;
|
||||
use crate::api::error::{ApiError, DbResultExt};
|
||||
use crate::auth::{Auth, Permissive};
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::api::EmptyResponse;
|
||||
use tranquil_pds::api::error::{ApiError, DbResultExt};
|
||||
use tranquil_pds::auth::{Auth, Permissive};
|
||||
use tranquil_pds::state::AppState;
|
||||
use axum::{
|
||||
extract::State,
|
||||
response::{IntoResponse, Response},
|
||||
@@ -10,17 +10,17 @@ use chrono::{Duration, Utc};
|
||||
use tracing::{info, warn};
|
||||
|
||||
fn generate_plc_token() -> String {
|
||||
crate::util::generate_token_code()
|
||||
tranquil_pds::util::generate_token_code()
|
||||
}
|
||||
|
||||
pub async fn request_plc_operation_signature(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Permissive>,
|
||||
) -> Result<Response, ApiError> {
|
||||
if let Err(e) = crate::auth::scope_check::check_identity_scope(
|
||||
if let Err(e) = tranquil_pds::auth::scope_check::check_identity_scope(
|
||||
&auth.auth_source,
|
||||
auth.scope.as_deref(),
|
||||
crate::oauth::scopes::IdentityAttr::Wildcard,
|
||||
tranquil_pds::oauth::scopes::IdentityAttr::Wildcard,
|
||||
) {
|
||||
return Ok(e);
|
||||
}
|
||||
@@ -41,7 +41,7 @@ pub async fn request_plc_operation_signature(
|
||||
.log_db_err("creating PLC token")?;
|
||||
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_plc_operation(
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_plc_operation(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
user_id,
|
||||
+10
-10
@@ -1,9 +1,9 @@
|
||||
use crate::api::ApiError;
|
||||
use crate::api::error::DbResultExt;
|
||||
use crate::auth::{Auth, Permissive};
|
||||
use crate::circuit_breaker::with_circuit_breaker;
|
||||
use crate::plc::{PlcClient, PlcError, PlcService, ServiceType, create_update_op, sign_operation};
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::api::ApiError;
|
||||
use tranquil_pds::api::error::DbResultExt;
|
||||
use tranquil_pds::auth::{Auth, Permissive};
|
||||
use tranquil_pds::circuit_breaker::with_circuit_breaker;
|
||||
use tranquil_pds::plc::{PlcError, PlcService, ServiceType, create_update_op, sign_operation};
|
||||
use tranquil_pds::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -44,10 +44,10 @@ pub async fn sign_plc_operation(
|
||||
auth: Auth<Permissive>,
|
||||
Json(input): Json<SignPlcOperationInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
if let Err(e) = crate::auth::scope_check::check_identity_scope(
|
||||
if let Err(e) = tranquil_pds::auth::scope_check::check_identity_scope(
|
||||
&auth.auth_source,
|
||||
auth.scope.as_deref(),
|
||||
crate::oauth::scopes::IdentityAttr::Wildcard,
|
||||
tranquil_pds::oauth::scopes::IdentityAttr::Wildcard,
|
||||
) {
|
||||
return Ok(e);
|
||||
}
|
||||
@@ -86,7 +86,7 @@ pub async fn sign_plc_operation(
|
||||
.log_db_err("fetching user key")?
|
||||
.ok_or_else(|| ApiError::InternalError(Some("User signing key not found".into())))?;
|
||||
|
||||
let key_bytes = crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version)
|
||||
let key_bytes = tranquil_pds::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version)
|
||||
.map_err(|e| {
|
||||
error!("Failed to decrypt user key: {}", e);
|
||||
ApiError::InternalError(None)
|
||||
@@ -97,7 +97,7 @@ pub async fn sign_plc_operation(
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let plc_client = PlcClient::with_cache(None, Some(state.cache.clone()));
|
||||
let plc_client = state.plc_client();
|
||||
let did_clone = did.clone();
|
||||
let last_op = with_circuit_breaker(&state.circuit_breakers.plc_directory, || async {
|
||||
plc_client.get_last_op(&did_clone).await
|
||||
+14
-14
@@ -1,9 +1,9 @@
|
||||
use crate::api::error::DbResultExt;
|
||||
use crate::api::{ApiError, EmptyResponse};
|
||||
use crate::auth::{Auth, Permissive};
|
||||
use crate::circuit_breaker::with_circuit_breaker;
|
||||
use crate::plc::{PlcClient, signing_key_to_did_key, validate_plc_operation};
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::api::error::DbResultExt;
|
||||
use tranquil_pds::api::{ApiError, EmptyResponse};
|
||||
use tranquil_pds::auth::{Auth, Permissive};
|
||||
use tranquil_pds::circuit_breaker::with_circuit_breaker;
|
||||
use tranquil_pds::plc::{signing_key_to_did_key, validate_plc_operation};
|
||||
use tranquil_pds::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -24,10 +24,10 @@ pub async fn submit_plc_operation(
|
||||
auth: Auth<Permissive>,
|
||||
Json(input): Json<SubmitPlcOperationInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
if let Err(e) = crate::auth::scope_check::check_identity_scope(
|
||||
if let Err(e) = tranquil_pds::auth::scope_check::check_identity_scope(
|
||||
&auth.auth_source,
|
||||
auth.scope.as_deref(),
|
||||
crate::oauth::scopes::IdentityAttr::Wildcard,
|
||||
tranquil_pds::oauth::scopes::IdentityAttr::Wildcard,
|
||||
) {
|
||||
return Ok(e);
|
||||
}
|
||||
@@ -57,7 +57,7 @@ pub async fn submit_plc_operation(
|
||||
.log_db_err("fetching user key")?
|
||||
.ok_or_else(|| ApiError::InternalError(Some("User signing key not found".into())))?;
|
||||
|
||||
let key_bytes = crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version)
|
||||
let key_bytes = tranquil_pds::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version)
|
||||
.map_err(|e| {
|
||||
error!("Failed to decrypt user key: {}", e);
|
||||
ApiError::InternalError(None)
|
||||
@@ -89,7 +89,7 @@ pub async fn submit_plc_operation(
|
||||
{
|
||||
let service_type = pds.get("type").and_then(|v| v.as_str());
|
||||
let endpoint = pds.get("endpoint").and_then(|v| v.as_str());
|
||||
if service_type != Some(crate::plc::ServiceType::Pds.as_str()) {
|
||||
if service_type != Some(tranquil_pds::plc::ServiceType::Pds.as_str()) {
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Incorrect type on atproto_pds service".into(),
|
||||
));
|
||||
@@ -120,7 +120,7 @@ pub async fn submit_plc_operation(
|
||||
));
|
||||
}
|
||||
}
|
||||
let plc_client = PlcClient::with_cache(None, Some(state.cache.clone()));
|
||||
let plc_client = state.plc_client();
|
||||
let operation_clone = input.operation.clone();
|
||||
let did_clone = did.clone();
|
||||
with_circuit_breaker(&state.circuit_breakers.plc_directory, || async {
|
||||
@@ -147,15 +147,15 @@ pub async fn submit_plc_operation(
|
||||
}
|
||||
let _ = state
|
||||
.cache
|
||||
.delete(&crate::cache_keys::handle_key(&user.handle))
|
||||
.delete(&tranquil_pds::cache_keys::handle_key(&user.handle))
|
||||
.await;
|
||||
let _ = state
|
||||
.cache
|
||||
.delete(&crate::cache_keys::plc_doc_key(did))
|
||||
.delete(&tranquil_pds::cache_keys::plc_doc_key(did))
|
||||
.await;
|
||||
let _ = state
|
||||
.cache
|
||||
.delete(&crate::cache_keys::plc_data_key(did))
|
||||
.delete(&tranquil_pds::cache_keys::plc_data_key(did))
|
||||
.await;
|
||||
if state.did_resolver.refresh_did(did).await.is_none() {
|
||||
warn!(did = %did, "Failed to refresh DID cache after PLC update");
|
||||
@@ -0,0 +1,117 @@
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::repo_ops::create_signed_commit;
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::Did;
|
||||
use jacquard_common::types::{integer::LimitedU32, string::Tid};
|
||||
use jacquard_repo::{mst::Mst, storage::BlockStore};
|
||||
use k256::ecdsa::SigningKey;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct PlcDidResult {
|
||||
pub did: Did,
|
||||
pub signing_key_bytes: Vec<u8>,
|
||||
pub signing_key: SigningKey,
|
||||
}
|
||||
|
||||
pub async fn create_plc_did(state: &AppState, handle: &str) -> Result<PlcDidResult, ApiError> {
|
||||
use k256::SecretKey;
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
let secret_key = SecretKey::random(&mut OsRng);
|
||||
let secret_key_bytes = secret_key.to_bytes().to_vec();
|
||||
let signing_key = SigningKey::from_slice(&secret_key_bytes).map_err(|e| {
|
||||
tracing::error!("Error creating signing key: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let did_str = submit_plc_genesis(state, &signing_key, handle).await?;
|
||||
let did: Did = did_str
|
||||
.parse()
|
||||
.map_err(|_| ApiError::InternalError(Some("PLC genesis returned invalid DID".into())))?;
|
||||
|
||||
Ok(PlcDidResult {
|
||||
did,
|
||||
signing_key_bytes: secret_key_bytes,
|
||||
signing_key,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn submit_plc_genesis(
|
||||
state: &AppState,
|
||||
signing_key: &SigningKey,
|
||||
handle: &str,
|
||||
) -> Result<String, ApiError> {
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let pds_endpoint = format!("https://{}", hostname);
|
||||
|
||||
let rotation_key = tranquil_config::get()
|
||||
.secrets
|
||||
.plc_rotation_key
|
||||
.clone()
|
||||
.unwrap_or_else(|| tranquil_pds::plc::signing_key_to_did_key(signing_key));
|
||||
|
||||
let genesis_result =
|
||||
tranquil_pds::plc::create_genesis_operation(signing_key, &rotation_key, handle, &pds_endpoint)
|
||||
.map_err(|e| {
|
||||
tracing::error!("Error creating PLC genesis operation: {:?}", e);
|
||||
ApiError::InternalError(Some("Failed to create PLC operation".into()))
|
||||
})?;
|
||||
|
||||
state
|
||||
.plc_client()
|
||||
.send_operation(&genesis_result.did, &genesis_result.signed_operation)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to submit PLC genesis operation: {:?}", e);
|
||||
ApiError::UpstreamErrorMsg(format!("Failed to register DID with PLC directory: {}", e))
|
||||
})?;
|
||||
|
||||
tracing::info!(did = %genesis_result.did, "Registered DID with PLC directory");
|
||||
Ok(genesis_result.did)
|
||||
}
|
||||
|
||||
pub struct GenesisRepo {
|
||||
pub encrypted_key_bytes: Vec<u8>,
|
||||
pub commit_cid: cid::Cid,
|
||||
pub mst_root_cid: cid::Cid,
|
||||
pub repo_rev: String,
|
||||
pub genesis_block_cids: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
pub async fn init_genesis_repo(
|
||||
state: &AppState,
|
||||
did: &Did,
|
||||
signing_key: &SigningKey,
|
||||
signing_key_bytes: &[u8],
|
||||
) -> Result<GenesisRepo, ApiError> {
|
||||
let encrypted_key_bytes = tranquil_pds::config::encrypt_key(signing_key_bytes).map_err(|e| {
|
||||
tracing::error!("Error encrypting signing key: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let mst = Mst::new(Arc::new(state.block_store.clone()));
|
||||
let mst_root = mst.persist().await.map_err(|e| {
|
||||
tracing::error!("Error persisting MST: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let rev = Tid::now(LimitedU32::MIN);
|
||||
let (commit_bytes, _sig) = create_signed_commit(did, mst_root, rev.as_ref(), None, signing_key)
|
||||
.map_err(|e| {
|
||||
tracing::error!("Error creating genesis commit: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
let commit_cid: cid::Cid = state.block_store.put(&commit_bytes).await.map_err(|e| {
|
||||
tracing::error!("Error saving genesis commit: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
|
||||
Ok(GenesisRepo {
|
||||
encrypted_key_bytes,
|
||||
commit_cid,
|
||||
mst_root_cid: mst_root,
|
||||
repo_rev: rev.as_ref().to_string(),
|
||||
genesis_block_cids: vec![mst_root.to_bytes(), commit_cid.to_bytes()],
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
pub mod actor;
|
||||
pub mod admin;
|
||||
pub mod age_assurance;
|
||||
pub mod backup;
|
||||
pub mod delegation;
|
||||
pub mod discord_webhook;
|
||||
pub mod identity;
|
||||
pub mod moderation;
|
||||
pub mod notification_prefs;
|
||||
pub mod repo;
|
||||
pub mod server;
|
||||
pub mod telegram_webhook;
|
||||
pub mod temp;
|
||||
pub mod verification;
|
||||
|
||||
use tranquil_pds::state::AppState;
|
||||
|
||||
pub fn api_routes() -> axum::Router<AppState> {
|
||||
use axum::routing::{get, post};
|
||||
|
||||
axum::Router::new()
|
||||
.route("/_health", get(server::health))
|
||||
.route(
|
||||
"/com.atproto.server.describeServer",
|
||||
get(server::describe_server),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.server.createAccount",
|
||||
post(identity::create_account),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.server.createSession",
|
||||
post(server::create_session),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.server.getSession",
|
||||
get(server::get_session),
|
||||
)
|
||||
.route("/_account.listSessions", get(server::list_sessions))
|
||||
.route("/_account.revokeSession", post(server::revoke_session))
|
||||
.route(
|
||||
"/_account.revokeAllSessions",
|
||||
post(server::revoke_all_sessions),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.server.deleteSession",
|
||||
post(server::delete_session),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.server.refreshSession",
|
||||
post(server::refresh_session),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.server.confirmSignup",
|
||||
post(server::confirm_signup),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.server.resendVerification",
|
||||
post(server::resend_verification),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.server.getServiceAuth",
|
||||
get(server::get_service_auth),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.identity.resolveHandle",
|
||||
get(identity::resolve_handle),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.repo.createRecord",
|
||||
post(repo::create_record),
|
||||
)
|
||||
.route("/com.atproto.repo.putRecord", post(repo::put_record))
|
||||
.route("/com.atproto.repo.getRecord", get(repo::get_record))
|
||||
.route(
|
||||
"/com.atproto.repo.deleteRecord",
|
||||
post(repo::delete_record),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.repo.listRecords",
|
||||
get(repo::list_records),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.repo.describeRepo",
|
||||
get(repo::describe_repo),
|
||||
)
|
||||
.route("/com.atproto.repo.uploadBlob", post(repo::upload_blob))
|
||||
.route(
|
||||
"/com.atproto.repo.applyWrites",
|
||||
post(repo::apply_writes),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.server.checkAccountStatus",
|
||||
get(server::check_account_status),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.identity.getRecommendedDidCredentials",
|
||||
get(identity::get_recommended_did_credentials),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.repo.listMissingBlobs",
|
||||
get(repo::list_missing_blobs),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.moderation.createReport",
|
||||
post(moderation::create_report),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.admin.getAccountInfo",
|
||||
get(admin::get_account_info),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.admin.getAccountInfos",
|
||||
get(admin::get_account_infos),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.admin.searchAccounts",
|
||||
get(admin::search_accounts),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.server.activateAccount",
|
||||
post(server::activate_account),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.server.deactivateAccount",
|
||||
post(server::deactivate_account),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.server.requestAccountDelete",
|
||||
post(server::request_account_delete),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.server.deleteAccount",
|
||||
post(server::delete_account),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.server.requestPasswordReset",
|
||||
post(server::request_password_reset),
|
||||
)
|
||||
.route(
|
||||
"/com.atproto.server.resetPassword",
|
||||
post(server::reset_password),
|
||||
)
|
||||
.route("/_account.changePassword", post(server::change_password))
|
||||
.route("/_account.removePassword", post(server::remove_password))
|
||||
.route("/_account.setPassword", post(server::set_password))
|
||||
.route("/_account.getPasswordStatus", get(server::get_password_status))
|
||||
.route("/_account.getReauthStatus", get(server::get_reauth_status))
|
||||
.route("/_account.reauthPassword", post(server::reauth_password))
|
||||
.route("/_account.reauthTotp", post(server::reauth_totp))
|
||||
.route("/_account.reauthPasskeyStart", post(server::reauth_passkey_start))
|
||||
.route("/_account.reauthPasskeyFinish", post(server::reauth_passkey_finish))
|
||||
.route("/_account.getLegacyLoginPreference", get(server::get_legacy_login_preference))
|
||||
.route("/_account.updateLegacyLoginPreference", post(server::update_legacy_login_preference))
|
||||
.route("/_account.updateLocale", post(server::update_locale))
|
||||
.route("/_account.listTrustedDevices", get(server::list_trusted_devices))
|
||||
.route("/_account.revokeTrustedDevice", post(server::revoke_trusted_device))
|
||||
.route("/_account.updateTrustedDevice", post(server::update_trusted_device))
|
||||
.route("/_account.createPasskeyAccount", post(server::create_passkey_account))
|
||||
.route("/_account.startPasskeyRegistrationForSetup", post(server::start_passkey_registration_for_setup))
|
||||
.route("/_account.completePasskeySetup", post(server::complete_passkey_setup))
|
||||
.route("/_account.requestPasskeyRecovery", post(server::request_passkey_recovery))
|
||||
.route("/_account.recoverPasskeyAccount", post(server::recover_passkey_account))
|
||||
.route("/_account.updateDidDocument", post(server::update_did_document))
|
||||
.route("/_account.getDidDocument", get(server::get_did_document))
|
||||
.route("/com.atproto.server.requestEmailUpdate", post(server::request_email_update))
|
||||
.route("/_checkEmailVerified", post(server::check_email_verified))
|
||||
.route("/_checkChannelVerified", post(server::check_channel_verified))
|
||||
.route("/com.atproto.server.confirmEmail", post(server::confirm_email))
|
||||
.route("/com.atproto.server.updateEmail", post(server::update_email))
|
||||
.route("/_account.authorizeEmailUpdate", get(server::authorize_email_update))
|
||||
.route("/_account.checkEmailUpdateStatus", get(server::check_email_update_status))
|
||||
.route("/_account.checkEmailInUse", post(server::check_email_in_use))
|
||||
.route("/_account.checkCommsChannelInUse", post(server::check_comms_channel_in_use))
|
||||
.route("/com.atproto.server.reserveSigningKey", post(server::reserve_signing_key))
|
||||
.route("/com.atproto.server.verifyMigrationEmail", post(server::verify_migration_email))
|
||||
.route("/com.atproto.server.resendMigrationVerification", post(server::resend_migration_verification))
|
||||
.route("/com.atproto.identity.updateHandle", post(identity::update_handle))
|
||||
.route("/com.atproto.identity.requestPlcOperationSignature", post(identity::request_plc_operation_signature))
|
||||
.route("/com.atproto.identity.signPlcOperation", post(identity::sign_plc_operation))
|
||||
.route("/com.atproto.identity.submitPlcOperation", post(identity::submit_plc_operation))
|
||||
.route("/_identity.verifyHandleOwnership", post(identity::verify_handle_ownership))
|
||||
.route("/com.atproto.repo.importRepo", post(repo::import_repo))
|
||||
.route("/com.atproto.admin.deleteAccount", post(admin::delete_account))
|
||||
.route("/com.atproto.admin.updateAccountEmail", post(admin::update_account_email))
|
||||
.route("/com.atproto.admin.updateAccountHandle", post(admin::update_account_handle))
|
||||
.route("/com.atproto.admin.updateAccountPassword", post(admin::update_account_password))
|
||||
.route("/com.atproto.server.listAppPasswords", get(server::list_app_passwords))
|
||||
.route("/com.atproto.server.createAppPassword", post(server::create_app_password))
|
||||
.route("/com.atproto.server.revokeAppPassword", post(server::revoke_app_password))
|
||||
.route("/com.atproto.server.createInviteCode", post(server::create_invite_code))
|
||||
.route("/com.atproto.server.createInviteCodes", post(server::create_invite_codes))
|
||||
.route("/com.atproto.server.getAccountInviteCodes", get(server::get_account_invite_codes))
|
||||
.route("/com.atproto.server.createTotpSecret", post(server::create_totp_secret))
|
||||
.route("/com.atproto.server.enableTotp", post(server::enable_totp))
|
||||
.route("/com.atproto.server.disableTotp", post(server::disable_totp))
|
||||
.route("/com.atproto.server.getTotpStatus", get(server::get_totp_status))
|
||||
.route("/com.atproto.server.regenerateBackupCodes", post(server::regenerate_backup_codes))
|
||||
.route("/com.atproto.server.startPasskeyRegistration", post(server::start_passkey_registration))
|
||||
.route("/com.atproto.server.finishPasskeyRegistration", post(server::finish_passkey_registration))
|
||||
.route("/com.atproto.server.listPasskeys", get(server::list_passkeys))
|
||||
.route("/com.atproto.server.deletePasskey", post(server::delete_passkey))
|
||||
.route("/com.atproto.server.updatePasskey", post(server::update_passkey))
|
||||
.route("/com.atproto.admin.getInviteCodes", get(admin::get_invite_codes))
|
||||
.route("/_admin.getServerStats", get(admin::get_server_stats))
|
||||
.route("/_server.getConfig", get(admin::get_server_config))
|
||||
.route("/_admin.updateServerConfig", post(admin::update_server_config))
|
||||
.route("/com.atproto.admin.disableAccountInvites", post(admin::disable_account_invites))
|
||||
.route("/com.atproto.admin.enableAccountInvites", post(admin::enable_account_invites))
|
||||
.route("/com.atproto.admin.disableInviteCodes", post(admin::disable_invite_codes))
|
||||
.route("/com.atproto.admin.getSubjectStatus", get(admin::get_subject_status))
|
||||
.route("/com.atproto.admin.updateSubjectStatus", post(admin::update_subject_status))
|
||||
.route("/com.atproto.admin.sendEmail", post(admin::send_email))
|
||||
.route("/app.bsky.actor.getPreferences", get(actor::get_preferences))
|
||||
.route("/app.bsky.actor.putPreferences", post(actor::put_preferences))
|
||||
.route("/com.atproto.temp.checkSignupQueue", get(temp::check_signup_queue))
|
||||
.route("/com.atproto.temp.dereferenceScope", post(temp::dereference_scope))
|
||||
.route("/_account.getNotificationPrefs", get(notification_prefs::get_notification_prefs))
|
||||
.route("/_account.updateNotificationPrefs", post(notification_prefs::update_notification_prefs))
|
||||
.route("/_account.getNotificationHistory", get(notification_prefs::get_notification_history))
|
||||
.route("/_account.confirmChannelVerification", post(verification::confirm_channel_verification))
|
||||
.route("/_account.verifyToken", post(server::verify_token))
|
||||
.route("/_delegation.listControllers", get(delegation::list_controllers))
|
||||
.route("/_delegation.addController", post(delegation::add_controller))
|
||||
.route("/_delegation.removeController", post(delegation::remove_controller))
|
||||
.route("/_delegation.updateControllerScopes", post(delegation::update_controller_scopes))
|
||||
.route("/_delegation.listControlledAccounts", get(delegation::list_controlled_accounts))
|
||||
.route("/_delegation.getAuditLog", get(delegation::get_audit_log))
|
||||
.route("/_delegation.getScopePresets", get(delegation::get_scope_presets))
|
||||
.route("/_delegation.createDelegatedAccount", post(delegation::create_delegated_account))
|
||||
.route("/_delegation.resolveController", get(delegation::resolve_controller))
|
||||
.route("/_backup.listBackups", get(backup::list_backups))
|
||||
.route("/_backup.getBackup", get(backup::get_backup))
|
||||
.route("/_backup.createBackup", post(backup::create_backup))
|
||||
.route("/_backup.deleteBackup", post(backup::delete_backup))
|
||||
.route("/_backup.setEnabled", post(backup::set_backup_enabled))
|
||||
.route("/_backup.exportBlobs", get(backup::export_blobs))
|
||||
.route("/app.bsky.ageassurance.getState", get(age_assurance::get_state))
|
||||
.route("/app.bsky.unspecced.getAgeAssuranceState", get(age_assurance::get_age_assurance_state))
|
||||
}
|
||||
|
||||
pub fn well_known_api_routes() -> axum::Router<AppState> {
|
||||
use axum::routing::get;
|
||||
|
||||
axum::Router::new()
|
||||
.route("/did.json", get(identity::well_known_did))
|
||||
.route("/atproto-did", get(identity::well_known_atproto_did))
|
||||
}
|
||||
|
||||
pub fn webhook_routes() -> axum::Router<AppState> {
|
||||
use axum::{extract::DefaultBodyLimit, routing::post};
|
||||
|
||||
axum::Router::new()
|
||||
.route(
|
||||
"/webhook/telegram",
|
||||
post(telegram_webhook::handle_telegram_webhook)
|
||||
.layer(DefaultBodyLimit::max(64 * 1024)),
|
||||
)
|
||||
.route(
|
||||
"/webhook/discord",
|
||||
post(discord_webhook::handle_discord_webhook)
|
||||
.layer(DefaultBodyLimit::max(64 * 1024)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn misc_routes() -> axum::Router<AppState> {
|
||||
use axum::routing::get;
|
||||
|
||||
axum::Router::new()
|
||||
.route("/health", get(server::health))
|
||||
.route("/robots.txt", get(server::robots_txt))
|
||||
.route("/favicon.ico", get(server::get_logo))
|
||||
.route("/u/{handle}/did.json", get(identity::user_did_doc))
|
||||
}
|
||||
+8
-8
@@ -1,7 +1,7 @@
|
||||
use crate::api::ApiError;
|
||||
use crate::api::proxy_client::{is_ssrf_safe, proxy_client};
|
||||
use crate::auth::{AnyUser, Auth};
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::api::ApiError;
|
||||
use tranquil_pds::api::proxy_client::{is_ssrf_safe, proxy_client};
|
||||
use tranquil_pds::auth::{AnyUser, Auth};
|
||||
use tranquil_pds::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -94,7 +94,7 @@ pub async fn create_report(
|
||||
|
||||
async fn proxy_to_report_service(
|
||||
state: &AppState,
|
||||
auth_user: &crate::auth::AuthenticatedUser,
|
||||
auth_user: &tranquil_pds::auth::AuthenticatedUser,
|
||||
service_url: &str,
|
||||
service_did: &str,
|
||||
input: &CreateReportInput,
|
||||
@@ -109,7 +109,7 @@ async fn proxy_to_report_service(
|
||||
Some(kb) => kb.clone(),
|
||||
None => match state.user_repo.get_with_key_by_did(&auth_user.did).await {
|
||||
Ok(Some(user_with_key)) => {
|
||||
match crate::config::decrypt_key(
|
||||
match tranquil_pds::config::decrypt_key(
|
||||
&user_with_key.key_bytes,
|
||||
user_with_key.encryption_version,
|
||||
) {
|
||||
@@ -135,7 +135,7 @@ async fn proxy_to_report_service(
|
||||
},
|
||||
};
|
||||
|
||||
let service_token = match crate::auth::create_service_token(
|
||||
let service_token = match tranquil_pds::auth::create_service_token(
|
||||
&auth_user.did,
|
||||
service_did,
|
||||
"com.atproto.moderation.createReport",
|
||||
@@ -211,7 +211,7 @@ async fn proxy_to_report_service(
|
||||
|
||||
async fn create_report_locally(
|
||||
state: &AppState,
|
||||
did: &crate::types::Did,
|
||||
did: &tranquil_pds::types::Did,
|
||||
is_takendown: bool,
|
||||
input: CreateReportInput,
|
||||
) -> Response {
|
||||
+13
-13
@@ -1,6 +1,6 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::{Active, Auth};
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::auth::{Active, Auth};
|
||||
use tranquil_pds::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -142,14 +142,14 @@ pub async fn request_channel_verification(
|
||||
handle: Option<&str>,
|
||||
) -> Result<String, ApiError> {
|
||||
let token =
|
||||
crate::auth::verification_token::generate_channel_update_token(did, channel, identifier);
|
||||
let formatted_token = crate::auth::verification_token::format_token_for_display(&token);
|
||||
tranquil_pds::auth::verification_token::generate_channel_update_token(did, channel, identifier);
|
||||
let formatted_token = tranquil_pds::auth::verification_token::format_token_for_display(&token);
|
||||
|
||||
match channel {
|
||||
CommsChannel::Email => {
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let handle_str = handle.unwrap_or("user");
|
||||
crate::comms::comms_repo::enqueue_email_update(
|
||||
tranquil_pds::comms::comms_repo::enqueue_email_update(
|
||||
state.infra_repo.as_ref(),
|
||||
user_id,
|
||||
identifier,
|
||||
@@ -183,12 +183,12 @@ pub async fn request_channel_verification(
|
||||
.as_ref()
|
||||
.and_then(|p| p.preferred_locale.as_deref())
|
||||
.unwrap_or("en");
|
||||
let strings = crate::comms::get_strings(locale);
|
||||
let body = crate::comms::format_message(
|
||||
let strings = tranquil_pds::comms::get_strings(locale);
|
||||
let body = tranquil_pds::comms::format_message(
|
||||
strings.channel_verification_body,
|
||||
&[("code", &formatted_token), ("verify_link", &verify_link)],
|
||||
);
|
||||
let subject = crate::comms::format_message(
|
||||
let subject = tranquil_pds::comms::format_message(
|
||||
strings.channel_verification_subject,
|
||||
&[("hostname", hostname)],
|
||||
);
|
||||
@@ -277,7 +277,7 @@ pub async fn update_notification_prefs(
|
||||
return Err(ApiError::InvalidRequest("Email cannot be empty".into()));
|
||||
}
|
||||
|
||||
if !crate::api::validation::is_valid_email(&email_clean) {
|
||||
if !tranquil_pds::api::validation::is_valid_email(&email_clean) {
|
||||
return Err(ApiError::InvalidEmail);
|
||||
}
|
||||
|
||||
@@ -310,7 +310,7 @@ pub async fn update_notification_prefs(
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
|
||||
info!(did = %auth.did, "Cleared Discord");
|
||||
} else if !crate::api::validation::is_valid_discord_username(&discord_clean) {
|
||||
} else if !tranquil_pds::api::validation::is_valid_discord_username(&discord_clean) {
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Invalid Discord username. Must be 2-32 lowercase characters (letters, numbers, underscores, periods)"
|
||||
.into(),
|
||||
@@ -340,7 +340,7 @@ pub async fn update_notification_prefs(
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
|
||||
info!(did = %auth.did, "Cleared Telegram username");
|
||||
} else if !crate::api::validation::is_valid_telegram_username(telegram_clean) {
|
||||
} else if !tranquil_pds::api::validation::is_valid_telegram_username(telegram_clean) {
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Invalid Telegram username. Must be 5-32 characters, alphanumeric or underscore"
|
||||
.into(),
|
||||
@@ -370,7 +370,7 @@ pub async fn update_notification_prefs(
|
||||
.await
|
||||
.map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?;
|
||||
info!(did = %auth.did, "Cleared Signal username");
|
||||
} else if !crate::comms::is_valid_signal_username(&signal_clean) {
|
||||
} else if !tranquil_pds::comms::is_valid_signal_username(&signal_clean) {
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Invalid Signal username. Must be 3-32 characters followed by .XX (e.g. username.01)"
|
||||
.into(),
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::api::error::{ApiError, DbResultExt};
|
||||
use crate::auth::{Auth, AuthAny, NotTakendown, Permissive, VerifyScope};
|
||||
use crate::delegation::DelegationActionType;
|
||||
use crate::state::AppState;
|
||||
use crate::types::{CidLink, Did};
|
||||
use crate::util::get_header_str;
|
||||
use tranquil_pds::api::error::{ApiError, DbResultExt};
|
||||
use tranquil_pds::auth::{Auth, AuthAny, NotTakendown, Permissive, VerifyScope};
|
||||
use tranquil_pds::delegation::DelegationActionType;
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::{CidLink, Did};
|
||||
use tranquil_pds::util::get_header_str;
|
||||
use axum::body::Body;
|
||||
use axum::{
|
||||
Json,
|
||||
+16
-16
@@ -1,11 +1,11 @@
|
||||
use crate::api::EmptyResponse;
|
||||
use crate::api::error::{ApiError, DbResultExt};
|
||||
use crate::api::repo::record::create_signed_commit;
|
||||
use crate::auth::{Auth, NotTakendown};
|
||||
use crate::state::AppState;
|
||||
use crate::sync::import::{ImportError, apply_import, parse_car};
|
||||
use crate::sync::verify::CarVerifier;
|
||||
use crate::types::Did;
|
||||
use tranquil_pds::api::EmptyResponse;
|
||||
use tranquil_pds::api::error::{ApiError, DbResultExt};
|
||||
use tranquil_pds::repo_ops::create_signed_commit;
|
||||
use tranquil_pds::auth::{Auth, NotTakendown};
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::sync::import::{ImportError, apply_import, parse_car};
|
||||
use tranquil_pds::sync::verify::CarVerifier;
|
||||
use tranquil_pds::types::Did;
|
||||
use axum::{
|
||||
body::Bytes,
|
||||
extract::State,
|
||||
@@ -121,7 +121,7 @@ pub async fn import_repo(
|
||||
verified.rev, verified.data_cid
|
||||
);
|
||||
}
|
||||
Err(crate::sync::verify::VerifyError::DidMismatch {
|
||||
Err(tranquil_pds::sync::verify::VerifyError::DidMismatch {
|
||||
commit_did,
|
||||
expected_did,
|
||||
}) => {
|
||||
@@ -130,7 +130,7 @@ pub async fn import_repo(
|
||||
commit_did, expected_did
|
||||
)));
|
||||
}
|
||||
Err(crate::sync::verify::VerifyError::MstValidationFailed(msg)) => {
|
||||
Err(tranquil_pds::sync::verify::VerifyError::MstValidationFailed(msg)) => {
|
||||
return Err(ApiError::InvalidRequest(format!(
|
||||
"MST validation failed: {}",
|
||||
msg
|
||||
@@ -154,7 +154,7 @@ pub async fn import_repo(
|
||||
verified.rev, verified.data_cid
|
||||
);
|
||||
}
|
||||
Err(crate::sync::verify::VerifyError::DidMismatch {
|
||||
Err(tranquil_pds::sync::verify::VerifyError::DidMismatch {
|
||||
commit_did,
|
||||
expected_did,
|
||||
}) => {
|
||||
@@ -163,24 +163,24 @@ pub async fn import_repo(
|
||||
commit_did, expected_did
|
||||
)));
|
||||
}
|
||||
Err(crate::sync::verify::VerifyError::InvalidSignature) => {
|
||||
Err(tranquil_pds::sync::verify::VerifyError::InvalidSignature) => {
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"CAR file commit signature verification failed".into(),
|
||||
));
|
||||
}
|
||||
Err(crate::sync::verify::VerifyError::DidResolutionFailed(msg)) => {
|
||||
Err(tranquil_pds::sync::verify::VerifyError::DidResolutionFailed(msg)) => {
|
||||
warn!("DID resolution failed during import verification: {}", msg);
|
||||
return Err(ApiError::InvalidRequest(format!(
|
||||
"Failed to verify DID: {}",
|
||||
msg
|
||||
)));
|
||||
}
|
||||
Err(crate::sync::verify::VerifyError::NoSigningKey) => {
|
||||
Err(tranquil_pds::sync::verify::VerifyError::NoSigningKey) => {
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"DID document does not contain a signing key".into(),
|
||||
));
|
||||
}
|
||||
Err(crate::sync::verify::VerifyError::MstValidationFailed(msg)) => {
|
||||
Err(tranquil_pds::sync::verify::VerifyError::MstValidationFailed(msg)) => {
|
||||
return Err(ApiError::InvalidRequest(format!(
|
||||
"MST validation failed: {}",
|
||||
msg
|
||||
@@ -264,7 +264,7 @@ pub async fn import_repo(
|
||||
ApiError::InternalError(Some("Signing key not found".into()))
|
||||
})?;
|
||||
let key_bytes =
|
||||
crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version)
|
||||
tranquil_pds::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version)
|
||||
.map_err(|e| {
|
||||
error!("Failed to decrypt signing key: {}", e);
|
||||
ApiError::InternalError(None)
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::state::AppState;
|
||||
use crate::types::AtIdentifier;
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::AtIdentifier;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Query, State},
|
||||
@@ -20,7 +20,7 @@ pub async fn describe_repo(
|
||||
) -> Response {
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let user_row = if input.repo.is_did() {
|
||||
let did: crate::types::Did = match input.repo.as_str().parse() {
|
||||
let did: tranquil_pds::types::Did = match input.repo.as_str().parse() {
|
||||
Ok(d) => d,
|
||||
Err(_) => return ApiError::InvalidRequest("Invalid DID format".into()).into_response(),
|
||||
};
|
||||
@@ -36,7 +36,7 @@ pub async fn describe_repo(
|
||||
} else {
|
||||
repo_str.to_string()
|
||||
};
|
||||
let handle: crate::types::Handle = match handle_str.parse() {
|
||||
let handle: tranquil_pds::types::Handle = match handle_str.parse() {
|
||||
Ok(h) => h,
|
||||
Err(_) => {
|
||||
return ApiError::InvalidRequest("Invalid handle format".into()).into_response();
|
||||
+11
-11
@@ -1,17 +1,17 @@
|
||||
use super::validation::validate_record_with_status;
|
||||
use super::validation_mode::{ValidationMode, deserialize_validation_mode};
|
||||
use crate::api::error::ApiError;
|
||||
use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log, extract_blob_cids};
|
||||
use crate::auth::{
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use crate::repo::record::utils::{CommitParams, RecordOp, commit_and_log, extract_blob_cids};
|
||||
use tranquil_pds::auth::{
|
||||
Active, Auth, WriteOpKind, require_not_migrated, require_verified_or_delegated,
|
||||
verify_batch_write_scopes,
|
||||
};
|
||||
use crate::cid_types::CommitCid;
|
||||
use crate::delegation::DelegationActionType;
|
||||
use crate::repo::tracking::TrackingBlockStore;
|
||||
use crate::state::AppState;
|
||||
use crate::types::{AtIdentifier, AtUri, Did, Nsid, Rkey};
|
||||
use crate::validation::ValidationStatus;
|
||||
use tranquil_pds::cid_types::CommitCid;
|
||||
use tranquil_pds::delegation::DelegationActionType;
|
||||
use tranquil_pds::repo::tracking::TrackingBlockStore;
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::{AtIdentifier, AtUri, Did, Nsid, Rkey};
|
||||
use tranquil_pds::validation::ValidationStatus;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -74,7 +74,7 @@ async fn process_single_write(
|
||||
};
|
||||
all_blob_cids.extend(extract_blob_cids(value));
|
||||
let rkey = rkey.clone().unwrap_or_else(Rkey::generate);
|
||||
let record_ipld = crate::util::json_to_ipld(value);
|
||||
let record_ipld = tranquil_pds::util::json_to_ipld(value);
|
||||
let record_bytes = serde_ipld_dagcbor::to_vec(&record_ipld).map_err(|_| {
|
||||
ApiError::InvalidRecord("Failed to serialize record".into()).into_response()
|
||||
})?;
|
||||
@@ -126,7 +126,7 @@ async fn process_single_write(
|
||||
}
|
||||
};
|
||||
all_blob_cids.extend(extract_blob_cids(value));
|
||||
let record_ipld = crate::util::json_to_ipld(value);
|
||||
let record_ipld = tranquil_pds::util::json_to_ipld(value);
|
||||
let record_bytes = serde_ipld_dagcbor::to_vec(&record_ipld).map_err(|_| {
|
||||
ApiError::InvalidRecord("Failed to serialize record".into()).into_response()
|
||||
})?;
|
||||
+11
-11
@@ -1,14 +1,14 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::api::repo::record::utils::{
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use crate::repo::record::utils::{
|
||||
CommitError, CommitParams, RecordOp, commit_and_log, get_current_root_cid,
|
||||
};
|
||||
use crate::api::repo::record::write::{CommitInfo, prepare_repo_write};
|
||||
use crate::auth::{Active, Auth, VerifyScope};
|
||||
use crate::cid_types::CommitCid;
|
||||
use crate::delegation::DelegationActionType;
|
||||
use crate::repo::tracking::TrackingBlockStore;
|
||||
use crate::state::AppState;
|
||||
use crate::types::{AtIdentifier, AtUri, Nsid, Rkey};
|
||||
use crate::repo::record::write::{CommitInfo, prepare_repo_write};
|
||||
use tranquil_pds::auth::{Active, Auth, VerifyScope};
|
||||
use tranquil_pds::cid_types::CommitCid;
|
||||
use tranquil_pds::delegation::DelegationActionType;
|
||||
use tranquil_pds::repo::tracking::TrackingBlockStore;
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::{AtIdentifier, AtUri, Nsid, Rkey};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -45,7 +45,7 @@ pub async fn delete_record(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<DeleteRecordInput>,
|
||||
) -> Result<Response, crate::api::error::ApiError> {
|
||||
) -> Result<Response, tranquil_pds::api::error::ApiError> {
|
||||
let scope_proof = match auth.verify_repo_delete(&input.collection) {
|
||||
Ok(proof) => proof,
|
||||
Err(e) => return Ok(e.into_response()),
|
||||
@@ -229,7 +229,7 @@ pub async fn delete_record(
|
||||
.into_response())
|
||||
}
|
||||
|
||||
use crate::types::Did;
|
||||
use tranquil_pds::types::Did;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn delete_record_internal(
|
||||
+8
-8
@@ -1,7 +1,7 @@
|
||||
use super::pagination::{PaginationDirection, deserialize_pagination_direction};
|
||||
use crate::api::error::ApiError;
|
||||
use crate::state::AppState;
|
||||
use crate::types::{AtIdentifier, Nsid, Rkey};
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::{AtIdentifier, Nsid, Rkey};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Query, State},
|
||||
@@ -61,7 +61,7 @@ pub async fn get_record(
|
||||
) -> Response {
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let user_id_opt = if input.repo.is_did() {
|
||||
let did: crate::types::Did = match input.repo.as_str().parse() {
|
||||
let did: tranquil_pds::types::Did = match input.repo.as_str().parse() {
|
||||
Ok(d) => d,
|
||||
Err(_) => return ApiError::InvalidRequest("Invalid DID format".into()).into_response(),
|
||||
};
|
||||
@@ -73,7 +73,7 @@ pub async fn get_record(
|
||||
} else {
|
||||
repo_str.to_string()
|
||||
};
|
||||
let handle: crate::types::Handle = match handle_str.parse() {
|
||||
let handle: tranquil_pds::types::Handle = match handle_str.parse() {
|
||||
Ok(h) => h,
|
||||
Err(_) => {
|
||||
return ApiError::InvalidRequest("Invalid handle format".into()).into_response();
|
||||
@@ -160,7 +160,7 @@ pub async fn list_records(
|
||||
) -> Response {
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let user_id_opt = if input.repo.is_did() {
|
||||
let did: crate::types::Did = match input.repo.as_str().parse() {
|
||||
let did: tranquil_pds::types::Did = match input.repo.as_str().parse() {
|
||||
Ok(d) => d,
|
||||
Err(_) => return ApiError::InvalidRequest("Invalid DID format".into()).into_response(),
|
||||
};
|
||||
@@ -172,7 +172,7 @@ pub async fn list_records(
|
||||
} else {
|
||||
repo_str.to_string()
|
||||
};
|
||||
let handle: crate::types::Handle = match handle_str.parse() {
|
||||
let handle: tranquil_pds::types::Handle = match handle_str.parse() {
|
||||
Ok(h) => h,
|
||||
Err(_) => {
|
||||
return ApiError::InvalidRequest("Invalid handle format".into()).into_response();
|
||||
@@ -198,7 +198,7 @@ pub async fn list_records(
|
||||
let cursor_rkey = input
|
||||
.cursor
|
||||
.as_ref()
|
||||
.and_then(|c| c.parse::<crate::types::Rkey>().ok());
|
||||
.and_then(|c| c.parse::<tranquil_pds::types::Rkey>().ok());
|
||||
let rows = match state
|
||||
.repo_repo
|
||||
.list_records(
|
||||
@@ -0,0 +1 @@
|
||||
pub use tranquil_pds::repo_ops::*;
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::types::{Nsid, Rkey};
|
||||
use crate::validation::{RecordValidator, ValidationError, ValidationStatus};
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::types::{Nsid, Rkey};
|
||||
use tranquil_pds::validation::{RecordValidator, ValidationError, ValidationStatus};
|
||||
use axum::response::Response;
|
||||
|
||||
pub async fn validate_record_with_status(
|
||||
+13
-13
@@ -1,20 +1,20 @@
|
||||
use super::validation::validate_record_with_status;
|
||||
use super::validation_mode::{ValidationMode, deserialize_validation_mode};
|
||||
use crate::api::error::ApiError;
|
||||
use crate::api::repo::record::utils::{
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use crate::repo::record::utils::{
|
||||
CommitParams, RecordOp, commit_and_log, extract_backlinks, extract_blob_cids,
|
||||
get_current_root_cid,
|
||||
};
|
||||
use crate::auth::{
|
||||
use tranquil_pds::auth::{
|
||||
Active, Auth, AuthSource, RepoScopeAction, ScopeVerified, VerifyScope, require_not_migrated,
|
||||
require_verified_or_delegated,
|
||||
};
|
||||
use crate::cid_types::CommitCid;
|
||||
use crate::delegation::DelegationActionType;
|
||||
use crate::repo::tracking::TrackingBlockStore;
|
||||
use crate::state::AppState;
|
||||
use crate::types::{AtIdentifier, AtUri, Did, Nsid, Rkey};
|
||||
use crate::validation::ValidationStatus;
|
||||
use tranquil_pds::cid_types::CommitCid;
|
||||
use tranquil_pds::delegation::DelegationActionType;
|
||||
use tranquil_pds::repo::tracking::TrackingBlockStore;
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::{AtIdentifier, AtUri, Did, Nsid, Rkey};
|
||||
use tranquil_pds::validation::ValidationStatus;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -104,7 +104,7 @@ pub async fn create_record(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<CreateRecordInput>,
|
||||
) -> Result<Response, crate::api::error::ApiError> {
|
||||
) -> Result<Response, tranquil_pds::api::error::ApiError> {
|
||||
let scope_proof = match auth.verify_repo_create(&input.collection) {
|
||||
Ok(proof) => proof,
|
||||
Err(e) => return Ok(e.into_response()),
|
||||
@@ -232,7 +232,7 @@ pub async fn create_record(
|
||||
}
|
||||
}
|
||||
|
||||
let record_ipld = crate::util::json_to_ipld(&input.record);
|
||||
let record_ipld = tranquil_pds::util::json_to_ipld(&input.record);
|
||||
let mut record_bytes = Vec::new();
|
||||
if serde_ipld_dagcbor::to_writer(&mut record_bytes, &record_ipld).is_err() {
|
||||
return Ok(ApiError::InvalidRecord("Failed to serialize record".into()).into_response());
|
||||
@@ -408,7 +408,7 @@ pub async fn put_record(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<PutRecordInput>,
|
||||
) -> Result<Response, crate::api::error::ApiError> {
|
||||
) -> Result<Response, tranquil_pds::api::error::ApiError> {
|
||||
let upsert_proof = match auth.verify_repo_upsert(&input.collection) {
|
||||
Ok(proof) => proof,
|
||||
Err(e) => return Ok(e.into_response()),
|
||||
@@ -476,7 +476,7 @@ pub async fn put_record(
|
||||
}
|
||||
}
|
||||
let existing_cid = mst.get(&key).await.ok().flatten();
|
||||
let record_ipld = crate::util::json_to_ipld(&input.record);
|
||||
let record_ipld = tranquil_pds::util::json_to_ipld(&input.record);
|
||||
let mut record_bytes = Vec::new();
|
||||
if serde_ipld_dagcbor::to_writer(&mut record_bytes, &record_ipld).is_err() {
|
||||
return Ok(ApiError::InvalidRecord("Failed to serialize record".into()).into_response());
|
||||
+31
-31
@@ -1,10 +1,10 @@
|
||||
use crate::api::EmptyResponse;
|
||||
use crate::api::error::{ApiError, DbResultExt};
|
||||
use crate::auth::{Auth, NotTakendown, Permissive, require_legacy_session_mfa};
|
||||
use crate::cache::Cache;
|
||||
use crate::plc::PlcClient;
|
||||
use crate::state::AppState;
|
||||
use crate::types::PlainPassword;
|
||||
use tranquil_pds::api::EmptyResponse;
|
||||
use tranquil_pds::api::error::{ApiError, DbResultExt};
|
||||
use tranquil_pds::auth::{Auth, NotTakendown, Permissive, require_legacy_session_mfa};
|
||||
use tranquil_pds::cache::Cache;
|
||||
use tranquil_pds::plc::PlcClient;
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::PlainPassword;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -117,7 +117,7 @@ pub async fn check_account_status(
|
||||
async fn is_valid_did_for_service(
|
||||
user_repo: &dyn tranquil_db_traits::UserRepository,
|
||||
cache: Arc<dyn Cache>,
|
||||
did: &crate::types::Did,
|
||||
did: &tranquil_pds::types::Did,
|
||||
) -> bool {
|
||||
assert_valid_did_document_for_service(user_repo, cache, did, false)
|
||||
.await
|
||||
@@ -127,7 +127,7 @@ async fn is_valid_did_for_service(
|
||||
async fn assert_valid_did_document_for_service(
|
||||
user_repo: &dyn tranquil_db_traits::UserRepository,
|
||||
cache: Arc<dyn Cache>,
|
||||
did: &crate::types::Did,
|
||||
did: &tranquil_pds::types::Did,
|
||||
with_retry: bool,
|
||||
) -> Result<(), ApiError> {
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
@@ -226,7 +226,7 @@ async fn assert_valid_did_document_for_service(
|
||||
|
||||
if let Some(key_info) = user_key {
|
||||
let key_bytes =
|
||||
crate::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version)
|
||||
tranquil_pds::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version)
|
||||
.map_err(|e| {
|
||||
error!("Failed to decrypt user key: {}", e);
|
||||
ApiError::InternalError(None)
|
||||
@@ -235,7 +235,7 @@ async fn assert_valid_did_document_for_service(
|
||||
error!("Failed to create signing key: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?;
|
||||
let expected_did_key = crate::plc::signing_key_to_did_key(&signing_key);
|
||||
let expected_did_key = tranquil_pds::plc::signing_key_to_did_key(&signing_key);
|
||||
|
||||
if doc_signing_key != Some(&expected_did_key) {
|
||||
warn!(
|
||||
@@ -248,7 +248,7 @@ async fn assert_valid_did_document_for_service(
|
||||
}
|
||||
}
|
||||
} else if let Some(host_and_path) = did.as_str().strip_prefix("did:web:") {
|
||||
let client = crate::api::proxy_client::did_resolution_client();
|
||||
let client = tranquil_pds::api::proxy_client::did_resolution_client();
|
||||
let decoded = host_and_path.replace("%3A", ":");
|
||||
let parts: Vec<&str> = decoded.split(':').collect();
|
||||
let (host, path_parts) = if parts.len() > 1 && parts[1].chars().all(|c| c.is_ascii_digit())
|
||||
@@ -284,7 +284,7 @@ async fn assert_valid_did_document_for_service(
|
||||
arr.iter().find(|svc| {
|
||||
svc.get("id").and_then(|id| id.as_str()) == Some("#atproto_pds")
|
||||
|| svc.get("type").and_then(|t| t.as_str())
|
||||
== Some(crate::plc::ServiceType::Pds.as_str())
|
||||
== Some(tranquil_pds::plc::ServiceType::Pds.as_str())
|
||||
})
|
||||
})
|
||||
.and_then(|svc| svc.get("serviceEndpoint"))
|
||||
@@ -314,11 +314,11 @@ pub async fn activate_account(
|
||||
auth.did
|
||||
);
|
||||
|
||||
if let Err(e) = crate::auth::scope_check::check_account_scope(
|
||||
if let Err(e) = tranquil_pds::auth::scope_check::check_account_scope(
|
||||
&auth.auth_source,
|
||||
auth.scope.as_deref(),
|
||||
crate::oauth::scopes::AccountAttr::Repo,
|
||||
crate::oauth::scopes::AccountAction::Manage,
|
||||
tranquil_pds::oauth::scopes::AccountAttr::Repo,
|
||||
tranquil_pds::oauth::scopes::AccountAction::Manage,
|
||||
) {
|
||||
info!("[MIGRATION] activateAccount: Scope check failed");
|
||||
return Ok(e);
|
||||
@@ -365,15 +365,15 @@ pub async fn activate_account(
|
||||
did
|
||||
);
|
||||
if let Some(ref h) = handle {
|
||||
let _ = state.cache.delete(&crate::cache_keys::handle_key(h)).await;
|
||||
let _ = state.cache.delete(&tranquil_pds::cache_keys::handle_key(h)).await;
|
||||
}
|
||||
let _ = state
|
||||
.cache
|
||||
.delete(&crate::cache_keys::plc_doc_key(&did))
|
||||
.delete(&tranquil_pds::cache_keys::plc_doc_key(&did))
|
||||
.await;
|
||||
let _ = state
|
||||
.cache
|
||||
.delete(&crate::cache_keys::plc_data_key(&did))
|
||||
.delete(&tranquil_pds::cache_keys::plc_data_key(&did))
|
||||
.await;
|
||||
if state.did_resolver.refresh_did(did.as_str()).await.is_none() {
|
||||
warn!(
|
||||
@@ -385,7 +385,7 @@ pub async fn activate_account(
|
||||
"[MIGRATION] activateAccount: Sequencing account event (active=true) for did={}",
|
||||
did
|
||||
);
|
||||
if let Err(e) = crate::api::repo::record::sequence_account_event(
|
||||
if let Err(e) = tranquil_pds::repo_ops::sequence_account_event(
|
||||
&state,
|
||||
&did,
|
||||
tranquil_db_traits::AccountStatus::Active,
|
||||
@@ -404,7 +404,7 @@ pub async fn activate_account(
|
||||
did, handle
|
||||
);
|
||||
let handle_typed = handle.clone();
|
||||
if let Err(e) = crate::api::repo::record::sequence_identity_event(
|
||||
if let Err(e) = tranquil_pds::repo_ops::sequence_identity_event(
|
||||
&state,
|
||||
&did,
|
||||
handle_typed.as_ref(),
|
||||
@@ -438,7 +438,7 @@ pub async fn activate_account(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Err(e) = crate::api::repo::record::sequence_sync_event(
|
||||
if let Err(e) = tranquil_pds::repo_ops::sequence_sync_event(
|
||||
&state,
|
||||
&did,
|
||||
root_cid_link.as_str(),
|
||||
@@ -483,11 +483,11 @@ pub async fn deactivate_account(
|
||||
auth: Auth<Permissive>,
|
||||
Json(input): Json<DeactivateAccountInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
if let Err(e) = crate::auth::scope_check::check_account_scope(
|
||||
if let Err(e) = tranquil_pds::auth::scope_check::check_account_scope(
|
||||
&auth.auth_source,
|
||||
auth.scope.as_deref(),
|
||||
crate::oauth::scopes::AccountAttr::Repo,
|
||||
crate::oauth::scopes::AccountAction::Manage,
|
||||
tranquil_pds::oauth::scopes::AccountAttr::Repo,
|
||||
tranquil_pds::oauth::scopes::AccountAction::Manage,
|
||||
) {
|
||||
return Ok(e);
|
||||
}
|
||||
@@ -507,9 +507,9 @@ pub async fn deactivate_account(
|
||||
match result {
|
||||
Ok(true) => {
|
||||
if let Some(ref h) = handle {
|
||||
let _ = state.cache.delete(&crate::cache_keys::handle_key(h)).await;
|
||||
let _ = state.cache.delete(&tranquil_pds::cache_keys::handle_key(h)).await;
|
||||
}
|
||||
if let Err(e) = crate::api::repo::record::sequence_account_event(
|
||||
if let Err(e) = tranquil_pds::repo_ops::sequence_account_event(
|
||||
&state,
|
||||
&did,
|
||||
tranquil_db_traits::AccountStatus::Deactivated,
|
||||
@@ -552,7 +552,7 @@ pub async fn request_account_delete(
|
||||
.await
|
||||
.log_db_err("creating deletion token")?;
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_account_deletion(
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_account_deletion(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
user_id,
|
||||
@@ -569,7 +569,7 @@ pub async fn request_account_delete(
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DeleteAccountInput {
|
||||
pub did: crate::types::Did,
|
||||
pub did: tranquil_pds::types::Did,
|
||||
pub password: PlainPassword,
|
||||
pub token: String,
|
||||
}
|
||||
@@ -642,7 +642,7 @@ pub async fn delete_account(
|
||||
error!("DB error deleting account: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
let account_seq = crate::api::repo::record::sequence_account_event(
|
||||
let account_seq = tranquil_pds::repo_ops::sequence_account_event(
|
||||
&state,
|
||||
did,
|
||||
tranquil_db_traits::AccountStatus::Deleted,
|
||||
@@ -666,7 +666,7 @@ pub async fn delete_account(
|
||||
}
|
||||
let _ = state
|
||||
.cache
|
||||
.delete(&crate::cache_keys::handle_key(&handle))
|
||||
.delete(&tranquil_pds::cache_keys::handle_key(&handle))
|
||||
.await;
|
||||
info!("Account {} deleted successfully", did);
|
||||
EmptyResponse::ok().into_response()
|
||||
+7
-7
@@ -1,9 +1,9 @@
|
||||
use crate::api::EmptyResponse;
|
||||
use crate::api::error::{ApiError, DbResultExt};
|
||||
use crate::auth::{Auth, NotTakendown, Permissive, generate_app_password};
|
||||
use crate::delegation::{DelegationActionType, intersect_scopes};
|
||||
use crate::rate_limit::{AppPasswordLimit, RateLimited};
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::api::EmptyResponse;
|
||||
use tranquil_pds::api::error::{ApiError, DbResultExt};
|
||||
use tranquil_pds::auth::{Auth, NotTakendown, Permissive, generate_app_password};
|
||||
use tranquil_pds::delegation::{DelegationActionType, intersect_scopes};
|
||||
use tranquil_pds::rate_limit::{AppPasswordLimit, RateLimited};
|
||||
use tranquil_pds::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -233,7 +233,7 @@ pub async fn revoke_app_password(
|
||||
.log_db_err("revoking sessions for app password")?;
|
||||
|
||||
futures::future::join_all(sessions_to_invalidate.iter().map(|jti| {
|
||||
let cache_key = crate::cache_keys::session_key(&auth.did, jti);
|
||||
let cache_key = tranquil_pds::cache_keys::session_key(&auth.did, jti);
|
||||
let cache = state.cache.clone();
|
||||
async move {
|
||||
let _ = cache.delete(&cache_key).await;
|
||||
+43
-43
@@ -1,8 +1,8 @@
|
||||
use crate::api::error::{ApiError, DbResultExt};
|
||||
use crate::api::{EmptyResponse, TokenRequiredResponse, VerifiedResponse};
|
||||
use crate::auth::{Auth, NotTakendown};
|
||||
use crate::rate_limit::{EmailUpdateLimit, RateLimited, VerificationCheckLimit};
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::api::error::{ApiError, DbResultExt};
|
||||
use tranquil_pds::api::{EmptyResponse, TokenRequiredResponse, VerifiedResponse};
|
||||
use tranquil_pds::auth::{Auth, NotTakendown};
|
||||
use tranquil_pds::rate_limit::{EmailUpdateLimit, RateLimited, VerificationCheckLimit};
|
||||
use tranquil_pds::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -20,7 +20,7 @@ use tranquil_db_traits::CommsChannel;
|
||||
const EMAIL_UPDATE_TTL: Duration = Duration::from_secs(30 * 60);
|
||||
|
||||
fn email_update_cache_key(did: &str) -> String {
|
||||
crate::cache_keys::email_update_key(did)
|
||||
tranquil_pds::cache_keys::email_update_key(did)
|
||||
}
|
||||
|
||||
fn hash_token(token: &str) -> String {
|
||||
@@ -49,11 +49,11 @@ pub async fn request_email_update(
|
||||
auth: Auth<NotTakendown>,
|
||||
input: Option<Json<RequestEmailUpdateInput>>,
|
||||
) -> Result<Response, ApiError> {
|
||||
if let Err(e) = crate::auth::scope_check::check_account_scope(
|
||||
if let Err(e) = tranquil_pds::auth::scope_check::check_account_scope(
|
||||
&auth.auth_source,
|
||||
auth.scope.as_deref(),
|
||||
crate::oauth::scopes::AccountAttr::Email,
|
||||
crate::oauth::scopes::AccountAction::Manage,
|
||||
tranquil_pds::oauth::scopes::AccountAttr::Email,
|
||||
tranquil_pds::oauth::scopes::AccountAction::Manage,
|
||||
) {
|
||||
return Ok(e);
|
||||
}
|
||||
@@ -74,10 +74,10 @@ pub async fn request_email_update(
|
||||
let token_required = user.email_verified;
|
||||
|
||||
if token_required {
|
||||
let token = crate::auth::email_token::create_email_token(
|
||||
let token = tranquil_pds::auth::email_token::create_email_token(
|
||||
state.cache.as_ref(),
|
||||
auth.did.as_str(),
|
||||
crate::auth::email_token::EmailTokenPurpose::UpdateEmail,
|
||||
tranquil_pds::auth::email_token::EmailTokenPurpose::UpdateEmail,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -89,7 +89,7 @@ pub async fn request_email_update(
|
||||
&& let Some(ref new_email) = inp.new_email
|
||||
{
|
||||
let new_email = new_email.trim().to_lowercase();
|
||||
if !new_email.is_empty() && crate::api::validation::is_valid_email(&new_email) {
|
||||
if !new_email.is_empty() && tranquil_pds::api::validation::is_valid_email(&new_email) {
|
||||
let pending = PendingEmailUpdate {
|
||||
new_email,
|
||||
token_hash: hash_token(&token),
|
||||
@@ -105,7 +105,7 @@ pub async fn request_email_update(
|
||||
}
|
||||
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_short_token_email(
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_short_token_email(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
user.id,
|
||||
@@ -135,11 +135,11 @@ pub async fn confirm_email(
|
||||
auth: Auth<NotTakendown>,
|
||||
Json(input): Json<ConfirmEmailInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
if let Err(e) = crate::auth::scope_check::check_account_scope(
|
||||
if let Err(e) = tranquil_pds::auth::scope_check::check_account_scope(
|
||||
&auth.auth_source,
|
||||
auth.scope.as_deref(),
|
||||
crate::oauth::scopes::AccountAttr::Email,
|
||||
crate::oauth::scopes::AccountAction::Manage,
|
||||
tranquil_pds::oauth::scopes::AccountAttr::Email,
|
||||
tranquil_pds::oauth::scopes::AccountAction::Manage,
|
||||
) {
|
||||
return Ok(e);
|
||||
}
|
||||
@@ -167,9 +167,9 @@ pub async fn confirm_email(
|
||||
}
|
||||
|
||||
let confirmation_code =
|
||||
crate::auth::verification_token::normalize_token_input(input.token.trim());
|
||||
tranquil_pds::auth::verification_token::normalize_token_input(input.token.trim());
|
||||
|
||||
let verified = crate::auth::verification_token::verify_signup_token(
|
||||
let verified = tranquil_pds::auth::verification_token::verify_signup_token(
|
||||
&confirmation_code,
|
||||
CommsChannel::Email,
|
||||
&provided_email,
|
||||
@@ -181,7 +181,7 @@ pub async fn confirm_email(
|
||||
return Err(ApiError::InvalidToken(None));
|
||||
}
|
||||
}
|
||||
Err(crate::auth::verification_token::VerifyError::Expired) => {
|
||||
Err(tranquil_pds::auth::verification_token::VerifyError::Expired) => {
|
||||
return Err(ApiError::ExpiredToken(None));
|
||||
}
|
||||
Err(_) => {
|
||||
@@ -213,11 +213,11 @@ pub async fn update_email(
|
||||
auth: Auth<NotTakendown>,
|
||||
Json(input): Json<UpdateEmailInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
if let Err(e) = crate::auth::scope_check::check_account_scope(
|
||||
if let Err(e) = tranquil_pds::auth::scope_check::check_account_scope(
|
||||
&auth.auth_source,
|
||||
auth.scope.as_deref(),
|
||||
crate::oauth::scopes::AccountAttr::Email,
|
||||
crate::oauth::scopes::AccountAction::Manage,
|
||||
tranquil_pds::oauth::scopes::AccountAttr::Email,
|
||||
tranquil_pds::oauth::scopes::AccountAction::Manage,
|
||||
) {
|
||||
return Ok(e);
|
||||
}
|
||||
@@ -235,7 +235,7 @@ pub async fn update_email(
|
||||
let email_verified = user.email_verified;
|
||||
let new_email = input.email.trim().to_lowercase();
|
||||
|
||||
if !crate::api::validation::is_valid_email(&new_email) {
|
||||
if !tranquil_pds::api::validation::is_valid_email(&new_email) {
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"This email address is not supported, please use a different email.".into(),
|
||||
));
|
||||
@@ -255,15 +255,15 @@ pub async fn update_email(
|
||||
.filter(|t| !t.is_empty())
|
||||
.ok_or(ApiError::TokenRequired)?;
|
||||
|
||||
crate::auth::email_token::validate_email_token(
|
||||
tranquil_pds::auth::email_token::validate_email_token(
|
||||
state.cache.as_ref(),
|
||||
did.as_str(),
|
||||
crate::auth::email_token::EmailTokenPurpose::UpdateEmail,
|
||||
tranquil_pds::auth::email_token::EmailTokenPurpose::UpdateEmail,
|
||||
token,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
crate::auth::email_token::TokenError::ExpiredToken => {
|
||||
tranquil_pds::auth::email_token::TokenError::ExpiredToken => {
|
||||
ApiError::ExpiredToken(None)
|
||||
}
|
||||
_ => ApiError::InvalidToken(None),
|
||||
@@ -303,24 +303,24 @@ pub async fn update_email(
|
||||
.filter(|t| !t.is_empty())
|
||||
.ok_or(ApiError::TokenRequired)?;
|
||||
|
||||
let short_token_result = crate::auth::email_token::validate_email_token(
|
||||
let short_token_result = tranquil_pds::auth::email_token::validate_email_token(
|
||||
state.cache.as_ref(),
|
||||
did.as_str(),
|
||||
crate::auth::email_token::EmailTokenPurpose::UpdateEmail,
|
||||
tranquil_pds::auth::email_token::EmailTokenPurpose::UpdateEmail,
|
||||
token,
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Err(e) = short_token_result {
|
||||
let confirmation_token =
|
||||
crate::auth::verification_token::normalize_token_input(token.trim());
|
||||
tranquil_pds::auth::verification_token::normalize_token_input(token.trim());
|
||||
|
||||
let current_email_lower = current_email
|
||||
.as_ref()
|
||||
.map(|e| e.to_lowercase())
|
||||
.unwrap_or_default();
|
||||
|
||||
let verified = crate::auth::verification_token::verify_channel_update_token(
|
||||
let verified = tranquil_pds::auth::verification_token::verify_channel_update_token(
|
||||
&confirmation_token,
|
||||
CommsChannel::Email,
|
||||
¤t_email_lower,
|
||||
@@ -332,9 +332,9 @@ pub async fn update_email(
|
||||
return Err(ApiError::InvalidToken(None));
|
||||
}
|
||||
}
|
||||
Err(crate::auth::verification_token::VerifyError::Expired) => {
|
||||
Err(tranquil_pds::auth::verification_token::VerifyError::Expired) => {
|
||||
return Err(match e {
|
||||
crate::auth::email_token::TokenError::ExpiredToken => {
|
||||
tranquil_pds::auth::email_token::TokenError::ExpiredToken => {
|
||||
ApiError::ExpiredToken(None)
|
||||
}
|
||||
_ => ApiError::InvalidToken(None),
|
||||
@@ -342,7 +342,7 @@ pub async fn update_email(
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(match e {
|
||||
crate::auth::email_token::TokenError::ExpiredToken => {
|
||||
tranquil_pds::auth::email_token::TokenError::ExpiredToken => {
|
||||
ApiError::ExpiredToken(None)
|
||||
}
|
||||
_ => ApiError::InvalidToken(None),
|
||||
@@ -359,15 +359,15 @@ pub async fn update_email(
|
||||
.await
|
||||
.log_db_err("updating email")?;
|
||||
|
||||
let verification_token = crate::auth::verification_token::generate_signup_token(
|
||||
let verification_token = tranquil_pds::auth::verification_token::generate_signup_token(
|
||||
did,
|
||||
CommsChannel::Email,
|
||||
&new_email,
|
||||
);
|
||||
let formatted_token =
|
||||
crate::auth::verification_token::format_token_for_display(&verification_token);
|
||||
tranquil_pds::auth::verification_token::format_token_for_display(&verification_token);
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_signup_verification(
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_signup_verification(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
user_id,
|
||||
@@ -423,7 +423,7 @@ pub async fn check_email_verified(
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CheckChannelVerifiedInput {
|
||||
pub did: crate::types::Did,
|
||||
pub did: tranquil_pds::types::Did,
|
||||
pub channel: CommsChannel,
|
||||
}
|
||||
|
||||
@@ -456,11 +456,11 @@ pub async fn authorize_email_update(
|
||||
_rate_limit: RateLimited<VerificationCheckLimit>,
|
||||
axum::extract::Query(query): axum::extract::Query<AuthorizeEmailUpdateQuery>,
|
||||
) -> Response {
|
||||
let verified = crate::auth::verification_token::verify_token_signature(&query.token);
|
||||
let verified = tranquil_pds::auth::verification_token::verify_token_signature(&query.token);
|
||||
|
||||
let token_data = match verified {
|
||||
Ok(data) => data,
|
||||
Err(crate::auth::verification_token::VerifyError::Expired) => {
|
||||
Err(tranquil_pds::auth::verification_token::VerifyError::Expired) => {
|
||||
warn!("authorize_email_update: token expired");
|
||||
return ApiError::ExpiredToken(None).into_response();
|
||||
}
|
||||
@@ -470,7 +470,7 @@ pub async fn authorize_email_update(
|
||||
}
|
||||
};
|
||||
|
||||
if token_data.purpose != crate::auth::verification_token::VerificationPurpose::ChannelUpdate {
|
||||
if token_data.purpose != tranquil_pds::auth::verification_token::VerificationPurpose::ChannelUpdate {
|
||||
warn!(
|
||||
"authorize_email_update: wrong purpose: {:?}",
|
||||
token_data.purpose
|
||||
@@ -544,11 +544,11 @@ pub async fn check_email_update_status(
|
||||
_rate_limit: RateLimited<VerificationCheckLimit>,
|
||||
auth: Auth<NotTakendown>,
|
||||
) -> Result<Response, ApiError> {
|
||||
if let Err(e) = crate::auth::scope_check::check_account_scope(
|
||||
if let Err(e) = tranquil_pds::auth::scope_check::check_account_scope(
|
||||
&auth.auth_source,
|
||||
auth.scope.as_deref(),
|
||||
crate::oauth::scopes::AccountAttr::Email,
|
||||
crate::oauth::scopes::AccountAction::Read,
|
||||
tranquil_pds::oauth::scopes::AccountAttr::Email,
|
||||
tranquil_pds::oauth::scopes::AccountAction::Read,
|
||||
) {
|
||||
return Ok(e);
|
||||
}
|
||||
+5
-5
@@ -1,8 +1,8 @@
|
||||
use crate::api::ApiError;
|
||||
use crate::api::error::DbResultExt;
|
||||
use crate::auth::{Admin, Auth, NotTakendown};
|
||||
use crate::state::AppState;
|
||||
use crate::types::Did;
|
||||
use tranquil_pds::api::ApiError;
|
||||
use tranquil_pds::api::error::DbResultExt;
|
||||
use tranquil_pds::auth::{Admin, Auth, NotTakendown};
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::Did;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::state::AppState;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::State,
|
||||
@@ -21,7 +21,7 @@ pub async fn get_logo(State(state): State<AppState>) -> Response {
|
||||
Some(c) if !c.is_empty() => c,
|
||||
_ => return StatusCode::NOT_FOUND.into_response(),
|
||||
};
|
||||
let cid = match crate::types::CidLink::new(&cid_str) {
|
||||
let cid = match tranquil_pds::types::CidLink::new(&cid_str) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return StatusCode::NOT_FOUND.into_response(),
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::BUILD_VERSION;
|
||||
use crate::state::AppState;
|
||||
use crate::util::{discord_app_id, discord_bot_username, telegram_bot_username};
|
||||
use tranquil_pds::BUILD_VERSION;
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::util::{discord_app_id, discord_bot_username, telegram_bot_username};
|
||||
use axum::{Json, extract::State, http::StatusCode, response::IntoResponse};
|
||||
use serde_json::json;
|
||||
|
||||
+9
-9
@@ -1,7 +1,7 @@
|
||||
use crate::api::ApiError;
|
||||
use crate::api::error::DbResultExt;
|
||||
use crate::auth::{Active, Auth};
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::api::ApiError;
|
||||
use tranquil_pds::api::error::DbResultExt;
|
||||
use tranquil_pds::auth::{Active, Auth};
|
||||
use tranquil_pds::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -145,7 +145,7 @@ pub async fn get_did_document(
|
||||
Ok((StatusCode::OK, Json(json!({ "didDocument": did_doc }))).into_response())
|
||||
}
|
||||
|
||||
async fn build_did_document(state: &AppState, did: &crate::types::Did) -> serde_json::Value {
|
||||
async fn build_did_document(state: &AppState, did: &tranquil_pds::types::Did) -> serde_json::Value {
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
|
||||
let user = match state.user_repo.get_user_for_did_doc_build(did).await {
|
||||
@@ -195,7 +195,7 @@ async fn build_did_document(state: &AppState, did: &crate::types::Did) -> serde_
|
||||
})).collect::<Vec<_>>(),
|
||||
"service": [{
|
||||
"id": "#atproto_pds",
|
||||
"type": crate::plc::ServiceType::Pds.as_str(),
|
||||
"type": tranquil_pds::plc::ServiceType::Pds.as_str(),
|
||||
"serviceEndpoint": service_endpoint
|
||||
}]
|
||||
});
|
||||
@@ -209,8 +209,8 @@ async fn build_did_document(state: &AppState, did: &crate::types::Did) -> serde_
|
||||
.flatten();
|
||||
|
||||
let public_key_multibase = match key_info {
|
||||
Some(info) => match crate::config::decrypt_key(&info.key_bytes, info.encryption_version) {
|
||||
Ok(key_bytes) => crate::api::identity::did::get_public_key_multibase(&key_bytes)
|
||||
Some(info) => match tranquil_pds::config::decrypt_key(&info.key_bytes, info.encryption_version) {
|
||||
Ok(key_bytes) => crate::identity::did::get_public_key_multibase(&key_bytes)
|
||||
.unwrap_or_else(|_| "error".to_string()),
|
||||
Err(_) => "error".to_string(),
|
||||
},
|
||||
@@ -243,7 +243,7 @@ async fn build_did_document(state: &AppState, did: &crate::types::Did) -> serde_
|
||||
}],
|
||||
"service": [{
|
||||
"id": "#atproto_pds",
|
||||
"type": crate::plc::ServiceType::Pds.as_str(),
|
||||
"type": tranquil_pds::plc::ServiceType::Pds.as_str(),
|
||||
"serviceEndpoint": service_endpoint
|
||||
}]
|
||||
})
|
||||
+36
-58
@@ -1,6 +1,6 @@
|
||||
use crate::api::SuccessResponse;
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::NormalizedLoginIdentifier;
|
||||
use tranquil_pds::api::SuccessResponse;
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::auth::NormalizedLoginIdentifier;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -19,12 +19,12 @@ use tracing::{debug, error, info, warn};
|
||||
use tranquil_db_traits::WebauthnChallengeType;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::repo::record::utils::create_signed_commit;
|
||||
use crate::auth::{ServiceTokenVerifier, generate_app_password, is_service_token};
|
||||
use crate::rate_limit::{AccountCreationLimit, PasswordResetLimit, RateLimited};
|
||||
use crate::state::AppState;
|
||||
use crate::types::{Did, Handle, PlainPassword};
|
||||
use crate::validation::validate_password;
|
||||
use tranquil_pds::repo_ops::create_signed_commit;
|
||||
use tranquil_pds::auth::{ServiceTokenVerifier, generate_app_password, is_service_token};
|
||||
use tranquil_pds::rate_limit::{AccountCreationLimit, PasswordResetLimit, RateLimited};
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::{Did, Handle, PlainPassword};
|
||||
use tranquil_pds::validation::validate_password;
|
||||
|
||||
fn generate_setup_token() -> String {
|
||||
let mut rng = rand::thread_rng();
|
||||
@@ -72,8 +72,8 @@ pub async fn create_passkey_account(
|
||||
headers: HeaderMap,
|
||||
Json(input): Json<CreatePasskeyAccountInput>,
|
||||
) -> Response {
|
||||
let byod_auth = if let Some(extracted) = crate::auth::extract_auth_token_from_header(
|
||||
crate::util::get_header_str(&headers, http::header::AUTHORIZATION),
|
||||
let byod_auth = if let Some(extracted) = tranquil_pds::auth::extract_auth_token_from_header(
|
||||
tranquil_pds::util::get_header_str(&headers, http::header::AUTHORIZATION),
|
||||
) {
|
||||
let token = extracted.token;
|
||||
if is_service_token(&token) {
|
||||
@@ -114,31 +114,9 @@ pub async fn create_passkey_account(
|
||||
|
||||
let cfg = tranquil_config::get();
|
||||
let hostname = &cfg.server.hostname;
|
||||
let available_domains = cfg.server.available_user_domain_list();
|
||||
let matched_domain = available_domains
|
||||
.iter()
|
||||
.filter(|d| input.handle.ends_with(&format!(".{}", d)))
|
||||
.max_by_key(|d| d.len());
|
||||
|
||||
let handle = if !input.handle.contains('.') || matched_domain.is_some() {
|
||||
let handle_to_validate = match matched_domain {
|
||||
Some(domain) => input
|
||||
.handle
|
||||
.strip_suffix(&format!(".{}", domain))
|
||||
.unwrap_or(&input.handle),
|
||||
None => &input.handle,
|
||||
};
|
||||
match crate::api::validation::validate_short_handle(handle_to_validate) {
|
||||
Ok(h) => format!("{}.{}", h, matched_domain.unwrap_or(&available_domains[0])),
|
||||
Err(_) => {
|
||||
return ApiError::InvalidHandle(None).into_response();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match crate::api::validation::validate_full_domain_handle(&input.handle) {
|
||||
Ok(h) => h,
|
||||
Err(_) => return ApiError::InvalidHandle(None).into_response(),
|
||||
}
|
||||
let handle = match tranquil_pds::api::validation::resolve_handle_input(&input.handle) {
|
||||
Ok(h) => h,
|
||||
Err(_) => return ApiError::InvalidHandle(None).into_response(),
|
||||
};
|
||||
|
||||
let email = input
|
||||
@@ -147,7 +125,7 @@ pub async fn create_passkey_account(
|
||||
.map(|e| e.trim().to_string())
|
||||
.filter(|e| !e.is_empty());
|
||||
if let Some(ref email) = email
|
||||
&& !crate::api::validation::is_valid_email(email)
|
||||
&& !tranquil_pds::api::validation::is_valid_email(email)
|
||||
{
|
||||
return ApiError::InvalidEmail.into_response();
|
||||
}
|
||||
@@ -184,7 +162,7 @@ pub async fn create_passkey_account(
|
||||
tranquil_db_traits::CommsChannel::Discord => match &input.discord_username {
|
||||
Some(username) if !username.trim().is_empty() => {
|
||||
let clean = username.trim().to_lowercase();
|
||||
if !crate::api::validation::is_valid_discord_username(&clean) {
|
||||
if !tranquil_pds::api::validation::is_valid_discord_username(&clean) {
|
||||
return ApiError::InvalidRequest(
|
||||
"Invalid Discord username. Must be 2-32 lowercase characters (letters, numbers, underscores, periods)".into(),
|
||||
).into_response();
|
||||
@@ -196,7 +174,7 @@ pub async fn create_passkey_account(
|
||||
tranquil_db_traits::CommsChannel::Telegram => match &input.telegram_username {
|
||||
Some(username) if !username.trim().is_empty() => {
|
||||
let clean = username.trim().trim_start_matches('@');
|
||||
if !crate::api::validation::is_valid_telegram_username(clean) {
|
||||
if !tranquil_pds::api::validation::is_valid_telegram_username(clean) {
|
||||
return ApiError::InvalidRequest(
|
||||
"Invalid Telegram username. Must be 5-32 characters, alphanumeric or underscore".into(),
|
||||
).into_response();
|
||||
@@ -250,7 +228,7 @@ pub async fn create_passkey_account(
|
||||
|
||||
let did = match did_type {
|
||||
"web" => {
|
||||
if !crate::api::server::meta::is_self_hosted_did_web_enabled() {
|
||||
if !tranquil_pds::util::is_self_hosted_did_web_enabled() {
|
||||
return ApiError::SelfHostedDidWebDisabled.into_response();
|
||||
}
|
||||
let encoded_handle = handle.replace(':', "%3A");
|
||||
@@ -284,7 +262,7 @@ pub async fn create_passkey_account(
|
||||
}
|
||||
info!(did = %d, "Creating external did:web passkey account (BYOD key)");
|
||||
} else {
|
||||
if let Err(e) = crate::api::identity::did::verify_did_web(
|
||||
if let Err(e) = crate::identity::did::verify_did_web(
|
||||
d,
|
||||
hostname,
|
||||
&input.handle,
|
||||
@@ -328,9 +306,9 @@ pub async fn create_passkey_account(
|
||||
.secrets
|
||||
.plc_rotation_key
|
||||
.clone()
|
||||
.unwrap_or_else(|| crate::plc::signing_key_to_did_key(&secret_key));
|
||||
.unwrap_or_else(|| tranquil_pds::plc::signing_key_to_did_key(&secret_key));
|
||||
|
||||
let genesis_result = match crate::plc::create_genesis_operation(
|
||||
let genesis_result = match tranquil_pds::plc::create_genesis_operation(
|
||||
&secret_key,
|
||||
&rotation_key,
|
||||
&handle,
|
||||
@@ -346,7 +324,7 @@ pub async fn create_passkey_account(
|
||||
}
|
||||
};
|
||||
|
||||
let plc_client = crate::plc::PlcClient::with_cache(None, Some(state.cache.clone()));
|
||||
let plc_client = tranquil_pds::plc::PlcClient::with_cache(None, Some(state.cache.clone()));
|
||||
if let Err(e) = plc_client
|
||||
.send_operation(&genesis_result.did, &genesis_result.signed_operation)
|
||||
.await
|
||||
@@ -381,7 +359,7 @@ pub async fn create_passkey_account(
|
||||
None
|
||||
};
|
||||
|
||||
let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) {
|
||||
let encrypted_key_bytes = match tranquil_pds::config::encrypt_key(&secret_key_bytes) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => {
|
||||
error!("Error encrypting signing key: {:?}", e);
|
||||
@@ -458,7 +436,7 @@ pub async fn create_passkey_account(
|
||||
setup_expires_at,
|
||||
deactivated_at,
|
||||
encrypted_key_bytes,
|
||||
encryption_version: crate::config::ENCRYPTION_VERSION,
|
||||
encryption_version: tranquil_pds::config::ENCRYPTION_VERSION,
|
||||
reserved_key_id,
|
||||
commit_cid: commit_cid.to_string(),
|
||||
repo_rev: rev.as_ref().to_string(),
|
||||
@@ -487,7 +465,7 @@ pub async fn create_passkey_account(
|
||||
let user_id = create_result.user_id;
|
||||
|
||||
if !is_byod_did_web {
|
||||
if let Err(e) = crate::api::repo::record::sequence_identity_event(
|
||||
if let Err(e) = tranquil_pds::repo_ops::sequence_identity_event(
|
||||
&state,
|
||||
&did_typed,
|
||||
Some(&handle_typed),
|
||||
@@ -496,7 +474,7 @@ pub async fn create_passkey_account(
|
||||
{
|
||||
warn!("Failed to sequence identity event for {}: {}", did, e);
|
||||
}
|
||||
if let Err(e) = crate::api::repo::record::sequence_account_event(
|
||||
if let Err(e) = tranquil_pds::repo_ops::sequence_account_event(
|
||||
&state,
|
||||
&did_typed,
|
||||
tranquil_db_traits::AccountStatus::Active,
|
||||
@@ -509,11 +487,11 @@ pub async fn create_passkey_account(
|
||||
"$type": "app.bsky.actor.profile",
|
||||
"displayName": handle
|
||||
});
|
||||
if let Err(e) = crate::api::repo::record::create_record_internal(
|
||||
if let Err(e) = tranquil_pds::repo_ops::create_record_internal(
|
||||
&state,
|
||||
&did_typed,
|
||||
&crate::types::PROFILE_COLLECTION,
|
||||
&crate::types::PROFILE_RKEY,
|
||||
&tranquil_pds::types::PROFILE_COLLECTION,
|
||||
&tranquil_pds::types::PROFILE_RKEY,
|
||||
&profile_record,
|
||||
)
|
||||
.await
|
||||
@@ -522,14 +500,14 @@ pub async fn create_passkey_account(
|
||||
}
|
||||
}
|
||||
|
||||
let verification_token = crate::auth::verification_token::generate_signup_token(
|
||||
let verification_token = tranquil_pds::auth::verification_token::generate_signup_token(
|
||||
&did_typed,
|
||||
verification_channel,
|
||||
&verification_recipient,
|
||||
);
|
||||
let formatted_token =
|
||||
crate::auth::verification_token::format_token_for_display(&verification_token);
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_signup_verification(
|
||||
tranquil_pds::auth::verification_token::format_token_for_display(&verification_token);
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_signup_verification(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
user_id,
|
||||
@@ -546,7 +524,7 @@ pub async fn create_passkey_account(
|
||||
info!(did = %did, handle = %handle, "Passkey-only account created, awaiting setup completion");
|
||||
|
||||
let access_jwt = if byod_auth.is_some() {
|
||||
match crate::auth::create_access_token_with_metadata(&did, &secret_key_bytes) {
|
||||
match tranquil_pds::auth::create_access_token_with_metadata(&did, &secret_key_bytes) {
|
||||
Ok(token_meta) => {
|
||||
let refresh_jti = uuid::Uuid::new_v4().to_string();
|
||||
let refresh_expires = chrono::Utc::now() + chrono::Duration::hours(24);
|
||||
@@ -558,7 +536,7 @@ pub async fn create_passkey_account(
|
||||
refresh_expires_at: refresh_expires,
|
||||
login_type: tranquil_db::LoginType::Modern,
|
||||
mfa_verified: false,
|
||||
scope: None,
|
||||
scope: Some("transition:generic".to_string()),
|
||||
controller_did: None,
|
||||
app_password_name: None,
|
||||
};
|
||||
@@ -887,7 +865,7 @@ pub async fn request_passkey_recovery(
|
||||
urlencoding::encode(&recovery_token)
|
||||
);
|
||||
|
||||
let _ = crate::comms::comms_repo::enqueue_passkey_recovery(
|
||||
let _ = tranquil_pds::comms::comms_repo::enqueue_passkey_recovery(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
user.id,
|
||||
@@ -968,7 +946,7 @@ pub async fn recover_passkey_account(
|
||||
}
|
||||
if let Ok(Some(prefs)) = state.user_repo.get_comms_prefs(user.id).await {
|
||||
let actual_channel =
|
||||
crate::comms::resolve_delivery_channel(&prefs, user.preferred_comms_channel);
|
||||
tranquil_pds::comms::resolve_delivery_channel(&prefs, user.preferred_comms_channel);
|
||||
if let Err(e) = state
|
||||
.user_repo
|
||||
.set_channel_verified(&input.did, actual_channel)
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
use crate::api::EmptyResponse;
|
||||
use crate::api::error::{ApiError, DbResultExt};
|
||||
use crate::auth::{Active, Auth, require_legacy_session_mfa, require_reauth_window};
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::api::EmptyResponse;
|
||||
use tranquil_pds::api::error::{ApiError, DbResultExt};
|
||||
use tranquil_pds::auth::{Active, Auth, require_legacy_session_mfa, require_reauth_window};
|
||||
use tranquil_pds::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -271,6 +271,6 @@ pub async fn update_passkey(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn has_passkeys_for_user(state: &AppState, did: &crate::types::Did) -> bool {
|
||||
pub async fn has_passkeys_for_user(state: &AppState, did: &tranquil_pds::types::Did) -> bool {
|
||||
state.user_repo.has_passkeys(did).await.unwrap_or(false)
|
||||
}
|
||||
+12
-12
@@ -1,13 +1,13 @@
|
||||
use crate::api::error::{ApiError, DbResultExt};
|
||||
use crate::api::{EmptyResponse, HasPasswordResponse, SuccessResponse};
|
||||
use crate::auth::{
|
||||
use tranquil_pds::api::error::{ApiError, DbResultExt};
|
||||
use tranquil_pds::api::{EmptyResponse, HasPasswordResponse, SuccessResponse};
|
||||
use tranquil_pds::auth::{
|
||||
Active, Auth, NormalizedLoginIdentifier, require_legacy_session_mfa, require_reauth_window,
|
||||
require_reauth_window_if_available,
|
||||
};
|
||||
use crate::rate_limit::{PasswordResetLimit, RateLimited, ResetPasswordLimit};
|
||||
use crate::state::AppState;
|
||||
use crate::types::PlainPassword;
|
||||
use crate::validation::validate_password;
|
||||
use tranquil_pds::rate_limit::{PasswordResetLimit, RateLimited, ResetPasswordLimit};
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::PlainPassword;
|
||||
use tranquil_pds::validation::validate_password;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -19,7 +19,7 @@ use serde::Deserialize;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
fn generate_reset_code() -> String {
|
||||
crate::util::generate_token_code()
|
||||
tranquil_pds::util::generate_token_code()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -78,7 +78,7 @@ pub async fn request_password_reset(
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_password_reset(
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_password_reset(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
user_id,
|
||||
@@ -170,7 +170,7 @@ pub async fn reset_password(
|
||||
}
|
||||
};
|
||||
futures::future::join_all(result.session_jtis.iter().map(|jti| {
|
||||
let cache_key = crate::cache_keys::session_key(&result.did, jti);
|
||||
let cache_key = tranquil_pds::cache_keys::session_key(&result.did, jti);
|
||||
let cache = state.cache.clone();
|
||||
async move {
|
||||
if let Err(e) = cache.delete(&cache_key).await {
|
||||
@@ -184,7 +184,7 @@ pub async fn reset_password(
|
||||
.await;
|
||||
if let Ok(Some(prefs)) = state.user_repo.get_comms_prefs(user_id).await {
|
||||
let actual_channel =
|
||||
crate::comms::resolve_delivery_channel(&prefs, user.preferred_comms_channel);
|
||||
tranquil_pds::comms::resolve_delivery_channel(&prefs, user.preferred_comms_channel);
|
||||
if let Err(e) = state
|
||||
.user_repo
|
||||
.set_channel_verified(&user.did, actual_channel)
|
||||
@@ -212,7 +212,7 @@ pub async fn change_password(
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<ChangePasswordInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
use crate::auth::verify_password_mfa;
|
||||
use tranquil_pds::auth::verify_password_mfa;
|
||||
|
||||
let session_mfa = match require_legacy_session_mfa(&state, &auth).await {
|
||||
Ok(proof) => proof,
|
||||
+18
-18
@@ -1,4 +1,4 @@
|
||||
use crate::api::error::{ApiError, DbResultExt};
|
||||
use tranquil_pds::api::error::{ApiError, DbResultExt};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -10,10 +10,10 @@ use serde::{Deserialize, Serialize};
|
||||
use tracing::{error, info, warn};
|
||||
use tranquil_db_traits::{SessionRepository, UserRepository, WebauthnChallengeType};
|
||||
|
||||
use crate::auth::{Active, Auth};
|
||||
use crate::rate_limit::{TotpVerifyLimit, check_user_rate_limit_with_message};
|
||||
use crate::state::AppState;
|
||||
use crate::types::PlainPassword;
|
||||
use tranquil_pds::auth::{Active, Auth};
|
||||
use tranquil_pds::rate_limit::{TotpVerifyLimit, check_user_rate_limit_with_message};
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::PlainPassword;
|
||||
|
||||
pub const REAUTH_WINDOW_SECONDS: i64 = 300;
|
||||
|
||||
@@ -125,7 +125,7 @@ pub async fn reauth_totp(
|
||||
.await?;
|
||||
|
||||
let valid =
|
||||
crate::api::server::totp::verify_totp_or_backup_for_user(&state, &auth.did, &input.code)
|
||||
crate::server::totp::verify_totp_or_backup_for_user(&state, &auth.did, &input.code)
|
||||
.await;
|
||||
|
||||
if !valid {
|
||||
@@ -276,11 +276,11 @@ pub async fn reauth_passkey_finish(
|
||||
|
||||
pub async fn update_last_reauth_cached(
|
||||
session_repo: &dyn SessionRepository,
|
||||
cache: &std::sync::Arc<dyn crate::cache::Cache>,
|
||||
did: &crate::types::Did,
|
||||
cache: &std::sync::Arc<dyn tranquil_pds::cache::Cache>,
|
||||
did: &tranquil_pds::types::Did,
|
||||
) -> Result<DateTime<Utc>, tranquil_db_traits::DbError> {
|
||||
let now = session_repo.update_last_reauth(did).await?;
|
||||
let cache_key = crate::cache_keys::reauth_key(did);
|
||||
let cache_key = tranquil_pds::cache_keys::reauth_key(did);
|
||||
let _ = cache
|
||||
.set(
|
||||
&cache_key,
|
||||
@@ -304,7 +304,7 @@ fn is_reauth_required(last_reauth_at: Option<DateTime<Utc>>) -> bool {
|
||||
async fn get_available_reauth_methods(
|
||||
user_repo: &dyn UserRepository,
|
||||
_session_repo: &dyn SessionRepository,
|
||||
did: &crate::types::Did,
|
||||
did: &tranquil_pds::types::Did,
|
||||
) -> Vec<ReauthMethod> {
|
||||
let mut methods = Vec::new();
|
||||
|
||||
@@ -334,7 +334,7 @@ async fn get_available_reauth_methods(
|
||||
|
||||
pub async fn check_reauth_required(
|
||||
session_repo: &dyn SessionRepository,
|
||||
did: &crate::types::Did,
|
||||
did: &tranquil_pds::types::Did,
|
||||
) -> bool {
|
||||
match session_repo.get_last_reauth_at(did).await {
|
||||
Ok(last_reauth_at) => is_reauth_required(last_reauth_at),
|
||||
@@ -344,10 +344,10 @@ pub async fn check_reauth_required(
|
||||
|
||||
pub async fn check_reauth_required_cached(
|
||||
session_repo: &dyn SessionRepository,
|
||||
cache: &std::sync::Arc<dyn crate::cache::Cache>,
|
||||
did: &crate::types::Did,
|
||||
cache: &std::sync::Arc<dyn tranquil_pds::cache::Cache>,
|
||||
did: &tranquil_pds::types::Did,
|
||||
) -> bool {
|
||||
let cache_key = crate::cache_keys::reauth_key(did);
|
||||
let cache_key = tranquil_pds::cache_keys::reauth_key(did);
|
||||
if let Some(timestamp_str) = cache.get(&cache_key).await
|
||||
&& let Ok(timestamp) = timestamp_str.parse::<i64>()
|
||||
{
|
||||
@@ -376,7 +376,7 @@ pub struct ReauthRequiredError {
|
||||
pub async fn reauth_required_response(
|
||||
user_repo: &dyn UserRepository,
|
||||
session_repo: &dyn SessionRepository,
|
||||
did: &crate::types::Did,
|
||||
did: &tranquil_pds::types::Did,
|
||||
) -> Response {
|
||||
let methods = get_available_reauth_methods(user_repo, session_repo, did).await;
|
||||
(
|
||||
@@ -392,7 +392,7 @@ pub async fn reauth_required_response(
|
||||
|
||||
pub async fn check_legacy_session_mfa(
|
||||
session_repo: &dyn SessionRepository,
|
||||
did: &crate::types::Did,
|
||||
did: &tranquil_pds::types::Did,
|
||||
) -> bool {
|
||||
match session_repo.get_session_mfa_status(did).await {
|
||||
Ok(Some(status)) => {
|
||||
@@ -416,7 +416,7 @@ pub async fn check_legacy_session_mfa(
|
||||
|
||||
pub async fn update_mfa_verified(
|
||||
session_repo: &dyn SessionRepository,
|
||||
did: &crate::types::Did,
|
||||
did: &tranquil_pds::types::Did,
|
||||
) -> Result<(), tranquil_db_traits::DbError> {
|
||||
session_repo.update_mfa_verified(did).await
|
||||
}
|
||||
@@ -424,7 +424,7 @@ pub async fn update_mfa_verified(
|
||||
pub async fn legacy_mfa_required_response(
|
||||
user_repo: &dyn UserRepository,
|
||||
session_repo: &dyn SessionRepository,
|
||||
did: &crate::types::Did,
|
||||
did: &tranquil_pds::types::Did,
|
||||
) -> Response {
|
||||
let methods = get_available_reauth_methods(user_repo, session_repo, did).await;
|
||||
(
|
||||
+19
-116
@@ -1,8 +1,7 @@
|
||||
use crate::AccountStatus;
|
||||
use crate::api::error::ApiError;
|
||||
use crate::state::AppState;
|
||||
use crate::types::Did;
|
||||
use axum::http::Method;
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::auth::extractor::{Auth, Permissive};
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::Did;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Query, State},
|
||||
@@ -10,7 +9,6 @@ use axum::{
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::LazyLock;
|
||||
use tracing::{error, info, warn};
|
||||
@@ -59,112 +57,25 @@ pub struct GetServiceAuthOutput {
|
||||
|
||||
pub async fn get_service_auth(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
auth: Auth<Permissive>,
|
||||
Query(params): Query<GetServiceAuthParams>,
|
||||
) -> Response {
|
||||
let auth_header = crate::util::get_header_str(&headers, axum::http::header::AUTHORIZATION);
|
||||
let dpop_proof = crate::util::get_header_str(&headers, crate::util::HEADER_DPOP);
|
||||
info!(
|
||||
has_auth_header = auth_header.is_some(),
|
||||
has_dpop_proof = dpop_proof.is_some(),
|
||||
did = %&auth.did,
|
||||
is_oauth = auth.is_oauth(),
|
||||
aud = %params.aud,
|
||||
lxm = ?params.lxm,
|
||||
"getServiceAuth called"
|
||||
);
|
||||
let auth_header = match auth_header {
|
||||
Some(h) => h.trim(),
|
||||
None => {
|
||||
warn!("getServiceAuth: no Authorization header");
|
||||
return ApiError::AuthenticationRequired.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let extracted = match crate::auth::extract_auth_token_from_header(Some(auth_header)) {
|
||||
Some(e) => e,
|
||||
None => {
|
||||
warn!(auth_scheme = ?auth_header.split_whitespace().next(), "getServiceAuth: invalid auth scheme");
|
||||
return ApiError::AuthenticationRequired.into_response();
|
||||
}
|
||||
};
|
||||
let token = extracted.token;
|
||||
|
||||
let auth_user = if extracted.scheme.is_dpop() {
|
||||
match crate::oauth::verify::verify_oauth_access_token(
|
||||
state.oauth_repo.as_ref(),
|
||||
&token,
|
||||
dpop_proof,
|
||||
Method::GET.as_str(),
|
||||
&crate::util::build_full_url(&format!(
|
||||
"/xrpc/com.atproto.server.getServiceAuth?aud={}&lxm={}",
|
||||
params.aud,
|
||||
params.lxm.as_ref().map_or("", |n| n.as_str())
|
||||
)),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
let did: Did = match result.did.parse() {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return ApiError::InternalError(Some("Invalid DID in token".into()))
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
crate::auth::AuthenticatedUser {
|
||||
did,
|
||||
is_admin: false,
|
||||
status: AccountStatus::Active,
|
||||
scope: result.scope,
|
||||
key_bytes: None,
|
||||
controller_did: None,
|
||||
auth_source: crate::auth::AuthSource::OAuth,
|
||||
}
|
||||
}
|
||||
Err(crate::oauth::OAuthError::UseDpopNonce(nonce)) => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
[("DPoP-Nonce", nonce)],
|
||||
Json(json!({
|
||||
"error": "use_dpop_nonce",
|
||||
"message": "DPoP nonce required"
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(crate::oauth::OAuthError::ExpiredToken(msg)) => {
|
||||
warn!(error = %msg, "getServiceAuth DPoP token expired");
|
||||
return ApiError::OAuthExpiredToken(Some(msg)).into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(error = ?e, "getServiceAuth DPoP auth validation failed");
|
||||
return ApiError::AuthenticationFailed(Some(format!("{:?}", e))).into_response();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match crate::auth::validate_bearer_token_for_service_auth(state.user_repo.as_ref(), &token)
|
||||
.await
|
||||
{
|
||||
Ok(user) => user,
|
||||
Err(e) => {
|
||||
warn!(error = ?e, "getServiceAuth auth validation failed");
|
||||
return ApiError::from(e).into_response();
|
||||
}
|
||||
}
|
||||
};
|
||||
info!(
|
||||
did = %&auth_user.did,
|
||||
is_oauth = auth_user.is_oauth(),
|
||||
has_key = auth_user.key_bytes.is_some(),
|
||||
"getServiceAuth auth validated"
|
||||
);
|
||||
let key_bytes = match &auth_user.key_bytes {
|
||||
let key_bytes = match &auth.key_bytes {
|
||||
Some(kb) => kb.clone(),
|
||||
None => {
|
||||
warn!(did = %&auth_user.did, "getServiceAuth: OAuth token has no key_bytes, fetching from DB");
|
||||
match state.user_repo.get_user_info_by_did(&auth_user.did).await {
|
||||
warn!(did = %&auth.did, "getServiceAuth: no key_bytes in auth, fetching from DB");
|
||||
match state.user_repo.get_user_info_by_did(&auth.did).await {
|
||||
Ok(Some(info)) => match info.key_bytes {
|
||||
Some(key_bytes_enc) => {
|
||||
match crate::config::decrypt_key(&key_bytes_enc, info.encryption_version) {
|
||||
match tranquil_pds::config::decrypt_key(&key_bytes_enc, info.encryption_version) {
|
||||
Ok(key) => key,
|
||||
Err(e) => {
|
||||
error!(error = ?e, "Failed to decrypt user key for service auth");
|
||||
@@ -201,16 +112,16 @@ pub async fn get_service_auth(
|
||||
let lxm_for_token = lxm.map_or("*", |n| n.as_str());
|
||||
|
||||
if let Some(method) = lxm {
|
||||
if let Err(e) = crate::auth::scope_check::check_rpc_scope(
|
||||
&auth_user.auth_source,
|
||||
auth_user.scope.as_deref(),
|
||||
if let Err(e) = tranquil_pds::auth::scope_check::check_rpc_scope(
|
||||
&auth.auth_source,
|
||||
auth.scope.as_deref(),
|
||||
params.aud.as_str(),
|
||||
method.as_str(),
|
||||
) {
|
||||
return e;
|
||||
}
|
||||
} else if auth_user.is_oauth() {
|
||||
let permissions = auth_user.permissions();
|
||||
} else if auth.is_oauth() {
|
||||
let permissions = auth.permissions();
|
||||
if !permissions.has_full_access() {
|
||||
return ApiError::InvalidRequest(
|
||||
"OAuth tokens with granular scopes must specify an lxm parameter".into(),
|
||||
@@ -219,15 +130,7 @@ pub async fn get_service_auth(
|
||||
}
|
||||
}
|
||||
|
||||
let is_takendown = state
|
||||
.user_repo
|
||||
.get_status_by_did(&auth_user.did)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some_and(|s| s.takedown_ref.is_some());
|
||||
|
||||
if is_takendown && lxm != Some(&*CREATE_ACCOUNT_NSID) {
|
||||
if auth.status.is_takendown() && lxm != Some(&*CREATE_ACCOUNT_NSID) {
|
||||
return ApiError::InvalidToken(Some("Bad token scope".into())).into_response();
|
||||
}
|
||||
|
||||
@@ -264,8 +167,8 @@ pub async fn get_service_auth(
|
||||
}
|
||||
}
|
||||
|
||||
let service_token = match crate::auth::create_service_token(
|
||||
&auth_user.did,
|
||||
let service_token = match tranquil_pds::auth::create_service_token(
|
||||
&auth.did,
|
||||
params.aud.as_str(),
|
||||
lxm_for_token,
|
||||
&key_bytes,
|
||||
+53
-53
@@ -1,12 +1,12 @@
|
||||
use crate::api::error::{ApiError, DbResultExt};
|
||||
use crate::api::{EmptyResponse, SuccessResponse};
|
||||
use crate::auth::{
|
||||
use tranquil_pds::api::error::{ApiError, DbResultExt};
|
||||
use tranquil_pds::api::{EmptyResponse, SuccessResponse};
|
||||
use tranquil_pds::auth::{
|
||||
Active, Auth, NormalizedLoginIdentifier, Permissive, require_legacy_session_mfa,
|
||||
require_reauth_window,
|
||||
};
|
||||
use crate::rate_limit::{LoginLimit, RateLimited, RefreshSessionLimit};
|
||||
use crate::state::AppState;
|
||||
use crate::types::{AccountState, Did, Handle, PlainPassword};
|
||||
use tranquil_pds::rate_limit::{LoginLimit, RateLimited, RefreshSessionLimit};
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::{AccountState, Did, Handle, PlainPassword};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -68,7 +68,7 @@ pub async fn create_session(
|
||||
let pds_host = &tranquil_config::get().server.hostname;
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let normalized_identifier =
|
||||
NormalizedLoginIdentifier::normalize(&input.identifier, &hostname_for_handles);
|
||||
NormalizedLoginIdentifier::normalize(&input.identifier, hostname_for_handles);
|
||||
info!(
|
||||
"Normalized identifier: {} -> {}",
|
||||
input.identifier, normalized_identifier
|
||||
@@ -93,7 +93,7 @@ pub async fn create_session(
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let key_bytes = match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) {
|
||||
let key_bytes = match tranquil_pds::config::decrypt_key(&row.key_bytes, row.encryption_version) {
|
||||
Ok(k) => k,
|
||||
Err(e) => {
|
||||
error!("Failed to decrypt user key: {:?}", e);
|
||||
@@ -173,12 +173,12 @@ pub async fn create_session(
|
||||
let has_totp = row.totp_enabled;
|
||||
let email_2fa_enabled = row.email_2fa_enabled;
|
||||
let is_legacy_login = has_totp || email_2fa_enabled;
|
||||
let twofa_ctx = crate::auth::legacy_2fa::Legacy2faContext {
|
||||
let twofa_ctx = tranquil_pds::auth::legacy_2fa::Legacy2faContext {
|
||||
email_2fa_enabled,
|
||||
has_totp,
|
||||
allow_legacy_login: row.allow_legacy_login,
|
||||
};
|
||||
match crate::auth::legacy_2fa::process_legacy_2fa(
|
||||
match tranquil_pds::auth::legacy_2fa::process_legacy_2fa(
|
||||
state.cache.as_ref(),
|
||||
&row.did,
|
||||
&twofa_ctx,
|
||||
@@ -186,14 +186,14 @@ pub async fn create_session(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(crate::auth::legacy_2fa::Legacy2faOutcome::NotRequired) => {}
|
||||
Ok(crate::auth::legacy_2fa::Legacy2faOutcome::Blocked) => {
|
||||
Ok(tranquil_pds::auth::legacy_2fa::Legacy2faOutcome::NotRequired) => {}
|
||||
Ok(tranquil_pds::auth::legacy_2fa::Legacy2faOutcome::Blocked) => {
|
||||
warn!("Legacy login blocked for TOTP-enabled account: {}", row.did);
|
||||
return ApiError::LegacyLoginBlocked.into_response();
|
||||
}
|
||||
Ok(crate::auth::legacy_2fa::Legacy2faOutcome::ChallengeSent(code)) => {
|
||||
Ok(tranquil_pds::auth::legacy_2fa::Legacy2faOutcome::ChallengeSent(code)) => {
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_2fa_code(
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_2fa_code(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
row.id,
|
||||
@@ -203,7 +203,7 @@ pub async fn create_session(
|
||||
.await
|
||||
{
|
||||
error!("Failed to send 2FA code: {:?}", e);
|
||||
crate::auth::legacy_2fa::clear_challenge(state.cache.as_ref(), &row.did).await;
|
||||
tranquil_pds::auth::legacy_2fa::clear_challenge(state.cache.as_ref(), &row.did).await;
|
||||
return ApiError::InternalError(Some(
|
||||
"Failed to send verification code. Please try again.".into(),
|
||||
))
|
||||
@@ -211,9 +211,9 @@ pub async fn create_session(
|
||||
}
|
||||
return ApiError::AuthFactorTokenRequired.into_response();
|
||||
}
|
||||
Ok(crate::auth::legacy_2fa::Legacy2faOutcome::Verified) => {}
|
||||
Err(crate::auth::legacy_2fa::Legacy2faFlowError::Challenge(e)) => {
|
||||
use crate::auth::legacy_2fa::ChallengeError;
|
||||
Ok(tranquil_pds::auth::legacy_2fa::Legacy2faOutcome::Verified) => {}
|
||||
Err(tranquil_pds::auth::legacy_2fa::Legacy2faFlowError::Challenge(e)) => {
|
||||
use tranquil_pds::auth::legacy_2fa::ChallengeError;
|
||||
return match e {
|
||||
ChallengeError::CacheUnavailable => {
|
||||
error!("Cache unavailable for 2FA, blocking legacy login");
|
||||
@@ -232,8 +232,8 @@ pub async fn create_session(
|
||||
}
|
||||
};
|
||||
}
|
||||
Err(crate::auth::legacy_2fa::Legacy2faFlowError::Validation(e)) => {
|
||||
use crate::auth::legacy_2fa::ValidationError;
|
||||
Err(tranquil_pds::auth::legacy_2fa::Legacy2faFlowError::Validation(e)) => {
|
||||
use tranquil_pds::auth::legacy_2fa::ValidationError;
|
||||
warn!("Invalid 2FA code for {}: {:?}", row.did, e);
|
||||
let msg = match e {
|
||||
ValidationError::TooManyAttempts => "Too many attempts. Please request a new code.",
|
||||
@@ -248,7 +248,7 @@ pub async fn create_session(
|
||||
return ApiError::InvalidCode(Some(msg.into())).into_response();
|
||||
}
|
||||
}
|
||||
let access_meta = match crate::auth::create_access_token_with_delegation(
|
||||
let access_meta = match tranquil_pds::auth::create_access_token_with_delegation(
|
||||
&row.did,
|
||||
&key_bytes,
|
||||
app_password_scopes.as_deref(),
|
||||
@@ -261,7 +261,7 @@ pub async fn create_session(
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let refresh_meta = match crate::auth::create_refresh_token_with_metadata(&row.did, &key_bytes) {
|
||||
let refresh_meta = match tranquil_pds::auth::create_refresh_token_with_metadata(&row.did, &key_bytes) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
error!("Failed to create refresh token: {:?}", e);
|
||||
@@ -297,7 +297,7 @@ pub async fn create_session(
|
||||
"Legacy login on TOTP-enabled account - sending notification"
|
||||
);
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_legacy_login(
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_legacy_login(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
row.id,
|
||||
@@ -406,18 +406,18 @@ pub async fn delete_session(
|
||||
headers: axum::http::HeaderMap,
|
||||
_auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let extracted = crate::auth::extract_auth_token_from_header(crate::util::get_header_str(
|
||||
let extracted = tranquil_pds::auth::extract_auth_token_from_header(tranquil_pds::util::get_header_str(
|
||||
&headers,
|
||||
http::header::AUTHORIZATION,
|
||||
))
|
||||
.ok_or(ApiError::AuthenticationRequired)?;
|
||||
let jti = crate::auth::get_jti_from_token(&extracted.token)
|
||||
let jti = tranquil_pds::auth::get_jti_from_token(&extracted.token)
|
||||
.map_err(|_| ApiError::AuthenticationFailed(None))?;
|
||||
let did = crate::auth::get_did_from_token(&extracted.token).ok();
|
||||
let did = tranquil_pds::auth::get_did_from_token(&extracted.token).ok();
|
||||
match state.session_repo.delete_session_by_access_jti(&jti).await {
|
||||
Ok(rows) if rows > 0 => {
|
||||
if let Some(did) = did {
|
||||
let session_cache_key = crate::cache_keys::session_key(&did, &jti);
|
||||
let session_cache_key = tranquil_pds::cache_keys::session_key(&did, &jti);
|
||||
let _ = state.cache.delete(&session_cache_key).await;
|
||||
}
|
||||
Ok(EmptyResponse::ok().into_response())
|
||||
@@ -432,7 +432,7 @@ pub async fn refresh_session(
|
||||
_rate_limit: RateLimited<RefreshSessionLimit>,
|
||||
headers: axum::http::HeaderMap,
|
||||
) -> Response {
|
||||
let extracted = match crate::auth::extract_auth_token_from_header(crate::util::get_header_str(
|
||||
let extracted = match tranquil_pds::auth::extract_auth_token_from_header(tranquil_pds::util::get_header_str(
|
||||
&headers,
|
||||
http::header::AUTHORIZATION,
|
||||
)) {
|
||||
@@ -440,7 +440,7 @@ pub async fn refresh_session(
|
||||
None => return ApiError::AuthenticationRequired.into_response(),
|
||||
};
|
||||
let refresh_token = extracted.token;
|
||||
let refresh_jti = match crate::auth::get_jti_from_token(&refresh_token) {
|
||||
let refresh_jti = match tranquil_pds::auth::get_jti_from_token(&refresh_token) {
|
||||
Ok(jti) => jti,
|
||||
Err(_) => {
|
||||
return ApiError::AuthenticationFailed(Some("Invalid token format".into()))
|
||||
@@ -473,7 +473,7 @@ pub async fn refresh_session(
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let key_bytes = match crate::config::decrypt_key(
|
||||
let key_bytes = match tranquil_pds::config::decrypt_key(
|
||||
&session_row.key_bytes,
|
||||
Some(session_row.encryption_version),
|
||||
) {
|
||||
@@ -483,11 +483,11 @@ pub async fn refresh_session(
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
if crate::auth::verify_refresh_token(&refresh_token, &key_bytes).is_err() {
|
||||
if tranquil_pds::auth::verify_refresh_token(&refresh_token, &key_bytes).is_err() {
|
||||
return ApiError::AuthenticationFailed(Some("Invalid refresh token".into()))
|
||||
.into_response();
|
||||
}
|
||||
let new_access_meta = match crate::auth::create_access_token_with_delegation(
|
||||
let new_access_meta = match tranquil_pds::auth::create_access_token_with_delegation(
|
||||
&session_row.did,
|
||||
&key_bytes,
|
||||
session_row.scope.as_deref(),
|
||||
@@ -501,7 +501,7 @@ pub async fn refresh_session(
|
||||
}
|
||||
};
|
||||
let new_refresh_meta =
|
||||
match crate::auth::create_refresh_token_with_metadata(&session_row.did, &key_bytes) {
|
||||
match tranquil_pds::auth::create_refresh_token_with_metadata(&session_row.did, &key_bytes) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
error!("Failed to create refresh token: {:?}", e);
|
||||
@@ -641,8 +641,8 @@ pub async fn confirm_signup(
|
||||
};
|
||||
|
||||
let normalized_token =
|
||||
crate::auth::verification_token::normalize_token_input(&input.verification_code);
|
||||
match crate::auth::verification_token::verify_signup_token(
|
||||
tranquil_pds::auth::verification_token::normalize_token_input(&input.verification_code);
|
||||
match tranquil_pds::auth::verification_token::verify_signup_token(
|
||||
&normalized_token,
|
||||
row.channel,
|
||||
&identifier,
|
||||
@@ -657,7 +657,7 @@ pub async fn confirm_signup(
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
Err(crate::auth::verification_token::VerifyError::Expired) => {
|
||||
Err(tranquil_pds::auth::verification_token::VerifyError::Expired) => {
|
||||
warn!("Verification code expired for user: {}", input.did);
|
||||
return ApiError::ExpiredToken(Some("Verification code has expired".into()))
|
||||
.into_response();
|
||||
@@ -668,7 +668,7 @@ pub async fn confirm_signup(
|
||||
}
|
||||
}
|
||||
|
||||
let key_bytes = match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) {
|
||||
let key_bytes = match tranquil_pds::config::decrypt_key(&row.key_bytes, row.encryption_version) {
|
||||
Ok(k) => k,
|
||||
Err(e) => {
|
||||
error!("Failed to decrypt user key: {:?}", e);
|
||||
@@ -676,14 +676,14 @@ pub async fn confirm_signup(
|
||||
}
|
||||
};
|
||||
|
||||
let access_meta = match crate::auth::create_access_token_with_metadata(&row.did, &key_bytes) {
|
||||
let access_meta = match tranquil_pds::auth::create_access_token_with_metadata(&row.did, &key_bytes) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
error!("Failed to create access token: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let refresh_meta = match crate::auth::create_refresh_token_with_metadata(&row.did, &key_bytes) {
|
||||
let refresh_meta = match tranquil_pds::auth::create_refresh_token_with_metadata(&row.did, &key_bytes) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
error!("Failed to create refresh token: {:?}", e);
|
||||
@@ -708,7 +708,7 @@ pub async fn confirm_signup(
|
||||
refresh_expires_at: refresh_meta.expires_at,
|
||||
login_type: tranquil_db_traits::LoginType::Modern,
|
||||
mfa_verified: false,
|
||||
scope: None,
|
||||
scope: Some("transition:generic transition:chat.bsky".to_string()),
|
||||
controller_did: None,
|
||||
app_password_name: None,
|
||||
};
|
||||
@@ -718,7 +718,7 @@ pub async fn confirm_signup(
|
||||
}
|
||||
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_welcome(
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_welcome(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
row.id,
|
||||
@@ -749,7 +749,7 @@ pub struct AutoResendResult {
|
||||
}
|
||||
|
||||
pub async fn auto_resend_verification(state: &AppState, did: &Did) -> Option<AutoResendResult> {
|
||||
let debounce_key = crate::cache_keys::auto_verify_sent_key(did.as_str());
|
||||
let debounce_key = tranquil_pds::cache_keys::auto_verify_sent_key(did.as_str());
|
||||
let debounced = state.cache.get(&debounce_key).await.is_some();
|
||||
let row = match state.user_repo.get_resend_verification_by_did(did).await {
|
||||
Ok(Some(row)) => row,
|
||||
@@ -789,11 +789,11 @@ pub async fn auto_resend_verification(state: &AppState, did: &Did) -> Option<Aut
|
||||
return Some(result);
|
||||
}
|
||||
let verification_token =
|
||||
crate::auth::verification_token::generate_signup_token(did, row.channel, &recipient);
|
||||
tranquil_pds::auth::verification_token::generate_signup_token(did, row.channel, &recipient);
|
||||
let formatted_token =
|
||||
crate::auth::verification_token::format_token_for_display(&verification_token);
|
||||
tranquil_pds::auth::verification_token::format_token_for_display(&verification_token);
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_signup_verification(
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_signup_verification(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
row.id,
|
||||
@@ -856,12 +856,12 @@ pub async fn resend_verification(
|
||||
};
|
||||
|
||||
let verification_token =
|
||||
crate::auth::verification_token::generate_signup_token(&input.did, row.channel, &recipient);
|
||||
tranquil_pds::auth::verification_token::generate_signup_token(&input.did, row.channel, &recipient);
|
||||
let formatted_token =
|
||||
crate::auth::verification_token::format_token_for_display(&verification_token);
|
||||
tranquil_pds::auth::verification_token::format_token_for_display(&verification_token);
|
||||
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_signup_verification(
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_signup_verification(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
row.id,
|
||||
@@ -910,7 +910,7 @@ pub async fn list_sessions(
|
||||
.get("authorization")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.strip_prefix("Bearer "))
|
||||
.and_then(|token| crate::auth::get_jti_from_token(token).ok());
|
||||
.and_then(|token| tranquil_pds::auth::get_jti_from_token(token).ok());
|
||||
|
||||
let jwt_rows = state
|
||||
.session_repo
|
||||
@@ -990,7 +990,7 @@ pub async fn revoke_session(
|
||||
.delete_session_by_id(session_id)
|
||||
.await
|
||||
.log_db_err("deleting session")?;
|
||||
let cache_key = crate::cache_keys::session_key(&auth.did, &access_jti);
|
||||
let cache_key = tranquil_pds::cache_keys::session_key(&auth.did, &access_jti);
|
||||
if let Err(e) = state.cache.delete(&cache_key).await {
|
||||
warn!("Failed to invalidate session cache: {:?}", e);
|
||||
}
|
||||
@@ -1020,10 +1020,10 @@ pub async fn revoke_all_sessions(
|
||||
headers: HeaderMap,
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let jti = crate::auth::extract_auth_token_from_header(
|
||||
let jti = tranquil_pds::auth::extract_auth_token_from_header(
|
||||
headers.get("authorization").and_then(|v| v.to_str().ok()),
|
||||
)
|
||||
.and_then(|extracted| crate::auth::get_jti_from_token(&extracted.token).ok())
|
||||
.and_then(|extracted| tranquil_pds::auth::get_jti_from_token(&extracted.token).ok())
|
||||
.ok_or(ApiError::InvalidToken(None))?;
|
||||
|
||||
if auth.is_oauth() {
|
||||
@@ -1119,7 +1119,7 @@ pub async fn update_legacy_login_preference(
|
||||
.into_response())
|
||||
}
|
||||
|
||||
use crate::comms::VALID_LOCALES;
|
||||
use tranquil_pds::comms::VALID_LOCALES;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -25,7 +25,7 @@ fn public_key_to_did_key(signing_key: &SigningKey) -> String {
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ReserveSigningKeyInput {
|
||||
pub did: Option<crate::types::Did>,
|
||||
pub did: Option<tranquil_pds::types::Did>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
+10
-10
@@ -1,14 +1,14 @@
|
||||
use crate::api::EmptyResponse;
|
||||
use crate::api::error::{ApiError, DbResultExt};
|
||||
use crate::auth::{
|
||||
use tranquil_pds::api::EmptyResponse;
|
||||
use tranquil_pds::api::error::{ApiError, DbResultExt};
|
||||
use tranquil_pds::auth::{
|
||||
Active, Auth, decrypt_totp_secret, encrypt_totp_secret, generate_backup_codes,
|
||||
generate_qr_png_base64, generate_totp_secret, generate_totp_uri, hash_backup_code,
|
||||
is_backup_code_format, require_legacy_session_mfa, verify_backup_code, verify_password_mfa,
|
||||
verify_totp_code, verify_totp_mfa,
|
||||
};
|
||||
use crate::rate_limit::{TotpVerifyLimit, check_user_rate_limit_with_message};
|
||||
use crate::state::AppState;
|
||||
use crate::types::PlainPassword;
|
||||
use tranquil_pds::rate_limit::{TotpVerifyLimit, check_user_rate_limit_with_message};
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::PlainPassword;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -186,7 +186,7 @@ pub async fn disable_totp(
|
||||
.await
|
||||
.log_db_err("deleting TOTP")?;
|
||||
|
||||
crate::auth::legacy_2fa::clear_challenge(state.cache.as_ref(), &auth.did).await;
|
||||
tranquil_pds::auth::legacy_2fa::clear_challenge(state.cache.as_ref(), &auth.did).await;
|
||||
|
||||
info!(did = %session_mfa.did(), "TOTP disabled (verified via {} and {})", password_mfa.method(), totp_mfa.method());
|
||||
|
||||
@@ -280,7 +280,7 @@ pub async fn regenerate_backup_codes(
|
||||
|
||||
async fn verify_backup_code_for_user(
|
||||
state: &AppState,
|
||||
did: &crate::types::Did,
|
||||
did: &tranquil_pds::types::Did,
|
||||
code: &str,
|
||||
) -> bool {
|
||||
let code = code.trim().to_uppercase();
|
||||
@@ -308,7 +308,7 @@ async fn verify_backup_code_for_user(
|
||||
|
||||
pub async fn verify_totp_or_backup_for_user(
|
||||
state: &AppState,
|
||||
did: &crate::types::Did,
|
||||
did: &tranquil_pds::types::Did,
|
||||
code: &str,
|
||||
) -> bool {
|
||||
use tranquil_db_traits::TotpRecordState;
|
||||
@@ -340,6 +340,6 @@ pub async fn verify_totp_or_backup_for_user(
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn has_totp_enabled(state: &AppState, did: &crate::types::Did) -> bool {
|
||||
pub async fn has_totp_enabled(state: &AppState, did: &tranquil_pds::types::Did) -> bool {
|
||||
state.user_repo.has_totp_enabled(did).await.unwrap_or(false)
|
||||
}
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
use crate::api::SuccessResponse;
|
||||
use crate::api::error::{ApiError, DbResultExt};
|
||||
use tranquil_pds::api::SuccessResponse;
|
||||
use tranquil_pds::api::error::{ApiError, DbResultExt};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -11,8 +11,8 @@ use tracing::{error, info};
|
||||
use tranquil_db_traits::OAuthRepository;
|
||||
use tranquil_types::DeviceId;
|
||||
|
||||
use crate::auth::{Active, Auth};
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::auth::{Active, Auth};
|
||||
use tranquil_pds::state::AppState;
|
||||
|
||||
const TRUST_DURATION_DAYS: i64 = 30;
|
||||
|
||||
+6
-6
@@ -1,10 +1,10 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::types::Did;
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::types::Did;
|
||||
use axum::{Json, extract::State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::state::AppState;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -71,10 +71,10 @@ pub async fn resend_migration_verification(
|
||||
}
|
||||
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let token = crate::auth::verification_token::generate_migration_token(&user.did, &email);
|
||||
let formatted_token = crate::auth::verification_token::format_token_for_display(&token);
|
||||
let token = tranquil_pds::auth::verification_token::generate_migration_token(&user.did, &email);
|
||||
let formatted_token = tranquil_pds::auth::verification_token::format_token_for_display(&token);
|
||||
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_migration_verification(
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_migration_verification(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
user.id,
|
||||
+6
-6
@@ -1,14 +1,14 @@
|
||||
use crate::api::error::{ApiError, DbResultExt};
|
||||
use crate::comms::comms_repo;
|
||||
use crate::types::Did;
|
||||
use tranquil_pds::api::error::{ApiError, DbResultExt};
|
||||
use tranquil_pds::comms::comms_repo;
|
||||
use tranquil_pds::types::Did;
|
||||
use axum::{Json, extract::State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::auth::verification_token::{
|
||||
use tranquil_pds::auth::verification_token::{
|
||||
VerificationPurpose, normalize_token_input, verify_token_signature,
|
||||
};
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_db_traits::CommsChannel;
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
@@ -46,7 +46,7 @@ pub async fn verify_token_internal(
|
||||
ApiError::from(e)
|
||||
})?;
|
||||
|
||||
let expected_hash = crate::auth::verification_token::hash_identifier(&identifier);
|
||||
let expected_hash = tranquil_pds::auth::verification_token::hash_identifier(&identifier);
|
||||
if token_data.identifier_hash != expected_hash {
|
||||
return Err(ApiError::IdentifierMismatch);
|
||||
}
|
||||
+2
-2
@@ -6,8 +6,8 @@ use axum::{
|
||||
use serde::Deserialize;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::comms::comms_repo;
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::comms::comms_repo;
|
||||
use tranquil_pds::state::AppState;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TelegramUpdate {
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::{Active, Auth, Permissive};
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::auth::{Active, Auth, Permissive};
|
||||
use tranquil_pds::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -57,7 +57,7 @@ pub async fn dereference_scope(
|
||||
|
||||
for part in scope_parts {
|
||||
if let Some(cid_str) = part.strip_prefix("ref:") {
|
||||
let cache_key = crate::cache_keys::scope_ref_key(cid_str);
|
||||
let cache_key = tranquil_pds::cache_keys::scope_ref_key(cid_str);
|
||||
if let Some(cached) = state.cache.get(&cache_key).await {
|
||||
for s in cached.split_whitespace() {
|
||||
if !resolved_scopes.contains(&s.to_string()) {
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
use crate::api::SuccessResponse;
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::api::SuccessResponse;
|
||||
use tranquil_pds::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
@@ -19,12 +19,12 @@ pub async fn confirm_channel_verification(
|
||||
State(state): State<AppState>,
|
||||
Json(input): Json<ConfirmChannelVerificationInput>,
|
||||
) -> Response {
|
||||
let token_input = crate::api::server::VerifyTokenInput {
|
||||
let token_input = crate::server::VerifyTokenInput {
|
||||
token: input.code,
|
||||
identifier: input.identifier,
|
||||
};
|
||||
|
||||
match crate::api::server::verify_token_internal(&state, token_input).await {
|
||||
match crate::server::verify_token_internal(&state, token_input).await {
|
||||
Ok(_output) => SuccessResponse::ok().into_response(),
|
||||
Err(e) => e.into_response(),
|
||||
}
|
||||
@@ -327,12 +327,10 @@ impl TranquilConfig {
|
||||
errors: &mut Vec<String>,
|
||||
) {
|
||||
self.validate_sso_provider(prefix, p, errors);
|
||||
if p.get_enabled() {
|
||||
if p.get_issuer().is_none() {
|
||||
errors.push(format!(
|
||||
"{prefix}.issuer is required when {prefix}.enabled = true"
|
||||
));
|
||||
}
|
||||
if p.get_enabled() && p.get_issuer().is_none() {
|
||||
errors.push(format!(
|
||||
"{prefix}.issuer is required when {prefix}.enabled = true"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ pub struct DelegationGrant {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DelegatedAccountInfo {
|
||||
pub did: Did,
|
||||
pub handle: Handle,
|
||||
@@ -28,12 +29,14 @@ pub struct DelegatedAccountInfo {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ControllerInfo {
|
||||
pub did: Did,
|
||||
pub handle: Handle,
|
||||
pub handle: Option<Handle>,
|
||||
pub granted_scopes: DbScope,
|
||||
pub granted_at: DateTime<Utc>,
|
||||
pub is_active: bool,
|
||||
pub is_local: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -48,6 +51,7 @@ pub enum DelegationActionType {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuditLogEntry {
|
||||
pub id: Uuid,
|
||||
pub delegated_did: Did,
|
||||
@@ -55,7 +59,9 @@ pub struct AuditLogEntry {
|
||||
pub controller_did: Option<Did>,
|
||||
pub action_type: DelegationActionType,
|
||||
pub action_details: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing)]
|
||||
pub ip_address: Option<String>,
|
||||
#[serde(skip_serializing)]
|
||||
pub user_agent: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
@@ -102,15 +108,8 @@ pub trait DelegationRepository: Send + Sync {
|
||||
controller_did: &Did,
|
||||
) -> Result<Vec<DelegatedAccountInfo>, DbError>;
|
||||
|
||||
async fn get_active_controllers_for_account(
|
||||
&self,
|
||||
delegated_did: &Did,
|
||||
) -> Result<Vec<ControllerInfo>, DbError>;
|
||||
|
||||
async fn count_active_controllers(&self, delegated_did: &Did) -> Result<i64, DbError>;
|
||||
|
||||
async fn has_any_controllers(&self, did: &Did) -> Result<bool, DbError>;
|
||||
|
||||
async fn controls_any_accounts(&self, did: &Did) -> Result<bool, DbError>;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -132,12 +131,5 @@ pub trait DelegationRepository: Send + Sync {
|
||||
offset: i64,
|
||||
) -> Result<Vec<AuditLogEntry>, DbError>;
|
||||
|
||||
async fn get_audit_log_by_controller(
|
||||
&self,
|
||||
controller_did: &Did,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
) -> Result<Vec<AuditLogEntry>, DbError>;
|
||||
|
||||
async fn count_audit_log_entries(&self, delegated_did: &Did) -> Result<i64, DbError>;
|
||||
}
|
||||
|
||||
@@ -185,13 +185,17 @@ impl DelegationRepository for PostgresDelegationRepository {
|
||||
let rows = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
u.did,
|
||||
u.handle,
|
||||
d.controller_did,
|
||||
u.handle as "handle?",
|
||||
d.granted_scopes,
|
||||
d.granted_at,
|
||||
(u.deactivated_at IS NULL AND u.takedown_ref IS NULL) as "is_active!"
|
||||
CASE WHEN u.did IS NOT NULL
|
||||
THEN u.deactivated_at IS NULL AND u.takedown_ref IS NULL
|
||||
ELSE true
|
||||
END as "is_active!",
|
||||
u.did IS NOT NULL as "is_local!"
|
||||
FROM account_delegations d
|
||||
JOIN users u ON u.did = d.controller_did
|
||||
LEFT JOIN users u ON u.did = d.controller_did
|
||||
WHERE d.delegated_did = $1 AND d.revoked_at IS NULL
|
||||
ORDER BY d.granted_at DESC
|
||||
"#,
|
||||
@@ -204,11 +208,12 @@ impl DelegationRepository for PostgresDelegationRepository {
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| ControllerInfo {
|
||||
did: r.did.into(),
|
||||
handle: r.handle.into(),
|
||||
did: r.controller_did.into(),
|
||||
handle: r.handle.map(Into::into),
|
||||
granted_scopes: DbScope::from_db(r.granted_scopes),
|
||||
granted_at: r.granted_at,
|
||||
is_active: r.is_active,
|
||||
is_local: r.is_local,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
@@ -249,54 +254,15 @@ impl DelegationRepository for PostgresDelegationRepository {
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn get_active_controllers_for_account(
|
||||
&self,
|
||||
delegated_did: &Did,
|
||||
) -> Result<Vec<ControllerInfo>, DbError> {
|
||||
let rows = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
u.did,
|
||||
u.handle,
|
||||
d.granted_scopes,
|
||||
d.granted_at,
|
||||
true as "is_active!"
|
||||
FROM account_delegations d
|
||||
JOIN users u ON u.did = d.controller_did
|
||||
WHERE d.delegated_did = $1
|
||||
AND d.revoked_at IS NULL
|
||||
AND u.deactivated_at IS NULL
|
||||
AND u.takedown_ref IS NULL
|
||||
ORDER BY d.granted_at DESC
|
||||
"#,
|
||||
delegated_did.as_str()
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| ControllerInfo {
|
||||
did: r.did.into(),
|
||||
handle: r.handle.into(),
|
||||
granted_scopes: DbScope::from_db(r.granted_scopes),
|
||||
granted_at: r.granted_at,
|
||||
is_active: r.is_active,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn count_active_controllers(&self, delegated_did: &Did) -> Result<i64, DbError> {
|
||||
let count = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT COUNT(*) as "count!"
|
||||
FROM account_delegations d
|
||||
JOIN users u ON u.did = d.controller_did
|
||||
LEFT JOIN users u ON u.did = d.controller_did
|
||||
WHERE d.delegated_did = $1
|
||||
AND d.revoked_at IS NULL
|
||||
AND u.deactivated_at IS NULL
|
||||
AND u.takedown_ref IS NULL
|
||||
AND (u.did IS NULL OR (u.deactivated_at IS NULL AND u.takedown_ref IS NULL))
|
||||
"#,
|
||||
delegated_did.as_str()
|
||||
)
|
||||
@@ -307,21 +273,6 @@ impl DelegationRepository for PostgresDelegationRepository {
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
async fn has_any_controllers(&self, did: &Did) -> Result<bool, DbError> {
|
||||
let exists = sqlx::query_scalar!(
|
||||
r#"SELECT EXISTS(
|
||||
SELECT 1 FROM account_delegations
|
||||
WHERE delegated_did = $1 AND revoked_at IS NULL
|
||||
) as "exists!""#,
|
||||
did.as_str()
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
async fn controls_any_accounts(&self, did: &Did) -> Result<bool, DbError> {
|
||||
let exists = sqlx::query_scalar!(
|
||||
r#"SELECT EXISTS(
|
||||
@@ -418,53 +369,6 @@ impl DelegationRepository for PostgresDelegationRepository {
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn get_audit_log_by_controller(
|
||||
&self,
|
||||
controller_did: &Did,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
) -> Result<Vec<AuditLogEntry>, DbError> {
|
||||
let rows = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
delegated_did,
|
||||
actor_did,
|
||||
controller_did,
|
||||
action_type as "action_type: PgDelegationActionType",
|
||||
action_details,
|
||||
ip_address,
|
||||
user_agent,
|
||||
created_at
|
||||
FROM delegation_audit_log
|
||||
WHERE controller_did = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
"#,
|
||||
controller_did.as_str(),
|
||||
limit,
|
||||
offset
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(map_sqlx_error)?;
|
||||
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| AuditLogEntry {
|
||||
id: r.id,
|
||||
delegated_did: r.delegated_did.into(),
|
||||
actor_did: r.actor_did.into(),
|
||||
controller_did: r.controller_did.map(Into::into),
|
||||
action_type: r.action_type.into(),
|
||||
action_details: r.action_details,
|
||||
ip_address: r.ip_address,
|
||||
user_agent: r.user_agent,
|
||||
created_at: r.created_at,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn count_audit_log_entries(&self, delegated_did: &Did) -> Result<i64, DbError> {
|
||||
let count = sqlx::query_scalar!(
|
||||
r#"SELECT COUNT(*) as "count!" FROM delegation_audit_log WHERE delegated_did = $1"#,
|
||||
|
||||
@@ -46,9 +46,7 @@ pub fn is_valid_uri(s: &str) -> bool {
|
||||
}
|
||||
|
||||
pub fn is_valid_cid(s: &str) -> bool {
|
||||
s.len() >= 8
|
||||
&& s.chars().all(|c| c.is_ascii_alphanumeric())
|
||||
&& s.starts_with(|c: char| c == 'b' || c == 'z' || c == 'Q')
|
||||
s.len() >= 8 && s.chars().all(|c| c.is_ascii_alphanumeric()) && s.starts_with(['b', 'z', 'Q'])
|
||||
}
|
||||
|
||||
pub fn is_valid_language(s: &str) -> bool {
|
||||
|
||||
@@ -339,15 +339,14 @@ fn validate_blob_ref(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(max_size) = lex_blob.max_size {
|
||||
if let Some(size) = obj.get("size").and_then(|v| v.as_u64()) {
|
||||
if size > max_size {
|
||||
return Err(LexValidationError::field(
|
||||
path,
|
||||
format!("blob size {} exceeds max_size {}", size, max_size),
|
||||
));
|
||||
}
|
||||
}
|
||||
if let (Some(max_size), Some(size)) =
|
||||
(lex_blob.max_size, obj.get("size").and_then(|v| v.as_u64()))
|
||||
&& size > max_size
|
||||
{
|
||||
return Err(LexValidationError::field(
|
||||
path,
|
||||
format!("blob size {} exceeds max_size {}", size, max_size),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
[package]
|
||||
name = "tranquil-oauth-server"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tranquil-pds = { workspace = true }
|
||||
tranquil-api = { workspace = true }
|
||||
tranquil-types = { workspace = true }
|
||||
tranquil-config = { workspace = true }
|
||||
tranquil-crypto = { workspace = true }
|
||||
tranquil-db-traits = { workspace = true }
|
||||
|
||||
axum = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
bcrypt = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
cid = { workspace = true }
|
||||
hmac = { workspace = true }
|
||||
http = { workspace = true }
|
||||
jacquard-common = { workspace = true }
|
||||
jacquard-repo = { workspace = true }
|
||||
k256 = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_urlencoded = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
subtle = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
urlencoding = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
webauthn-rs = { workspace = true }
|
||||
+38
-38
@@ -1,16 +1,16 @@
|
||||
use crate::auth::{BareLoginIdentifier, NormalizedLoginIdentifier};
|
||||
use crate::comms::comms_repo::enqueue_2fa_code;
|
||||
use crate::oauth::{
|
||||
use tranquil_pds::auth::{BareLoginIdentifier, NormalizedLoginIdentifier};
|
||||
use tranquil_pds::comms::comms_repo::enqueue_2fa_code;
|
||||
use tranquil_pds::oauth::{
|
||||
AuthFlow, ClientMetadataCache, Code, DeviceData, DeviceId, OAuthError, Prompt, SessionId,
|
||||
db::should_show_consent, scopes::expand_include_scopes,
|
||||
};
|
||||
use crate::rate_limit::{
|
||||
use tranquil_pds::rate_limit::{
|
||||
OAuthAuthorizeLimit, OAuthRateLimited, OAuthRegisterCompleteLimit, TotpVerifyLimit,
|
||||
check_user_rate_limit,
|
||||
};
|
||||
use crate::state::AppState;
|
||||
use crate::types::{Did, Handle, PlainPassword};
|
||||
use crate::util::extract_client_ip;
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::{Did, Handle, PlainPassword};
|
||||
use tranquil_pds::util::extract_client_ip;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Query, State},
|
||||
@@ -95,7 +95,7 @@ fn extract_device_cookie(headers: &HeaderMap) -> Option<tranquil_types::DeviceId
|
||||
cookie_str.split(';').map(|c| c.trim()).find_map(|cookie| {
|
||||
cookie
|
||||
.strip_prefix(&format!("{}=", DEVICE_COOKIE_NAME))
|
||||
.and_then(|value| crate::config::AuthConfig::get().verify_device_cookie(value))
|
||||
.and_then(|value| tranquil_pds::config::AuthConfig::get().verify_device_cookie(value))
|
||||
.map(tranquil_types::DeviceId::new)
|
||||
})
|
||||
})
|
||||
@@ -109,7 +109,7 @@ fn extract_user_agent(headers: &HeaderMap) -> Option<String> {
|
||||
}
|
||||
|
||||
fn make_device_cookie(device_id: &tranquil_types::DeviceId) -> String {
|
||||
let signed_value = crate::config::AuthConfig::get().sign_device_cookie(device_id.as_str());
|
||||
let signed_value = tranquil_pds::config::AuthConfig::get().sign_device_cookie(device_id.as_str());
|
||||
format!(
|
||||
"{}={}; Path=/oauth; HttpOnly; Secure; SameSite=Lax; Max-Age=31536000",
|
||||
DEVICE_COOKIE_NAME, signed_value
|
||||
@@ -256,7 +256,7 @@ pub async fn authorize_get(
|
||||
if let Some(ref login_hint) = request_data.parameters.login_hint {
|
||||
tracing::info!(login_hint = %login_hint, "Checking login_hint for delegation");
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let normalized = NormalizedLoginIdentifier::normalize(login_hint, &hostname_for_handles);
|
||||
let normalized = NormalizedLoginIdentifier::normalize(login_hint, hostname_for_handles);
|
||||
tracing::info!(normalized = %normalized, "Normalized login_hint");
|
||||
|
||||
match state
|
||||
@@ -343,7 +343,7 @@ pub async fn authorize_get_json(
|
||||
.oauth_repo
|
||||
.get_authorization_request(&request_id_json)
|
||||
.await
|
||||
.map_err(crate::oauth::db_err_to_oauth)?
|
||||
.map_err(tranquil_pds::oauth::db_err_to_oauth)?
|
||||
.ok_or_else(|| OAuthError::InvalidRequest("Invalid or expired request_uri".to_string()))?;
|
||||
if request_data.expires_at < Utc::now() {
|
||||
let _ = state
|
||||
@@ -530,7 +530,7 @@ pub async fn authorize_post(
|
||||
};
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let normalized_username =
|
||||
NormalizedLoginIdentifier::normalize(&form.username, &hostname_for_handles);
|
||||
NormalizedLoginIdentifier::normalize(&form.username, hostname_for_handles);
|
||||
tracing::debug!(
|
||||
original_username = %form.username,
|
||||
normalized_username = %normalized_username,
|
||||
@@ -626,7 +626,7 @@ pub async fn authorize_post(
|
||||
}
|
||||
let is_verified = user.channel_verification.has_any_verified();
|
||||
if !is_verified {
|
||||
let resend_info = crate::api::server::auto_resend_verification(&state, &user.did).await;
|
||||
let resend_info = tranquil_api::server::auto_resend_verification(&state, &user.did).await;
|
||||
let handle = resend_info
|
||||
.as_ref()
|
||||
.map(|r| r.handle.to_string())
|
||||
@@ -653,11 +653,11 @@ pub async fn authorize_post(
|
||||
url_encode("account_not_verified")
|
||||
));
|
||||
}
|
||||
let has_totp = crate::api::server::has_totp_enabled(&state, &user.did).await;
|
||||
let has_totp = tranquil_api::server::has_totp_enabled(&state, &user.did).await;
|
||||
if has_totp {
|
||||
let device_cookie = extract_device_cookie(&headers);
|
||||
let device_is_trusted = if let Some(ref dev_id) = device_cookie {
|
||||
crate::api::server::is_device_trusted(state.oauth_repo.as_ref(), dev_id, &user.did)
|
||||
tranquil_api::server::is_device_trusted(state.oauth_repo.as_ref(), dev_id, &user.did)
|
||||
.await
|
||||
} else {
|
||||
false
|
||||
@@ -665,7 +665,7 @@ pub async fn authorize_post(
|
||||
|
||||
if device_is_trusted {
|
||||
if let Some(ref dev_id) = device_cookie {
|
||||
let _ = crate::api::server::extend_device_trust(state.oauth_repo.as_ref(), dev_id)
|
||||
let _ = tranquil_api::server::extend_device_trust(state.oauth_repo.as_ref(), dev_id)
|
||||
.await;
|
||||
}
|
||||
} else {
|
||||
@@ -978,7 +978,7 @@ pub async fn authorize_select(
|
||||
};
|
||||
let is_verified = user.channel_verification.has_any_verified();
|
||||
if !is_verified {
|
||||
let resend_info = crate::api::server::auto_resend_verification(&state, &did).await;
|
||||
let resend_info = tranquil_api::server::auto_resend_verification(&state, &did).await;
|
||||
return (
|
||||
StatusCode::FORBIDDEN,
|
||||
Json(serde_json::json!({
|
||||
@@ -991,11 +991,11 @@ pub async fn authorize_select(
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let has_totp = crate::api::server::has_totp_enabled(&state, &did).await;
|
||||
let has_totp = tranquil_api::server::has_totp_enabled(&state, &did).await;
|
||||
let select_early_device_typed = device_id.clone();
|
||||
if has_totp {
|
||||
let device_is_trusted =
|
||||
crate::api::server::is_device_trusted(state.oauth_repo.as_ref(), &device_id, &did)
|
||||
tranquil_api::server::is_device_trusted(state.oauth_repo.as_ref(), &device_id, &did)
|
||||
.await;
|
||||
if !device_is_trusted {
|
||||
if state
|
||||
@@ -1016,7 +1016,7 @@ pub async fn authorize_select(
|
||||
.into_response();
|
||||
}
|
||||
let _ =
|
||||
crate::api::server::extend_device_trust(state.oauth_repo.as_ref(), &device_id).await;
|
||||
tranquil_api::server::extend_device_trust(state.oauth_repo.as_ref(), &device_id).await;
|
||||
}
|
||||
if user.two_factor_enabled {
|
||||
let _ = state
|
||||
@@ -1413,7 +1413,7 @@ pub async fn consent_get(
|
||||
};
|
||||
|
||||
let effective_scope_str = if let Some(ref grant) = delegation_grant {
|
||||
crate::delegation::intersect_scopes(requested_scope_str, grant.granted_scopes.as_str())
|
||||
tranquil_pds::delegation::intersect_scopes(requested_scope_str, grant.granted_scopes.as_str())
|
||||
} else {
|
||||
requested_scope_str.to_string()
|
||||
};
|
||||
@@ -1445,7 +1445,7 @@ pub async fn consent_get(
|
||||
.iter()
|
||||
.map(|scope| {
|
||||
let (category, required, description, display_name) = if let Some(def) =
|
||||
crate::oauth::scopes::SCOPE_DEFINITIONS.get(*scope)
|
||||
tranquil_pds::oauth::scopes::SCOPE_DEFINITIONS.get(*scope)
|
||||
{
|
||||
let desc = if *scope == "atproto" && has_granular_scopes {
|
||||
"AT Protocol baseline scope (permissions determined by selected options below)"
|
||||
@@ -1510,7 +1510,7 @@ pub async fn consent_get(
|
||||
.map(|h| h.to_string());
|
||||
|
||||
let level = if let Some(ref grant) = delegation_grant {
|
||||
let preset = crate::delegation::SCOPE_PRESETS
|
||||
let preset = tranquil_pds::delegation::SCOPE_PRESETS
|
||||
.iter()
|
||||
.find(|p| p.scopes == grant.granted_scopes.as_str());
|
||||
preset
|
||||
@@ -1622,7 +1622,7 @@ pub async fn consent_post(
|
||||
};
|
||||
|
||||
let effective_scope_str = if let Some(ref grant) = delegation_grant {
|
||||
crate::delegation::intersect_scopes(original_scope_str, grant.granted_scopes.as_str())
|
||||
tranquil_pds::delegation::intersect_scopes(original_scope_str, grant.granted_scopes.as_str())
|
||||
} else {
|
||||
original_scope_str.to_string()
|
||||
};
|
||||
@@ -1955,7 +1955,7 @@ pub async fn authorize_2fa_post(
|
||||
);
|
||||
}
|
||||
};
|
||||
if !crate::api::server::has_totp_enabled(&state, &did).await {
|
||||
if !tranquil_api::server::has_totp_enabled(&state, &did).await {
|
||||
return json_error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"invalid_request",
|
||||
@@ -1973,7 +1973,7 @@ pub async fn authorize_2fa_post(
|
||||
}
|
||||
};
|
||||
let totp_valid =
|
||||
crate::api::server::verify_totp_or_backup_for_user(&state, &did, &form.code).await;
|
||||
tranquil_api::server::verify_totp_or_backup_for_user(&state, &did, &form.code).await;
|
||||
if !totp_valid {
|
||||
return json_error(
|
||||
StatusCode::FORBIDDEN,
|
||||
@@ -2011,7 +2011,7 @@ pub async fn authorize_2fa_post(
|
||||
.oauth_repo
|
||||
.upsert_account_device(&did, &trust_device_id)
|
||||
.await;
|
||||
let _ = crate::api::server::trust_device(state.oauth_repo.as_ref(), &trust_device_id).await;
|
||||
let _ = tranquil_api::server::trust_device(state.oauth_repo.as_ref(), &trust_device_id).await;
|
||||
}
|
||||
let requested_scope_str = request_data
|
||||
.parameters
|
||||
@@ -2102,7 +2102,7 @@ pub async fn check_user_has_passkeys(
|
||||
) -> Response {
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let bare_identifier =
|
||||
BareLoginIdentifier::from_identifier(&query.identifier, &hostname_for_handles);
|
||||
BareLoginIdentifier::from_identifier(&query.identifier, hostname_for_handles);
|
||||
|
||||
let user = state
|
||||
.user_repo
|
||||
@@ -2110,7 +2110,7 @@ pub async fn check_user_has_passkeys(
|
||||
.await;
|
||||
|
||||
let has_passkeys = match user {
|
||||
Ok(Some(u)) => crate::api::server::has_passkeys_for_user(&state, &u.did).await,
|
||||
Ok(Some(u)) => tranquil_api::server::has_passkeys_for_user(&state, &u.did).await,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
@@ -2134,7 +2134,7 @@ pub async fn check_user_security_status(
|
||||
) -> Response {
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let normalized_identifier =
|
||||
NormalizedLoginIdentifier::normalize(&query.identifier, &hostname_for_handles);
|
||||
NormalizedLoginIdentifier::normalize(&query.identifier, hostname_for_handles);
|
||||
|
||||
let user = state
|
||||
.user_repo
|
||||
@@ -2149,8 +2149,8 @@ pub async fn check_user_security_status(
|
||||
Option<String>,
|
||||
) = match user {
|
||||
Ok(Some(u)) => {
|
||||
let passkeys = crate::api::server::has_passkeys_for_user(&state, &u.did).await;
|
||||
let totp = crate::api::server::has_totp_enabled(&state, &u.did).await;
|
||||
let passkeys = tranquil_api::server::has_passkeys_for_user(&state, &u.did).await;
|
||||
let totp = tranquil_api::server::has_totp_enabled(&state, &u.did).await;
|
||||
let has_pw = u.password_hash.is_some();
|
||||
let has_controllers = state
|
||||
.delegation_repo
|
||||
@@ -2242,7 +2242,7 @@ pub async fn passkey_start(
|
||||
|
||||
let hostname_for_handles = tranquil_config::get().server.hostname_without_port();
|
||||
let normalized_username =
|
||||
NormalizedLoginIdentifier::normalize(&form.identifier, &hostname_for_handles);
|
||||
NormalizedLoginIdentifier::normalize(&form.identifier, hostname_for_handles);
|
||||
|
||||
let user = match state
|
||||
.user_repo
|
||||
@@ -2297,7 +2297,7 @@ pub async fn passkey_start(
|
||||
let is_verified = user.channel_verification.has_any_verified();
|
||||
|
||||
if !is_verified {
|
||||
let resend_info = crate::api::server::auto_resend_verification(&state, &user.did).await;
|
||||
let resend_info = tranquil_api::server::auto_resend_verification(&state, &user.did).await;
|
||||
return (
|
||||
StatusCode::FORBIDDEN,
|
||||
Json(serde_json::json!({
|
||||
@@ -3125,14 +3125,14 @@ pub async fn authorize_passkey_finish(
|
||||
if has_totp {
|
||||
let device_cookie = extract_device_cookie(&headers);
|
||||
let device_is_trusted = if let Some(ref dev_id) = device_cookie {
|
||||
crate::api::server::is_device_trusted(state.oauth_repo.as_ref(), dev_id, &did).await
|
||||
tranquil_api::server::is_device_trusted(state.oauth_repo.as_ref(), dev_id, &did).await
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if device_is_trusted {
|
||||
if let Some(ref dev_id) = device_cookie {
|
||||
let _ = crate::api::server::extend_device_trust(state.oauth_repo.as_ref(), dev_id)
|
||||
let _ = tranquil_api::server::extend_device_trust(state.oauth_repo.as_ref(), dev_id)
|
||||
.await;
|
||||
}
|
||||
} else {
|
||||
@@ -3395,7 +3395,7 @@ pub async fn register_complete(
|
||||
};
|
||||
|
||||
if !is_verified {
|
||||
let resend_info = crate::api::server::auto_resend_verification(&state, &did).await;
|
||||
let resend_info = tranquil_api::server::auto_resend_verification(&state, &did).await;
|
||||
return (
|
||||
StatusCode::FORBIDDEN,
|
||||
Json(serde_json::json!({
|
||||
@@ -3503,7 +3503,7 @@ pub async fn register_complete(
|
||||
pub async fn establish_session(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
auth: crate::auth::Auth<crate::auth::Active>,
|
||||
auth: tranquil_pds::auth::Auth<tranquil_pds::auth::Active>,
|
||||
) -> Response {
|
||||
let did = &auth.did;
|
||||
|
||||
@@ -0,0 +1,584 @@
|
||||
use tranquil_pds::auth::{Active, Auth};
|
||||
use tranquil_pds::delegation::DelegationActionType;
|
||||
use tranquil_pds::oauth::client::{build_client_metadata, delegation_oauth_urls};
|
||||
use tranquil_pds::rate_limit::{LoginLimit, OAuthRateLimited, TotpVerifyLimit};
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::PlainPassword;
|
||||
use tranquil_pds::util::extract_client_ip;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Query, State},
|
||||
http::HeaderMap,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tranquil_pds::oauth::RequestData;
|
||||
use tranquil_types::did_doc::{extract_handle, extract_pds_endpoint};
|
||||
use tranquil_types::{Did, RequestId};
|
||||
|
||||
#[allow(clippy::result_large_err)]
|
||||
fn parse_did(s: &str, label: &str) -> Result<Did, Response> {
|
||||
s.parse()
|
||||
.map_err(|_| DelegationAuthResponse::err(format!("Invalid {} DID", label)))
|
||||
}
|
||||
|
||||
async fn get_auth_request(state: &AppState, request_uri: &str) -> Result<RequestData, Response> {
|
||||
let request_id = RequestId::from(request_uri.to_string());
|
||||
match state
|
||||
.oauth_repo
|
||||
.get_authorization_request(&request_id)
|
||||
.await
|
||||
{
|
||||
Ok(Some(r)) => Ok(r),
|
||||
Ok(None) => Err(DelegationAuthResponse::err(
|
||||
"Authorization request not found",
|
||||
)),
|
||||
Err(_) => Err(DelegationAuthResponse::err("Server error")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_delegation_grant(
|
||||
state: &AppState,
|
||||
delegated_did: &Did,
|
||||
controller_did: &Did,
|
||||
) -> Result<tranquil_db_traits::DelegationGrant, Response> {
|
||||
match state
|
||||
.delegation_repo
|
||||
.get_delegation(delegated_did, controller_did)
|
||||
.await
|
||||
{
|
||||
Ok(Some(g)) => Ok(g),
|
||||
Ok(None) => Err(DelegationAuthResponse::err(
|
||||
"No delegation grant found for this controller",
|
||||
)),
|
||||
Err(_) => Err(DelegationAuthResponse::err("Server error")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn finalize_delegation_auth(
|
||||
state: &AppState,
|
||||
request_uri: &str,
|
||||
delegated_did: &Did,
|
||||
controller_did: &Did,
|
||||
details: serde_json::Value,
|
||||
ip: Option<&str>,
|
||||
user_agent: Option<&str>,
|
||||
) -> Response {
|
||||
let _ = state
|
||||
.delegation_repo
|
||||
.log_delegation_action(
|
||||
delegated_did,
|
||||
controller_did,
|
||||
Some(controller_did),
|
||||
DelegationActionType::TokenIssued,
|
||||
Some(details),
|
||||
ip,
|
||||
user_agent,
|
||||
)
|
||||
.await;
|
||||
consent_redirect(request_uri)
|
||||
}
|
||||
|
||||
async fn bind_delegation_to_request(
|
||||
state: &AppState,
|
||||
request_uri: &str,
|
||||
delegated_did: &Did,
|
||||
controller_did: &Did,
|
||||
) -> Result<(), Response> {
|
||||
let request_id = RequestId::from(request_uri.to_string());
|
||||
state
|
||||
.oauth_repo
|
||||
.set_request_did(&request_id, delegated_did)
|
||||
.await
|
||||
.map_err(|_| DelegationAuthResponse::err("Failed to update authorization request"))?;
|
||||
state
|
||||
.oauth_repo
|
||||
.set_controller_did(&request_id, controller_did)
|
||||
.await
|
||||
.map_err(|_| DelegationAuthResponse::err("Failed to update authorization request"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn consent_url(request_uri: &str) -> String {
|
||||
format!(
|
||||
"/app/oauth/consent?request_uri={}",
|
||||
urlencoding::encode(request_uri)
|
||||
)
|
||||
}
|
||||
|
||||
fn consent_redirect(request_uri: &str) -> Response {
|
||||
DelegationAuthResponse::redirect(consent_url(request_uri))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DelegationAuthSubmit {
|
||||
pub request_uri: String,
|
||||
pub delegated_did: Option<String>,
|
||||
pub controller_did: String,
|
||||
pub password: Option<PlainPassword>,
|
||||
#[serde(default)]
|
||||
pub remember_device: bool,
|
||||
pub auth_method: Option<String>,
|
||||
}
|
||||
|
||||
enum DelegationAuthResponse {
|
||||
Redirect(String),
|
||||
NeedsTotp(String),
|
||||
Error(String),
|
||||
TotpError(String),
|
||||
}
|
||||
|
||||
impl DelegationAuthResponse {
|
||||
fn err(msg: impl Into<String>) -> Response {
|
||||
Self::Error(msg.into()).into_response()
|
||||
}
|
||||
|
||||
fn redirect(uri: impl Into<String>) -> Response {
|
||||
Self::Redirect(uri.into()).into_response()
|
||||
}
|
||||
|
||||
fn needs_totp(uri: impl Into<String>) -> Response {
|
||||
Self::NeedsTotp(uri.into()).into_response()
|
||||
}
|
||||
|
||||
fn totp_error(msg: impl Into<String>) -> Response {
|
||||
Self::TotpError(msg.into()).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for DelegationAuthResponse {
|
||||
fn into_response(self) -> Response {
|
||||
let (success, needs_totp, redirect_uri, error) = match self {
|
||||
Self::Redirect(uri) => (true, None, Some(uri), None),
|
||||
Self::NeedsTotp(uri) => (true, Some(true), Some(uri), None),
|
||||
Self::Error(msg) => (false, None, None, Some(msg)),
|
||||
Self::TotpError(msg) => (false, Some(true), None, Some(msg)),
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Body {
|
||||
success: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
needs_totp: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
redirect_uri: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
Json(Body {
|
||||
success,
|
||||
needs_totp,
|
||||
redirect_uri,
|
||||
error,
|
||||
})
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delegation_auth(
|
||||
State(state): State<AppState>,
|
||||
rate_limit: OAuthRateLimited<LoginLimit>,
|
||||
headers: HeaderMap,
|
||||
Json(form): Json<DelegationAuthSubmit>,
|
||||
) -> Response {
|
||||
let client_ip = rate_limit.client_ip();
|
||||
let request = match get_auth_request(&state, &form.request_uri).await {
|
||||
Ok(r) => r,
|
||||
Err(resp) => return resp,
|
||||
};
|
||||
|
||||
let delegated_did = if let Some(did_str) = form.delegated_did.as_ref() {
|
||||
match parse_did(did_str, "delegated") {
|
||||
Ok(d) => d,
|
||||
Err(resp) => return resp,
|
||||
}
|
||||
} else if let Some(did) = request.did.clone() {
|
||||
did
|
||||
} else {
|
||||
return DelegationAuthResponse::err("No delegated account selected");
|
||||
};
|
||||
|
||||
let controller_did = match parse_did(&form.controller_did, "controller") {
|
||||
Ok(d) => d,
|
||||
Err(resp) => return resp,
|
||||
};
|
||||
|
||||
let grant = match get_delegation_grant(&state, &delegated_did, &controller_did).await {
|
||||
Ok(g) => g,
|
||||
Err(resp) => return resp,
|
||||
};
|
||||
|
||||
let is_cross_pds = form.auth_method.as_deref() == Some("cross_pds");
|
||||
let controller_local = state
|
||||
.user_repo
|
||||
.get_auth_info_by_did(&controller_did)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
if is_cross_pds || controller_local.is_none() {
|
||||
let did_doc = match state
|
||||
.plc_client()
|
||||
.get_document(controller_did.as_str())
|
||||
.await
|
||||
{
|
||||
Ok(doc) => doc,
|
||||
Err(_) => {
|
||||
return DelegationAuthResponse::err("Failed to resolve controller DID");
|
||||
}
|
||||
};
|
||||
|
||||
let pds_url = match extract_pds_endpoint(&did_doc) {
|
||||
Some(url) => url,
|
||||
None => {
|
||||
return DelegationAuthResponse::err("Controller has no PDS endpoint");
|
||||
}
|
||||
};
|
||||
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let urls = delegation_oauth_urls(hostname);
|
||||
let login_hint = extract_handle(&did_doc);
|
||||
let (par_result, auth_state, oauth_state) = match state
|
||||
.cross_pds_oauth
|
||||
.initiate_par(
|
||||
&pds_url,
|
||||
&urls,
|
||||
login_hint.as_deref(),
|
||||
&form.request_uri,
|
||||
&controller_did,
|
||||
&delegated_did,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
tracing::error!("Cross-PDS PAR failed: {:?}", e);
|
||||
return DelegationAuthResponse::err("Failed to initiate cross-PDS authentication");
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = state
|
||||
.cross_pds_oauth
|
||||
.store_auth_state(&oauth_state, &auth_state)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to store cross-PDS auth state: {:?}", e);
|
||||
return DelegationAuthResponse::err(
|
||||
"Internal error preparing cross-PDS authentication",
|
||||
);
|
||||
}
|
||||
|
||||
return DelegationAuthResponse::redirect(par_result.authorize_url);
|
||||
}
|
||||
|
||||
let controller = controller_local.unwrap();
|
||||
|
||||
if controller.deactivated_at.is_some() {
|
||||
return DelegationAuthResponse::err("Controller account is deactivated");
|
||||
}
|
||||
|
||||
if controller.takedown_ref.is_some() {
|
||||
return DelegationAuthResponse::err("Controller account has been taken down");
|
||||
}
|
||||
|
||||
let password = match form.password {
|
||||
Some(ref pw) => pw,
|
||||
None => {
|
||||
return DelegationAuthResponse::err("Password required for local controller");
|
||||
}
|
||||
};
|
||||
|
||||
let password_valid = controller
|
||||
.password_hash
|
||||
.as_ref()
|
||||
.map(|hash| bcrypt::verify(password, hash).unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
|
||||
if !password_valid {
|
||||
return DelegationAuthResponse::err("Invalid password");
|
||||
}
|
||||
|
||||
if let Err(resp) =
|
||||
bind_delegation_to_request(&state, &form.request_uri, &delegated_did, &controller_did).await
|
||||
{
|
||||
return resp;
|
||||
}
|
||||
|
||||
let has_totp = tranquil_api::server::has_totp_enabled(&state, &controller_did).await;
|
||||
if has_totp {
|
||||
return DelegationAuthResponse::needs_totp(format!(
|
||||
"/app/oauth/delegation-totp?request_uri={}",
|
||||
urlencoding::encode(&form.request_uri)
|
||||
));
|
||||
}
|
||||
|
||||
let user_agent = tranquil_pds::util::extract_user_agent(&headers);
|
||||
|
||||
finalize_delegation_auth(
|
||||
&state,
|
||||
&form.request_uri,
|
||||
&delegated_did,
|
||||
&controller_did,
|
||||
serde_json::json!({
|
||||
"client_id": request.client_id,
|
||||
"granted_scopes": grant.granted_scopes
|
||||
}),
|
||||
Some(client_ip),
|
||||
user_agent.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DelegationTotpSubmit {
|
||||
pub request_uri: String,
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
pub async fn delegation_totp_verify(
|
||||
State(state): State<AppState>,
|
||||
rate_limit: OAuthRateLimited<TotpVerifyLimit>,
|
||||
headers: HeaderMap,
|
||||
Json(form): Json<DelegationTotpSubmit>,
|
||||
) -> Response {
|
||||
let client_ip = rate_limit.client_ip();
|
||||
let request = match get_auth_request(&state, &form.request_uri).await {
|
||||
Ok(r) => r,
|
||||
Err(resp) => return resp,
|
||||
};
|
||||
|
||||
let controller_did = match request.controller_did {
|
||||
Some(did) => did,
|
||||
None => return DelegationAuthResponse::err("Controller not authenticated"),
|
||||
};
|
||||
|
||||
let delegated_did = match request.did {
|
||||
Some(did) => did,
|
||||
None => return DelegationAuthResponse::err("No delegated account"),
|
||||
};
|
||||
|
||||
let grant = match get_delegation_grant(&state, &delegated_did, &controller_did).await {
|
||||
Ok(g) => g,
|
||||
Err(resp) => return resp,
|
||||
};
|
||||
|
||||
let totp_valid =
|
||||
tranquil_api::server::verify_totp_or_backup_for_user(&state, &controller_did, &form.code)
|
||||
.await;
|
||||
if !totp_valid {
|
||||
return DelegationAuthResponse::totp_error("Invalid TOTP code");
|
||||
}
|
||||
|
||||
let user_agent = tranquil_pds::util::extract_user_agent(&headers);
|
||||
|
||||
finalize_delegation_auth(
|
||||
&state,
|
||||
&form.request_uri,
|
||||
&delegated_did,
|
||||
&controller_did,
|
||||
serde_json::json!({
|
||||
"client_id": request.client_id,
|
||||
"granted_scopes": grant.granted_scopes
|
||||
}),
|
||||
Some(client_ip),
|
||||
user_agent.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DelegationTokenAuthSubmit {
|
||||
pub request_uri: String,
|
||||
pub delegated_did: String,
|
||||
}
|
||||
|
||||
pub async fn delegation_auth_token(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
auth: Auth<Active>,
|
||||
Json(form): Json<DelegationTokenAuthSubmit>,
|
||||
) -> Response {
|
||||
let controller_did = &auth.did;
|
||||
|
||||
let delegated_did = match parse_did(&form.delegated_did, "delegated") {
|
||||
Ok(d) => d,
|
||||
Err(resp) => return resp,
|
||||
};
|
||||
|
||||
let request = match get_auth_request(&state, &form.request_uri).await {
|
||||
Ok(r) => r,
|
||||
Err(resp) => return resp,
|
||||
};
|
||||
|
||||
let grant = match get_delegation_grant(&state, &delegated_did, controller_did).await {
|
||||
Ok(g) => g,
|
||||
Err(resp) => return resp,
|
||||
};
|
||||
|
||||
if let Err(resp) =
|
||||
bind_delegation_to_request(&state, &form.request_uri, &delegated_did, controller_did).await
|
||||
{
|
||||
return resp;
|
||||
}
|
||||
|
||||
let ip = extract_client_ip(&headers, None);
|
||||
let user_agent = tranquil_pds::util::extract_user_agent(&headers);
|
||||
|
||||
finalize_delegation_auth(
|
||||
&state,
|
||||
&form.request_uri,
|
||||
&delegated_did,
|
||||
controller_did,
|
||||
serde_json::json!({
|
||||
"client_id": request.client_id,
|
||||
"granted_scopes": grant.granted_scopes,
|
||||
"auth_method": "token"
|
||||
}),
|
||||
Some(&ip),
|
||||
user_agent.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CrossPdsCallbackParams {
|
||||
pub code: String,
|
||||
pub state: String,
|
||||
pub iss: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn delegation_callback(
|
||||
State(state): State<AppState>,
|
||||
_rate_limit: OAuthRateLimited<LoginLimit>,
|
||||
Query(params): Query<CrossPdsCallbackParams>,
|
||||
) -> Response {
|
||||
let auth_state = match state
|
||||
.cross_pds_oauth
|
||||
.retrieve_auth_state(¶ms.state)
|
||||
.await
|
||||
{
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to retrieve cross-PDS auth state: {:?}", e);
|
||||
return (
|
||||
axum::http::StatusCode::BAD_REQUEST,
|
||||
"Cross-PDS auth state expired or invalid",
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(ref expected_issuer) = auth_state.expected_issuer {
|
||||
match ¶ms.iss {
|
||||
Some(iss) if iss != expected_issuer => {
|
||||
tracing::error!(
|
||||
"Cross-PDS issuer mismatch: expected {}, got {}",
|
||||
expected_issuer,
|
||||
iss
|
||||
);
|
||||
return (
|
||||
axum::http::StatusCode::FORBIDDEN,
|
||||
"Authorization server issuer mismatch",
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
None => {
|
||||
tracing::error!(
|
||||
"Cross-PDS callback missing iss parameter (expected {}), possible mix-up attack",
|
||||
expected_issuer
|
||||
);
|
||||
return (
|
||||
axum::http::StatusCode::BAD_REQUEST,
|
||||
"Missing required iss parameter",
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let urls = delegation_oauth_urls(hostname);
|
||||
|
||||
let returned_sub = match state
|
||||
.cross_pds_oauth
|
||||
.exchange_code(
|
||||
&auth_state,
|
||||
¶ms.code,
|
||||
&urls.client_id,
|
||||
&urls.redirect_uri,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(sub) => sub,
|
||||
Err(e) => {
|
||||
tracing::error!("Cross-PDS token exchange failed: {:?}", e);
|
||||
return (
|
||||
axum::http::StatusCode::BAD_GATEWAY,
|
||||
"Controller authentication failed",
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if returned_sub != auth_state.controller_did.as_str() {
|
||||
tracing::error!(
|
||||
"Cross-PDS DID mismatch: expected {}, got {}",
|
||||
auth_state.controller_did,
|
||||
returned_sub
|
||||
);
|
||||
return (axum::http::StatusCode::FORBIDDEN, "Controller DID mismatch").into_response();
|
||||
}
|
||||
|
||||
let delegated_did = &auth_state.delegated_did;
|
||||
let controller_did = &auth_state.controller_did;
|
||||
|
||||
if let Err(_) = get_delegation_grant(&state, delegated_did, controller_did).await {
|
||||
tracing::warn!(
|
||||
"Delegation grant revoked during cross-PDS auth: {} -> {}",
|
||||
controller_did,
|
||||
delegated_did
|
||||
);
|
||||
return (
|
||||
axum::http::StatusCode::FORBIDDEN,
|
||||
"Delegation grant has been revoked",
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
if let Err(resp) = bind_delegation_to_request(
|
||||
&state,
|
||||
&auth_state.original_request_uri,
|
||||
delegated_did,
|
||||
controller_did,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return resp;
|
||||
}
|
||||
|
||||
let _ = state
|
||||
.delegation_repo
|
||||
.log_delegation_action(
|
||||
delegated_did,
|
||||
controller_did,
|
||||
Some(controller_did),
|
||||
DelegationActionType::TokenIssued,
|
||||
Some(serde_json::json!({
|
||||
"auth_method": "cross_pds",
|
||||
"controller_pds": auth_state.controller_pds_url
|
||||
})),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
Redirect::temporary(&consent_url(&auth_state.original_request_uri)).into_response()
|
||||
}
|
||||
|
||||
pub async fn delegation_client_metadata(State(_state): State<AppState>) -> Response {
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
let metadata = build_client_metadata(hostname);
|
||||
Json(metadata).into_response()
|
||||
}
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::oauth::jwks::{JwkSet, create_jwk_set};
|
||||
use crate::state::AppState;
|
||||
use crate::jwks::{JwkSet, create_jwk_set};
|
||||
use tranquil_pds::state::AppState;
|
||||
use axum::{Json, extract::State};
|
||||
use http::{HeaderName, header};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -129,8 +129,8 @@ pub async fn oauth_authorization_server(
|
||||
}
|
||||
|
||||
pub async fn oauth_jwks(State(_state): State<AppState>) -> Json<JwkSet> {
|
||||
use crate::config::AuthConfig;
|
||||
use crate::oauth::jwks::Jwk;
|
||||
use tranquil_pds::config::AuthConfig;
|
||||
use crate::jwks::Jwk;
|
||||
let config = AuthConfig::get();
|
||||
let server_key = Jwk {
|
||||
kty: "EC".to_string(),
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
use crate::oauth::{
|
||||
use tranquil_pds::oauth::{
|
||||
AuthorizationRequestParameters, ClientAuth, ClientMetadataCache, CodeChallengeMethod,
|
||||
OAuthError, Prompt, RequestData, RequestId, ResponseMode, ResponseType,
|
||||
scopes::{ParsedScope, parse_scope},
|
||||
};
|
||||
use crate::rate_limit::{OAuthParLimit, OAuthRateLimited};
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::rate_limit::{OAuthParLimit, OAuthRateLimited};
|
||||
use tranquil_pds::state::AppState;
|
||||
use axum::body::Bytes;
|
||||
use axum::{Json, extract::State, http::HeaderMap};
|
||||
use chrono::{Duration, Utc};
|
||||
@@ -118,7 +118,7 @@ pub async fn pushed_authorization_request(
|
||||
.oauth_repo
|
||||
.create_authorization_request(&request_id_typed, &request_data)
|
||||
.await
|
||||
.map_err(crate::oauth::db_err_to_oauth)?;
|
||||
.map_err(tranquil_pds::oauth::db_err_to_oauth)?;
|
||||
tokio::spawn({
|
||||
let oauth_repo = state.oauth_repo.clone();
|
||||
async move {
|
||||
@@ -159,7 +159,7 @@ fn determine_client_auth(request: &ParRequest) -> Result<ClientAuth, OAuthError>
|
||||
|
||||
fn validate_scope(
|
||||
requested_scope: &Option<String>,
|
||||
client_metadata: &crate::oauth::ClientMetadata,
|
||||
client_metadata: &tranquil_pds::oauth::ClientMetadata,
|
||||
) -> Result<Option<String>, OAuthError> {
|
||||
let scope_str = match requested_scope {
|
||||
Some(s) if !s.is_empty() => s,
|
||||
+11
-11
@@ -2,16 +2,16 @@ use super::helpers::{create_access_token_with_delegation, verify_pkce};
|
||||
use super::types::{
|
||||
RequestClientAuth, TokenGrant, TokenResponse, TokenType, ValidatedTokenRequest,
|
||||
};
|
||||
use crate::config::AuthConfig;
|
||||
use crate::delegation::intersect_scopes;
|
||||
use crate::oauth::{
|
||||
use tranquil_pds::config::AuthConfig;
|
||||
use tranquil_pds::delegation::intersect_scopes;
|
||||
use tranquil_pds::oauth::{
|
||||
AuthFlow, ClientAuth, ClientMetadataCache, DPoPVerifier, OAuthError, RefreshToken, TokenData,
|
||||
TokenId,
|
||||
db::{enforce_token_limit_for_user, lookup_refresh_token},
|
||||
scopes::expand_include_scopes,
|
||||
verify_client_auth,
|
||||
};
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::state::AppState;
|
||||
use axum::Json;
|
||||
use axum::http::{HeaderMap, Method};
|
||||
use chrono::{Duration, Utc};
|
||||
@@ -50,7 +50,7 @@ pub async fn handle_authorization_code_grant(
|
||||
.oauth_repo
|
||||
.consume_authorization_request_by_code(&auth_code)
|
||||
.await
|
||||
.map_err(crate::oauth::db_err_to_oauth)?
|
||||
.map_err(tranquil_pds::oauth::db_err_to_oauth)?
|
||||
.ok_or_else(|| OAuthError::InvalidGrant("Invalid or expired code".to_string()))?;
|
||||
|
||||
let flow = AuthFlow::from_request_data(auth_request)
|
||||
@@ -107,7 +107,7 @@ pub async fn handle_authorization_code_grant(
|
||||
.oauth_repo
|
||||
.check_and_record_dpop_jti(&result.jti)
|
||||
.await
|
||||
.map_err(crate::oauth::db_err_to_oauth)?
|
||||
.map_err(tranquil_pds::oauth::db_err_to_oauth)?
|
||||
{
|
||||
return Err(OAuthError::InvalidDpopProof(
|
||||
"DPoP proof has already been used".to_string(),
|
||||
@@ -201,7 +201,7 @@ pub async fn handle_authorization_code_grant(
|
||||
.oauth_repo
|
||||
.create_token(&token_data)
|
||||
.await
|
||||
.map_err(crate::oauth::db_err_to_oauth)?;
|
||||
.map_err(tranquil_pds::oauth::db_err_to_oauth)?;
|
||||
tracing::info!(
|
||||
did = %did,
|
||||
token_id = %token_id.0,
|
||||
@@ -321,7 +321,7 @@ pub async fn handle_refresh_token_grant(
|
||||
.oauth_repo
|
||||
.delete_token_family(original_token_id)
|
||||
.await
|
||||
.map_err(crate::oauth::db_err_to_oauth)?;
|
||||
.map_err(tranquil_pds::oauth::db_err_to_oauth)?;
|
||||
return Err(OAuthError::InvalidGrant(
|
||||
"Refresh token reuse detected, token family revoked".to_string(),
|
||||
));
|
||||
@@ -332,7 +332,7 @@ pub async fn handle_refresh_token_grant(
|
||||
.oauth_repo
|
||||
.delete_token_family(db_id)
|
||||
.await
|
||||
.map_err(crate::oauth::db_err_to_oauth)?;
|
||||
.map_err(tranquil_pds::oauth::db_err_to_oauth)?;
|
||||
return Err(OAuthError::InvalidGrant(
|
||||
"Refresh token has expired".to_string(),
|
||||
));
|
||||
@@ -354,7 +354,7 @@ pub async fn handle_refresh_token_grant(
|
||||
.oauth_repo
|
||||
.check_and_record_dpop_jti(&result.jti)
|
||||
.await
|
||||
.map_err(crate::oauth::db_err_to_oauth)?
|
||||
.map_err(tranquil_pds::oauth::db_err_to_oauth)?
|
||||
{
|
||||
return Err(OAuthError::InvalidDpopProof(
|
||||
"DPoP proof has already been used".to_string(),
|
||||
@@ -387,7 +387,7 @@ pub async fn handle_refresh_token_grant(
|
||||
.oauth_repo
|
||||
.rotate_token(db_id, &new_refresh_typed, new_expires_at)
|
||||
.await
|
||||
.map_err(crate::oauth::db_err_to_oauth)?;
|
||||
.map_err(tranquil_pds::oauth::db_err_to_oauth)?;
|
||||
tracing::info!(
|
||||
did = %token_data.did,
|
||||
new_expires_at = %new_expires_at,
|
||||
+4
-7
@@ -1,10 +1,10 @@
|
||||
use crate::config::AuthConfig;
|
||||
use crate::oauth::OAuthError;
|
||||
use tranquil_pds::config::AuthConfig;
|
||||
use tranquil_pds::oauth::OAuthError;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
use chrono::Utc;
|
||||
use hmac::Mac;
|
||||
use sha2::{Digest, Sha256};
|
||||
use sha2::Sha256;
|
||||
use subtle::ConstantTimeEq;
|
||||
|
||||
const ACCESS_TOKEN_EXPIRY_SECONDS: i64 = 300;
|
||||
@@ -17,10 +17,7 @@ pub struct TokenClaims {
|
||||
}
|
||||
|
||||
pub fn verify_pkce(code_challenge: &str, code_verifier: &str) -> Result<(), OAuthError> {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(code_verifier.as_bytes());
|
||||
let hash = hasher.finalize();
|
||||
let computed_challenge = URL_SAFE_NO_PAD.encode(hash);
|
||||
let computed_challenge = tranquil_pds::oauth::compute_pkce_challenge(code_verifier);
|
||||
if !bool::from(
|
||||
computed_challenge
|
||||
.as_bytes()
|
||||
+6
-6
@@ -1,7 +1,7 @@
|
||||
use super::helpers::extract_token_claims;
|
||||
use crate::oauth::OAuthError;
|
||||
use crate::rate_limit::{OAuthIntrospectLimit, OAuthRateLimited};
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::oauth::OAuthError;
|
||||
use tranquil_pds::rate_limit::{OAuthIntrospectLimit, OAuthRateLimited};
|
||||
use tranquil_pds::state::AppState;
|
||||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
use axum::{Form, Json};
|
||||
@@ -27,20 +27,20 @@ pub async fn revoke_token(
|
||||
.oauth_repo
|
||||
.get_token_by_refresh_token(&refresh_token)
|
||||
.await
|
||||
.map_err(crate::oauth::db_err_to_oauth)?
|
||||
.map_err(tranquil_pds::oauth::db_err_to_oauth)?
|
||||
{
|
||||
state
|
||||
.oauth_repo
|
||||
.delete_token_family(db_id)
|
||||
.await
|
||||
.map_err(crate::oauth::db_err_to_oauth)?;
|
||||
.map_err(tranquil_pds::oauth::db_err_to_oauth)?;
|
||||
} else {
|
||||
let token_id = TokenId::from(token.clone());
|
||||
state
|
||||
.oauth_repo
|
||||
.delete_token(&token_id)
|
||||
.await
|
||||
.map_err(crate::oauth::db_err_to_oauth)?;
|
||||
.map_err(tranquil_pds::oauth::db_err_to_oauth)?;
|
||||
}
|
||||
}
|
||||
Ok(StatusCode::OK)
|
||||
+4
-4
@@ -3,9 +3,9 @@ mod helpers;
|
||||
mod introspect;
|
||||
mod types;
|
||||
|
||||
use crate::oauth::OAuthError;
|
||||
use crate::rate_limit::{OAuthRateLimited, OAuthTokenLimit};
|
||||
use crate::state::AppState;
|
||||
use tranquil_pds::oauth::OAuthError;
|
||||
use tranquil_pds::rate_limit::{OAuthRateLimited, OAuthTokenLimit};
|
||||
use tranquil_pds::state::AppState;
|
||||
use axum::body::Bytes;
|
||||
use axum::{Json, extract::State, http::HeaderMap};
|
||||
|
||||
@@ -41,7 +41,7 @@ pub async fn token_endpoint(
|
||||
));
|
||||
};
|
||||
let dpop_proof = headers
|
||||
.get(crate::util::HEADER_DPOP)
|
||||
.get(tranquil_pds::util::HEADER_DPOP)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.to_string());
|
||||
let validated = request.validate()?;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
use crate::oauth::OAuthError;
|
||||
use tranquil_pds::oauth::OAuthError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -0,0 +1,124 @@
|
||||
pub mod endpoints;
|
||||
pub mod jwks;
|
||||
pub mod sso_endpoints;
|
||||
|
||||
use tranquil_pds::state::AppState;
|
||||
|
||||
pub fn oauth_routes() -> axum::Router<AppState> {
|
||||
use axum::{middleware, routing::{get, post}};
|
||||
|
||||
axum::Router::new()
|
||||
.route("/jwks", get(endpoints::oauth_jwks))
|
||||
.route("/par", post(endpoints::pushed_authorization_request))
|
||||
.route("/authorize", get(endpoints::authorize_get))
|
||||
.route("/authorize", post(endpoints::authorize_post))
|
||||
.route(
|
||||
"/authorize/accounts",
|
||||
get(endpoints::authorize_accounts),
|
||||
)
|
||||
.route(
|
||||
"/authorize/select",
|
||||
post(endpoints::authorize_select),
|
||||
)
|
||||
.route("/authorize/2fa", get(endpoints::authorize_2fa_get))
|
||||
.route("/authorize/2fa", post(endpoints::authorize_2fa_post))
|
||||
.route(
|
||||
"/authorize/passkey",
|
||||
get(endpoints::authorize_passkey_start),
|
||||
)
|
||||
.route(
|
||||
"/authorize/passkey",
|
||||
post(endpoints::authorize_passkey_finish),
|
||||
)
|
||||
.route(
|
||||
"/passkey/check",
|
||||
get(endpoints::check_user_has_passkeys),
|
||||
)
|
||||
.route(
|
||||
"/security-status",
|
||||
get(endpoints::check_user_security_status),
|
||||
)
|
||||
.route("/passkey/start", post(endpoints::passkey_start))
|
||||
.route("/passkey/finish", post(endpoints::passkey_finish))
|
||||
.route("/authorize/deny", post(endpoints::authorize_deny))
|
||||
.route(
|
||||
"/register/complete",
|
||||
post(endpoints::register_complete),
|
||||
)
|
||||
.route(
|
||||
"/establish-session",
|
||||
post(endpoints::establish_session),
|
||||
)
|
||||
.route("/authorize/consent", get(endpoints::consent_get))
|
||||
.route("/authorize/consent", post(endpoints::consent_post))
|
||||
.route("/authorize/renew", post(endpoints::authorize_renew))
|
||||
.route(
|
||||
"/authorize/redirect",
|
||||
get(endpoints::authorize_redirect),
|
||||
)
|
||||
.route("/delegation/auth", post(endpoints::delegation_auth))
|
||||
.route(
|
||||
"/delegation/auth-token",
|
||||
post(endpoints::delegation_auth_token),
|
||||
)
|
||||
.route(
|
||||
"/delegation/totp",
|
||||
post(endpoints::delegation_totp_verify),
|
||||
)
|
||||
.route(
|
||||
"/delegation/callback",
|
||||
get(endpoints::delegation_callback),
|
||||
)
|
||||
.route(
|
||||
"/delegation/client-metadata",
|
||||
get(endpoints::delegation_client_metadata),
|
||||
)
|
||||
.route("/token", post(endpoints::token_endpoint))
|
||||
.route("/revoke", post(endpoints::revoke_token))
|
||||
.route("/introspect", post(endpoints::introspect_token))
|
||||
.route("/sso/providers", get(sso_endpoints::get_sso_providers))
|
||||
.route("/sso/initiate", post(sso_endpoints::sso_initiate))
|
||||
.route(
|
||||
"/sso/callback",
|
||||
get(sso_endpoints::sso_callback).post(sso_endpoints::sso_callback_post),
|
||||
)
|
||||
.route("/sso/linked", get(sso_endpoints::get_linked_accounts))
|
||||
.route("/sso/unlink", post(sso_endpoints::unlink_account))
|
||||
.route(
|
||||
"/sso/pending-registration",
|
||||
get(sso_endpoints::get_pending_registration),
|
||||
)
|
||||
.route(
|
||||
"/sso/complete-registration",
|
||||
post(sso_endpoints::complete_registration),
|
||||
)
|
||||
.route(
|
||||
"/sso/check-handle-available",
|
||||
get(sso_endpoints::check_handle_available),
|
||||
)
|
||||
.layer(middleware::from_fn(tranquil_pds::oauth::verify::dpop_nonce_middleware))
|
||||
}
|
||||
|
||||
pub fn well_known_oauth_routes() -> axum::Router<AppState> {
|
||||
use axum::routing::get;
|
||||
|
||||
axum::Router::new()
|
||||
.route(
|
||||
"/oauth-protected-resource",
|
||||
get(endpoints::oauth_protected_resource),
|
||||
)
|
||||
.route(
|
||||
"/oauth-authorization-server",
|
||||
get(endpoints::oauth_authorization_server),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn frontend_client_metadata_route() -> axum::Router<AppState> {
|
||||
use axum::routing::get;
|
||||
|
||||
axum::Router::new()
|
||||
.route(
|
||||
"/oauth-client-metadata.json",
|
||||
get(endpoints::frontend_client_metadata),
|
||||
)
|
||||
}
|
||||
+46
-53
@@ -9,22 +9,15 @@ use serde::{Deserialize, Serialize};
|
||||
use tranquil_db_traits::{SsoAction, SsoProviderType};
|
||||
use tranquil_types::RequestId;
|
||||
|
||||
use super::config::SsoConfig;
|
||||
use crate::api::error::ApiError;
|
||||
use crate::auth::extractor::extract_auth_token_from_header;
|
||||
use crate::auth::{generate_app_password, validate_bearer_token_cached};
|
||||
use crate::rate_limit::{
|
||||
use tranquil_pds::sso::SsoConfig;
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::auth::extractor::extract_auth_token_from_header;
|
||||
use tranquil_pds::auth::{generate_app_password, validate_bearer_token_cached};
|
||||
use tranquil_pds::rate_limit::{
|
||||
AccountCreationLimit, RateLimited, SsoCallbackLimit, SsoInitiateLimit, SsoUnlinkLimit,
|
||||
check_user_rate_limit_with_message,
|
||||
};
|
||||
use crate::state::AppState;
|
||||
|
||||
fn generate_state() -> String {
|
||||
use rand::RngCore;
|
||||
let mut bytes = [0u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut bytes);
|
||||
URL_SAFE_NO_PAD.encode(bytes)
|
||||
}
|
||||
use tranquil_pds::state::AppState;
|
||||
|
||||
fn generate_nonce() -> String {
|
||||
use rand::RngCore;
|
||||
@@ -129,7 +122,7 @@ pub async fn sso_initiate(
|
||||
}
|
||||
};
|
||||
|
||||
let sso_state = generate_state();
|
||||
let sso_state = tranquil_pds::util::generate_random_token();
|
||||
let nonce = generate_nonce();
|
||||
let redirect_uri = SsoConfig::get_redirect_uri();
|
||||
|
||||
@@ -367,7 +360,7 @@ async fn handle_sso_login(
|
||||
state: &AppState,
|
||||
request_uri: &str,
|
||||
provider: SsoProviderType,
|
||||
user_info: &crate::sso::providers::SsoUserInfo,
|
||||
user_info: &tranquil_pds::sso::providers::SsoUserInfo,
|
||||
) -> Response {
|
||||
let identity = match state
|
||||
.sso_repo
|
||||
@@ -482,7 +475,7 @@ async fn handle_sso_link(
|
||||
state: &AppState,
|
||||
did: tranquil_types::Did,
|
||||
provider: SsoProviderType,
|
||||
user_info: &crate::sso::providers::SsoUserInfo,
|
||||
user_info: &tranquil_pds::sso::providers::SsoUserInfo,
|
||||
) -> Response {
|
||||
let existing = state
|
||||
.sso_repo
|
||||
@@ -555,7 +548,7 @@ async fn handle_sso_register(
|
||||
state: &AppState,
|
||||
request_uri: &str,
|
||||
provider: SsoProviderType,
|
||||
user_info: &crate::sso::providers::SsoUserInfo,
|
||||
user_info: &tranquil_pds::sso::providers::SsoUserInfo,
|
||||
) -> Response {
|
||||
match state
|
||||
.sso_repo
|
||||
@@ -616,7 +609,7 @@ pub struct LinkedAccountsResponse {
|
||||
|
||||
pub async fn get_linked_accounts(
|
||||
State(state): State<AppState>,
|
||||
auth: crate::auth::Auth<crate::auth::Active>,
|
||||
auth: tranquil_pds::auth::Auth<tranquil_pds::auth::Active>,
|
||||
) -> Result<Json<LinkedAccountsResponse>, ApiError> {
|
||||
let identities = state
|
||||
.sso_repo
|
||||
@@ -651,7 +644,7 @@ pub struct UnlinkAccountResponse {
|
||||
|
||||
pub async fn unlink_account(
|
||||
State(state): State<AppState>,
|
||||
auth: crate::auth::Auth<crate::auth::Active>,
|
||||
auth: tranquil_pds::auth::Auth<tranquil_pds::auth::Active>,
|
||||
Json(input): Json<UnlinkAccountRequest>,
|
||||
) -> Result<Json<UnlinkAccountResponse>, ApiError> {
|
||||
let _rate_limit = check_user_rate_limit_with_message::<SsoUnlinkLimit>(
|
||||
@@ -763,7 +756,7 @@ pub async fn check_handle_available(
|
||||
}));
|
||||
}
|
||||
|
||||
let validated = match crate::api::validation::validate_short_handle(&query.handle) {
|
||||
let validated = match tranquil_pds::api::validation::validate_short_handle(&query.handle) {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
return Ok(Json(CheckHandleResponse {
|
||||
@@ -774,14 +767,14 @@ pub async fn check_handle_available(
|
||||
};
|
||||
|
||||
let available_domains = tranquil_config::get().server.available_user_domain_list();
|
||||
if let Some(ref d) = query.domain {
|
||||
if !available_domains.iter().any(|ad| ad == d) {
|
||||
return Err(ApiError::InvalidRequest("Unknown user domain".into()));
|
||||
}
|
||||
if let Some(ref d) = query.domain
|
||||
&& !available_domains.iter().any(|ad| ad == d)
|
||||
{
|
||||
return Err(ApiError::InvalidRequest("Unknown user domain".into()));
|
||||
}
|
||||
let domain = query.domain.as_deref().unwrap_or(&available_domains[0]);
|
||||
let full_handle = format!("{}.{}", validated, domain);
|
||||
let handle_typed: crate::types::Handle = match full_handle.parse() {
|
||||
let handle_typed: tranquil_pds::types::Handle = match full_handle.parse() {
|
||||
Ok(h) => h,
|
||||
Err(_) => return Err(ApiError::InvalidHandle(None)),
|
||||
};
|
||||
@@ -879,12 +872,12 @@ pub async fn complete_registration(
|
||||
.unwrap_or(&input.handle),
|
||||
None => &input.handle,
|
||||
};
|
||||
match crate::api::validation::validate_short_handle(handle_to_validate) {
|
||||
match tranquil_pds::api::validation::validate_short_handle(handle_to_validate) {
|
||||
Ok(h) => format!("{}.{}", h, matched_domain.unwrap_or(&available_domains[0])),
|
||||
Err(_) => return Err(ApiError::InvalidHandle(None)),
|
||||
}
|
||||
} else {
|
||||
match crate::api::validation::validate_full_domain_handle(&input.handle) {
|
||||
match tranquil_pds::api::validation::validate_full_domain_handle(&input.handle) {
|
||||
Ok(h) => h,
|
||||
Err(_) => return Err(ApiError::InvalidHandle(None)),
|
||||
}
|
||||
@@ -914,7 +907,7 @@ pub async fn complete_registration(
|
||||
tranquil_db_traits::CommsChannel::Discord => match &input.discord_username {
|
||||
Some(username) if !username.trim().is_empty() => {
|
||||
let clean = username.trim().to_lowercase();
|
||||
if !crate::api::validation::is_valid_discord_username(&clean) {
|
||||
if !tranquil_pds::api::validation::is_valid_discord_username(&clean) {
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Invalid Discord username. Must be 2-32 lowercase characters (letters, numbers, underscores, periods)".into(),
|
||||
));
|
||||
@@ -926,7 +919,7 @@ pub async fn complete_registration(
|
||||
tranquil_db_traits::CommsChannel::Telegram => match &input.telegram_username {
|
||||
Some(username) if !username.trim().is_empty() => {
|
||||
let clean = username.trim().trim_start_matches('@');
|
||||
if !crate::api::validation::is_valid_telegram_username(clean) {
|
||||
if !tranquil_pds::api::validation::is_valid_telegram_username(clean) {
|
||||
return Err(ApiError::InvalidRequest(
|
||||
"Invalid Telegram username. Must be 5-32 characters, alphanumeric or underscore".into(),
|
||||
));
|
||||
@@ -960,7 +953,7 @@ pub async fn complete_registration(
|
||||
if e.len() > 254 {
|
||||
return Err(ApiError::InvalidEmail);
|
||||
}
|
||||
if !crate::api::validation::is_valid_email(e) {
|
||||
if !tranquil_pds::api::validation::is_valid_email(e) {
|
||||
return Err(ApiError::InvalidEmail);
|
||||
}
|
||||
Some(e.clone())
|
||||
@@ -981,7 +974,7 @@ pub async fn complete_registration(
|
||||
None
|
||||
};
|
||||
|
||||
let handle_typed: crate::types::Handle =
|
||||
let handle_typed: tranquil_pds::types::Handle =
|
||||
handle.parse().map_err(|_| ApiError::InvalidHandle(None))?;
|
||||
let reserved = state
|
||||
.user_repo
|
||||
@@ -1008,7 +1001,7 @@ pub async fn complete_registration(
|
||||
|
||||
let did = match did_type {
|
||||
"web" => {
|
||||
if !crate::api::server::meta::is_self_hosted_did_web_enabled() {
|
||||
if !tranquil_pds::util::is_self_hosted_did_web_enabled() {
|
||||
return Err(ApiError::SelfHostedDidWebDisabled);
|
||||
}
|
||||
let encoded_handle = handle.replace(':', "%3A");
|
||||
@@ -1038,9 +1031,9 @@ pub async fn complete_registration(
|
||||
.secrets
|
||||
.plc_rotation_key
|
||||
.clone()
|
||||
.unwrap_or_else(|| crate::plc::signing_key_to_did_key(&signing_key));
|
||||
.unwrap_or_else(|| tranquil_pds::plc::signing_key_to_did_key(&signing_key));
|
||||
|
||||
let genesis_result = match crate::plc::create_genesis_operation(
|
||||
let genesis_result = match tranquil_pds::plc::create_genesis_operation(
|
||||
&signing_key,
|
||||
&rotation_key,
|
||||
&handle,
|
||||
@@ -1055,7 +1048,7 @@ pub async fn complete_registration(
|
||||
}
|
||||
};
|
||||
|
||||
let plc_client = crate::plc::PlcClient::with_cache(None, Some(state.cache.clone()));
|
||||
let plc_client = tranquil_pds::plc::PlcClient::with_cache(None, Some(state.cache.clone()));
|
||||
if let Err(e) = plc_client
|
||||
.send_operation(&genesis_result.did, &genesis_result.signed_operation)
|
||||
.await
|
||||
@@ -1071,7 +1064,7 @@ pub async fn complete_registration(
|
||||
};
|
||||
tracing::info!(did = %did, handle = %handle, provider = %pending_preview.provider.as_str(), "Created DID for SSO account");
|
||||
|
||||
let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) {
|
||||
let encrypted_key_bytes = match tranquil_pds::config::encrypt_key(&secret_key_bytes) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => {
|
||||
tracing::error!("Error encrypting signing key: {:?}", e);
|
||||
@@ -1089,10 +1082,10 @@ pub async fn complete_registration(
|
||||
};
|
||||
|
||||
let rev = Tid::now(LimitedU32::MIN);
|
||||
let did_typed: crate::types::Did = did
|
||||
let did_typed: tranquil_pds::types::Did = did
|
||||
.parse()
|
||||
.map_err(|_| ApiError::InternalError(Some("Invalid DID".into())))?;
|
||||
let (commit_bytes, _sig) = match crate::api::repo::record::utils::create_signed_commit(
|
||||
let (commit_bytes, _sig) = match tranquil_pds::repo_ops::create_signed_commit(
|
||||
&did_typed,
|
||||
mst_root,
|
||||
rev.as_ref(),
|
||||
@@ -1146,7 +1139,7 @@ pub async fn complete_registration(
|
||||
.map(|s| s.trim().trim_start_matches('@').to_lowercase())
|
||||
.filter(|s| !s.is_empty()),
|
||||
encrypted_key_bytes: encrypted_key_bytes.clone(),
|
||||
encryption_version: crate::config::ENCRYPTION_VERSION,
|
||||
encryption_version: tranquil_pds::config::ENCRYPTION_VERSION,
|
||||
commit_cid: commit_cid.to_string(),
|
||||
repo_rev: rev.as_ref().to_string(),
|
||||
genesis_block_cids,
|
||||
@@ -1189,12 +1182,12 @@ pub async fn complete_registration(
|
||||
.await;
|
||||
|
||||
if let Err(e) =
|
||||
crate::api::repo::record::sequence_identity_event(&state, &did_typed, Some(&handle_typed))
|
||||
tranquil_pds::repo_ops::sequence_identity_event(&state, &did_typed, Some(&handle_typed))
|
||||
.await
|
||||
{
|
||||
tracing::warn!("Failed to sequence identity event for {}: {}", did, e);
|
||||
}
|
||||
if let Err(e) = crate::api::repo::record::sequence_account_event(
|
||||
if let Err(e) = tranquil_pds::repo_ops::sequence_account_event(
|
||||
&state,
|
||||
&did_typed,
|
||||
tranquil_db_traits::AccountStatus::Active,
|
||||
@@ -1208,11 +1201,11 @@ pub async fn complete_registration(
|
||||
"$type": "app.bsky.actor.profile",
|
||||
"displayName": handle_typed.as_str()
|
||||
});
|
||||
if let Err(e) = crate::api::repo::record::create_record_internal(
|
||||
if let Err(e) = tranquil_pds::repo_ops::create_record_internal(
|
||||
&state,
|
||||
&did_typed,
|
||||
&crate::types::PROFILE_COLLECTION,
|
||||
&crate::types::PROFILE_RKEY,
|
||||
&tranquil_pds::types::PROFILE_COLLECTION,
|
||||
&tranquil_pds::types::PROFILE_RKEY,
|
||||
&profile_record,
|
||||
)
|
||||
.await
|
||||
@@ -1287,9 +1280,9 @@ pub async fn complete_registration(
|
||||
tracing::info!(did = %did, "Auto-verified email from SSO provider");
|
||||
|
||||
if is_standalone {
|
||||
let key_bytes = match crate::config::decrypt_key(
|
||||
let key_bytes = match tranquil_pds::config::decrypt_key(
|
||||
&encrypted_key_bytes,
|
||||
Some(crate::config::ENCRYPTION_VERSION),
|
||||
Some(tranquil_pds::config::ENCRYPTION_VERSION),
|
||||
) {
|
||||
Ok(k) => k,
|
||||
Err(e) => {
|
||||
@@ -1298,7 +1291,7 @@ pub async fn complete_registration(
|
||||
}
|
||||
};
|
||||
|
||||
let access_meta = match crate::auth::create_access_token_with_metadata(&did, &key_bytes)
|
||||
let access_meta = match tranquil_pds::auth::create_access_token_with_metadata(&did, &key_bytes)
|
||||
{
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
@@ -1307,7 +1300,7 @@ pub async fn complete_registration(
|
||||
}
|
||||
};
|
||||
let refresh_meta =
|
||||
match crate::auth::create_refresh_token_with_metadata(&did, &key_bytes) {
|
||||
match tranquil_pds::auth::create_refresh_token_with_metadata(&did, &key_bytes) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to create refresh token: {:?}", e);
|
||||
@@ -1323,7 +1316,7 @@ pub async fn complete_registration(
|
||||
refresh_expires_at: refresh_meta.expires_at,
|
||||
login_type: tranquil_db_traits::LoginType::Modern,
|
||||
mfa_verified: false,
|
||||
scope: None,
|
||||
scope: Some("transition:generic".to_string()),
|
||||
controller_did: None,
|
||||
app_password_name: None,
|
||||
};
|
||||
@@ -1333,7 +1326,7 @@ pub async fn complete_registration(
|
||||
}
|
||||
|
||||
let hostname = &tranquil_config::get().server.hostname;
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_welcome(
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_welcome(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
user_id.unwrap_or(uuid::Uuid::nil()),
|
||||
@@ -1370,14 +1363,14 @@ pub async fn complete_registration(
|
||||
}
|
||||
|
||||
if let Some(uid) = user_id {
|
||||
let verification_token = crate::auth::verification_token::generate_signup_token(
|
||||
let verification_token = tranquil_pds::auth::verification_token::generate_signup_token(
|
||||
&did_typed,
|
||||
verification_channel,
|
||||
&verification_recipient,
|
||||
);
|
||||
let formatted_token =
|
||||
crate::auth::verification_token::format_token_for_display(&verification_token);
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_signup_verification(
|
||||
tranquil_pds::auth::verification_token::format_token_for_display(&verification_token);
|
||||
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_signup_verification(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
uid,
|
||||
@@ -385,6 +385,87 @@ pub fn compute_access_token_hash(access_token: &str) -> String {
|
||||
URL_SAFE_NO_PAD.encode(hash)
|
||||
}
|
||||
|
||||
pub fn compute_pkce_challenge(verifier: &str) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(verifier.as_bytes());
|
||||
URL_SAFE_NO_PAD.encode(hasher.finalize())
|
||||
}
|
||||
|
||||
pub fn es256_signing_key_to_jwk(key: &p256::ecdsa::SigningKey) -> Result<DPoPJwk, OAuthError> {
|
||||
let point = key.verifying_key().to_encoded_point(false);
|
||||
let x = URL_SAFE_NO_PAD.encode(
|
||||
point
|
||||
.x()
|
||||
.ok_or_else(|| OAuthError::InvalidDpopProof("invalid EC key: missing x".into()))?,
|
||||
);
|
||||
let y = URL_SAFE_NO_PAD.encode(
|
||||
point
|
||||
.y()
|
||||
.ok_or_else(|| OAuthError::InvalidDpopProof("invalid EC key: missing y".into()))?,
|
||||
);
|
||||
Ok(DPoPJwk {
|
||||
kty: "EC".to_string(),
|
||||
crv: Some("P-256".to_string()),
|
||||
x: Some(x),
|
||||
y: Some(y),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_dpop_proof(
|
||||
signing_key: &p256::ecdsa::SigningKey,
|
||||
method: &str,
|
||||
url: &str,
|
||||
nonce: Option<&str>,
|
||||
access_token_hash: Option<&str>,
|
||||
) -> Result<String, OAuthError> {
|
||||
use p256::ecdsa::signature::Signer;
|
||||
|
||||
let jwk = es256_signing_key_to_jwk(signing_key)?;
|
||||
|
||||
let header = serde_json::json!({
|
||||
"typ": "dpop+jwt",
|
||||
"alg": "ES256",
|
||||
"jwk": jwk
|
||||
});
|
||||
|
||||
let jti = {
|
||||
use rand::Rng;
|
||||
let bytes: [u8; 16] = rand::thread_rng().r#gen();
|
||||
URL_SAFE_NO_PAD.encode(bytes)
|
||||
};
|
||||
|
||||
let mut payload = serde_json::json!({
|
||||
"jti": jti,
|
||||
"htm": method,
|
||||
"htu": url,
|
||||
"iat": Utc::now().timestamp()
|
||||
});
|
||||
if let Some(n) = nonce {
|
||||
payload["nonce"] = serde_json::Value::String(n.to_string());
|
||||
}
|
||||
if let Some(ath) = access_token_hash {
|
||||
payload["ath"] = serde_json::Value::String(ath.to_string());
|
||||
}
|
||||
|
||||
let header_b64 = URL_SAFE_NO_PAD.encode(
|
||||
serde_json::to_vec(&header).map_err(|e| OAuthError::InvalidDpopProof(e.to_string()))?,
|
||||
);
|
||||
let payload_b64 = URL_SAFE_NO_PAD.encode(
|
||||
serde_json::to_vec(&payload).map_err(|e| OAuthError::InvalidDpopProof(e.to_string()))?,
|
||||
);
|
||||
|
||||
let signing_input = format!("{}.{}", header_b64, payload_b64);
|
||||
let signature: p256::ecdsa::Signature = signing_key.sign(signing_input.as_bytes());
|
||||
let sig_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
|
||||
|
||||
Ok(format!("{}.{}.{}", header_b64, payload_b64, sig_b64))
|
||||
}
|
||||
|
||||
pub fn compute_es256_jkt(signing_key: &p256::ecdsa::SigningKey) -> Result<String, OAuthError> {
|
||||
let jwk = es256_signing_key_to_jwk(signing_key)?;
|
||||
compute_jwk_thumbprint(&jwk)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -6,7 +6,9 @@ mod types;
|
||||
pub use client::{ClientMetadata, ClientMetadataCache, verify_client_auth};
|
||||
pub use dpop::{
|
||||
DPoPJwk, DPoPProofHeader, DPoPProofPayload, DPoPVerifier, DPoPVerifyResult,
|
||||
compute_access_token_hash, compute_jwk_thumbprint,
|
||||
compute_access_token_hash, compute_es256_jkt, compute_jwk_thumbprint, compute_pkce_challenge,
|
||||
create_dpop_proof,
|
||||
es256_signing_key_to_jwk,
|
||||
};
|
||||
pub use error::OAuthError;
|
||||
pub use types::{
|
||||
|
||||
@@ -31,8 +31,6 @@ bs58 = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
cid = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
dotenvy = { workspace = true }
|
||||
ed25519-dalek = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
@@ -75,7 +73,6 @@ tower = { workspace = true }
|
||||
tower-http = { workspace = true }
|
||||
tower-layer = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
urlencoding = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
webauthn-rs = { workspace = true }
|
||||
@@ -97,4 +94,7 @@ ctor = { workspace = true }
|
||||
testcontainers = { workspace = true }
|
||||
testcontainers-modules = { workspace = true }
|
||||
tranquil-ripple = { workspace = true }
|
||||
tranquil-sync = { workspace = true }
|
||||
tranquil-api = { workspace = true }
|
||||
tranquil-oauth-server = { workspace = true }
|
||||
wiremock = { workspace = true }
|
||||
|
||||
@@ -1,22 +1,8 @@
|
||||
pub mod actor;
|
||||
pub mod admin;
|
||||
pub mod age_assurance;
|
||||
pub mod backup;
|
||||
pub mod delegation;
|
||||
pub mod discord_webhook;
|
||||
pub mod error;
|
||||
pub mod identity;
|
||||
pub mod moderation;
|
||||
pub mod notification_prefs;
|
||||
pub mod proxy;
|
||||
pub mod proxy_client;
|
||||
pub mod repo;
|
||||
pub mod responses;
|
||||
pub mod server;
|
||||
pub mod telegram_webhook;
|
||||
pub mod temp;
|
||||
pub mod validation;
|
||||
pub mod verification;
|
||||
|
||||
pub use error::ApiError;
|
||||
pub use proxy_client::{AtUriParts, proxy_client, validate_at_uri, validate_limit};
|
||||
|
||||
@@ -308,6 +308,31 @@ pub fn validate_short_handle(handle: &str) -> Result<String, HandleValidationErr
|
||||
validate_service_handle(handle, ReservedHandlePolicy::Reject)
|
||||
}
|
||||
|
||||
pub fn resolve_handle_input(input: &str) -> Result<String, HandleValidationError> {
|
||||
let available_domains = tranquil_config::get().server.available_user_domain_list();
|
||||
let matched_domain = available_domains
|
||||
.iter()
|
||||
.filter(|d| input.ends_with(&format!(".{}", d)))
|
||||
.max_by_key(|d| d.len());
|
||||
|
||||
if !input.contains('.') || matched_domain.is_some() {
|
||||
let handle_to_validate = match matched_domain {
|
||||
Some(domain) => input
|
||||
.strip_suffix(&format!(".{}", domain))
|
||||
.unwrap_or(input),
|
||||
None => input,
|
||||
};
|
||||
let validated = validate_short_handle(handle_to_validate)?;
|
||||
Ok(format!(
|
||||
"{}.{}",
|
||||
validated,
|
||||
matched_domain.unwrap_or(&available_domains[0])
|
||||
))
|
||||
} else {
|
||||
validate_full_domain_handle(input)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_service_handle(
|
||||
handle: &str,
|
||||
reserved_policy: ReservedHandlePolicy,
|
||||
|
||||
@@ -74,7 +74,7 @@ pub async fn require_legacy_session_mfa<'a>(
|
||||
state: &AppState,
|
||||
user: &'a AuthenticatedUser,
|
||||
) -> Result<MfaVerified<'a>, Response> {
|
||||
use crate::api::server::reauth::{check_legacy_session_mfa, legacy_mfa_required_response};
|
||||
use crate::auth::reauth::{check_legacy_session_mfa, legacy_mfa_required_response};
|
||||
|
||||
if check_legacy_session_mfa(&*state.session_repo, &user.did).await {
|
||||
Ok(MfaVerified::from_session_reauth(user))
|
||||
@@ -87,7 +87,7 @@ pub async fn require_reauth_window<'a>(
|
||||
state: &AppState,
|
||||
user: &'a AuthenticatedUser,
|
||||
) -> Result<MfaVerified<'a>, Response> {
|
||||
use crate::api::server::reauth::{REAUTH_WINDOW_SECONDS, reauth_required_response};
|
||||
use crate::auth::reauth::{REAUTH_WINDOW_SECONDS, reauth_required_response};
|
||||
use chrono::Utc;
|
||||
|
||||
let status = state
|
||||
@@ -117,7 +117,7 @@ pub async fn require_reauth_window_if_available<'a>(
|
||||
state: &AppState,
|
||||
user: &'a AuthenticatedUser,
|
||||
) -> Result<Option<MfaVerified<'a>>, Response> {
|
||||
use crate::api::server::reauth::{check_reauth_required_cached, reauth_required_response};
|
||||
use crate::auth::reauth::{check_reauth_required_cached, reauth_required_response};
|
||||
|
||||
let has_password = state
|
||||
.user_repo
|
||||
|
||||
@@ -13,6 +13,7 @@ use tranquil_db_traits::OAuthRepository;
|
||||
pub mod account_verified;
|
||||
pub mod email_token;
|
||||
pub mod extractor;
|
||||
pub mod reauth;
|
||||
pub mod legacy_2fa;
|
||||
pub mod login_identifier;
|
||||
pub mod mfa_verified;
|
||||
@@ -205,7 +206,7 @@ impl AuthenticatedUser {
|
||||
return ScopePermissions::from_scope_string(Some(scope));
|
||||
}
|
||||
if !self.is_oauth() {
|
||||
return ScopePermissions::from_scope_string(Some("atproto"));
|
||||
return ScopePermissions::from_scope_string(Some("transition:generic transition:chat.bsky"));
|
||||
}
|
||||
ScopePermissions::from_scope_string(self.scope.as_deref())
|
||||
}
|
||||
@@ -355,7 +356,7 @@ async fn validate_bearer_token_with_options_internal(
|
||||
)
|
||||
.await;
|
||||
|
||||
let status_cache_key = crate::cache_keys::user_status_key(&did.to_string());
|
||||
let status_cache_key = crate::cache_keys::user_status_key(did.as_ref());
|
||||
let cached = CachedUserStatus {
|
||||
deactivated: user.deactivated_at.is_some(),
|
||||
takendown: user.takedown_ref.is_some(),
|
||||
@@ -394,7 +395,7 @@ async fn validate_bearer_token_with_options_internal(
|
||||
match verify_access_token_typed(token, &decrypted_key) {
|
||||
Ok(token_data) => {
|
||||
let jti = &token_data.claims.jti;
|
||||
let session_cache_key = crate::cache_keys::session_key(&did, &jti);
|
||||
let session_cache_key = crate::cache_keys::session_key(&did, jti);
|
||||
let mut session_valid = false;
|
||||
|
||||
if let Some(c) = cache {
|
||||
@@ -530,6 +531,7 @@ pub enum AccountRequirement {
|
||||
AnyStatus,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn validate_token_with_dpop(
|
||||
user_repo: &dyn UserRepository,
|
||||
oauth_repo: &dyn OAuthRepository,
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
use axum::{
|
||||
Json,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use chrono::Utc;
|
||||
use serde::Serialize;
|
||||
use tranquil_db_traits::{SessionRepository, UserRepository};
|
||||
|
||||
pub const REAUTH_WINDOW_SECONDS: i64 = 300;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ReauthMethod {
|
||||
Password,
|
||||
Totp,
|
||||
Passkey,
|
||||
}
|
||||
|
||||
fn is_reauth_required(last_reauth_at: Option<chrono::DateTime<Utc>>) -> bool {
|
||||
match last_reauth_at {
|
||||
None => true,
|
||||
Some(t) => {
|
||||
let elapsed = Utc::now().signed_duration_since(t);
|
||||
elapsed.num_seconds() > REAUTH_WINDOW_SECONDS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_available_reauth_methods(
|
||||
user_repo: &dyn UserRepository,
|
||||
_session_repo: &dyn SessionRepository,
|
||||
did: &crate::types::Did,
|
||||
) -> Vec<ReauthMethod> {
|
||||
let mut methods = Vec::new();
|
||||
|
||||
let has_password = user_repo
|
||||
.get_password_hash_by_did(did)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some();
|
||||
|
||||
if has_password {
|
||||
methods.push(ReauthMethod::Password);
|
||||
}
|
||||
|
||||
let has_totp = user_repo.has_totp_enabled(did).await.unwrap_or(false);
|
||||
if has_totp {
|
||||
methods.push(ReauthMethod::Totp);
|
||||
}
|
||||
|
||||
let has_passkeys = user_repo.has_passkeys(did).await.unwrap_or(false);
|
||||
if has_passkeys {
|
||||
methods.push(ReauthMethod::Passkey);
|
||||
}
|
||||
|
||||
methods
|
||||
}
|
||||
|
||||
pub async fn check_reauth_required_cached(
|
||||
session_repo: &dyn SessionRepository,
|
||||
cache: &std::sync::Arc<dyn crate::cache::Cache>,
|
||||
did: &crate::types::Did,
|
||||
) -> bool {
|
||||
let cache_key = crate::cache_keys::reauth_key(did);
|
||||
if let Some(timestamp_str) = cache.get(&cache_key).await
|
||||
&& let Ok(timestamp) = timestamp_str.parse::<i64>()
|
||||
{
|
||||
let reauth_time = chrono::DateTime::from_timestamp(timestamp, 0);
|
||||
if let Some(t) = reauth_time {
|
||||
let elapsed = Utc::now().signed_duration_since(t);
|
||||
if elapsed.num_seconds() <= REAUTH_WINDOW_SECONDS {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
match session_repo.get_last_reauth_at(did).await {
|
||||
Ok(last_reauth_at) => is_reauth_required(last_reauth_at),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReauthRequiredError {
|
||||
pub error: String,
|
||||
pub message: String,
|
||||
pub reauth_methods: Vec<ReauthMethod>,
|
||||
}
|
||||
|
||||
pub async fn reauth_required_response(
|
||||
user_repo: &dyn UserRepository,
|
||||
session_repo: &dyn SessionRepository,
|
||||
did: &crate::types::Did,
|
||||
) -> Response {
|
||||
let methods = get_available_reauth_methods(user_repo, session_repo, did).await;
|
||||
(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(ReauthRequiredError {
|
||||
error: "ReauthRequired".to_string(),
|
||||
message: "Re-authentication required for this action".to_string(),
|
||||
reauth_methods: methods,
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub async fn check_legacy_session_mfa(
|
||||
session_repo: &dyn SessionRepository,
|
||||
did: &crate::types::Did,
|
||||
) -> bool {
|
||||
match session_repo.get_session_mfa_status(did).await {
|
||||
Ok(Some(status)) => {
|
||||
if status.login_type.is_modern() {
|
||||
return true;
|
||||
}
|
||||
if status.mfa_verified {
|
||||
return true;
|
||||
}
|
||||
if let Some(last_reauth) = status.last_reauth_at {
|
||||
let elapsed = chrono::Utc::now().signed_duration_since(last_reauth);
|
||||
if elapsed.num_seconds() <= REAUTH_WINDOW_SECONDS {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MfaVerificationRequiredError {
|
||||
pub error: String,
|
||||
pub message: String,
|
||||
pub reauth_methods: Vec<ReauthMethod>,
|
||||
}
|
||||
|
||||
pub async fn legacy_mfa_required_response(
|
||||
user_repo: &dyn UserRepository,
|
||||
session_repo: &dyn SessionRepository,
|
||||
did: &crate::types::Did,
|
||||
) -> Response {
|
||||
let methods = get_available_reauth_methods(user_repo, session_repo, did).await;
|
||||
(
|
||||
StatusCode::FORBIDDEN,
|
||||
Json(MfaVerificationRequiredError {
|
||||
error: "MfaVerificationRequired".to_string(),
|
||||
message: "This sensitive operation requires MFA verification. Your session was created via a legacy app that doesn't support MFA during login.".to_string(),
|
||||
reauth_methods: methods,
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::circuit_breaker::CircuitBreaker;
|
||||
use crate::sync::firehose::SequencedEvent;
|
||||
use tranquil_db_traits::SequencedEvent;
|
||||
use reqwest::Client;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
@@ -2,11 +2,50 @@ pub mod roles;
|
||||
pub mod scopes;
|
||||
|
||||
pub use roles::{
|
||||
CanAddControllers, CanBeController, CanControlAccounts, verify_can_add_controllers,
|
||||
verify_can_be_controller, verify_can_control_accounts,
|
||||
CanAddControllers, CanControlAccounts, verify_can_add_controllers,
|
||||
verify_can_control_accounts,
|
||||
};
|
||||
pub use scopes::{
|
||||
InvalidDelegationScopeError, SCOPE_PRESETS, ScopePreset, ValidatedDelegationScope,
|
||||
intersect_scopes, validate_delegation_scopes,
|
||||
intersect_scopes,
|
||||
};
|
||||
pub use tranquil_db_traits::DelegationActionType;
|
||||
|
||||
use crate::state::AppState;
|
||||
use crate::types::Did;
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ResolvedIdentity {
|
||||
pub did: Did,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub handle: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pds_url: Option<String>,
|
||||
pub is_local: bool,
|
||||
}
|
||||
|
||||
pub async fn resolve_identity(state: &AppState, did: &Did) -> Option<ResolvedIdentity> {
|
||||
let is_local = state
|
||||
.user_repo
|
||||
.get_by_did(did)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some();
|
||||
|
||||
let did_doc = state
|
||||
.did_resolver
|
||||
.resolve_did_document(did.as_str())
|
||||
.await?;
|
||||
|
||||
let pds_url = tranquil_types::did_doc::extract_pds_endpoint(&did_doc);
|
||||
let handle = tranquil_types::did_doc::extract_handle(&did_doc);
|
||||
|
||||
Some(ResolvedIdentity {
|
||||
did: did.clone(),
|
||||
handle,
|
||||
pds_url,
|
||||
is_local,
|
||||
})
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user