From bb287cbd076940bd09f6afb61b642370d020f91e Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 10 Apr 2020 16:55:57 -0400 Subject: Adding an admin settings page. - Fixes #620 - Adding a UserListing component. Fixes #627 --- docker/dev/docker-compose.yml | 2 +- docker/prod/docker-compose.yml | 2 +- docs/src/contributing_websocket_http_api.md | 99 +++++++++--- server/src/api/site.rs | 70 ++++++++ server/src/routes/api.rs | 2 + server/src/routes/index.rs | 1 + server/src/settings.rs | 12 ++ server/src/websocket/mod.rs | 2 + server/src/websocket/server.rs | 10 ++ ui/src/components/admin-settings.tsx | 241 ++++++++++++++++++++++++++++ ui/src/components/comment-node.tsx | 27 ++-- ui/src/components/main.tsx | 24 +-- ui/src/components/navbar.tsx | 26 +++ ui/src/components/post-listing.tsx | 23 +-- ui/src/components/private-message-form.tsx | 25 +-- ui/src/components/private-message.tsx | 1 + ui/src/components/search.tsx | 23 +-- ui/src/components/sidebar.tsx | 21 +-- ui/src/components/site-form.tsx | 7 + ui/src/components/symbols.tsx | 3 + ui/src/components/user-listing.tsx | 36 +++++ ui/src/index.tsx | 116 ++++++------- ui/src/interfaces.ts | 22 ++- ui/src/services/WebSocketService.ts | 13 ++ ui/translations/en.json | 4 + 25 files changed, 633 insertions(+), 179 deletions(-) create mode 100644 ui/src/components/admin-settings.tsx create mode 100644 ui/src/components/user-listing.tsx diff --git a/docker/dev/docker-compose.yml b/docker/dev/docker-compose.yml index a7d289b2..3c52d1e5 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 325effa6..a1b36162 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 a73a1c13..f228f94e 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 6bd90149..3720a2c4 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/routes/api.rs b/server/src/routes/api.rs index 29a360e4..36a55f96 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 c1c363c9..45ce204e 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), diff --git a/server/src/settings.rs b/server/src/settings.rs index 875323e9..216c057e 100644 --- a/server/src/settings.rs +++ b/server/src/settings.rs @@ -1,6 +1,8 @@ use config::{Config, ConfigError, Environment, File}; +use failure::Error; use serde::Deserialize; use std::env; +use std::fs; use std::net::IpAddr; static CONFIG_FILE_DEFAULTS: &str = "config/defaults.hjson"; @@ -112,4 +114,14 @@ 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)?; + Self::init()?; + Self::read_config_file() + } } diff --git a/server/src/websocket/mod.rs b/server/src/websocket/mod.rs index a1feede2..c7136423 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 831f12ee..0f2d2d26 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 00000000..56af7114 --- /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')}
+
+
+ +
+