diff --git a/.gitignore b/.gitignore index 7aeaa31b7..9f7fa1e3c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ build/ .idea/ ui/src/translations docker/dev/volumes -docker/federation/volumes +docker/federation-test/volumes diff --git a/docker/dev/docker-compose.yml b/docker/dev/docker-compose.yml index a7d289b21..3c52d1e54 100644 --- a/docker/dev/docker-compose.yml +++ b/docker/dev/docker-compose.yml @@ -21,7 +21,7 @@ services: environment: - RUST_LOG=debug volumes: - - ../lemmy.hjson:/config/config.hjson:ro + - ../lemmy.hjson:/config/config.hjson depends_on: - postgres - pictshare diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index 727b7307b..905514653 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -19,7 +19,7 @@ services: environment: - RUST_LOG=error volumes: - - ./lemmy.hjson:/config/config.hjson:ro + - ./lemmy.hjson:/config/config.hjson depends_on: - postgres - pictshare diff --git a/docs/src/contributing_websocket_http_api.md b/docs/src/contributing_websocket_http_api.md index a73a1c133..f228f94e0 100644 --- a/docs/src/contributing_websocket_http_api.md +++ b/docs/src/contributing_websocket_http_api.md @@ -92,85 +92,93 @@ - [Request](#request-17) - [Response](#response-17) - [HTTP](#http-18) - * [Community](#community) - + [Get Community](#get-community) + + [Get Site Config](#get-site-config) - [Request](#request-18) - [Response](#response-18) - [HTTP](#http-19) - + [Create Community](#create-community) + + [Save Site Config](#save-site-config) - [Request](#request-19) - [Response](#response-19) - [HTTP](#http-20) - + [List Communities](#list-communities) + * [Community](#community) + + [Get Community](#get-community) - [Request](#request-20) - [Response](#response-20) - [HTTP](#http-21) - + [Ban from Community](#ban-from-community) + + [Create Community](#create-community) - [Request](#request-21) - [Response](#response-21) - [HTTP](#http-22) - + [Add Mod to Community](#add-mod-to-community) + + [List Communities](#list-communities) - [Request](#request-22) - [Response](#response-22) - [HTTP](#http-23) - + [Edit Community](#edit-community) + + [Ban from Community](#ban-from-community) - [Request](#request-23) - [Response](#response-23) - [HTTP](#http-24) - + [Follow Community](#follow-community) + + [Add Mod to Community](#add-mod-to-community) - [Request](#request-24) - [Response](#response-24) - [HTTP](#http-25) - + [Get Followed Communities](#get-followed-communities) + + [Edit Community](#edit-community) - [Request](#request-25) - [Response](#response-25) - [HTTP](#http-26) - + [Transfer Community](#transfer-community) + + [Follow Community](#follow-community) - [Request](#request-26) - [Response](#response-26) - [HTTP](#http-27) - * [Post](#post) - + [Create Post](#create-post) + + [Get Followed Communities](#get-followed-communities) - [Request](#request-27) - [Response](#response-27) - [HTTP](#http-28) - + [Get Post](#get-post) + + [Transfer Community](#transfer-community) - [Request](#request-28) - [Response](#response-28) - [HTTP](#http-29) - + [Get Posts](#get-posts) + * [Post](#post) + + [Create Post](#create-post) - [Request](#request-29) - [Response](#response-29) - [HTTP](#http-30) - + [Create Post Like](#create-post-like) + + [Get Post](#get-post) - [Request](#request-30) - [Response](#response-30) - [HTTP](#http-31) - + [Edit Post](#edit-post) + + [Get Posts](#get-posts) - [Request](#request-31) - [Response](#response-31) - [HTTP](#http-32) - + [Save Post](#save-post) + + [Create Post Like](#create-post-like) - [Request](#request-32) - [Response](#response-32) - [HTTP](#http-33) - * [Comment](#comment) - + [Create Comment](#create-comment) + + [Edit Post](#edit-post) - [Request](#request-33) - [Response](#response-33) - [HTTP](#http-34) - + [Edit Comment](#edit-comment) + + [Save Post](#save-post) - [Request](#request-34) - [Response](#response-34) - [HTTP](#http-35) - + [Save Comment](#save-comment) + * [Comment](#comment) + + [Create Comment](#create-comment) - [Request](#request-35) - [Response](#response-35) - [HTTP](#http-36) - + [Create Comment Like](#create-comment-like) + + [Edit Comment](#edit-comment) - [Request](#request-36) - [Response](#response-36) - [HTTP](#http-37) + + [Save Comment](#save-comment) + - [Request](#request-37) + - [Response](#response-37) + - [HTTP](#http-38) + + [Create Comment Like](#create-comment-like) + - [Request](#request-38) + - [Response](#response-38) + - [HTTP](#http-39) * [RSS / Atom feeds](#rss--atom-feeds) + [All](#all) + [Community](#community-1) @@ -779,6 +787,53 @@ Search types are `All, Comments, Posts, Communities, Users, Url` `POST /site/transfer` +#### Get Site Config +##### Request +```rust +{ + op: "GetSiteConfig", + data: { + auth: String + } +} +``` +##### Response +```rust +{ + op: "GetSiteConfig", + data: { + config_hjson: String, + } +} +``` +##### HTTP + +`GET /site/config` + +#### Save Site Config +##### Request +```rust +{ + op: "SaveSiteConfig", + data: { + config_hjson: String, + auth: String + } +} +``` +##### Response +```rust +{ + op: "SaveSiteConfig", + data: { + config_hjson: String, + } +} +``` +##### HTTP + +`PUT /site/config` + ### Community #### Get Community ##### Request diff --git a/server/src/api/site.rs b/server/src/api/site.rs index 6bd90149b..3720a2c4c 100644 --- a/server/src/api/site.rs +++ b/server/src/api/site.rs @@ -97,6 +97,22 @@ pub struct TransferSite { auth: String, } +#[derive(Serialize, Deserialize)] +pub struct GetSiteConfig { + auth: String, +} + +#[derive(Serialize, Deserialize)] +pub struct GetSiteConfigResponse { + config_hjson: String, +} + +#[derive(Serialize, Deserialize)] +pub struct SaveSiteConfig { + config_hjson: String, + auth: String, +} + impl Perform for Oper { fn perform(&self, conn: &PgConnection) -> Result { let _data: &ListCategories = &self.data; @@ -510,3 +526,57 @@ impl Perform for Oper { }) } } + +impl Perform for Oper { + fn perform(&self, conn: &PgConnection) -> Result { + let data: &GetSiteConfig = &self.data; + + let claims = match Claims::decode(&data.auth) { + Ok(claims) => claims.claims, + Err(_e) => return Err(APIError::err("not_logged_in").into()), + }; + + let user_id = claims.id; + + // Only let admins read this + let admins = UserView::admins(&conn)?; + let admin_ids: Vec = admins.into_iter().map(|m| m.id).collect(); + + if !admin_ids.contains(&user_id) { + return Err(APIError::err("not_an_admin").into()); + } + + let config_hjson = Settings::read_config_file()?; + + Ok(GetSiteConfigResponse { config_hjson }) + } +} + +impl Perform for Oper { + fn perform(&self, conn: &PgConnection) -> Result { + let data: &SaveSiteConfig = &self.data; + + let claims = match Claims::decode(&data.auth) { + Ok(claims) => claims.claims, + Err(_e) => return Err(APIError::err("not_logged_in").into()), + }; + + let user_id = claims.id; + + // Only let admins read this + let admins = UserView::admins(&conn)?; + let admin_ids: Vec = admins.into_iter().map(|m| m.id).collect(); + + if !admin_ids.contains(&user_id) { + return Err(APIError::err("not_an_admin").into()); + } + + // Make sure docker doesn't have :ro at the end of the volume, so its not a read-only filesystem + let config_hjson = match Settings::save_config_file(&data.config_hjson) { + Ok(config_hjson) => config_hjson, + Err(_e) => return Err(APIError::err("couldnt_update_site").into()), + }; + + Ok(GetSiteConfigResponse { config_hjson }) + } +} diff --git a/server/src/api/user.rs b/server/src/api/user.rs index 056a2a846..40e099694 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -253,7 +253,7 @@ impl Perform for Oper { // Register the new user let user_form = UserForm { name: data.username.to_owned(), - fedi_name: Settings::get().hostname.to_owned(), + fedi_name: Settings::get().hostname, email: data.email.to_owned(), matrix_user_id: None, avatar: None, diff --git a/server/src/lib.rs b/server/src/lib.rs index 8257dab9a..9bbfe251a 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -112,7 +112,7 @@ pub fn send_email( to_username: &str, html: &str, ) -> Result<(), String> { - let email_config = Settings::get().email.as_ref().ok_or("no_email_setup")?; + let email_config = Settings::get().email.ok_or("no_email_setup")?; let email = Email::builder() .to((to_email, to_username)) @@ -127,7 +127,7 @@ pub fn send_email( } else { SmtpClient::new(&email_config.smtp_server, ClientSecurity::None).unwrap() } - .hello_name(ClientId::Domain(Settings::get().hostname.to_owned())) + .hello_name(ClientId::Domain(Settings::get().hostname)) .smtp_utf8(true) .authentication_mechanism(Mechanism::Plain) .connection_reuse(ConnectionReuseParameters::ReuseUnlimited); diff --git a/server/src/main.rs b/server/src/main.rs index 601c2e0dc..f38875275 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -39,6 +39,7 @@ async fn main() -> io::Result<()> { // Create Http server with websocket support HttpServer::new(move || { + let settings = Settings::get(); App::new() .wrap(middleware::Logger::default()) .data(pool.clone()) @@ -58,7 +59,7 @@ async fn main() -> io::Result<()> { )) .service(actix_files::Files::new( "/docs", - settings.front_end_dir.to_owned() + "/documentation", + settings.front_end_dir + "/documentation", )) }) .bind((settings.bind, settings.port))? diff --git a/server/src/routes/api.rs b/server/src/routes/api.rs index 29a360e4e..36a55f960 100644 --- a/server/src/routes/api.rs +++ b/server/src/routes/api.rs @@ -52,6 +52,8 @@ pub fn config(cfg: &mut web::ServiceConfig) { .route("/api/v1/site", web::post().to(route_post::)) .route("/api/v1/site", web::put().to(route_post::)) .route("/api/v1/site/transfer", web::post().to(route_post::)) + .route("/api/v1/site/config", web::get().to(route_get::)) + .route("/api/v1/site/config", web::put().to(route_post::)) .route("/api/v1/admin/add", web::post().to(route_post::)) .route("/api/v1/user/ban", web::post().to(route_post::)) // User account actions diff --git a/server/src/routes/index.rs b/server/src/routes/index.rs index c1c363c98..2e192df41 100644 --- a/server/src/routes/index.rs +++ b/server/src/routes/index.rs @@ -33,6 +33,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { .route("/modlog/community/{community_id}", web::get().to(index)) .route("/modlog", web::get().to(index)) .route("/setup", web::get().to(index)) + .route("/admin", web::get().to(index)) .route( "/search/q/{q}/type/{type}/sort/{sort}/page/{page}", web::get().to(index), @@ -44,6 +45,6 @@ pub fn config(cfg: &mut web::ServiceConfig) { async fn index() -> Result { Ok(NamedFile::open( - Settings::get().front_end_dir.to_owned() + "/index.html", + Settings::get().front_end_dir + "/index.html", )?) } diff --git a/server/src/settings.rs b/server/src/settings.rs index 875323e96..6e5667cb2 100644 --- a/server/src/settings.rs +++ b/server/src/settings.rs @@ -1,12 +1,15 @@ use config::{Config, ConfigError, Environment, File}; +use failure::Error; use serde::Deserialize; use std::env; +use std::fs; use std::net::IpAddr; +use std::sync::RwLock; static CONFIG_FILE_DEFAULTS: &str = "config/defaults.hjson"; static CONFIG_FILE: &str = "config/config.hjson"; -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] pub struct Settings { pub setup: Option, pub database: Database, @@ -20,7 +23,7 @@ pub struct Settings { pub federation_enabled: bool, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] pub struct Setup { pub admin_username: String, pub admin_password: String, @@ -28,7 +31,7 @@ pub struct Setup { pub site_name: String, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] pub struct RateLimitConfig { pub message: i32, pub message_per_second: i32, @@ -38,7 +41,7 @@ pub struct RateLimitConfig { pub register_per_second: i32, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] pub struct EmailConfig { pub smtp_server: String, pub smtp_login: Option, @@ -47,7 +50,7 @@ pub struct EmailConfig { pub use_tls: bool, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] pub struct Database { pub user: String, pub password: String, @@ -58,12 +61,10 @@ pub struct Database { } lazy_static! { - static ref SETTINGS: Settings = { - match Settings::init() { - Ok(c) => c, - Err(e) => panic!("{}", e), - } - }; + static ref SETTINGS: RwLock = RwLock::new(match Settings::init() { + Ok(c) => c, + Err(e) => panic!("{}", e), + }); } impl Settings { @@ -89,8 +90,8 @@ impl Settings { } /// Returns the config as a struct. - pub fn get() -> &'static Self { - &SETTINGS + pub fn get() -> Self { + SETTINGS.read().unwrap().to_owned() } /// Returns the postgres connection url. If LEMMY_DATABASE_URL is set, that is used, @@ -112,4 +113,22 @@ impl Settings { pub fn api_endpoint(&self) -> String { format!("{}/api/v1", self.hostname) } + + pub fn read_config_file() -> Result { + Ok(fs::read_to_string(CONFIG_FILE)?) + } + + pub fn save_config_file(data: &str) -> Result { + fs::write(CONFIG_FILE, data)?; + + // Reload the new settings + // From https://stackoverflow.com/questions/29654927/how-do-i-assign-a-string-to-a-mutable-static-variable/47181804#47181804 + let mut new_settings = SETTINGS.write().unwrap(); + *new_settings = match Settings::init() { + Ok(c) => c, + Err(e) => panic!("{}", e), + }; + + Self::read_config_file() + } } diff --git a/server/src/websocket/mod.rs b/server/src/websocket/mod.rs index a1feede25..c7136423c 100644 --- a/server/src/websocket/mod.rs +++ b/server/src/websocket/mod.rs @@ -46,4 +46,6 @@ pub enum UserOperation { GetPrivateMessages, UserJoin, GetComments, + GetSiteConfig, + SaveSiteConfig, } diff --git a/server/src/websocket/server.rs b/server/src/websocket/server.rs index 831f12ee1..0f2d2d26f 100644 --- a/server/src/websocket/server.rs +++ b/server/src/websocket/server.rs @@ -708,6 +708,16 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result { + let get_site_config: GetSiteConfig = serde_json::from_str(data)?; + let res = Oper::new(get_site_config).perform(&conn)?; + to_json_string(&user_operation, &res) + } + UserOperation::SaveSiteConfig => { + let save_site_config: SaveSiteConfig = serde_json::from_str(data)?; + let res = Oper::new(save_site_config).perform(&conn)?; + to_json_string(&user_operation, &res) + } UserOperation::Search => { do_user_operation::(user_operation, data, &conn) } diff --git a/ui/src/components/admin-settings.tsx b/ui/src/components/admin-settings.tsx new file mode 100644 index 000000000..56af71149 --- /dev/null +++ b/ui/src/components/admin-settings.tsx @@ -0,0 +1,241 @@ +import { Component, linkEvent } from 'inferno'; +import { Subscription } from 'rxjs'; +import { retryWhen, delay, take } from 'rxjs/operators'; +import { + UserOperation, + SiteResponse, + GetSiteResponse, + SiteConfigForm, + GetSiteConfigResponse, + WebSocketJsonResponse, +} from '../interfaces'; +import { WebSocketService } from '../services'; +import { wsJsonToRes, capitalizeFirstLetter, toast, randomStr } from '../utils'; +import autosize from 'autosize'; +import { SiteForm } from './site-form'; +import { UserListing } from './user-listing'; +import { i18n } from '../i18next'; + +interface AdminSettingsState { + siteRes: GetSiteResponse; + siteConfigRes: GetSiteConfigResponse; + siteConfigForm: SiteConfigForm; + loading: boolean; + siteConfigLoading: boolean; +} + +export class AdminSettings extends Component { + private siteConfigTextAreaId = `site-config-${randomStr()}`; + private subscription: Subscription; + private emptyState: AdminSettingsState = { + siteRes: { + site: { + id: null, + name: null, + creator_id: null, + creator_name: null, + published: null, + number_of_users: null, + number_of_posts: null, + number_of_comments: null, + number_of_communities: null, + enable_downvotes: null, + open_registration: null, + enable_nsfw: null, + }, + admins: [], + banned: [], + online: null, + }, + siteConfigForm: { + config_hjson: null, + auth: null, + }, + siteConfigRes: { + config_hjson: null, + }, + loading: true, + siteConfigLoading: null, + }; + + constructor(props: any, context: any) { + super(props, context); + + this.state = this.emptyState; + + this.subscription = WebSocketService.Instance.subject + .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) + .subscribe( + msg => this.parseMessage(msg), + err => console.error(err), + () => console.log('complete') + ); + + WebSocketService.Instance.getSite(); + WebSocketService.Instance.getSiteConfig(); + } + + componentWillUnmount() { + this.subscription.unsubscribe(); + } + + render() { + return ( +
+ {this.state.loading ? ( +
+ + + +
+ ) : ( +
+
+ + {this.admins()} + {this.bannedUsers()} +
+
{this.adminSettings()}
+
+ )} +
+ ); + } + + admins() { + return ( + <> +
{capitalizeFirstLetter(i18n.t('admins'))}
+
    + {this.state.siteRes.admins.map(admin => ( +
  • + +
  • + ))} +
+ + ); + } + + bannedUsers() { + return ( + <> +
{i18n.t('banned_users')}
+
    + {this.state.siteRes.banned.map(banned => ( +
  • + +
  • + ))} +
+ + ); + } + + adminSettings() { + return ( +
+
{i18n.t('admin_settings')}
+
+
+ +
+