···2020 ("logo_uri", "LOGO_URI"),
2121 ("tos_uri", "TOS_URI"),
2222 ("policy_uri", "POLICY_URI"),
2323- ("oauth_scopes", "OAUTH_SCOPES"),
2423];
25242625/// Resolve a setting value: check the DB first, then fall back to env var.
···331331332332 let oauth_state_store = DbStateStore::new(db_pool.clone(), db_backend);
333333334334- // Load OAuth scopes from the settings DB so they can be managed at runtime
335335- // without restarting HappyView. Falls back to just `atproto` if unset.
336336- let oauth_scopes =
337337- match happyview::admin::settings::get_setting(&db_pool, "oauth_scopes", db_backend).await {
338338- Some(s) => {
339339- let parsed = happyview::auth::parse_scope_string(&s);
340340- if parsed.is_empty() {
341341- vec![Scope::Known(KnownScope::Atproto)]
342342- } else {
343343- parsed
344344- }
345345- }
346346- None => vec![Scope::Known(KnownScope::Atproto)],
347347- };
334334+ // HappyView's own default OAuth client always uses the `atproto` scope.
335335+ // API clients configure their own scopes via the API Clients settings page.
336336+ let oauth_scopes = vec![Scope::Known(KnownScope::Atproto)];
348337349338 let oauth_client = if is_loopback {
350339 info!("Using loopback OAuth client metadata (local development)");
+23-10
src/server.rs
···9999}
100100101101async fn config_endpoint(State(state): State<AppState>) -> Json<serde_json::Value> {
102102+ let pool = &state.db;
103103+ let backend = state.db_backend;
104104+105105+ let app_name = crate::admin::settings::get_setting(pool, "app_name", backend)
106106+ .await
107107+ .or_else(|| state.config.app_name.clone());
108108+109109+ let has_logo_data = crate::admin::settings::get_setting(pool, "logo_data", backend)
110110+ .await
111111+ .is_some();
112112+ let logo_url = if has_logo_data {
113113+ Some(format!(
114114+ "{}/settings/logo",
115115+ state.config.public_url.trim_end_matches('/')
116116+ ))
117117+ } else {
118118+ crate::admin::settings::get_setting(pool, "logo_uri", backend)
119119+ .await
120120+ .or_else(|| state.config.logo_uri.clone())
121121+ };
122122+102123 Json(serde_json::json!({
103124 "public_url": state.config.public_url,
104125 "version": env!("CARGO_PKG_VERSION"),
···108129 "plc_url": state.config.plc_url,
109130 "default_rate_limit_capacity": state.config.default_rate_limit_capacity,
110131 "default_rate_limit_refill_rate": state.config.default_rate_limit_refill_rate,
132132+ "app_name": app_name,
133133+ "logo_url": logo_url,
111134 }))
112135}
113136···153176154177 if let Some(uri) = crate::admin::settings::get_setting(pool, "policy_uri", backend).await {
155178 metadata["policy_uri"] = serde_json::Value::String(uri);
156156- }
157157-158158- // OAuth scopes: override from the settings DB so admins can manage scopes without
159159- // restarting HappyView. The authorization server fetches this endpoint to validate
160160- // scope requests at PAR time, so this value is authoritative for non-loopback clients.
161161- if let Some(scopes) = crate::admin::settings::get_setting(pool, "oauth_scopes", backend).await {
162162- let normalized = scopes.split_whitespace().collect::<Vec<_>>().join(" ");
163163- if !normalized.is_empty() {
164164- metadata["scope"] = serde_json::Value::String(normalized);
165165- }
166179 }
167180168181 Json(metadata)
···3333export type { LabelerSummary } from "@/types/labelers"
3434export type { RecordLabel } from "@/types/records"
3535export type { ApiClientSummary, CreateApiClientResponse } from "@/types/api-clients"
3636-export type { SettingEntry, OAuthSettings } from "@/types/settings"
3737-export { OAUTH_SETTING_KEYS } from "@/types/settings"
3636+export type { SettingEntry, InstanceSettings } from "@/types/settings"
3737+export { INSTANCE_SETTING_KEYS } from "@/types/settings"
3838export type {
3939 ExternalProvider,
4040 LinkedAccount,