mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-08-22 01:06:05 +00:00
feat: add back built-in frontend hosting to the backend
This commit is contained in:
Generated
+28
@@ -2743,6 +2743,12 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-range-header"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
@@ -3613,6 +3619,16 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
@@ -5976,13 +5992,19 @@ dependencies = [
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"http-range-header",
|
||||
"httpdate",
|
||||
"iri-string",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6414,6 +6436,12 @@ version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.18"
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@ tokio-util = "0.7.18"
|
||||
tokio-tungstenite = { version = "0.28", features = ["rustls-tls-webpki-roots"] }
|
||||
totp-rs = { version = "5", features = ["qr"] }
|
||||
tower = "0.5"
|
||||
tower-http = { version = "0.6", features = ["cors"] }
|
||||
tower-http = { version = "0.6", features = ["fs", "cors"] }
|
||||
tower-layer = "0.3"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
|
||||
@@ -96,6 +96,9 @@ pub struct TranquilConfig {
|
||||
#[config(nested)]
|
||||
pub server: ServerConfig,
|
||||
|
||||
#[config(nested)]
|
||||
pub frontend: FrontendConfig,
|
||||
|
||||
#[config(nested)]
|
||||
pub database: DatabaseConfig,
|
||||
|
||||
@@ -482,6 +485,18 @@ impl ServerConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct FrontendConfig {
|
||||
/// Whether to enable the built in serving of the frontend.
|
||||
#[config(env = "FRONTEND_ENABLED", default = true)]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Directory to serve as the frontend. The oauth_client_metadata.json will have any references to
|
||||
/// the frontend hostname replaced by the configured frontend hostname.
|
||||
#[config(env = "FRONTEND_DIR", default = "/var/lib/tranquil-pds/frontend")]
|
||||
pub dir: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
pub struct DatabaseConfig {
|
||||
/// PostgreSQL connection URL.
|
||||
|
||||
@@ -83,11 +83,12 @@ aws-config = { workspace = true, optional = true }
|
||||
aws-sdk-s3 = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
default = ["s3", "valkey"]
|
||||
default = ["frontend", "s3", "valkey"]
|
||||
external-infra = []
|
||||
s3-storage = ["tranquil-storage/s3", "dep:aws-config", "dep:aws-sdk-s3"]
|
||||
s3 = ["s3-storage"]
|
||||
valkey = ["tranquil-cache/valkey", "dep:redis"]
|
||||
frontend = []
|
||||
|
||||
[dev-dependencies]
|
||||
ciborium = { workspace = true }
|
||||
|
||||
@@ -39,7 +39,10 @@ use http::StatusCode;
|
||||
use serde_json::json;
|
||||
use state::AppState;
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
use tower_http::{
|
||||
cors::{Any, CorsLayer},
|
||||
services::{ServeDir, ServeFile},
|
||||
};
|
||||
pub use tranquil_db_traits::AccountStatus;
|
||||
pub use types::{AccountState, AtIdentifier, AtUri, Did, Handle, Nsid, Rkey};
|
||||
|
||||
@@ -650,7 +653,9 @@ pub fn app(state: AppState) -> Router {
|
||||
get(oauth::endpoints::oauth_authorization_server),
|
||||
);
|
||||
|
||||
Router::new()
|
||||
if cfg!(feature = "frontend") {}
|
||||
|
||||
let router = Router::new()
|
||||
.nest_service("/xrpc", xrpc_service)
|
||||
.nest("/oauth", oauth_router)
|
||||
.nest("/.well-known", well_known_router)
|
||||
@@ -695,7 +700,35 @@ pub fn app(state: AppState) -> Router {
|
||||
util::HEADER_ATPROTO_CONTENT_LABELERS,
|
||||
]),
|
||||
)
|
||||
.with_state(state)
|
||||
.with_state(state);
|
||||
|
||||
if cfg!(feature = "frontend") && tranquil_config::get().frontend.enabled {
|
||||
let frontend_dir = &tranquil_config::get().frontend.dir;
|
||||
let index_path = format!("{}/index.html", frontend_dir);
|
||||
let homepage_path = format!("{}/homepage.html", frontend_dir);
|
||||
|
||||
let homepage_exists = std::path::Path::new(&homepage_path).exists();
|
||||
let homepage_file = if homepage_exists {
|
||||
homepage_path
|
||||
} else {
|
||||
index_path.clone()
|
||||
};
|
||||
|
||||
let spa_router = Router::new().fallback_service(ServeFile::new(&index_path));
|
||||
|
||||
let serve_dir = ServeDir::new(&frontend_dir).not_found_service(ServeFile::new(&index_path));
|
||||
|
||||
return router
|
||||
.route(
|
||||
"/oauth-client-metadata.json",
|
||||
get(oauth::endpoints::frontend_client_metadata),
|
||||
)
|
||||
.route_service("/", ServeFile::new(&homepage_file))
|
||||
.nest("/app", spa_router)
|
||||
.fallback_service(serve_dir);
|
||||
}
|
||||
|
||||
router
|
||||
}
|
||||
|
||||
async fn rewrite_422_to_400(response: axum::response::Response) -> axum::response::Response {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::oauth::jwks::{JwkSet, create_jwk_set};
|
||||
use crate::state::AppState;
|
||||
use axum::{Json, extract::State};
|
||||
use http::{HeaderName, header};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -140,3 +143,20 @@ pub async fn oauth_jwks(State(_state): State<AppState>) -> Json<JwkSet> {
|
||||
};
|
||||
Json(create_jwk_set(vec![server_key]))
|
||||
}
|
||||
|
||||
pub async fn frontend_client_metadata()
|
||||
-> axum::response::Result<([(HeaderName, &'static str); 1], String)> {
|
||||
let frontend_hostname = &tranquil_config::get().server.hostname;
|
||||
let metadata_string = tokio::fs::read_to_string(format!(
|
||||
"{}/oauth-client-metadata.json",
|
||||
&tranquil_config::get().frontend.dir
|
||||
))
|
||||
.await
|
||||
// TODO: consider if a better conversion can be done here.
|
||||
.map_err(|io_err| io_err.to_string())?;
|
||||
|
||||
Ok((
|
||||
[(header::CONTENT_TYPE, "application/json")],
|
||||
metadata_string.replace("__FRONTEND_HOSTNAME__", frontend_hostname),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ http {
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
|
||||
location = /oauth/client-metadata.json {
|
||||
location = /oauth-client-metadata.json {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
@@ -179,7 +179,7 @@ server {
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
|
||||
location = /oauth/client-metadata.json {
|
||||
location = /oauth-client-metadata.json {
|
||||
root /var/www/tranquil-pds;
|
||||
default_type application/json;
|
||||
sub_filter_once off;
|
||||
|
||||
@@ -10,12 +10,12 @@ server {
|
||||
gzip_vary on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
|
||||
|
||||
location = /oauth/client-metadata.json {
|
||||
location = /oauth-client-metadata.json {
|
||||
default_type application/json;
|
||||
sub_filter_once off;
|
||||
sub_filter_types application/json;
|
||||
sub_filter '__PDS_HOSTNAME__' $host;
|
||||
try_files /oauth/client-metadata.json =404;
|
||||
sub_filter '__FRONTEND_HOSTNAME__' $host;
|
||||
try_files /oauth-client-metadata.json =404;
|
||||
}
|
||||
|
||||
location /assets/ {
|
||||
|
||||
+3
-3
@@ -10,12 +10,12 @@ server {
|
||||
gzip_vary on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
|
||||
|
||||
location = /oauth/client-metadata.json {
|
||||
location = /oauth-client-metadata.json {
|
||||
default_type application/json;
|
||||
sub_filter_once off;
|
||||
sub_filter_types application/json;
|
||||
sub_filter '__PDS_HOSTNAME__' $host;
|
||||
try_files /oauth/client-metadata.json =404;
|
||||
sub_filter '__FRONTEND_HOSTNAME__' $host;
|
||||
try_files /oauth-client-metadata.json =404;
|
||||
}
|
||||
|
||||
location /assets/ {
|
||||
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"client_id": "https://__PDS_HOSTNAME__/oauth/client-metadata.json",
|
||||
"client_name": "PDS Account Manager",
|
||||
"client_uri": "https://__PDS_HOSTNAME__",
|
||||
"client_id": "https://__FRONTEND_HOSTNAME__/oauth-client-metadata.json",
|
||||
"client_name": "Tranquil PDS Account Manager",
|
||||
"client_uri": "https://__FRONTEND_HOSTNAME__",
|
||||
"redirect_uris": [
|
||||
"https://__PDS_HOSTNAME__/app/",
|
||||
"https://__PDS_HOSTNAME__/app/migrate"
|
||||
"https://__FRONTEND_HOSTNAME__/app/",
|
||||
"https://__FRONTEND_HOSTNAME__/app/migrate"
|
||||
],
|
||||
"grant_types": ["authorization_code", "refresh_token"],
|
||||
"response_types": ["code"],
|
||||
@@ -1055,7 +1055,7 @@ export function createLocalClient(): AtprotoClient {
|
||||
}
|
||||
|
||||
export function getMigrationOAuthClientId(): string {
|
||||
return `${globalThis.location.origin}/oauth/client-metadata.json`;
|
||||
return `${globalThis.location.origin}/oauth-client-metadata.json`;
|
||||
}
|
||||
|
||||
export function getMigrationOAuthRedirectUri(): string {
|
||||
|
||||
@@ -14,7 +14,7 @@ const SCOPES = [
|
||||
].join(" ");
|
||||
|
||||
const CLIENT_ID = !(import.meta.env.DEV)
|
||||
? `${globalThis.location.origin}/oauth/client-metadata.json`
|
||||
? `${globalThis.location.origin}/oauth-client-metadata.json`
|
||||
: `http://localhost/?scope=${SCOPES}`;
|
||||
|
||||
const REDIRECT_URI = `${globalThis.location.origin}/app/`;
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
client_id: `${hostname}/oauth/client-metadata.json`,
|
||||
client_id: `${hostname}/oauth-client-metadata.json`,
|
||||
redirect_uri: `${hostname}/app/`,
|
||||
response_type: 'code',
|
||||
scope: 'atproto',
|
||||
|
||||
@@ -166,7 +166,7 @@ describe("migration/atproto-client", () => {
|
||||
|
||||
it("builds authorization URL with required parameters", () => {
|
||||
const url = buildOAuthAuthorizationUrl(mockMetadata, {
|
||||
clientId: "https://example.com/oauth/client-metadata.json",
|
||||
clientId: "https://example.com/oauth-client-metadata.json",
|
||||
redirectUri: "https://example.com/migrate",
|
||||
codeChallenge: "abc123",
|
||||
state: "state123",
|
||||
@@ -177,7 +177,7 @@ describe("migration/atproto-client", () => {
|
||||
expect(parsed.pathname).toBe("/oauth/authorize");
|
||||
expect(parsed.searchParams.get("response_type")).toBe("code");
|
||||
expect(parsed.searchParams.get("client_id")).toBe(
|
||||
"https://example.com/oauth/client-metadata.json",
|
||||
"https://example.com/oauth-client-metadata.json",
|
||||
);
|
||||
expect(parsed.searchParams.get("redirect_uri")).toBe(
|
||||
"https://example.com/migrate",
|
||||
@@ -256,7 +256,7 @@ describe("migration/atproto-client", () => {
|
||||
it("returns client metadata URL based on origin", () => {
|
||||
const clientId = getMigrationOAuthClientId();
|
||||
expect(clientId).toBe(
|
||||
`${globalThis.location.origin}/oauth/client-metadata.json`,
|
||||
`${globalThis.location.origin}/oauth-client-metadata.json`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -261,7 +261,7 @@ in {
|
||||
}
|
||||
|
||||
(lib.optionalAttrs (cfg.frontend.package != null) {
|
||||
"= /oauth/client-metadata.json" = {
|
||||
"= /oauth-client-metadata.json" = {
|
||||
root = "${cfg.frontend.package}";
|
||||
extraConfig = ''
|
||||
default_type application/json;
|
||||
|
||||
+2
-2
@@ -92,14 +92,14 @@ http {
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
|
||||
location = /oauth/client-metadata.json {
|
||||
location = /oauth-client-metadata.json {
|
||||
proxy_pass http://frontend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once off;
|
||||
sub_filter_types application/json;
|
||||
sub_filter '__PDS_HOSTNAME__' $host;
|
||||
sub_filter '__FRONTEND_HOSTNAME__' $host;
|
||||
}
|
||||
|
||||
location /oauth/ {
|
||||
|
||||
@@ -147,10 +147,10 @@ pkgs.testers.nixosTest {
|
||||
code = http_status("/xrpc/_health", host="alice.pds.test")
|
||||
assert code == "200", f"subdomain routing failed: {code}"
|
||||
|
||||
with subtest("client-metadata.json served with host substitution"):
|
||||
meta_raw = http_get("/oauth/client-metadata.json")
|
||||
with subtest("oauth-client-metadata.json served with host substitution"):
|
||||
meta_raw = http_get("/oauth-client-metadata.json")
|
||||
meta = json.loads(meta_raw)
|
||||
assert "client_id" in meta, f"no client_id in client-metadata: {meta}"
|
||||
assert "client_id" in meta, f"no client_id in oauth-client-metadata: {meta}"
|
||||
assert "pds.test" in meta_raw, "host substitution did not apply"
|
||||
|
||||
with subtest("static assets location exists"):
|
||||
|
||||
Reference in New Issue
Block a user