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
This commit is contained in:
mgabdev 2020-09-11 17:27:00 -05:00
parent 1baa123e25
commit 6d85c76c8f
13 changed files with 435 additions and 71 deletions

@ -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

@ -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

@ -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) => {

@ -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 <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))

@ -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,6 +186,33 @@ class GroupCreate extends ImmutablePureComponent {
</React.Fragment>
}
<Textarea
title={`${intl.formatMessage(messages.description)} *`}
value={descriptionValue}
onChange={onDescriptionChange}
placeholder={intl.formatMessage(messages.descriptionPlaceholder)}
disabled={isSubmitting}
isRequired
/>
<Divider isInvisible />
<div className={_s.d}>
<Text className={[_s.pl15, _s.mb10].join(' ')} size='small' weight='medium' color='secondary'>
{intl.formatMessage(messages.categoryTitle)} *
</Text>
<Select
value={category}
onChange={onChangeGroupCategory}
options={categoriesOptions}
/>
<Text className={[_s.mt5, _s.pl15].join(' ')} size='small' color='tertiary'>
{intl.formatMessage(messages.categoryDescription)}
</Text>
<Divider isInvisible />
</div>
<Input
id='group-tags'
title={intl.formatMessage(messages.tagsTitle)}
@ -187,37 +220,11 @@ class GroupCreate extends ImmutablePureComponent {
onChange={onChangeGroupTags}
disabled={isSubmitting}
/>
<Text className={[_s.mt5, _s.pl15]} size='small' color='secondary'>
<Text className={[_s.mt5, _s.pl15, _s.mb15, _s.pb5].join(' ')} size='small' color='tertiary'>
{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)}
</Text>
<Select
value={category}
onChange={onChangeGroupCategory}
options={categoriesOptions}
/>
<Text className={[_s.mt5, _s.pl15].join(' ')} size='small' color='secondary'>
{intl.formatMessage(messages.categoryDescription)}
</Text>
<Divider isInvisible />
</div>
<Textarea
title={intl.formatMessage(messages.description)}
value={descriptionValue}
onChange={onDescriptionChange}
placeholder={intl.formatMessage(messages.descriptionPlaceholder)}
disabled={isSubmitting}
/>
<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}
<Divider />
<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.isPrivateDescription)}
<Text className={[_s.mt5, _s.pl15, _s.mb15, _s.pb5].join(' ')} size='small' color='tertiary'>
{intl.formatMessage(messages.passwordDescription)}
</Text>
<Divider isInvisible />
<Switch
label={'Visible'}
id='group-isvisible'
checked={isVisible}
onChange={onChangeGroupIsVisible}
/>
<Text className={_s.mt5} size='small' color='secondary'>
{intl.formatMessage(messages.isVisibleDescription)}
</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: 'A unique id that links to this group. (Cannot be changed)' },
tagsTitle: { id: 'groups.form.tags_title', defaultMessage: 'Tags' },
tagsDescription: { id: 'groups.form.tags_description', defaultMessage: 'Add tags seperated by commas to increase group visibility' },
tagsDescription: { id: 'groups.form.tags_description', defaultMessage: '(Optional) Add tags seperated by commas to increase group visibility' },
passwordTitle: { id: 'groups.form.password_title', defaultMessage: 'Password' },
passwordDescription: { id: 'groups.form.password_description', defaultMessage: '(Optional) Add a password to restrict access to this group. This password is NOT encrypted and is only visible to group admins.' },
categoryTitle: { id: 'groups.form.category_title', defaultMessage: 'Category' },
categoryDescription: { id: 'groups.form.category_description', defaultMessage: 'Add a general category for your group' },
description: { id: 'groups.form.description', defaultMessage: 'Enter the group description' },
coverImage: { id: 'groups.form.coverImage', defaultMessage: 'Upload a banner image' },
coverImageDescription: { id: 'groups.form.coverImage_description', defaultMessage: 'Accepted image types: .jpg, .png' },
coverImageChange: { id: 'groups.form.coverImageChange', defaultMessage: 'Banner image selected' },
description: { id: 'groups.form.description', defaultMessage: 'Description' },
coverImage: { id: 'groups.form.coverImage', defaultMessage: 'Cover image' },
coverImageDescription: { id: 'groups.form.coverImage_description', defaultMessage: '(Optional) Max: 5MB. Accepted image types: .jpg, .png' },
coverImageChange: { id: 'groups.form.coverImageChange', defaultMessage: 'Cover image selected' },
create: { id: 'groups.form.create', defaultMessage: 'Create group' },
update: { id: 'groups.form.update', defaultMessage: 'Update group' },
titlePlaceholder: { id: 'groups.form.title_placeholder', defaultMessage: 'New group title...' },
@ -313,6 +347,7 @@ const mapStateToProps = (state, { params }) => {
isAdmin,
error: (groupId && !group) || (group && !isAdmin),
titleValue: state.getIn(['group_editor', 'title']),
passwordValue: state.getIn(['group_editor', 'password']),
descriptionValue: state.getIn(['group_editor', 'description']),
coverImage: state.getIn(['group_editor', 'coverImage']),
isSubmitting: state.getIn(['group_editor', 'isSubmitting']),
@ -333,6 +368,9 @@ const mapDispatchToProps = (dispatch) => ({
onDescriptionChange(value) {
dispatch(changeGroupDescription(value))
},
onChangeGroupPassword(value) {
dispatch(changeGroupPassword(value))
},
onCoverImageChange(imageData) {
dispatch(changeGroupCoverImage(imageData))
},
@ -382,6 +420,7 @@ GroupCreate.propTypes = {
onDescriptionChange: PropTypes.func.isRequired,
onChangeGroupId: PropTypes.func.isRequired,
onChangeGroupTags: PropTypes.func.isRequired,
onChangeGroupPassword: PropTypes.func.isRequired,
onChangeGroupCategory: PropTypes.func.isRequired,
onChangeGroupIsPrivate: PropTypes.func.isRequired,
onChangeGroupIsVisible: PropTypes.func.isRequired,

@ -9,6 +9,7 @@ import {
GROUP_EDITOR_RESET,
GROUP_EDITOR_SETUP,
GROUP_EDITOR_TITLE_CHANGE,
GROUP_EDITOR_PASSWORD_CHANGE,
GROUP_EDITOR_DESCRIPTION_CHANGE,
GROUP_EDITOR_COVER_IMAGE_CHANGE,
GROUP_EDITOR_ID_CHANGE,
@ -24,6 +25,7 @@ const initialState = ImmutableMap({
isSubmitting: false,
isChanged: false,
title: '',
password: '',
description: '',
id: '',
tags: '',
@ -48,6 +50,7 @@ export default function groupEditorReducer(state = initialState, action) {
return state.withMutations((map) => {
map.set('groupId', action.group.get('id'))
map.set('title', action.group.get('title'))
map.set('password', action.group.get('password'))
map.set('description', action.group.get('description'))
map.set('tags', tags)
map.set('isPrivate', action.group.get('is_private'))
@ -66,6 +69,11 @@ export default function groupEditorReducer(state = initialState, action) {
map.set('description', action.description)
map.set('isChanged', true)
})
case GROUP_EDITOR_PASSWORD_CHANGE:
return state.withMutations((map) => {
map.set('password', action.password)
map.set('isChanged', true)
})
case GROUP_EDITOR_COVER_IMAGE_CHANGE:
return state.withMutations((map) => {
map.set('coverImage', action.value)

@ -6,6 +6,10 @@ import {
GROUP_SORT,
GROUP_TIMELINE_SORT,
GROUP_TIMELINE_TOP_SORT,
GROUP_CHECK_PASSWORD_RESET,
GROUP_CHECK_PASSWORD_REQUEST,
GROUP_CHECK_PASSWORD_SUCCESS,
GROUP_CHECK_PASSWORD_FAIL,
} from '../actions/groups'
import {
GROUP_TIMELINE_SORTING_TYPE_TOP,
@ -18,6 +22,7 @@ const tabs = ['new', 'featured', 'member', 'admin']
const initialState = ImmutableMap({
sortByValue: GROUP_TIMELINE_SORTING_TYPE_NEWEST,
sortByTopValue: '',
passwordCheck: ImmutableMap(),
new: ImmutableMap({
isFetched: false,
isLoading: false,
@ -80,6 +85,32 @@ export default function groupLists(state = initialState, action) {
mutable.set('sortByValue', GROUP_TIMELINE_SORTING_TYPE_TOP)
mutable.set('sortByTopValue', action.sortValue)
})
case GROUP_CHECK_PASSWORD_RESET:
return state.withMutations((mutable) => {
mutable.setIn(['passwordCheck', 'isError'], false)
mutable.setIn(['passwordCheck', 'isSuccess'], false)
mutable.setIn(['passwordCheck', 'isLoading'], false)
})
case GROUP_CHECK_PASSWORD_REQUEST:
return state.withMutations((mutable) => {
mutable.setIn(['passwordCheck', 'isError'], false)
mutable.setIn(['passwordCheck', 'isSuccess'], false)
mutable.setIn(['passwordCheck', 'isLoading'], true)
})
case GROUP_CHECK_PASSWORD_SUCCESS:
return state.withMutations((mutable) => {
mutable.setIn(['passwordCheck', 'isError'], false)
mutable.setIn(['passwordCheck', 'isSuccess'], true)
mutable.setIn(['passwordCheck', 'isLoading'], false)
})
case GROUP_CHECK_PASSWORD_FAIL:
return state.withMutations((mutable) => {
mutable.setIn(['passwordCheck', 'isError'], true)
mutable.setIn(['passwordCheck', 'isSuccess'], false)
mutable.setIn(['passwordCheck', 'isLoading'], false)
})
default:
return state
}

@ -4,12 +4,25 @@ class REST::GroupSerializer < ActiveModel::Serializer
include RoutingHelper
attributes :id, :title, :description, :description_html, :cover_image_url, :is_archived,
:member_count, :created_at, :is_private, :is_visible, :slug, :tags, :group_category
:member_count, :created_at, :is_private, :is_visible, :slug, :tags, :group_category, :password,
:has_password
def id
object.id.to_s
end
def has_password
return !!password
end
def password
if object.group_accounts.where(account_id: current_user.account.id, role: :admin).exists?
object.password
else
nil
end
end
def group_category
if object.group_categories
object.group_categories

@ -87,11 +87,16 @@ class Rack::Attack
API_DELETE_REBLOG_REGEX = /\A\/api\/v1\/statuses\/[\d]+\/unreblog/.freeze
API_DELETE_STATUS_REGEX = /\A\/api\/v1\/statuses\/[\d]+/.freeze
API_POST_GROUP_PASSWORD_CHECK_REGEX = /\A\/api\/v1\/groups\/[\d]+\/password/.freeze
throttle('throttle_api_delete', limit: 30, period: 30.minutes) do |req|
req.authenticated_user_id if (req.post? && req.path =~ API_DELETE_REBLOG_REGEX) || (req.delete? && req.path =~ API_DELETE_STATUS_REGEX)
end
throttle('throttle_group_password_check', limit: 5, period: 1.minute) do |req|
req.authenticated_user_id if req.post? && req.path =~ API_POST_GROUP_PASSWORD_CHECK_REGEX
end
throttle('protected_paths', limit: 25, period: 5.minutes) do |req|
req.remote_ip if req.post? && req.path =~ PROTECTED_PATHS_REGEX
end