+28
Cargo.lock
+28
Cargo.lock
···
2743
"pin-project-lite",
2744
]
2745
2746
[[package]]
2747
name = "httparse"
2748
version = "1.10.1"
···
3613
source = "registry+https://github.com/rust-lang/crates.io-index"
3614
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
3615
3616
[[package]]
3617
name = "minimal-lexical"
3618
version = "0.2.1"
···
5976
"http 1.4.0",
5977
"http-body 1.0.1",
5978
"http-body-util",
5979
"iri-string",
5980
"pin-project-lite",
5981
"tokio",
5982
"tokio-util",
5983
"tower",
5984
"tower-layer",
5985
"tower-service",
5986
]
5987
5988
[[package]]
···
6414
source = "registry+https://github.com/rust-lang/crates.io-index"
6415
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
6416
6417
[[package]]
6418
name = "unicode-bidi"
6419
version = "0.3.18"
···
2743
"pin-project-lite",
2744
]
2745
2746
+
[[package]]
2747
+
name = "http-range-header"
2748
+
version = "0.4.2"
2749
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2750
+
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
2751
+
2752
[[package]]
2753
name = "httparse"
2754
version = "1.10.1"
···
3619
source = "registry+https://github.com/rust-lang/crates.io-index"
3620
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
3621
3622
+
[[package]]
3623
+
name = "mime_guess"
3624
+
version = "2.0.5"
3625
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3626
+
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
3627
+
dependencies = [
3628
+
"mime",
3629
+
"unicase",
3630
+
]
3631
+
3632
[[package]]
3633
name = "minimal-lexical"
3634
version = "0.2.1"
···
5992
"http 1.4.0",
5993
"http-body 1.0.1",
5994
"http-body-util",
5995
+
"http-range-header",
5996
+
"httpdate",
5997
"iri-string",
5998
+
"mime",
5999
+
"mime_guess",
6000
+
"percent-encoding",
6001
"pin-project-lite",
6002
"tokio",
6003
"tokio-util",
6004
"tower",
6005
"tower-layer",
6006
"tower-service",
6007
+
"tracing",
6008
]
6009
6010
[[package]]
···
6436
source = "registry+https://github.com/rust-lang/crates.io-index"
6437
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
6438
6439
+
[[package]]
6440
+
name = "unicase"
6441
+
version = "2.9.0"
6442
+
source = "registry+https://github.com/rust-lang/crates.io-index"
6443
+
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
6444
+
6445
[[package]]
6446
name = "unicode-bidi"
6447
version = "0.3.18"
+1
-1
Cargo.toml
+1
-1
Cargo.toml
+15
crates/tranquil-config/src/lib.rs
+15
crates/tranquil-config/src/lib.rs
···
96
#[config(nested)]
97
pub server: ServerConfig,
98
99
+
#[config(nested)]
100
+
pub frontend: FrontendConfig,
101
+
102
#[config(nested)]
103
pub database: DatabaseConfig,
104
···
485
}
486
}
487
488
+
#[derive(Debug, Config)]
489
+
pub struct FrontendConfig {
490
+
/// Whether to enable the built in serving of the frontend.
491
+
#[config(env = "FRONTEND_ENABLED", default = true)]
492
+
pub enabled: bool,
493
+
494
+
/// Directory to serve as the frontend. The oauth_client_metadata.json will have any references to
495
+
/// the frontend hostname replaced by the configured frontend hostname.
496
+
#[config(env = "FRONTEND_DIR", default = "/var/lib/tranquil-pds/frontend")]
497
+
pub dir: String,
498
+
}
499
+
500
#[derive(Debug, Config)]
501
pub struct DatabaseConfig {
502
/// PostgreSQL connection URL.
+2
-1
crates/tranquil-pds/Cargo.toml
+2
-1
crates/tranquil-pds/Cargo.toml
···
83
aws-sdk-s3 = { workspace = true, optional = true }
84
85
[features]
86
-
default = ["s3", "valkey"]
87
external-infra = []
88
s3-storage = ["tranquil-storage/s3", "dep:aws-config", "dep:aws-sdk-s3"]
89
s3 = ["s3-storage"]
90
valkey = ["tranquil-cache/valkey", "dep:redis"]
91
92
[dev-dependencies]
93
ciborium = { workspace = true }
···
83
aws-sdk-s3 = { workspace = true, optional = true }
84
85
[features]
86
+
default = ["frontend", "s3", "valkey"]
87
external-infra = []
88
s3-storage = ["tranquil-storage/s3", "dep:aws-config", "dep:aws-sdk-s3"]
89
s3 = ["s3-storage"]
90
valkey = ["tranquil-cache/valkey", "dep:redis"]
91
+
frontend = []
92
93
[dev-dependencies]
94
ciborium = { workspace = true }
+36
-3
crates/tranquil-pds/src/lib.rs
+36
-3
crates/tranquil-pds/src/lib.rs
···
39
use serde_json::json;
40
use state::AppState;
41
use tower::ServiceBuilder;
42
-
use tower_http::cors::{Any, CorsLayer};
43
pub use tranquil_db_traits::AccountStatus;
44
pub use types::{AccountState, AtIdentifier, AtUri, Did, Handle, Nsid, Rkey};
45
···
640
get(oauth::endpoints::oauth_authorization_server),
641
);
642
643
-
Router::new()
644
.nest_service("/xrpc", xrpc_service)
645
.nest("/oauth", oauth_router)
646
.nest("/.well-known", well_known_router)
···
685
util::HEADER_ATPROTO_CONTENT_LABELERS,
686
]),
687
)
688
-
.with_state(state)
689
}
690
691
async fn rewrite_422_to_400(response: axum::response::Response) -> axum::response::Response {
···
39
use serde_json::json;
40
use state::AppState;
41
use tower::ServiceBuilder;
42
+
use tower_http::{
43
+
cors::{Any, CorsLayer},
44
+
services::{ServeDir, ServeFile},
45
+
};
46
pub use tranquil_db_traits::AccountStatus;
47
pub use types::{AccountState, AtIdentifier, AtUri, Did, Handle, Nsid, Rkey};
48
···
643
get(oauth::endpoints::oauth_authorization_server),
644
);
645
646
+
if cfg!(feature = "frontend") {}
647
+
648
+
let router = Router::new()
649
.nest_service("/xrpc", xrpc_service)
650
.nest("/oauth", oauth_router)
651
.nest("/.well-known", well_known_router)
···
690
util::HEADER_ATPROTO_CONTENT_LABELERS,
691
]),
692
)
693
+
.with_state(state);
694
+
695
+
if cfg!(feature = "frontend") && tranquil_config::get().frontend.enabled {
696
+
let frontend_dir = &tranquil_config::get().frontend.dir;
697
+
let index_path = format!("{}/index.html", frontend_dir);
698
+
let homepage_path = format!("{}/homepage.html", frontend_dir);
699
+
700
+
let homepage_exists = std::path::Path::new(&homepage_path).exists();
701
+
let homepage_file = if homepage_exists {
702
+
homepage_path
703
+
} else {
704
+
index_path.clone()
705
+
};
706
+
707
+
let spa_router = Router::new().fallback_service(ServeFile::new(&index_path));
708
+
709
+
let serve_dir = ServeDir::new(&frontend_dir).not_found_service(ServeFile::new(&index_path));
710
+
711
+
return router
712
+
.route(
713
+
"/oauth-client-metadata.json",
714
+
get(oauth::endpoints::frontend_client_metadata),
715
+
)
716
+
.route_service("/", ServeFile::new(&homepage_file))
717
+
.nest("/app", spa_router)
718
+
.fallback_service(serve_dir);
719
+
}
720
+
721
+
router
722
}
723
724
async fn rewrite_422_to_400(response: axum::response::Response) -> axum::response::Response {
+20
crates/tranquil-pds/src/oauth/endpoints/metadata.rs
+20
crates/tranquil-pds/src/oauth/endpoints/metadata.rs
···
1
+
use std::fmt::Debug;
2
+
3
use crate::oauth::jwks::{JwkSet, create_jwk_set};
4
use crate::state::AppState;
5
use axum::{Json, extract::State};
6
+
use http::{HeaderName, header};
7
use serde::{Deserialize, Serialize};
8
9
#[derive(Debug, Serialize, Deserialize)]
···
143
};
144
Json(create_jwk_set(vec![server_key]))
145
}
146
+
147
+
pub async fn frontend_client_metadata()
148
+
-> axum::response::Result<([(HeaderName, &'static str); 1], String)> {
149
+
let frontend_hostname = &tranquil_config::get().server.hostname;
150
+
let metadata_string = tokio::fs::read_to_string(format!(
151
+
"{}/oauth-client-metadata.json",
152
+
&tranquil_config::get().frontend.dir
153
+
))
154
+
.await
155
+
// TODO: consider if a better conversion can be done here.
156
+
.map_err(|io_err| io_err.to_string())?;
157
+
158
+
Ok((
159
+
[(header::CONTENT_TYPE, "application/json")],
160
+
metadata_string.replace("__FRONTEND_HOSTNAME__", frontend_hostname),
161
+
))
162
+
}
+1
-1
deploy/nginx/nginx-quadlet.conf
+1
-1
deploy/nginx/nginx-quadlet.conf
+1
-1
docs/install-debian.md
+1
-1
docs/install-debian.md
+3
-3
frontend/nginx-quadlet.conf
+3
-3
frontend/nginx-quadlet.conf
···
10
gzip_vary on;
11
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
12
13
-
location = /oauth/client-metadata.json {
14
default_type application/json;
15
sub_filter_once off;
16
sub_filter_types application/json;
17
-
sub_filter '__PDS_HOSTNAME__' $host;
18
-
try_files /oauth/client-metadata.json =404;
19
}
20
21
location /assets/ {
···
10
gzip_vary on;
11
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
12
13
+
location = /oauth-client-metadata.json {
14
default_type application/json;
15
sub_filter_once off;
16
sub_filter_types application/json;
17
+
sub_filter '__FRONTEND_HOSTNAME__' $host;
18
+
try_files /oauth-client-metadata.json =404;
19
}
20
21
location /assets/ {
+3
-3
frontend/nginx.conf
+3
-3
frontend/nginx.conf
···
10
gzip_vary on;
11
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
12
13
-
location = /oauth/client-metadata.json {
14
default_type application/json;
15
sub_filter_once off;
16
sub_filter_types application/json;
17
-
sub_filter '__PDS_HOSTNAME__' $host;
18
-
try_files /oauth/client-metadata.json =404;
19
}
20
21
location /assets/ {
···
10
gzip_vary on;
11
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
12
13
+
location = /oauth-client-metadata.json {
14
default_type application/json;
15
sub_filter_once off;
16
sub_filter_types application/json;
17
+
sub_filter '__FRONTEND_HOSTNAME__' $host;
18
+
try_files /oauth-client-metadata.json =404;
19
}
20
21
location /assets/ {
+5
-5
frontend/public/oauth/client-metadata.json
frontend/public/oauth-client-metadata.json
+5
-5
frontend/public/oauth/client-metadata.json
frontend/public/oauth-client-metadata.json
···
1
{
2
-
"client_id": "https://__PDS_HOSTNAME__/oauth/client-metadata.json",
3
-
"client_name": "PDS Account Manager",
4
-
"client_uri": "https://__PDS_HOSTNAME__",
5
"redirect_uris": [
6
-
"https://__PDS_HOSTNAME__/app/",
7
-
"https://__PDS_HOSTNAME__/app/migrate"
8
],
9
"grant_types": ["authorization_code", "refresh_token"],
10
"response_types": ["code"],
···
1
{
2
+
"client_id": "https://__FRONTEND_HOSTNAME__/oauth-client-metadata.json",
3
+
"client_name": "Tranquil PDS Account Manager",
4
+
"client_uri": "https://__FRONTEND_HOSTNAME__",
5
"redirect_uris": [
6
+
"https://__FRONTEND_HOSTNAME__/app/",
7
+
"https://__FRONTEND_HOSTNAME__/app/migrate"
8
],
9
"grant_types": ["authorization_code", "refresh_token"],
10
"response_types": ["code"],
+1
-1
frontend/src/lib/migration/atproto-client.ts
+1
-1
frontend/src/lib/migration/atproto-client.ts
+1
-1
frontend/src/lib/oauth.ts
+1
-1
frontend/src/lib/oauth.ts
+1
-1
frontend/src/routes/ActAs.svelte
+1
-1
frontend/src/routes/ActAs.svelte
+3
-3
frontend/src/tests/migration/atproto-client.test.ts
+3
-3
frontend/src/tests/migration/atproto-client.test.ts
···
166
167
it("builds authorization URL with required parameters", () => {
168
const url = buildOAuthAuthorizationUrl(mockMetadata, {
169
-
clientId: "https://example.com/oauth/client-metadata.json",
170
redirectUri: "https://example.com/migrate",
171
codeChallenge: "abc123",
172
state: "state123",
···
177
expect(parsed.pathname).toBe("/oauth/authorize");
178
expect(parsed.searchParams.get("response_type")).toBe("code");
179
expect(parsed.searchParams.get("client_id")).toBe(
180
-
"https://example.com/oauth/client-metadata.json",
181
);
182
expect(parsed.searchParams.get("redirect_uri")).toBe(
183
"https://example.com/migrate",
···
256
it("returns client metadata URL based on origin", () => {
257
const clientId = getMigrationOAuthClientId();
258
expect(clientId).toBe(
259
-
`${globalThis.location.origin}/oauth/client-metadata.json`,
260
);
261
});
262
});
···
166
167
it("builds authorization URL with required parameters", () => {
168
const url = buildOAuthAuthorizationUrl(mockMetadata, {
169
+
clientId: "https://example.com/oauth-client-metadata.json",
170
redirectUri: "https://example.com/migrate",
171
codeChallenge: "abc123",
172
state: "state123",
···
177
expect(parsed.pathname).toBe("/oauth/authorize");
178
expect(parsed.searchParams.get("response_type")).toBe("code");
179
expect(parsed.searchParams.get("client_id")).toBe(
180
+
"https://example.com/oauth-client-metadata.json",
181
);
182
expect(parsed.searchParams.get("redirect_uri")).toBe(
183
"https://example.com/migrate",
···
256
it("returns client metadata URL based on origin", () => {
257
const clientId = getMigrationOAuthClientId();
258
expect(clientId).toBe(
259
+
`${globalThis.location.origin}/oauth-client-metadata.json`,
260
);
261
});
262
});
+1
-1
module.nix
+1
-1
module.nix
+2
-2
nginx.frontend.conf
+2
-2
nginx.frontend.conf
···
92
proxy_request_buffering off;
93
}
94
95
-
location = /oauth/client-metadata.json {
96
proxy_pass http://frontend;
97
proxy_http_version 1.1;
98
proxy_set_header Host $host;
99
proxy_set_header Accept-Encoding "";
100
sub_filter_once off;
101
sub_filter_types application/json;
102
-
sub_filter '__PDS_HOSTNAME__' $host;
103
}
104
105
location /oauth/ {
···
92
proxy_request_buffering off;
93
}
94
95
+
location = /oauth-client-metadata.json {
96
proxy_pass http://frontend;
97
proxy_http_version 1.1;
98
proxy_set_header Host $host;
99
proxy_set_header Accept-Encoding "";
100
sub_filter_once off;
101
sub_filter_types application/json;
102
+
sub_filter '__FRONTEND_HOSTNAME__' $host;
103
}
104
105
location /oauth/ {
+3
-3
test.nix
+3
-3
test.nix
···
147
code = http_status("/xrpc/_health", host="alice.pds.test")
148
assert code == "200", f"subdomain routing failed: {code}"
149
150
-
with subtest("client-metadata.json served with host substitution"):
151
-
meta_raw = http_get("/oauth/client-metadata.json")
152
meta = json.loads(meta_raw)
153
-
assert "client_id" in meta, f"no client_id in client-metadata: {meta}"
154
assert "pds.test" in meta_raw, "host substitution did not apply"
155
156
with subtest("static assets location exists"):
···
147
code = http_status("/xrpc/_health", host="alice.pds.test")
148
assert code == "200", f"subdomain routing failed: {code}"
149
150
+
with subtest("oauth-client-metadata.json served with host substitution"):
151
+
meta_raw = http_get("/oauth-client-metadata.json")
152
meta = json.loads(meta_raw)
153
+
assert "client_id" in meta, f"no client_id in oauth-client-metadata: {meta}"
154
assert "pds.test" in meta_raw, "host substitution did not apply"
155
156
with subtest("static assets location exists"):
History
5 rounds
0 comments
expand 0 comments
pull request successfully merged