diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 5a6c7d672..e0270d4c7 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -30,6 +30,6 @@ In the Lemmy community we strive to go the extra step to look out for each other And if someone takes issue with something you said or did, resist the urge to be defensive. Just stop doing what it was they complained about and apologize. Even if you feel you were misinterpreted or unfairly accused, chances are good there was something you could’ve communicated better — remember that it’s your responsibility to make others comfortable. Everyone wants to get along and we are all here first and foremost because we want to talk about cool technology. You will find that people will be eager to assume good intent and forgive as long as you earn their trust. -The enforcement policies listed above apply to all official Lemmy venues; including git repositories under [github.com/dessalines/lemmy](https://github.com/dessalines/lemmy) and [yerbamate.dev/dessalines/lemmy](https://yerbamate.dev/dessalines/lemmy), the [Matrix channel](https://matrix.to/#/!BZVTUuEiNmRcbFeLeI:matrix.org?via=matrix.org&via=privacytools.io&via=permaweb.io); and all instances under lemmy.ml. For other projects adopting the Rust Code of Conduct, please contact the maintainers of those projects for enforcement. If you wish to use this code of conduct for your own project, consider explicitly mentioning your moderation policy or making a copy with your own moderation policy so as to avoid confusion. +The enforcement policies listed above apply to all official Lemmy venues; including git repositories under [github.com/LemmyNet/lemmy](https://github.com/LemmyNet/lemmy) and [yerbamate.dev/dessalines/lemmy](https://yerbamate.dev/dessalines/lemmy), the [Matrix channel](https://matrix.to/#/!BZVTUuEiNmRcbFeLeI:matrix.org?via=matrix.org&via=privacytools.io&via=permaweb.io); and all instances under lemmy.ml. For other projects adopting the Rust Code of Conduct, please contact the maintainers of those projects for enforcement. If you wish to use this code of conduct for your own project, consider explicitly mentioning your moderation policy or making a copy with your own moderation policy so as to avoid confusion. Adapted from the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct), which is based on the [Node.js Policy on Trolling](http://blog.izs.me/post/30036893703/policy-on-trolling) as well as the [Contributor Covenant v1.3.0](https://www.contributor-covenant.org/version/1/3/0/). diff --git a/README.md b/README.md index 2adad59c7..723d535ff 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@
-![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/dessalines/lemmy.svg) -[![Build Status](https://travis-ci.org/dessalines/lemmy.svg?branch=master)](https://travis-ci.org/dessalines/lemmy) -[![GitHub issues](https://img.shields.io/github/issues-raw/dessalines/lemmy.svg)](https://github.com/dessalines/lemmy/issues) +![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/LemmyNet/lemmy.svg) +[![Build Status](https://travis-ci.org/LemmyNet/lemmy.svg?branch=master)](https://travis-ci.org/LemmyNet/lemmy) +[![GitHub issues](https://img.shields.io/github/issues-raw/LemmyNet/lemmy.svg)](https://github.com/LemmyNet/lemmy/issues) [![Docker Pulls](https://img.shields.io/docker/pulls/dessalines/lemmy.svg)](https://cloud.docker.com/repository/docker/dessalines/lemmy/) [![Translation status](http://weblate.yerbamate.dev/widgets/lemmy/-/lemmy/svg-badge.svg)](http://weblate.yerbamate.dev/engage/lemmy/) -[![License](https://img.shields.io/github/license/dessalines/lemmy.svg)](LICENSE) -![GitHub stars](https://img.shields.io/github/stars/dessalines/lemmy?style=social) +[![License](https://img.shields.io/github/license/LemmyNet/lemmy.svg)](LICENSE) +![GitHub stars](https://img.shields.io/github/stars/LemmyNet/lemmy?style=social)

@@ -22,11 +22,11 @@ · Documentation · - Report Bug + Report Bug · - Request Feature + Request Feature · - Releases + Releases

@@ -36,7 +36,7 @@ Front Page|Post ---|--- ![main screen](https://i.imgur.com/kZSRcRu.png)|![chat screen](https://i.imgur.com/4XghNh6.png) -[Lemmy](https://github.com/dessalines/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse). +[Lemmy](https://github.com/LemmyNet/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse). For a link aggregator, this means a user registered on one server can subscribe to forums on any other server, and can have discussions with users registered elsewhere. @@ -108,8 +108,9 @@ Each lemmy server can set its own moderation policy; appointing site-wide admins Lemmy is free, open-source software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. -- [Support on Liberapay.](https://liberapay.com/Lemmy) +- [Support on Liberapay](https://liberapay.com/Lemmy). - [Support on Patreon](https://www.patreon.com/dessalines). +- [Support on OpenCollective](https://opencollective.com/lemmy). - [List of Sponsors](https://dev.lemmy.ml/sponsors). ### Crypto @@ -132,7 +133,7 @@ If you want to help with translating, take a look at [Weblate](https://weblate.y - [Mastodon](https://mastodon.social/@LemmyDev) - [![Mastodon Follow](https://img.shields.io/mastodon/follow/810572?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@LemmyDev) - [Matrix](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org) - [![Matrix](https://img.shields.io/matrix/rust-reddit-fediverse:matrix.org.svg?label=matrix-chat)](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org) -- [GitHub](https://github.com/dessalines/lemmy) +- [GitHub](https://github.com/LemmyNet/lemmy) - [Gitea](https://yerbamate.dev/dessalines/lemmy) - [GitLab](https://gitlab.com/dessalines/lemmy) diff --git a/RELEASES.md b/RELEASES.md index 4d86f6dab..5a4c7645e 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,6 +1,6 @@ # Lemmy v0.6.0 Release (2020-01-16) -`v0.6.0` is here, and we've closed [41 issues!](https://github.com/dessalines/lemmy/milestone/15?closed=1) +`v0.6.0` is here, and we've closed [41 issues!](https://github.com/LemmyNet/lemmy/milestone/15?closed=1) This is the biggest release by far: @@ -10,7 +10,7 @@ This is the biggest release by far: - Can set a custom language. - Lemmy-wide settings to disable downvotes, and close registration. - A better documentation system, hosted in lemmy itself. -- [Huge DB performance gains](https://github.com/dessalines/lemmy/issues/411) (everthing down to < `30ms`) by using materialized views. +- [Huge DB performance gains](https://github.com/LemmyNet/lemmy/issues/411) (everthing down to < `30ms`) by using materialized views. - Fixed major issue with similar post URL and title searching. - Upgraded to Actix `2.0` - Faster comment / post voting. diff --git a/ansible/VERSION b/ansible/VERSION index 83ca525b7..25690dbe8 100644 --- a/ansible/VERSION +++ b/ansible/VERSION @@ -1 +1 @@ -v0.6.44 +v0.6.49 diff --git a/docker/dev/deploy.sh b/docker/dev/deploy.sh index db2294fab..09b21b0b7 100755 --- a/docker/dev/deploy.sh +++ b/docker/dev/deploy.sh @@ -72,5 +72,5 @@ git push origin $new_tag git push # Pushing to any ansible deploys -cd ../../ansible || exit -ansible-playbook lemmy.yml --become +cd ../../../lemmy-ansible || exit +ansible-playbook -i prod playbooks/site.yml --vault-password-file vault_pass diff --git a/docker/dev/dev_deploy.sh b/docker/dev/dev_deploy.sh deleted file mode 100755 index ef41434b3..000000000 --- a/docker/dev/dev_deploy.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh - -# Building from the dev branch for dev servers -git checkout dev - -# Rebuilding dev docker -docker-compose build -docker tag dev_lemmy:latest dessalines/lemmy:dev -docker push dessalines/lemmy:dev - -# SSH and pull it -ssh $LEMMY_USER@$LEMMY_HOST "cd ~/git/lemmy/docker/dev && docker pull dessalines/lemmy:dev && docker-compose up -d" 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/dev/test_deploy.sh b/docker/dev/test_deploy.sh new file mode 100755 index 000000000..1ae91f654 --- /dev/null +++ b/docker/dev/test_deploy.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +# Rebuilding dev docker +docker-compose build +docker tag dev_lemmy:latest dessalines/lemmy:test +docker push dessalines/lemmy:test + +# Run the playbook +pushd ../../../lemmy-ansible +ansible-playbook -i test playbooks/site.yml --vault-password-file vault_pass +popd diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index 76f5ae14d..905514653 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -12,14 +12,14 @@ services: restart: always lemmy: - image: dessalines/lemmy:v0.6.44 + image: dessalines/lemmy:v0.6.49 ports: - "127.0.0.1:8536:8536" restart: always 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/SUMMARY.md b/docs/src/SUMMARY.md index 70c423c79..bff5cbf64 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -10,6 +10,7 @@ - [Install with Ansible](administration_install_ansible.md) - [Install with Kubernetes](administration_install_kubernetes.md) - [Configuration](administration_configuration.md) + - [Backup and Restore](administration_backup_and_restore.md) - [Contributing](contributing.md) - [Docker Development](contributing_docker_development.md) - [Local Development](contributing_local_development.md) diff --git a/docs/src/about.md b/docs/src/about.md index 33ecb211a..31081f485 100644 --- a/docs/src/about.md +++ b/docs/src/about.md @@ -4,7 +4,7 @@ Front Page|Post ---|--- ![main screen](https://i.imgur.com/kZSRcRu.png)|![chat screen](https://i.imgur.com/4XghNh6.png) -[Lemmy](https://github.com/dessalines/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse). +[Lemmy](https://github.com/LemmyNet/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse). For a link aggregator, this means a user registered on one server can subscribe to forums on any other server, and can have discussions with users registered elsewhere. diff --git a/docs/src/about_goals.md b/docs/src/about_goals.md index caa6948a0..e0427481c 100644 --- a/docs/src/about_goals.md +++ b/docs/src/about_goals.md @@ -51,3 +51,4 @@ - [Activitypub implementers guide](https://socialhub.activitypub.rocks/t/draft-guide-for-new-activitypub-implementers/479) - [Data storage questions](https://socialhub.activitypub.rocks/t/data-storage-questions/579/3) - [Activitypub as it has been understood](https://flak.tedunangst.com/post/ActivityPub-as-it-has-been-understood) +- [Asonix http signatures in rust](https://git.asonix.dog/Aardwolf/http-signature-normalization) diff --git a/docs/src/administration_backup_and_restore.md b/docs/src/administration_backup_and_restore.md new file mode 100644 index 000000000..fe97cf880 --- /dev/null +++ b/docs/src/administration_backup_and_restore.md @@ -0,0 +1,44 @@ +# Backup and Restore Guide + +## Docker and Ansible + +When using docker or ansible, there should be a `volumes` folder, which contains both the database, and all the pictures. Copy this folder to the new instance to restore your data. + +### Incremental Database backup + +To incrementally backup the DB to an `.sql` file, you can run: + +```bash +docker exec -t FOLDERNAME_postgres_1 pg_dumpall -c -U lemmy > lemmy_dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql +``` +### A Sample backup script + +```bash +#!/bin/sh +# DB Backup +ssh MY_USER@MY_IP "docker exec -t FOLDERNAME_postgres_1 pg_dumpall -c -U lemmy" > ~/BACKUP_LOCATION/INSTANCE_NAME_dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql + +# Volumes folder Backup +rsync -avP -zz --rsync-path="sudo rsync" MY_USER@MY_IP:/LEMMY_LOCATION/volumes ~/BACKUP_LOCATION/FOLDERNAME +``` + +### Restoring the DB + +If you need to restore from a `pg_dumpall` file, you need to first clear out your existing database + +```bash +# Drop the existing DB +docker exec -i FOLDERNAME_postgres_1 psql -U lemmy -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" + +# Restore from the .sql backup +cat db_dump.sql | docker exec -i FOLDERNAME_postgres_1 psql -U lemmy # restores the db + +# This also might be necessary when doing a db import with a different password. +docker exec -i FOLDERNAME_postgres_1 psql -U lemmy -c "alter user lemmy with password 'bleh'" +``` + +## More resources + +- https://stackoverflow.com/questions/24718706/backup-restore-a-dockerized-postgresql-database + + diff --git a/docs/src/administration_install_ansible.md b/docs/src/administration_install_ansible.md index bf5e67493..77d901b36 100644 --- a/docs/src/administration_install_ansible.md +++ b/docs/src/administration_install_ansible.md @@ -7,7 +7,7 @@ First, you need to [install Ansible on your local computer](https://docs.ansible Then run the following commands on your local computer: ```bash -git clone https://github.com/dessalines/lemmy.git +git clone https://github.com/LemmyNet/lemmy.git cd lemmy/ansible/ cp inventory.example inventory nano inventory # enter your server, domain, contact email @@ -19,4 +19,4 @@ To update to a new version, just run the following in your local Lemmy repo: git pull origin master cd ansible ansible-playbook lemmy.yml --become -``` \ No newline at end of file +``` diff --git a/docs/src/contributing.md b/docs/src/contributing.md index 9a01ad5d9..4eabd6fc7 100644 --- a/docs/src/contributing.md +++ b/docs/src/contributing.md @@ -4,13 +4,14 @@ Information about contributing to Lemmy, whether it is translating, testing, des ## Issue tracking / Repositories -- [GitHub (for issues)](https://github.com/dessalines/lemmy) +- [GitHub (for issues)](https://github.com/LemmyNet/lemmy) - [Gitea](https://yerbamate.dev/dessalines/lemmy) - [GitLab](https://gitlab.com/dessalines/lemmy) ## Translating -Go [here](https://github.com/dessalines/lemmy#translations) for translation instructions. +Check out [Lemmy's Weblate](https://weblate.yerbamate.dev/projects/lemmy/) for translations. + ## Architecture diff --git a/docs/src/contributing_docker_development.md b/docs/src/contributing_docker_development.md index d5ab58293..092398219 100644 --- a/docs/src/contributing_docker_development.md +++ b/docs/src/contributing_docker_development.md @@ -3,7 +3,7 @@ ## Running ```bash -git clone https://github.com/dessalines/lemmy +git clone https://github.com/LemmyNet/lemmy cd lemmy/docker/dev ./docker_update.sh # This builds and runs it, updating for your changes ``` diff --git a/docs/src/contributing_local_development.md b/docs/src/contributing_local_development.md index 175b000c7..e823c9d1b 100644 --- a/docs/src/contributing_local_development.md +++ b/docs/src/contributing_local_development.md @@ -22,7 +22,7 @@ export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy #### Running ```bash -git clone https://github.com/dessalines/lemmy +git clone https://github.com/LemmyNet/lemmy cd lemmy ./install.sh # For live coding, where both the front and back end, automagically reload on any save, do: 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 ad45e8d14..4202fea06 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; @@ -514,3 +530,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/lib.rs b/server/src/lib.rs index e45311eec..2c78cfc26 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -115,7 +115,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)) @@ -130,7 +130,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 59dc2cb75..88d62eb99 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -57,6 +57,7 @@ async fn main() -> Result<(), Error> { // Create Http server with websocket support Ok( HttpServer::new(move || { + let settings = Settings::get(); App::new() .wrap(middleware::Logger::default()) .data(pool.clone()) 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 29d5966ba..8c3cd6a12 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: Federation, } -#[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, @@ -65,12 +68,10 @@ pub struct Federation { } 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 { @@ -96,8 +97,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, @@ -119,4 +120,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/version.rs b/server/src/version.rs index 1491a0a2c..f02e9ae5f 100644 --- a/server/src/version.rs +++ b/server/src/version.rs @@ -1 +1 @@ -pub const VERSION: &str = "v0.6.44"; +pub const VERSION: &str = "v0.6.49"; 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 f205c91e6..faa8041ce 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/assets/css/main.css b/ui/assets/css/main.css index 1c8206e35..bf249e5bf 100644 --- a/ui/assets/css/main.css +++ b/ui/assets/css/main.css @@ -156,7 +156,7 @@ hr { } .emoji { - height: 1.2em !important; + max-height: 1.2em !important; } .text-wrap-truncate { diff --git a/ui/package.json b/ui/package.json index 7d946614c..d2eb1de9e 100644 --- a/ui/package.json +++ b/ui/package.json @@ -14,19 +14,20 @@ }, "keywords": [], "dependencies": { + "@joeattardi/emoji-button": "^2.12.1", "@types/autosize": "^3.0.6", - "@types/js-cookie": "^2.2.5", + "@types/js-cookie": "^2.2.6", "@types/jwt-decode": "^2.2.1", "@types/markdown-it": "^0.0.9", "@types/markdown-it-container": "^2.0.2", - "@types/node": "^13.9.2", + "@types/node": "^13.11.1", "autosize": "^4.0.2", "bootswatch": "^4.3.1", - "classcat": "^1.1.3", + "classcat": "^4.0.2", "dotenv": "^8.2.0", "emoji-short-name": "^1.0.0", - "husky": "^4.2.3", - "i18next": "^19.3.3", + "husky": "^4.2.5", + "i18next": "^19.4.1", "inferno": "^7.4.2", "inferno-i18next": "nimbusec-oss/inferno-i18next", "inferno-router": "^7.4.2", @@ -37,26 +38,26 @@ "markdown-it-emoji": "^1.4.0", "mobius1-selectr": "^2.4.13", "moment": "^2.24.0", - "prettier": "^1.18.2", + "prettier": "^2.0.4", "reconnecting-websocket": "^4.4.0", - "rxjs": "^6.4.0", - "terser": "^4.6.7", - "tippy.js": "^6.1.0", + "rxjs": "^6.5.5", + "terser": "^4.6.11", + "tippy.js": "^6.1.1", "toastify-js": "^1.7.0", - "tributejs": "^5.1.2", + "tributejs": "^5.1.3", "twemoji": "^12.1.2", "ws": "^7.2.3" }, "devDependencies": { "eslint": "^6.5.1", "eslint-plugin-inferno": "^7.14.3", - "eslint-plugin-jane": "^7.2.0", + "eslint-plugin-jane": "^7.2.1", "fuse-box": "^3.1.3", - "lint-staged": "^10.0.8", - "sortpack": "^2.1.2", - "ts-node": "^8.7.0", - "ts-transform-classcat": "^0.0.2", - "ts-transform-inferno": "^4.0.2", + "lint-staged": "^10.1.3", + "sortpack": "^2.1.4", + "ts-node": "^8.8.2", + "ts-transform-classcat": "^1.0.0", + "ts-transform-inferno": "^4.0.3", "typescript": "^3.8.3" }, "engines": { 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')}
+
+
+ +
+