diff --git a/app/controllers/api/v1/groups/accounts_controller.rb b/app/controllers/api/v1/groups/accounts_controller.rb index 9836c200..0ca8795d 100644 --- a/app/controllers/api/v1/groups/accounts_controller.rb +++ b/app/controllers/api/v1/groups/accounts_controller.rb @@ -19,10 +19,18 @@ class Api::V1::Groups::AccountsController < Api::BaseController def create authorize @group, :join? - @group.accounts << current_account + if !@group.password.nil? + render json: { error: true, message: 'Unable to join group. Incorrect password.' }, status: 422 + end - if current_user.allows_group_in_home_feed? - current_user.force_regeneration! + if @group.is_private + @group.join_requests << current_account + else + @group.accounts << current_account + + if current_user.allows_group_in_home_feed? + current_user.force_regeneration! + end end render json: @group, serializer: REST::GroupRelationshipSerializer, relationships: relationships diff --git a/app/controllers/api/v1/groups/password_controller.rb b/app/controllers/api/v1/groups/password_controller.rb new file mode 100644 index 00000000..d0804077 --- /dev/null +++ b/app/controllers/api/v1/groups/password_controller.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class Api::V1::Groups::PasswordController < Api::BaseController + + include Authorization + + before_action :require_user! + before_action :set_group + + respond_to :json + + def create + authorize @group, :join? + + if params[:password] == @group.password + if @group.is_private + @group.join_requests << current_account + render json: @group, serializer: REST::GroupRelationshipSerializer, relationships: relationships + else + @group.accounts << current_account + render json: @group, serializer: REST::GroupRelationshipSerializer, relationships: relationships + end + else + render json: { error: true, message: 'Invalid group password' }, status: 403 + end + end + + private + + def set_group + @group = Group.find(params[:group_id]) + end + + def relationships + GroupRelationshipsPresenter.new([@group.id], current_user.account_id) + end + +end diff --git a/app/javascript/gabsocial/actions/group_editor.js b/app/javascript/gabsocial/actions/group_editor.js index ab4d306b..2e86e6ca 100644 --- a/app/javascript/gabsocial/actions/group_editor.js +++ b/app/javascript/gabsocial/actions/group_editor.js @@ -10,6 +10,7 @@ export const GROUP_UPDATE_SUCCESS = 'GROUP_UPDATE_SUCCESS' export const GROUP_UPDATE_FAIL = 'GROUP_UPDATE_FAIL' export const GROUP_EDITOR_TITLE_CHANGE = 'GROUP_EDITOR_TITLE_CHANGE' +export const GROUP_EDITOR_PASSWORD_CHANGE = 'GROUP_EDITOR_PASSWORD_CHANGE' export const GROUP_EDITOR_DESCRIPTION_CHANGE = 'GROUP_EDITOR_DESCRIPTION_CHANGE' export const GROUP_EDITOR_COVER_IMAGE_CHANGE = 'GROUP_EDITOR_COVER_IMAGE_CHANGE' export const GROUP_EDITOR_ID_CHANGE = 'GROUP_EDITOR_ID_CHANGE' @@ -33,10 +34,12 @@ export const submit = (routerHistory) => (dispatch, getState) => { const category = getState().getIn(['group_editor', 'category']) const isPrivate = getState().getIn(['group_editor', 'isPrivate']) const isVisible = getState().getIn(['group_editor', 'isVisible']) - const slug = getState().getIn(['group_editor', 'id']) + const slug = getState().getIn(['group_editor', 'id'], null) + const password = getState().getIn(['group_editor', 'password'], null) const options = { title, + password, description, coverImage, tags, @@ -65,6 +68,7 @@ const create = (options, routerHistory) => (dispatch, getState) => { formData.append('group_category_id', options.category) formData.append('is_private', options.isPrivate) formData.append('is_visible', options.isVisible) + formData.append('password', options.password) if (options.coverImage !== null) { formData.append('cover_image', options.coverImage) @@ -108,8 +112,11 @@ const update = (groupId, options, routerHistory) => (dispatch, getState) => { formData.append('group_category_id', options.category) formData.append('is_private', options.isPrivate) formData.append('is_visible', options.isVisible) - formData.append('slug', options.slug) + formData.append('password', options.password) + if (!!options.slug) { + formData.append('slug', options.slug) + } if (options.coverImage !== null) { formData.append('cover_image', options.coverImage) } @@ -153,6 +160,11 @@ export const changeGroupTitle = (title) => ({ title, }) +export const changeGroupPassword = (password) => ({ + type: GROUP_EDITOR_PASSWORD_CHANGE, + password, +}) + export const changeGroupDescription = (description) => ({ type: GROUP_EDITOR_DESCRIPTION_CHANGE, description, diff --git a/app/javascript/gabsocial/actions/groups.js b/app/javascript/gabsocial/actions/groups.js index a99111a7..63ffdc39 100644 --- a/app/javascript/gabsocial/actions/groups.js +++ b/app/javascript/gabsocial/actions/groups.js @@ -77,6 +77,11 @@ export const GROUP_UPDATE_ROLE_REQUEST = 'GROUP_UPDATE_ROLE_REQUEST'; export const GROUP_UPDATE_ROLE_SUCCESS = 'GROUP_UPDATE_ROLE_SUCCESS'; export const GROUP_UPDATE_ROLE_FAIL = 'GROUP_UPDATE_ROLE_FAIL'; +export const GROUP_CHECK_PASSWORD_RESET = 'GROUP_CHECK_PASSWORD_RESET'; +export const GROUP_CHECK_PASSWORD_REQUEST = 'GROUP_CHECK_PASSWORD_REQUEST'; +export const GROUP_CHECK_PASSWORD_SUCCESS = 'GROUP_CHECK_PASSWORD_SUCCESS'; +export const GROUP_CHECK_PASSWORD_FAIL = 'GROUP_CHECK_PASSWORD_FAIL'; + export const GROUP_PIN_STATUS_REQUEST = 'GROUP_PIN_STATUS_REQUEST' export const GROUP_PIN_STATUS_SUCCESS = 'GROUP_PIN_STATUS_SUCCESS' export const GROUP_PIN_STATUS_FAIL = 'GROUP_PIN_STATUS_FAIL' @@ -609,6 +614,45 @@ export function updateRoleFail(groupId, id, error) { }; }; +export function checkGroupPassword(groupId, password) { + return (dispatch, getState) => { + if (!me) return + + dispatch(checkGroupPasswordRequest()) + + api(getState).post(`/api/v1/groups/${groupId}/password`, { password }).then((response) => { + dispatch(joinGroupSuccess(response.data)) + dispatch(checkGroupPasswordSuccess()) + }).catch(error => { + dispatch(checkGroupPasswordFail(error)) + }) + } +} + +export function checkGroupPasswordReset() { + return { + type: GROUP_CHECK_PASSWORD_RESET, + } +} + +export function checkGroupPasswordRequest() { + return { + type: GROUP_CHECK_PASSWORD_REQUEST, + } +} + +export function checkGroupPasswordSuccess() { + return { + type: GROUP_CHECK_PASSWORD_SUCCESS, + } +} + +export function checkGroupPasswordFail(error) { + return { + type: GROUP_CHECK_PASSWORD_FAIL, + error, + } +} export function fetchJoinRequests(id) { return (dispatch, getState) => { diff --git a/app/javascript/gabsocial/components/modal/group_password_modal.js b/app/javascript/gabsocial/components/modal/group_password_modal.js new file mode 100644 index 00000000..8ecb3435 --- /dev/null +++ b/app/javascript/gabsocial/components/modal/group_password_modal.js @@ -0,0 +1,157 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import ImmutablePureComponent from 'react-immutable-pure-component' +import ImmutablePropTypes from 'react-immutable-proptypes' +import { defineMessages, injectIntl } from 'react-intl' +import { + joinGroup, + checkGroupPassword, + checkGroupPasswordReset, +} from '../../actions/groups' +import ModalLayout from './modal_layout' +import Button from '../button' +import Input from '../input' +import Text from '../text' + +class GroupPasswordModal extends ImmutablePureComponent { + + state = { + text: '', + isError: false, + } + + componentDidMount() { + const { url } = this.props + this.props.onCheckGroupPasswordReset() + } + + componentDidUpdate(prevProps) { + if (this.props.group !== prevProps.group) { + this.props.onCheckGroupPasswordReset() + } + if (this.props.passwordCheckIsError && prevProps.passwordCheckIsLoading) { + this.setState({ isError: true }) + } + if (this.props.passwordCheckIsSuccess) { + this.props.onClose() + } + } + + componentWillUnmount() { + this.props.onCheckGroupPasswordReset() + } + + handlePasswordChange = (value) => { + this.setState({ + text: value, + isError: false, + }) + } + + handleOnClick = () => { + this.props.onCheckGroupPassword(this.props.group.get('id'), this.state.text) + } + + render() { + const { + intl, + group, + onClose, + passwordCheckIsLoading, + passwordCheckIsError, + passwordCheckIsSuccess, + } = this.props + const { text, isError } = this.state + + if (!group) { + //loading + return
+ } + + const hasPassword = group.get('has_password') + const isPrivate = group.get('is_private') + + const instructions = isPrivate ? 'Enter the group password and then your join request will be sent to the group admin.' : 'Enter the group password to join the group.' + + return ( + +
+
+ { + isError && + There was an error submitting the form. + } + + + + {instructions} + +
+ + + +
+
+ ) + } + +} + +const messages = defineMessages({ + title: { id: 'group.password_required', defaultMessage: 'Group password required' }, + submit: { id: 'report.submit', defaultMessage: 'Submit' }, +}) + +const mapStateToProps = (state) => ({ + passwordCheckIsLoading: state.getIn(['group_lists', 'passwordCheck', 'isLoading'], false), + passwordCheckIsError: state.getIn(['group_lists', 'passwordCheck', 'isError'], false), + passwordCheckIsSuccess: state.getIn(['group_lists', 'passwordCheck', 'isSuccess'], false), +}) + +const mapDispatchToProps = (dispatch) => ({ + onCheckGroupPassword(groupId, password) { + dispatch(checkGroupPassword(groupId, password)) + }, + onCheckGroupPasswordReset() { + dispatch(checkGroupPasswordReset()) + }, + onJoinGroup(groupId) { + dispatch(joinGroup(groupId)) + }, +}) + +GroupPasswordModal.propTypes = { + group: ImmutablePropTypes.map, + intl: PropTypes.object.isRequired, + onClose: PropTypes.func.isRequired, + onError: PropTypes.func.isRequired, + onCheckGroupPassword: PropTypes.func.isRequired, + onCheckGroupPasswordReset: PropTypes.func.isRequired, + onJoinGrouponJoinGroup: PropTypes.func.isRequired, + passwordCheckIsLoading: PropTypes.bool.isRequired, + passwordCheckIsError: PropTypes.bool.isRequired, + passwordCheckIsSuccess: PropTypes.bool.isRequired, +} + +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(GroupPasswordModal)) \ No newline at end of file diff --git a/app/javascript/gabsocial/components/modal/modal_root.js b/app/javascript/gabsocial/components/modal/modal_root.js index f015ed22..4304ab5e 100644 --- a/app/javascript/gabsocial/components/modal/modal_root.js +++ b/app/javascript/gabsocial/components/modal/modal_root.js @@ -19,6 +19,7 @@ import { MODAL_EMBED, MODAL_GROUP_CREATE, MODAL_GROUP_DELETE, + MODAL_GROUP_PASSWORD, MODAL_HASHTAG_TIMELINE_SETTINGS, MODAL_HOME_TIMELINE_SETTINGS, MODAL_HOTKEYS, @@ -51,6 +52,7 @@ import { GroupCreateModal, GroupDeleteModal, GroupMembersModal, + GroupPasswordModal, GroupRemovedAccountsModal, HashtagTimelineSettingsModal, HomeTimelineSettingsModal, @@ -84,6 +86,7 @@ MODAL_COMPONENTS[MODAL_EDIT_PROFILE] = EditProfileModal MODAL_COMPONENTS[MODAL_EMBED] = EmbedModal MODAL_COMPONENTS[MODAL_GROUP_CREATE] = GroupCreateModal MODAL_COMPONENTS[MODAL_GROUP_DELETE] = GroupDeleteModal +MODAL_COMPONENTS[MODAL_GROUP_PASSWORD] = GroupPasswordModal MODAL_COMPONENTS[MODAL_HASHTAG_TIMELINE_SETTINGS] = HashtagTimelineSettingsModal MODAL_COMPONENTS[MODAL_HOME_TIMELINE_SETTINGS] = HomeTimelineSettingsModal MODAL_COMPONENTS[MODAL_HOTKEYS] = HotkeysModal diff --git a/app/javascript/gabsocial/components/panel/group_info_panel.js b/app/javascript/gabsocial/components/panel/group_info_panel.js index d53998ef..54d91878 100644 --- a/app/javascript/gabsocial/components/panel/group_info_panel.js +++ b/app/javascript/gabsocial/components/panel/group_info_panel.js @@ -35,7 +35,7 @@ class GroupInfoPanel extends ImmutablePureComponent { ) } - const isAdmin = relationships ? relationships.get('admin') : false + const isAdminOrMod = relationships ? (relationships.get('admin') || relationships.get('moderator')) : false const groupId = !!group ? group.get('id') : '' const slug = !!group ? !!group.get('slug') ? `g/${group.get('slug')}` : undefined : undefined const isPrivate = !!group ? group.get('is_private') : false @@ -129,18 +129,18 @@ class GroupInfoPanel extends ImmutablePureComponent { ? - +