Commit 6d85c76c authored by mgabdev's avatar mgabdev

Added ability to set password for groups

• Added:
- ability to set password for groups
- GroupPasswordModal
- checks for if has password
- rate limiting in rack_attack
parent 1baa123e
......@@ -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 @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!
if current_user.allows_group_in_home_feed?
current_user.force_regeneration!
end
end
render json: @group, serializer: REST::GroupRelationshipSerializer, relationships: relationships
......
# 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
......@@ -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,
......
......@@ -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) => {
......
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 <div/>
}
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 (
<ModalLayout
title={intl.formatMessage(messages.title)}
onClose={onClose}
width={360}
>
<div className={_s.d}>
<div className={[_s.d, _s.my10].join(' ')}>
{
isError &&
<Text color='error' className={[_s.pb15, _s.px15].join(' ')}>There was an error submitting the form.</Text>
}
<Input
isDisabled={passwordCheckIsLoading}
type='text'
value={text}
placeholder='•••••••••••'
id='group-password'
title='Enter group password'
onChange={this.handlePasswordChange}
/>
<Text className={[_s.my10, _s.ml15].join(' ')} size='small' color='secondary'>
{instructions}
</Text>
</div>
<Button
isDisabled={passwordCheckIsLoading}
onClick={this.handleOnClick}
icon={passwordCheckIsLoading ? 'loading' : null}
iconSize='20px'
className={[_s.aiCenter, _s.jcCenter].join(' ')}
>
<Text color='inherit' className={_s.px10}>{intl.formatMessage(messages.submit)}</Text>
</Button>
</div>
</ModalLayout>
)
}
}
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
......@@ -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
......
......@@ -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 {
<Text size='small' color='inherit' className={_s.px5}>?</Text>
</Button>
</GroupInfoPanelRow>
<Divider isSmall />
<GroupInfoPanelRow title={intl.formatMessage(messages.members)} icon='group'>
<Button
isText
color={isAdmin ? 'brand' : 'primary'}
color={isAdminOrMod ? 'brand' : 'primary'}
backgroundColor='none'
className={_s.mlAuto}
to={isAdmin ? `/groups/${groupId}/members` : undefined}
to={isAdminOrMod ? `/groups/${groupId}/members` : undefined}
>
<Text color='inherit' weight={isAdmin ? 'medium' : 'normal'} size='normal' className={isAdmin ? _s.underline_onHover : undefined}>
<Text color='inherit' weight={isAdminOrMod ? 'medium' : 'normal'} size='normal' className={isAdminOrMod ? _s.underline_onHover : undefined}>
{shortNumberFormat(group.get('member_count'))}
&nbsp;
{intl.formatMessage(messages.members)}
......@@ -186,9 +186,14 @@ class GroupInfoPanel extends ImmutablePureComponent {
{
tags.map((tag) => (
<div className={[_s.mr5, _s.mb5].join(' ')}>
<Text size='small' className={[_s.bgSecondary, _s.radiusSmall, _s.px10, _s.py2, _s.lineHeight15].join(' ')}>
{tag}
</Text>
<NavLink
to={`/groups/browse/tags/${slugify(tag)}`}
className={_s.noUnderline}
>
<Text size='small' className={[_s.bgSecondary, _s.radiusSmall, _s.px10, _s.py2, _s.lineHeight15].join(' ')}>
{tag}
</Text>
</NavLink>
</div>
))
}
......
......@@ -48,6 +48,7 @@ export const MODAL_EDIT_SHORTCUTS = 'EDIT_SHORTCUTS'
export const MODAL_EMBED = 'EMBED'
export const MODAL_GROUP_CREATE = 'GROUP_CREATE'
export const MODAL_GROUP_DELETE = 'GROUP_DELETE'
export const MODAL_GROUP_PASSWORD = 'GROUP_PASSWORD'
export const MODAL_HASHTAG_TIMELINE_SETTINGS = 'HASHTAG_TIMELINE_SETTINGS'
export const MODAL_HOME_TIMELINE_SETTINGS = 'HOME_TIMELINE_SETTINGS'
export const MODAL_HOTKEYS = 'HOTKEYS'
......
......@@ -7,6 +7,7 @@ import { defineMessages, injectIntl } from 'react-intl'
import isObject from 'lodash.isobject'
import {
changeGroupTitle,
changeGroupPassword,
changeGroupDescription,
changeGroupCoverImage,
changeGroupId,
......@@ -92,9 +93,11 @@ class GroupCreate extends ImmutablePureComponent {
error,
titleValue,
descriptionValue,
passwordValue,
coverImage,
intl,
onTitleChange,
onChangeGroupPassword,
onDescriptionChange,
onChangeGroupId,
onChangeGroupTags,
......@@ -145,15 +148,18 @@ class GroupCreate extends ImmutablePureComponent {
}
}
const submitDisabled = ((!titleValue || !category || !descriptionValue) && !groupId) || isSubmitting
return (
<Form onSubmit={onSubmit}>
<Input
id='group-title'
title={intl.formatMessage(messages.title)}
title={`${intl.formatMessage(messages.title)} *`}
value={titleValue}
onChange={onTitleChange}
disabled={isSubmitting}
placeholder={intl.formatMessage(messages.titlePlaceholder)}
isRequired
/>
<Divider isInvisible />
......@@ -180,44 +186,45 @@ class GroupCreate extends ImmutablePureComponent {
</React.Fragment>
}
<Input
id='group-tags'
title={intl.formatMessage(messages.tagsTitle)}
value={tags}
onChange={onChangeGroupTags}
<Textarea
title={`${intl.formatMessage(messages.description)} *`}
value={descriptionValue}
onChange={onDescriptionChange}
placeholder={intl.formatMessage(messages.descriptionPlaceholder)}
disabled={isSubmitting}
isRequired
/>
<Text className={[_s.mt5, _s.pl15]} size='small' color='secondary'>
{intl.formatMessage(messages.tagsDescription)}
</Text>
<Divider isInvisible />
<div className={_s.d}>
<Text className={[_s.pl15, _s.mb10].join(' ')} size='small' weight='medium' color='secondary'>
{intl.formatMessage(messages.categoryTitle)}
{intl.formatMessage(messages.categoryTitle)} *
</Text>
<Select
value={category}
onChange={onChangeGroupCategory}
options={categoriesOptions}
/>
<Text className={[_s.mt5, _s.pl15].join(' ')} size='small' color='secondary'>
<Text className={[_s.mt5, _s.pl15].join(' ')} size='small' color='tertiary'>
{intl.formatMessage(messages.categoryDescription)}
</Text>
<Divider isInvisible />
</div>
<Textarea
title={intl.formatMessage(messages.description)}
value={descriptionValue}
onChange={onDescriptionChange}
placeholder={intl.formatMessage(messages.descriptionPlaceholder)}
<Input
id='group-tags'
title={intl.formatMessage(messages.tagsTitle)}
value={tags}
onChange={onChangeGroupTags}
disabled={isSubmitting}
/>
<Text className={[_s.mt5, _s.pl15, _s.mb15, _s.pb5].join(' ')} size='small' color='tertiary'>
{intl.formatMessage(messages.tagsDescription)}
</Text>
<Divider isInvisible />
<Divider />
<FileInput
disabled={isSubmitting}
......@@ -229,41 +236,66 @@ class GroupCreate extends ImmutablePureComponent {
height='145px'
isBordered
/>
<Text className={[_s.mt5, _s.pl15].join(' ')} size='small' color='secondary'>
<Text className={[_s.mt5, _s.pl15, _s.mb15, _s.pb5].join(' ')} size='small' color='tertiary'>
{intl.formatMessage(messages.coverImageDescription)}
</Text>
<Divider isInvisible />
<Switch
label={'Private'}
id='group-isprivate'
checked={isPrivate}
onChange={onChangeGroupIsPrivate}
/>
<Text className={_s.mt5} size='small' color='secondary'>
{intl.formatMessage(messages.isPrivateDescription)}
</Text>
<Divider />
<Divider isInvisible />
<Switch
label={'Visible'}
id='group-isvisible'
checked={isVisible}
onChange={onChangeGroupIsVisible}
<Input
id='group-password'
title={intl.formatMessage(messages.passwordTitle)}
value={passwordValue}
onChange={onChangeGroupPassword}
disabled={isSubmitting}
/>
<Text className={_s.mt5} size='small' color='secondary'>
{intl.formatMessage(messages.isVisibleDescription)}
<Text className={[_s.mt5, _s.pl15, _s.mb15, _s.pb5].join(' ')} size='small' color='tertiary'>
{intl.formatMessage(messages.passwordDescription)}
</Text>
<Divider />
<div className={[_s.d, _s.pl15].join(' ')}>
<Switch
label={'Private'}
id='group-isprivate'
checked={isPrivate}
onChange={onChangeGroupIsPrivate}
labelProps={{
size: 'small',
weight: 'medium',
color: 'secondary',
}}
/>
<Text className={_s.mt5} size='small' color='tertiary'>
{intl.formatMessage(messages.isPrivateDescription)}
</Text>
<Divider isInvisible />
<Switch
label={'Visible'}
id='group-isvisible'
checked={isVisible}
onChange={onChangeGroupIsVisible}
labelProps={{
size: 'small',
weight: 'medium',
color: 'secondary',
}}
/>
<Text className={_s.mt5} size='small' color='tertiary'>
{intl.formatMessage(messages.isVisibleDescription)}
</Text>
</div>
<Divider isInvisible />
<Button
isDisabled={!titleValue || !descriptionValue && !isSubmitting}
isDisabled={submitDisabled}
onClick={this.handleSubmit}
>
<Text color='inherit' align='center'>
<Text color='inherit' align='center' weight='medium'>
{intl.formatMessage(!!group ? messages.update : messages.create)}
</Text>
</Button>
......@@ -280,13 +312,15 @@ const messages = defineMessages({
idTitle: { id: 'groups.form.id_title', defaultMessage: 'Unique id' },
idDescription: { id: 'groups.form.id_description', defaultMessage: