1
0
mirror of https://git.ngram.ca/OpenJam/rc-servers synced 2026-08-23 23:08:52 +00:00

Add server-wide and user-only federation controls (#133)

### Description

Completes #122

### Please confirm

- [x] I am the legal owner or represent the legal owner of all work submitted (including LLM-generated code, if any)
- [x] I consent to my changes being added to this FOSS project
- [x] I have confirmed that this does not add new errors or warnings with `utils/clippy.sh`
- [ ] This PR used LLMs to generate some or all of the code changes

Reviewed-on: https://git.ngram.ca/OpenJam/rc-servers/pulls/133
This commit is contained in:
NG (Graham)
2026-06-25 01:49:11 +00:00
committed by NGnius
parent e65d0eeb80
commit 995bafa70c
22 changed files with 522 additions and 15 deletions

View File

@@ -28,6 +28,7 @@ async fn main() -> std::io::Result<()> {
let server_settings = actix_web::web::Data::new(<oj_rc_core::ConfigImpl as oj_rc_core::ConfigProvider<()>>::server_config(&config));
let server_links = actix_web::web::Data::new(<oj_rc_core::ConfigImpl as oj_rc_core::ConfigProvider<()>>::url_links(&config));
let server_fed = actix_web::web::Data::new(<oj_rc_core::ConfigImpl as oj_rc_core::ConfigProvider<()>>::federation(&config));
let parsers = oj_rc_core::cubes::CubeParsers::new(&config);
@@ -77,6 +78,7 @@ async fn main() -> std::io::Result<()> {
.app_data(handlebars_ref.clone())
.app_data(server_settings.clone())
.app_data(server_links.clone())
.app_data(server_fed.clone())
.app_data(auth_ref.clone())
.app_data(importers_ref.clone())
.app_data(parsers_ref.clone())
@@ -95,6 +97,12 @@ async fn main() -> std::io::Result<()> {
.service(web::garage::import::get_new)
.service(web::garage::import::post)
.service(web::garage::selected::get)
.service(web::user_federation::get)
.service(web::user_federation::post)
.service(web::user_federation::post_remove)
.service(web::user_federation::post_add)
.service(web::user_federation::post_off)
.service(web::user_federation::post_on)
.service(api::config::get)
})
.bind((cli_args.ip, cli_args.port))?

View File

@@ -15,6 +15,7 @@ struct RenderData {
account: AccountData,
sanction: SanctionData,
social: SocialData,
fediverse: FederationData,
}
#[derive(Serialize, Deserialize)]
@@ -87,6 +88,12 @@ struct SocialData {
chats: Vec<String>,
}
#[derive(Serialize, Deserialize)]
struct FederationData {
enabled: bool,
defederated: Vec<String>,
}
pub async fn dashboard_impl(handlebars_ref: Data<handlebars::Handlebars<'_>>, auth: Data<Box<oj_rc_core::UserImpl>>, factory: Data<oj_rc_core::factory::Factory>, server_config: Data<oj_rc_core::persist::config::ServerConfig>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
match super::try_auth_user(user_opt, auth.as_ref(), &req).await? {
super::LoginReturn::AuthFail(resp) => Ok(resp),
@@ -111,6 +118,10 @@ pub async fn dashboard_impl(handlebars_ref: Data<handlebars::Handlebars<'_>>, au
Ok(x) => x,
Err(e) => return Ok(fallback_render(e, user.as_ref(), handlebars_ref.as_ref(), &req)),
};
let fedi_stats = match build_fedi_data(user.as_ref()).await {
Ok(x) => x,
Err(e) => return Ok(fallback_render(e, user.as_ref(), handlebars_ref.as_ref(), &req)),
};
Ok(super::render_ok(
RenderData {
display_name: user.display_name().to_owned(),
@@ -128,6 +139,7 @@ pub async fn dashboard_impl(handlebars_ref: Data<handlebars::Handlebars<'_>>, au
account: account_stats,
sanction: sanction_stats,
social: social_stats,
fediverse: fedi_stats,
},
handlebars_ref.as_ref(),
FORM_NAME,
@@ -193,7 +205,11 @@ fn fallback_render(error: Box<dyn std::error::Error>, user: &dyn oj_rc_core::per
clan: None,
friends: 0,
friends_of: 0,
chats: Vec::default()
chats: Vec::default(),
},
fediverse: FederationData {
enabled: false,
defederated: Vec::default(),
}
},
format!("Dashboard loading failed: {}", error),
@@ -304,6 +320,14 @@ async fn build_social_data(user: &dyn oj_rc_core::persist::user::WebUser) -> Res
})
}
async fn build_fedi_data(user: &dyn oj_rc_core::persist::user::WebUser) -> Result<FederationData, Box<dyn std::error::Error>> {
let info = user.fedi_get().await;
Ok(FederationData {
enabled: info.enabled,
defederated: info.defederated,
})
}
#[get("/dashboard")]
pub async fn get(handlebars_ref: Data<handlebars::Handlebars<'_>>, auth: Data<Box<oj_rc_core::UserImpl>>, factory: Data<oj_rc_core::factory::Factory>, server_config: Data<oj_rc_core::persist::config::ServerConfig>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
dashboard_impl(handlebars_ref, auth, factory, server_config, user_opt, req).await

View File

@@ -10,6 +10,7 @@ struct RenderData {
display_name: Option<String>,
server: ServerDetails,
links: LinkDetails,
fediverse: FederationDetails,
}
#[derive(Serialize, Deserialize)]
@@ -57,10 +58,31 @@ fn links_details(links: &oj_rc_core::persist::config::LinksConfig) -> LinkDetail
}
}
#[derive(Serialize, Deserialize)]
struct FederationDetails {
enabled: bool,
alias: std::collections::HashMap<String, String>,
defederated: Vec<String>,
}
fn fedi_details(fedi: &Option<oj_rc_core::persist::config::Federation>) -> FederationDetails {
fedi.as_ref().map(|f| FederationDetails {
enabled: true,
alias: f.aliases.clone(),
defederated: f.defederated.clone(),
})
.unwrap_or_else(|| FederationDetails {
enabled: false,
alias: Default::default(),
defederated: Default::default(),
})
}
#[get("/")]
pub async fn get(handlebars_ref: Data<handlebars::Handlebars<'_>>, auth: Data<Box<oj_rc_core::UserImpl>>, server_config: Data<oj_rc_core::persist::config::ServerConfig>, server_links: Data<oj_rc_core::persist::config::LinksConfig>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
pub async fn get(handlebars_ref: Data<handlebars::Handlebars<'_>>, auth: Data<Box<oj_rc_core::UserImpl>>, server_config: Data<oj_rc_core::persist::config::ServerConfig>, server_links: Data<oj_rc_core::persist::config::LinksConfig>, server_fedi: Data<Option<oj_rc_core::persist::config::Federation>>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
let server_info = server_details(server_config.as_ref());
let links_info = links_details(server_links.as_ref());
let federation_info = fedi_details(server_fedi.as_ref());
if let Some(user) = user_opt {
match super::try_auth_user(Some(user), auth.as_ref(), &req).await? {
super::LoginReturn::AuthFail(resp) => Ok(resp),
@@ -71,6 +93,7 @@ pub async fn get(handlebars_ref: Data<handlebars::Handlebars<'_>>, auth: Data<Bo
display_name: Some(user.display_name().to_owned()),
server: server_info,
links: links_info,
fediverse: federation_info,
},
handlebars_ref.as_ref(),
FORM_NAME,
@@ -87,6 +110,7 @@ pub async fn get(handlebars_ref: Data<handlebars::Handlebars<'_>>, auth: Data<Bo
display_name: None,
server: server_info,
links: links_info,
fediverse: federation_info,
},
handlebars_ref.as_ref(),
FORM_NAME,

View File

@@ -3,6 +3,7 @@ pub mod login;
pub mod favicon;
pub mod index;
pub mod garage;
pub mod user_federation;
use serde::Serialize;
use actix_web::{web::{Html, Redirect}, Responder};

View File

@@ -0,0 +1,143 @@
use actix_web::{get, post, web::{Data, Path, Redirect, Form}, Responder, HttpRequest};
use actix_identity::Identity;
use serde::{Serialize, Deserialize};
use crate::web::{LoginReturn, try_auth_user, render_ok};
const FORM_NAME: &str = "user_federation";
#[derive(Serialize, Deserialize)]
struct RenderData {
display_name: String,
public_id: String,
enabled: bool,
defederated: Vec<String>,
}
async fn list_impl(handlebars_ref: Data<handlebars::Handlebars<'_>>, auth: Data<Box<oj_rc_core::UserImpl>>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
match try_auth_user(user_opt, auth.as_ref(), &req).await? {
LoginReturn::AuthFail(resp) => Ok(resp),
LoginReturn::Success(user) => {
let fedi = user.fedi_get().await;
let html = render_ok(
RenderData {
display_name: user.display_name().to_owned(),
public_id: user.public_id().to_owned(),
enabled: fedi.enabled,
defederated: fedi.defederated,
},
handlebars_ref.as_ref(),
FORM_NAME,
);
Ok(
html
.respond_to(&req)
.map_into_boxed_body()
)
}
}
}
#[get("/federation/list")]
pub async fn get(handlebars_ref: Data<handlebars::Handlebars<'_>>, auth: Data<Box<oj_rc_core::UserImpl>>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
list_impl(handlebars_ref, auth, user_opt, req).await
}
#[post("/federation/list")]
pub async fn post(handlebars_ref: Data<handlebars::Handlebars<'_>>, auth: Data<Box<oj_rc_core::UserImpl>>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
list_impl(handlebars_ref, auth, user_opt, req).await
}
#[post("/federation/list/remove/{domain}")]
pub async fn post_remove(auth: Data<Box<oj_rc_core::UserImpl>>, user_opt: Option<Identity>, req: HttpRequest, domain: Path<String>) -> Result<impl Responder, actix_web::error::Error> {
match try_auth_user(user_opt, auth.as_ref(), &req).await? {
LoginReturn::AuthFail(resp) => Ok(resp),
LoginReturn::Success(user) => {
let mut fedi = user.fedi_get().await;
if let Some((i, _domain_name)) = fedi.defederated.iter().enumerate().find(|(_i, domain_name)| *domain_name == &*domain) {
fedi.defederated.remove(i);
user.fedi_set(fedi).await;
} else {
log::warn!("Failed to find domain {} in defederated list for user {}", &*domain, user.public_id());
}
let resp = Redirect::to("/federation/list")
.respond_to(&req)
.map_into_boxed_body();
Ok(
resp
.respond_to(&req)
.map_into_boxed_body()
)
}
}
}
#[derive(Serialize, Deserialize)]
struct AddForm {
domain: String,
}
#[post("/federation/list/add")]
pub async fn post_add(auth: Data<Box<oj_rc_core::UserImpl>>, user_opt: Option<Identity>, req: HttpRequest, form: Form<AddForm>) -> Result<impl Responder, actix_web::error::Error> {
match try_auth_user(user_opt, auth.as_ref(), &req).await? {
LoginReturn::AuthFail(resp) => Ok(resp),
LoginReturn::Success(user) => {
let mut fedi = user.fedi_get().await;
let sanitized_domain = form.domain.trim().to_lowercase();
if fedi.defederated.contains(&sanitized_domain) {
log::warn!("Domain {} already in defederated list for user {}", sanitized_domain, user.public_id());
} else {
fedi.defederated.push(sanitized_domain);
user.fedi_set(fedi).await;
}
let resp = Redirect::to("/federation/list")
.respond_to(&req)
.map_into_boxed_body();
Ok(
resp
.respond_to(&req)
.map_into_boxed_body()
)
}
}
}
#[post("/federation/off")]
pub async fn post_off(auth: Data<Box<oj_rc_core::UserImpl>>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
match try_auth_user(user_opt, auth.as_ref(), &req).await? {
LoginReturn::AuthFail(resp) => Ok(resp),
LoginReturn::Success(user) => {
let mut fedi = user.fedi_get().await;
fedi.enabled = false;
user.fedi_set(fedi).await;
let resp = Redirect::to("/federation/list")
.respond_to(&req)
.map_into_boxed_body();
Ok(
resp
.respond_to(&req)
.map_into_boxed_body()
)
}
}
}
#[post("/federation/on")]
pub async fn post_on(auth: Data<Box<oj_rc_core::UserImpl>>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
match try_auth_user(user_opt, auth.as_ref(), &req).await? {
LoginReturn::AuthFail(resp) => Ok(resp),
LoginReturn::Success(user) => {
let mut fedi = user.fedi_get().await;
fedi.enabled = true;
user.fedi_set(fedi).await;
let resp = Redirect::to("/federation/list")
.respond_to(&req)
.map_into_boxed_body();
Ok(
resp
.respond_to(&req)
.map_into_boxed_body()
)
}
}
}