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

@@ -200,10 +200,18 @@
<div class="access-box">
<h4 class="access-box-title">Federation</h4>
<div class="box-details">
<div class="box-details-key">TODO</div> <!-- TODO -->
<div class="box-details-value">TODO</div>
<div class="box-details-key">Enabled</div>
<div class="box-details-value">{{form.fediverse.enabled}}</div>
<div class="box-details-key">Blocked</div>
<div class="box-details-value">
{{#each form.fediverse.defederated}}
<span>{{this}}</span><br/>
{{else}}
<span>[None]</span>
{{/each}}
</div>
</div>
<a href="#"> <!-- TODO -->
<a href="/federation/list"> <!-- TODO -->
<div class="access-box-banner">
Manage My Federation
</div>

View File

@@ -50,7 +50,7 @@
{{else}}
<input type="hidden" id="garage_id" name="garage_id" value="-1"/>
{{/if}}
<div class="access-row-box">
<div class="access-row-box access-row-box-input">
<label>Import with</label>
<select id="plugin" name="plugin" required>
{{#each form.importers}}
@@ -58,11 +58,11 @@
{{/each}}
</select>
</div>
<div class="access-row-box">
<input type="file" id="files" name="files" required {{#if form.garage}} {{else}}multiple{{/if}} />
<div class="access-row-box access-row-box-input">
<input class="form-input-box" type="file" id="files" name="files" required {{#if form.garage}} {{else}}multiple{{/if}} />
</div>
<div class="access-row-box">
<input type="submit" value="{{#if form.garage}}Override garage slot {{form.garage.slot}}{{else}}Import{{/if}}" />
<div class="access-row-box form-submit access-row-box-input">
<input class="form-submit-button" style="width:50%;" type="submit" value="{{#if form.garage}}Override garage slot {{form.garage.slot}}{{else}}Import{{/if}}" />
</div>
</div>
</form>

View File

@@ -85,10 +85,30 @@
<div class="box-details-value"><a class="box-details-link" href="{{form.links.wiki}}">{{form.links.wiki}}</a></div>
</div>
</div>
<!--<div class="access-box">
Grid item 3
</div>
<div class="access-box">
<h4 class="access-box-title">Federation</h4>
<div class="box-details">
<div class="box-details-key">Enabled</div>
<div class="box-details-value">{{form.fediverse.enabled}}</div>
<div class="box-details-key">Aliases</div>
<div class="box-details-value">
{{#each form.fediverse.alias}}
<span>{{@key}} -> {{this}}</span><br/>
{{else}}
<span>[None]</span>
{{/each}}
</div>
<div class="box-details-key">Blocked</div>
<div class="box-details-value">
{{#each form.fediverse.defederated}}
<span>{{this}}</span><br/>
{{else}}
<span>[None]</span>
{{/each}}
</div>
</div>
</div>
<!--<div class="access-box">
Grid item 4
</div>
<div class="access-box">

View File

@@ -98,6 +98,9 @@
background-color: lightgrey;
font-size: 20px;
}
input[type=submit] {
cursor: pointer;
}
.input-required-hint::after {
color: red;
/*content: "*required";*/

View File

@@ -36,6 +36,34 @@ body {
.form-padded {
padding: 0 25px;
}
input:required:invalid.form-input-box {
border-color: red;
}
input:required:invalid.form-input-box::after {
content: "*required";
color: red;
font-size: 12px;
}
input {
width: 100%;
border: thin solid white;
background-color: lightgrey;
}
input[type=submit] {
cursor: pointer;
}
.form-submit {
margin: 0 auto;
}
.form-submit-button {
background-color: black;
color: lightgrey;
}
.input-required-hint::after {
color: red;
/*content: "*required";*/
font-size: 12px;
}
h1, h2, h3, h4, h5 {
text-align: center;
}
@@ -55,6 +83,10 @@ form {
padding-right: 25px;
padding-bottom: 250px;
}
.inline-form {
padding: 0;
display: contents;
}
.form-error {
position: relative;
width: 100%;
@@ -112,6 +144,9 @@ form {
text-align: center;
width: 100%;
}
.access-row-box-input {
padding: 5px;
}
.access-box-title {
padding: 5px;
margin: 0 auto;
@@ -144,6 +179,13 @@ form {
color: black;
text-align: left;
}
.box-details-input {
padding: 0;
margin: auto 0;
}
.box-details-input-text {
padding-right: 6px;
}
.export-plugin {
color: white;
}

View File

@@ -0,0 +1,90 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- Primary Meta Tags -->
<title>Garages - Openjam</title>
<meta name="title" content="Federation - Openjam" />
<meta name="description" content="User federation controls for third-party Robocraft servers" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://git.ngram.ca/OpenJam/rc-servers" />
<meta property="og:title" content="Federation - Openjam" />
<meta property="og:description" content="View your federation settings for third-party Robocraft servers" />
<meta property="og:image" content="https://git.ngram.ca/OpenJam/rc-servers/raw/branch/main/assets/robocraft/favicon.jpg" />
<!-- Twitter -->
<meta property="twitter:card" content="summary" />
<meta property="twitter:url" content="https://git.ngram.ca/OpenJam/rc-servers" />
<meta property="twitter:title" content="Federation - Openjam" />
<meta property="twitter:description" content="View your federation settings for third-party Robocraft servers" />
<meta property="twitter:image" content="https://git.ngram.ca/OpenJam/rc-servers/raw/branch/main/assets/robocraft/favicon.jpg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico">
{{> stylesheet}}
</head>
<body>
<div class="form-stripe">
<div class="form-header-right">
Welcome {{form.display_name}}! |
<a href="/">Home</a>
</div>
<h1 class="form-banner">{{form.display_name}} Federation Settings</h1>
{{#if error}}
<div class="form-error">
<p class="form-error-message">{{error}}</p>
</div>
{{/if}}
<div class="access-grid">
<div class="access-box">
{{#if form.enabled}}
<h4 class="access-box-title">Disable Federation</h4>
<form class="inline-form box-details" method="post" action="./off">
<div class="box-details-value">Turn Off</div>
<div class="box-details-key box-details-input"><input class="form-submit-button" type="submit" value="Confirm" /></div>
</form>
{{else}}
<h4 class="access-box-title">Enable Federation</h4>
<form class="inline-form box-details" method="post" action="./on">
<div class="box-details-value">Turn On</div>
<div class="box-details-key box-details-input"><input class="form-submit-button" type="submit" value="Confirm" /></div>
</form>
{{/if}}
</div>
<div class="access-box">
<h4 class="access-box-title">Defederated Domains</h4>
<div class="box-details">
{{#each form.defederated}}
<form class="inline-form" method="post" action="./list/remove/{{this}}">
<div class="box-details-key">{{this}}</div>
<div class="box-details-input box-details-value">
<input type="submit" value="Remove" />
</div>
</form>
{{else}}
<div class="box-details-key">No Domains</div>
<div class="box-details-value">To Remove</div>
{{/each}}
</div>
</div>
<div class="access-box">
<h4 class="access-box-title">Defederated A Domain</h4>
<form class="inline-form box-details" method="post" action="./list/add">
<div class="box-details-value box-details-input box-details-input-text"><input class="form-input-box" placeholder="example.com" type="text" id="domain" name="domain" required /></div>
<div class="box-details-key box-details-input"><input class="form-submit-button" type="submit" value="Add" /></div>
</form>
</div>
</div>
<footer>
<div class="footer">
{{version}}
| <a href="{{source_url}}#readme">About</a>
| <a href="https://liberapay.com/NGram">Support Me</a>
</div>
</footer>
</div>
</body>
</html>

View File

@@ -8,7 +8,7 @@ use polariton::serdes::TypePrefix;
use crate::persist::config::SelfValidator;
use super::super::{MovementCategoryData, MovementData, Cube, ItemCategory, ItemTier, BattleConfig, Settings, ChatConfig, FactoryConfig, ItemShopConfig};
use super::super::{MovementCategoryData, MovementData, Cube, ItemCategory, ItemTier, BattleConfig, Settings, ChatConfig, FactoryConfig, ItemShopConfig, FederationConfig};
const CUBE_CONFIG_FILENAME: &str = "config.json";
@@ -22,6 +22,8 @@ pub struct CubeConfig {
factory: FactoryConfig,
shop: ItemShopConfig,
settings: Settings,
#[serde(default = "super::super::default_federation")]
federation: FederationConfig,
}
impl CubeConfig {
@@ -623,7 +625,15 @@ impl <C: Clone + Send> super::ConfigProvider<C> for CubeConfig {
factory: self.factory.redacted_clone(),
shop: self.shop.clone(),
settings: self.settings.redacted_clone(),
federation: self.federation.clone(),
};
serde_json::to_string(&redacted_self).expect("Failed to re-serialize config.json")
}
fn federation(&self) -> Option<super::Federation> {
self.federation.enabled.then(|| super::Federation {
aliases: self.federation.aliases.clone(),
defederated: self.federation.defederated.clone(),
})
}
}

View File

@@ -2,7 +2,7 @@ mod cubes_json;
pub use cubes_json::CubeConfig;
mod traits;
pub use traits::{ConfigProvider, DevMessageProvider, ServerConfig, GarageUpgrades, GarageUpgradeIncrement, ChatSystemConfig, GameEventSequence, GameEvents, GameRotationStrategy, GameEvent, GameMap, GameVisibility, GameType, SingleplayerConfig, VehicleInfo, VehicleDescriptor, QueueChangeMode, Point, Sphere, MapConfig, LinksConfig, FakePlayer, ClientEmulator, EnergyConfig, BattleArenaResolver, PitSettings, PitWinCondition, TeamDeathMatchSettings, ShopEntriesResolver, ShopAction, ShopGain, PromoCode, MultiplayerSettings, BattleArenaCrystalParams, VehicleValidators, TeamChoosers, FactoryConfig, PlatformConfig};
pub use traits::{ConfigProvider, DevMessageProvider, ServerConfig, GarageUpgrades, GarageUpgradeIncrement, ChatSystemConfig, GameEventSequence, GameEvents, GameRotationStrategy, GameEvent, GameMap, GameVisibility, GameType, SingleplayerConfig, VehicleInfo, VehicleDescriptor, QueueChangeMode, Point, Sphere, MapConfig, LinksConfig, FakePlayer, ClientEmulator, EnergyConfig, BattleArenaResolver, PitSettings, PitWinCondition, TeamDeathMatchSettings, ShopEntriesResolver, ShopAction, ShopGain, PromoCode, MultiplayerSettings, BattleArenaCrystalParams, VehicleValidators, TeamChoosers, FactoryConfig, PlatformConfig, Federation};
pub(super) use traits::RedactedClone;
mod validation;

View File

@@ -46,6 +46,8 @@ pub trait ConfigProvider<C: Clone> {
fn garage_slot_limit(&self) -> i32;
fn team_choosers(&self) -> TeamChoosers;
fn redacted_json(&self) -> String;
/// None when federation is not enabled
fn federation(&self) -> Option<Federation>;
}
pub struct DevMessageProvider<C: Clone> {
@@ -579,3 +581,8 @@ pub struct PlatformConfig {
pub trait RedactedClone {
fn redacted_clone(&self) -> Self;
}
pub struct Federation {
pub aliases: std::collections::HashMap<String, String>,
pub defederated: Vec<String>,
}

View File

@@ -0,0 +1,33 @@
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct FederationConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_aliases")]
pub aliases: std::collections::HashMap<String, String>,
#[serde(default = "default_defederated")]
pub defederated: Vec<String>,
}
pub fn default_federation() -> FederationConfig {
FederationConfig {
enabled: false,
aliases: default_aliases(),
defederated: default_defederated(),
}
}
fn default_aliases() -> std::collections::HashMap<String, String> {
let mut alias_map = std::collections::HashMap::with_capacity(3);
alias_map.insert("rc.ngram.ca".to_owned(), "society.rc.ngram.ca".to_owned());
alias_map.insert("robocraft.online".to_owned(), "society.robocraft.online".to_owned());
alias_map.insert("robocraftgame.co.uk".to_owned(), "society.robocraftgame.co.uk".to_owned());
alias_map
}
fn default_defederated() -> Vec<String> {
vec![
"robocraftgame.com".to_owned(),
]
}

View File

@@ -53,6 +53,10 @@ pub use vehicle_validator::VehicleValidator;
pub(crate) mod conversion;
pub use conversion::{CubeConversionData, FromConversionData, ToConversionData};
mod federation;
pub use federation::FederationConfig;
pub(crate) use federation::default_federation;
const VALID_ROBOT: &[u8] = &[64,
0,
0,

View File

@@ -17,6 +17,8 @@ pub struct AccountProvider {
pub(super) intercom_http_client: std::sync::Arc<reqwest::Client>,
pub(super) secret: std::sync::Arc<Vec<u8>>,
db: std::sync::Arc<oj_rc_database::Database>,
#[allow(dead_code)]
federation: Option<crate::persist::config::Federation>,
}
impl AccountProvider {
@@ -29,6 +31,7 @@ impl AccountProvider {
log::debug!("Connecting to user database URI: {}", database_uri);
let db = oj_rc_database::Database::init(&database_uri).await
.map_err(|e| std::io::Error::new(std::io::ErrorKind::NotConnected, e))?;
let federation_conf = <crate::persist::config::ConfigImpl as ConfigProvider<()>>::federation(conf);
Ok(Self {
cubes: std::sync::Arc::new(<crate::persist::config::ConfigImpl as ConfigProvider<()>>::ids(conf)),
garage_upgrades: std::sync::Arc::new(<crate::persist::config::ConfigImpl as ConfigProvider<()>>::garage_upgrades(conf)),
@@ -42,6 +45,7 @@ impl AccountProvider {
intercom_http_client: std::sync::Arc::new(reqwest::Client::new()),
secret: std::sync::Arc::new(secret),
db: std::sync::Arc::new(db),
federation: federation_conf,
})
}

View File

@@ -65,5 +65,69 @@ impl super::CommonUser for UserData {
))
}
async fn fedi_get(&self) -> super::Federation {
match self.db.user_aux_by_user_id_and_descriptor(self.account.id, oj_rc_database::schema::user_aux::Descriptor::Federation).await {
Ok(Some(fed)) => {
match serde_json::from_str(&fed.data) {
Ok(f) => f,
Err(e) => {
log::error!("Failed to deserialize user_aux Federation for user {}: {} (fedi_get)", self.account.id, e);
super::Federation::default()
}
}
},
Ok(None) => {
// DB upgrade path; return default and also insert it into database
let default_fedi = super::Federation::default();
let data_str = serde_json::to_string_pretty(&default_fedi).expect("Failed to serialize user_aux Federation data (fedi_get)");
let model = oj_rc_database::schema::user_aux::ActiveModel {
id: oj_rc_database::sea_orm::ActiveValue::NotSet,
user_id: oj_rc_database::sea_orm::ActiveValue::Set(self.account.id),
creation_time: oj_rc_database::sea_orm::ActiveValue::Set(chrono::Utc::now().timestamp()),
descriptor: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::user_aux::Descriptor::Federation),
data: oj_rc_database::sea_orm::ActiveValue::Set(data_str),
};
if let Err(e) = self.db.insert_user_aux(vec![model]).await {
log::error!("Failed to insert user_aux Federation for user {}: {} (fedi_get)", self.account.id, e);
}
default_fedi
},
Err(e) => {
log::error!("Failed to retrieve user_aux Federation for user {}: {} (fedi_get)", self.account.id, e);
super::Federation::default()
}
}
}
async fn fedi_set(&self, fedi: super::Federation) -> bool {
let data_str = serde_json::to_string_pretty(&fedi).expect("Failed to serialize user_aux Federation data (fedi_set)");
let model = oj_rc_database::schema::user_aux::ActiveModel {
id: oj_rc_database::sea_orm::ActiveValue::NotSet,
user_id: oj_rc_database::sea_orm::ActiveValue::NotSet,
creation_time: oj_rc_database::sea_orm::ActiveValue::NotSet,
descriptor: oj_rc_database::sea_orm::ActiveValue::NotSet,
data: oj_rc_database::sea_orm::ActiveValue::Set(data_str),
};
match self.db.update_user_aux_by_user_id_and_descriptor(model.clone(), self.account.id, oj_rc_database::schema::user_aux::Descriptor::Federation).await {
Ok(Some(_fed)) => true,
Ok(None) => {
// DB upgrade path; insert it into database instead
let mut model = model;
model.user_id = oj_rc_database::sea_orm::ActiveValue::Set(self.account.id);
model.creation_time = oj_rc_database::sea_orm::ActiveValue::Set(chrono::Utc::now().timestamp());
model.descriptor = oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::user_aux::Descriptor::Federation);
match self.db.insert_user_aux(vec![model]).await {
Ok(_) => true,
Err(e) => {
log::error!("Failed to insert user_aux Federation for user {}: {} (fedi_get)", self.account.id, e);
false
}
}
},
Err(e) => {
log::error!("Failed to update user_aux Federation for user {}: {} (fedi_get)", self.account.id, e);
false
}
}
}
}

View File

@@ -0,0 +1,16 @@
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Federation {
pub enabled: bool,
pub defederated: Vec<String>,
}
impl std::default::Default for Federation {
fn default() -> Self {
Self {
enabled: true,
defederated: Vec::default(),
}
}
}

View File

@@ -28,6 +28,9 @@ mod team;
pub use team::{TeamChooser, StandardTeamChooser};
mod web;
mod federation;
pub use federation::Federation;
pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key";
pub const USERS_DIR: &str = "accounts";

View File

@@ -467,6 +467,8 @@ pub trait CommonUser: Send + Sync {
async fn db_metrics(&self) -> oj_rc_database::DatabaseMetrics;
async fn db_counters(&self) -> Vec<(&'static str, i64)>;
async fn currency(&self, ty: CurrencyType, op: CurrencyOp) -> Result<u64, polariton_server::operations::SimpleOpError>;
async fn fedi_get(&self) -> super::Federation;
async fn fedi_set(&self, fedi: super::Federation) -> bool;
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]

View File

@@ -44,4 +44,5 @@ pub enum Descriptor {
SubscribedChannels, // Vec<String>, JSON
AvatarId, // u32, u32::MAX means custom avatar
RedeemedPromoCodes, // Vec<String>, JSON
Federation, // oj_rc_Core::persist::user::Federation, JSON
}

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()
)
}
}
}