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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -98,6 +98,9 @@
|
||||
background-color: lightgrey;
|
||||
font-size: 20px;
|
||||
}
|
||||
input[type=submit] {
|
||||
cursor: pointer;
|
||||
}
|
||||
.input-required-hint::after {
|
||||
color: red;
|
||||
/*content: "*required";*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
90
assets/templates/rc_society/user_federation.html.hbs
Normal file
90
assets/templates/rc_society/user_federation.html.hbs
Normal 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>
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
33
rc_core/src/persist/federation.rs
Normal file
33
rc_core/src/persist/federation.rs
Normal 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(),
|
||||
]
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
rc_core/src/persist/user/federation.rs
Normal file
16
rc_core/src/persist/user/federation.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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};
|
||||
|
||||
143
rc_society/src/web/user_federation.rs
Normal file
143
rc_society/src/web/user_federation.rs
Normal 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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user