+28
Cargo.lock
+28
Cargo.lock
···
2743
2743
"pin-project-lite",
2744
2744
]
2745
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
+
2746
2752
[[package]]
2747
2753
name = "httparse"
2748
2754
version = "1.10.1"
···
3613
3619
source = "registry+https://github.com/rust-lang/crates.io-index"
3614
3620
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
3615
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
+
3616
3632
[[package]]
3617
3633
name = "minimal-lexical"
3618
3634
version = "0.2.1"
···
5976
5992
"http 1.4.0",
5977
5993
"http-body 1.0.1",
5978
5994
"http-body-util",
5995
+
"http-range-header",
5996
+
"httpdate",
5979
5997
"iri-string",
5998
+
"mime",
5999
+
"mime_guess",
6000
+
"percent-encoding",
5980
6001
"pin-project-lite",
5981
6002
"tokio",
5982
6003
"tokio-util",
5983
6004
"tower",
5984
6005
"tower-layer",
5985
6006
"tower-service",
6007
+
"tracing",
5986
6008
]
5987
6009
5988
6010
[[package]]
···
6414
6436
source = "registry+https://github.com/rust-lang/crates.io-index"
6415
6437
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
6416
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
+
6417
6445
[[package]]
6418
6446
name = "unicode-bidi"
6419
6447
version = "0.3.18"
+1
-1
Cargo.toml
+1
-1
Cargo.toml
···
100
100
tokio-tungstenite = { version = "0.28", features = ["rustls-tls-webpki-roots"] }
101
101
totp-rs = { version = "5", features = ["qr"] }
102
102
tower = "0.5"
103
-
tower-http = { version = "0.6", features = ["cors"] }
103
+
tower-http = { version = "0.6", features = ["fs", "cors"] }
104
104
tower-layer = "0.3"
105
105
tracing = "0.1"
106
106
tracing-subscriber = "0.3"
+15
crates/tranquil-config/src/lib.rs
+15
crates/tranquil-config/src/lib.rs
···
96
96
#[config(nested)]
97
97
pub server: ServerConfig,
98
98
99
+
#[config(nested)]
100
+
pub frontend: FrontendConfig,
101
+
99
102
#[config(nested)]
100
103
pub database: DatabaseConfig,
101
104
···
482
485
}
483
486
}
484
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
+
485
500
#[derive(Debug, Config)]
486
501
pub struct DatabaseConfig {
487
502
/// PostgreSQL connection URL.
+2
-1
crates/tranquil-pds/Cargo.toml
+2
-1
crates/tranquil-pds/Cargo.toml
···
83
83
aws-sdk-s3 = { workspace = true, optional = true }
84
84
85
85
[features]
86
-
default = ["s3", "valkey"]
86
+
default = ["frontend", "s3", "valkey"]
87
87
external-infra = []
88
88
s3-storage = ["tranquil-storage/s3", "dep:aws-config", "dep:aws-sdk-s3"]
89
89
s3 = ["s3-storage"]
90
90
valkey = ["tranquil-cache/valkey", "dep:redis"]
91
+
frontend = []
91
92
92
93
[dev-dependencies]
93
94
ciborium = { workspace = true }
+36
-3
crates/tranquil-pds/src/lib.rs
+36
-3
crates/tranquil-pds/src/lib.rs
···
39
39
use serde_json::json;
40
40
use state::AppState;
41
41
use tower::ServiceBuilder;
42
-
use tower_http::cors::{Any, CorsLayer};
42
+
use tower_http::{
43
+
cors::{Any, CorsLayer},
44
+
services::{ServeDir, ServeFile},
45
+
};
43
46
pub use tranquil_db_traits::AccountStatus;
44
47
pub use types::{AccountState, AtIdentifier, AtUri, Did, Handle, Nsid, Rkey};
45
48
···
640
643
get(oauth::endpoints::oauth_authorization_server),
641
644
);
642
645
643
-
Router::new()
646
+
if cfg!(feature = "frontend") {}
647
+
648
+
let router = Router::new()
644
649
.nest_service("/xrpc", xrpc_service)
645
650
.nest("/oauth", oauth_router)
646
651
.nest("/.well-known", well_known_router)
···
685
690
util::HEADER_ATPROTO_CONTENT_LABELERS,
686
691
]),
687
692
)
688
-
.with_state(state)
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
689
722
}
690
723
691
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
+
1
3
use crate::oauth::jwks::{JwkSet, create_jwk_set};
2
4
use crate::state::AppState;
3
5
use axum::{Json, extract::State};
6
+
use http::{HeaderName, header};
4
7
use serde::{Deserialize, Serialize};
5
8
6
9
#[derive(Debug, Serialize, Deserialize)]
···
140
143
};
141
144
Json(create_jwk_set(vec![server_key]))
142
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
10
gzip_vary on;
11
11
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
12
12
13
-
location = /oauth/client-metadata.json {
13
+
location = /oauth-client-metadata.json {
14
14
default_type application/json;
15
15
sub_filter_once off;
16
16
sub_filter_types application/json;
17
-
sub_filter '__PDS_HOSTNAME__' $host;
18
-
try_files /oauth/client-metadata.json =404;
17
+
sub_filter '__FRONTEND_HOSTNAME__' $host;
18
+
try_files /oauth-client-metadata.json =404;
19
19
}
20
20
21
21
location /assets/ {
+3
-3
frontend/nginx.conf
+3
-3
frontend/nginx.conf
···
10
10
gzip_vary on;
11
11
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
12
12
13
-
location = /oauth/client-metadata.json {
13
+
location = /oauth-client-metadata.json {
14
14
default_type application/json;
15
15
sub_filter_once off;
16
16
sub_filter_types application/json;
17
-
sub_filter '__PDS_HOSTNAME__' $host;
18
-
try_files /oauth/client-metadata.json =404;
17
+
sub_filter '__FRONTEND_HOSTNAME__' $host;
18
+
try_files /oauth-client-metadata.json =404;
19
19
}
20
20
21
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
1
{
2
-
"client_id": "https://__PDS_HOSTNAME__/oauth/client-metadata.json",
3
-
"client_name": "PDS Account Manager",
4
-
"client_uri": "https://__PDS_HOSTNAME__",
2
+
"client_id": "https://__FRONTEND_HOSTNAME__/oauth-client-metadata.json",
3
+
"client_name": "Tranquil PDS Account Manager",
4
+
"client_uri": "https://__FRONTEND_HOSTNAME__",
5
5
"redirect_uris": [
6
-
"https://__PDS_HOSTNAME__/app/",
7
-
"https://__PDS_HOSTNAME__/app/migrate"
6
+
"https://__FRONTEND_HOSTNAME__/app/",
7
+
"https://__FRONTEND_HOSTNAME__/app/migrate"
8
8
],
9
9
"grant_types": ["authorization_code", "refresh_token"],
10
10
"response_types": ["code"],
+1
-1
frontend/src/lib/migration/atproto-client.ts
+1
-1
frontend/src/lib/migration/atproto-client.ts
···
1055
1055
}
1056
1056
1057
1057
export function getMigrationOAuthClientId(): string {
1058
-
return `${globalThis.location.origin}/oauth/client-metadata.json`;
1058
+
return `${globalThis.location.origin}/oauth-client-metadata.json`;
1059
1059
}
1060
1060
1061
1061
export function getMigrationOAuthRedirectUri(): string {
+1
-1
frontend/src/lib/oauth.ts
+1
-1
frontend/src/lib/oauth.ts
···
14
14
].join(" ");
15
15
16
16
const CLIENT_ID = !(import.meta.env.DEV)
17
-
? `${globalThis.location.origin}/oauth/client-metadata.json`
17
+
? `${globalThis.location.origin}/oauth-client-metadata.json`
18
18
: `http://localhost/?scope=${SCOPES}`;
19
19
20
20
const REDIRECT_URI = `${globalThis.location.origin}/app/`;
+1
-1
frontend/src/routes/ActAs.svelte
+1
-1
frontend/src/routes/ActAs.svelte
···
51
51
method: 'POST',
52
52
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
53
53
body: new URLSearchParams({
54
-
client_id: `${hostname}/oauth/client-metadata.json`,
54
+
client_id: `${hostname}/oauth-client-metadata.json`,
55
55
redirect_uri: `${hostname}/app/`,
56
56
response_type: 'code',
57
57
scope: 'atproto',
+3
-3
frontend/src/tests/migration/atproto-client.test.ts
+3
-3
frontend/src/tests/migration/atproto-client.test.ts
···
166
166
167
167
it("builds authorization URL with required parameters", () => {
168
168
const url = buildOAuthAuthorizationUrl(mockMetadata, {
169
-
clientId: "https://example.com/oauth/client-metadata.json",
169
+
clientId: "https://example.com/oauth-client-metadata.json",
170
170
redirectUri: "https://example.com/migrate",
171
171
codeChallenge: "abc123",
172
172
state: "state123",
···
177
177
expect(parsed.pathname).toBe("/oauth/authorize");
178
178
expect(parsed.searchParams.get("response_type")).toBe("code");
179
179
expect(parsed.searchParams.get("client_id")).toBe(
180
-
"https://example.com/oauth/client-metadata.json",
180
+
"https://example.com/oauth-client-metadata.json",
181
181
);
182
182
expect(parsed.searchParams.get("redirect_uri")).toBe(
183
183
"https://example.com/migrate",
···
256
256
it("returns client metadata URL based on origin", () => {
257
257
const clientId = getMigrationOAuthClientId();
258
258
expect(clientId).toBe(
259
-
`${globalThis.location.origin}/oauth/client-metadata.json`,
259
+
`${globalThis.location.origin}/oauth-client-metadata.json`,
260
260
);
261
261
});
262
262
});
+1
-1
module.nix
+1
-1
module.nix
+2
-2
nginx.frontend.conf
+2
-2
nginx.frontend.conf
···
92
92
proxy_request_buffering off;
93
93
}
94
94
95
-
location = /oauth/client-metadata.json {
95
+
location = /oauth-client-metadata.json {
96
96
proxy_pass http://frontend;
97
97
proxy_http_version 1.1;
98
98
proxy_set_header Host $host;
99
99
proxy_set_header Accept-Encoding "";
100
100
sub_filter_once off;
101
101
sub_filter_types application/json;
102
-
sub_filter '__PDS_HOSTNAME__' $host;
102
+
sub_filter '__FRONTEND_HOSTNAME__' $host;
103
103
}
104
104
105
105
location /oauth/ {
+3
-3
test.nix
+3
-3
test.nix
···
147
147
code = http_status("/xrpc/_health", host="alice.pds.test")
148
148
assert code == "200", f"subdomain routing failed: {code}"
149
149
150
-
with subtest("client-metadata.json served with host substitution"):
151
-
meta_raw = http_get("/oauth/client-metadata.json")
150
+
with subtest("oauth-client-metadata.json served with host substitution"):
151
+
meta_raw = http_get("/oauth-client-metadata.json")
152
152
meta = json.loads(meta_raw)
153
-
assert "client_id" in meta, f"no client_id in client-metadata: {meta}"
153
+
assert "client_id" in meta, f"no client_id in oauth-client-metadata: {meta}"
154
154
assert "pds.test" in meta_raw, "host substitution did not apply"
155
155
156
156
with subtest("static assets location exists"):
History
5 rounds
0 comments
expand 0 comments
pull request successfully merged