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:
parent
1baa123e25
commit
6d85c76c8f
@ -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
|
||||
|
38
app/controllers/api/v1/groups/password_controller.rb
Normal file
38
app/controllers/api/v1/groups/password_controller.rb
Normal file
@ -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
|
||||
@ -135,12 +135,12 @@ class GroupInfoPanel extends ImmutablePureComponent {
|
||||
<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'))}
|
||||
|
||||
{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 />
|
||||
<Divider />
|
||||
|
||||
<Switch
|
||||
label={'Private'}
|
||||
id='group-isprivate'
|
||||
checked={isPrivate}
|
||||
onChange={onChangeGroupIsPrivate}
|
||||
<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 />
|
||||
<Divider />
|
||||
|
||||
<Switch
|
||||
label={'Visible'}
|
||||
id='group-isvisible'
|
||||
checked={isVisible}
|
||||
onChange={onChangeGroupIsVisible}
|
||||
/>
|
||||
<Text className={_s.mt5} size='small' color='secondary'>
|
||||
{intl.formatMessage(messages.isVisibleDescription)}
|
||||
</Text>
|
||||
<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
|
||||
|
Loading…
Reference in New Issue
Block a user