Compare commits

...

7 Commits

11 changed files with 250 additions and 54 deletions

View File

@ -10,6 +10,18 @@ APP_NAME = os.environ.get("APP_NAME")
APP_URL = os.environ.get("APP_URL")
def has_group_permission(user, group, permission=None):
if not user or not group:
return False
if user["_id"] in group.get("admins", []):
return True
if group["_id"] not in user.get("groups", []):
return False
if permission:
return permission in group.get("memberPermissions", [])
return False
def create(user, data):
if not data:
raise util.errors.BadRequest("Invalid request")
@ -24,6 +36,13 @@ def create(user, data):
"name": data["name"],
"description": data.get("description", ""),
"closed": data.get("closed", False),
"memberPermissions": [
"viewMembers",
"viewNoticeboard",
"postNoticeboard",
"viewProjects",
"postProjects",
],
}
result = db.groups.insert_one(group)
group["_id"] = result.inserted_id
@ -43,6 +62,10 @@ def get_one(user, id):
group = db.groups.find_one({"_id": id})
if not group:
raise util.errors.NotFound("Group not found")
if group.get("image"):
group["imageUrl"] = uploads.get_presigned_url(
"groups/{0}/{1}".format(id, group["image"])
)
group["adminUsers"] = list(
db.users.find(
{"_id": {"$in": group.get("admins", [])}}, {"username": 1, "avatar": 1}
@ -64,7 +87,7 @@ def update(user, id, update):
raise util.errors.NotFound("Group not found")
if user["_id"] not in group.get("admins", []):
raise util.errors.Forbidden("You're not a group admin")
allowed_keys = ["name", "description", "closed"]
allowed_keys = ["name", "description", "closed", "memberPermissions", "image"]
updater = util.build_updater(update, allowed_keys)
if updater:
db.groups.update_one({"_id": id}, updater)
@ -90,11 +113,13 @@ def create_entry(user, id, data):
raise util.errors.BadRequest("Invalid request")
db = database.get_db()
id = ObjectId(id)
group = db.groups.find_one({"_id": id}, {"admins": 1, "name": 1})
group = db.groups.find_one({"_id": id})
if not group:
raise util.errors.NotFound("Group not found")
if group["_id"] not in user.get("groups", []):
raise util.errors.Forbidden("You must be a member to write in the feed")
if not has_group_permission(user, group, "postNoticeboard"):
raise util.errors.Forbidden("You don't have permission to post in the feed")
entry = {
"createdAt": datetime.datetime.now(),
"group": id,
@ -158,11 +183,13 @@ def create_entry(user, id, data):
def get_entries(user, id):
db = database.get_db()
id = ObjectId(id)
group = db.groups.find_one({"_id": id}, {"admins": 1})
group = db.groups.find_one({"_id": id})
if not group:
raise util.errors.NotFound("Group not found")
if id not in user.get("groups", []):
raise util.errors.BadRequest("You're not a member of this group")
if not has_group_permission(user, group, "viewNoticeboard"):
raise util.errors.Forbidden("You don't have permission to view the feed")
entries = list(
db.groupEntries.find({"group": id}).sort("createdAt", pymongo.DESCENDING)
)
@ -211,7 +238,7 @@ def create_entry_reply(user, id, entry_id, data):
db = database.get_db()
id = ObjectId(id)
entry_id = ObjectId(entry_id)
group = db.groups.find_one({"_id": id}, {"admins": 1, "name": 1})
group = db.groups.find_one({"_id": id})
if not group:
raise util.errors.NotFound("Group not found")
entry = db.groupEntries.find_one({"_id": entry_id})
@ -219,6 +246,8 @@ def create_entry_reply(user, id, entry_id, data):
raise util.errors.NotFound("Entry to reply to not found")
if group["_id"] not in user.get("groups", []):
raise util.errors.Forbidden("You must be a member to write in the feed")
if not has_group_permission(user, group, "postNoticeboard"):
raise util.errors.Forbidden("You don't have permission to post in the feed")
reply = {
"createdAt": datetime.datetime.now(),
"group": id,
@ -346,11 +375,13 @@ def create_member(user, id, user_id, invited=False):
def get_members(user, id):
db = database.get_db()
id = ObjectId(id)
group = db.groups.find_one({"_id": id}, {"admins": 1})
group = db.groups.find_one({"_id": id})
if not group:
raise util.errors.NotFound("Group not found")
if id not in user.get("groups", []) and "root" not in user.get("roles", []):
raise util.errors.Forbidden("You need to be a member to see the member list")
if not has_group_permission(user, group, "viewMembers"):
raise util.errors.Forbidden("You don't have permission to view the member list")
members = list(
db.users.find(
{"groups": id}, {"username": 1, "avatar": 1, "bio": 1, "groups": 1}
@ -385,14 +416,52 @@ def delete_member(user, id, user_id):
return {"deletedMember": user_id}
def create_admin(user, id, user_id):
id = ObjectId(id)
user_id = ObjectId(user_id)
db = database.get_db()
group = db.groups.find_one({"_id": id}, {"admins": 1})
if not group:
raise util.errors.NotFound("Group not found")
if user["_id"] not in group.get("admins", []):
raise util.errors.Forbidden("You can't add this admin")
if user_id in group.get("admins", []):
raise util.errors.Forbidden("This user is already an admin")
db.groups.update_one({"_id": id}, {"$addToSet": {"admins": user_id}})
return {"createdAdmin": user_id}
def delete_admin(user, id, user_id):
id = ObjectId(id)
user_id = ObjectId(user_id)
db = database.get_db()
group = db.groups.find_one({"_id": id}, {"admins": 1})
if not group:
raise util.errors.NotFound("Group not found")
if user_id != user["_id"] and user["_id"] not in group.get("admins", []):
raise util.errors.Forbidden("You can't remove this admin")
if user_id not in group.get("admins", []):
raise util.errors.Forbidden("This user is not an admin")
if len(group["admins"]) == 1:
raise util.errors.Forbidden(
"There needs to be at least one admin in this group"
)
db.groups.update_one({"_id": id}, {"$pull": {"admins": user_id}})
return {"deletedAdmin": user_id}
def get_projects(user, id):
db = database.get_db()
id = ObjectId(id)
group = db.groups.find_one({"_id": id}, {"admins": 1})
group = db.groups.find_one({"_id": id})
if not group:
raise util.errors.NotFound("Group not found")
if id not in user.get("groups", []):
raise util.errors.Forbidden("You need to be a member to see the project list")
if not has_group_permission(user, group, "viewProjects"):
raise util.errors.Forbidden(
"You don't have permission to view the project list"
)
projects = list(
db.projects.find(
{"groupVisibility": id},

View File

@ -1,4 +1,5 @@
import datetime
import re
from bson.objectid import ObjectId
from util import database, util
from api import uploads
@ -99,6 +100,10 @@ def update(user, username, data):
if "username" in data:
if not data.get("username") or len(data["username"]) < 3:
raise util.errors.BadRequest("New username is not valid")
if not re.match("^[a-z0-9_]+$", data["username"]):
raise util.errors.BadRequest(
"Usernames can only contain letters, numbers, and underscores"
)
if db.users.count_documents({"username": data["username"].lower()}):
raise util.errors.BadRequest("A user with this username already exists")
data["username"] = data["username"].lower()

View File

@ -478,6 +478,8 @@ def group_route(id):
"name": fields.Str(),
"description": fields.Str(),
"closed": fields.Bool(),
"memberPermissions": fields.List(fields.Str()),
"image": fields.Str(allow_none=True),
}
)
def group_route_put(args, id):
@ -542,6 +544,18 @@ def group_member_route(id, user_id):
)
@app.route("/groups/<id>/admins/<user_id>", methods=["PUT", "DELETE"])
def group_admin_route(id, user_id):
if request.method == "DELETE":
return util.jsonify(
groups.delete_admin(util.get_user(required=True), id, user_id)
)
if request.method == "PUT":
return util.jsonify(
groups.create_admin(util.get_user(required=True), id, user_id)
)
@app.route("/groups/<id>/projects", methods=["GET"])
def group_projects_route(id):
return util.jsonify(groups.get_projects(util.get_user(required=True), id))

View File

@ -57,5 +57,11 @@ export const groups = {
},
deleteInvitation (id, inviteId, success, fail) {
api.authenticatedRequest('DELETE', `/groups/${id}/invitations/${inviteId}`, null, success, fail)
},
createAdmin (id, userId, success, fail) {
api.authenticatedRequest('PUT', `/groups/${id}/admins/${userId}`, null, success, fail)
},
deleteAdmin (id, userId, success, fail) {
api.authenticatedRequest('DELETE', `/groups/${id}/admins/${userId}`, null, success, fail)
}
}

View File

@ -84,7 +84,7 @@ const FeedMessage = connect(
</div>}
{!post.inReplyTo &&
<div style={{ padding: 10 }}>
{replyingTo !== post._id && !post.inReplyTo && onReplyPosted &&
{utils.hasGroupPermission(user, group, 'postNoticeboard') && replyingTo !== post._id && !post.inReplyTo && onReplyPosted &&
<Button size='mini' basic primary icon='reply' content='Write a reply' onClick={() => updateReplyingTo(post._id)} />}
{post.user === user?._id && !utils.hasSubscription(user, 'messages.replied') && onReplyPosted &&
<Button size='mini' basic icon='envelope' content='Get notified if someone replies' as={Link} to='/settings/notifications' />}

View File

@ -29,8 +29,8 @@ const Badge = styled.div`
left: -8px;
`
const Username = styled.span`
font-size: ${props => props.compact ? 10 : 12}px;
margin-left: 5px;
font-size: ${props => props.compact ? 12 : 14}px;
margin-left: 3px;
color: black;
`

View File

@ -2,6 +2,7 @@ import React, { useEffect } from 'react'
import { Loader, Button, Segment } from 'semantic-ui-react'
import { useSelector, useDispatch } from 'react-redux'
import { useParams } from 'react-router-dom'
import { toast } from 'react-toastify'
import utils from '../../../utils/utils.js'
import actions from '../../../actions'
import api from '../../../api'
@ -34,26 +35,42 @@ function Feed () {
})
}, [dispatch, id, myGroups.length])
const toggleEmailSub = (key, enable) => {
if (enable) { api.users.createEmailSubscription(user.username, key, ({ subscriptions }) => dispatch(actions.users.updateSubscriptions(user._id, subscriptions)), err => toast.error(err.message)) } else { api.users.deleteEmailSubscription(user.username, key, ({ subscriptions }) => dispatch(actions.users.updateSubscriptions(user._id, subscriptions)), err => toast.error(err.message)) }
}
const mainEntries = entries && entries.filter(e => !e.inReplyTo)
return (
<div>
{utils.isInGroup(user, group._id) &&
{utils.hasGroupPermission(user, group, 'viewNoticeboard') &&
<>
<div style={{ display: 'flex', justifyContent: 'end', marginBottom: 10 }}>
{utils.hasEmailSubscription(user, `groupFeed-${group._id}`)
? <Button color='blue' basic size='tiny' icon='check' content='Subscribed to email updates' onClick={e => toggleEmailSub(`groupFeed-${group._id}`, false)} />
: <Button color='blue' size='tiny' icon='rss' content='Subscribe to updates' onClick={e => toggleEmailSub(`groupFeed-${group._id}`, true)} />}
</div>
{utils.hasGroupPermission(user, group, 'postNoticeboard') &&
<>
{replyingTo
? <Button style={{ marginBottom: 20 }} color='teal' content='Write a new post' onClick={() => dispatch(actions.posts.updateReplyingTo(null))} />
: <NewFeedMessage user={user} group={group} forType='group' onPosted={e => dispatch(actions.groups.receiveEntry(e))} />}
</>}
{loadingEntries && !mainEntries?.length &&
<div style={{ textAlign: 'center' }}>
<Loader inline='centered' active />
<p style={{ marginTop: 20 }}><strong>Loading the notice board. Hold tight...</strong></p>
</div>}
{!loadingEntries && !mainEntries?.length &&
<Segment placeholder textAlign='center'>
<img src={MessagesImage} alt='Messages' style={{ display: 'block', margin: '0px auto', maxWidth: 300 }} />
<h2>No posts yet</h2>
<p>Be the first here by writing a new post.</p>
</Segment>}
{mainEntries?.map(e =>
<FeedMessage key={e._id} user={user} forType='group' group={group} post={e} replies={entries.filter(r => r.inReplyTo === e._id)} onDeleted={id => dispatch(actions.groups.deleteEntry(id))} onReplyPosted={e => dispatch(actions.groups.receiveEntry(e))} />
)}

View File

@ -1,6 +1,6 @@
import React, { useEffect } from 'react'
import { Helmet } from 'react-helmet'
import { Segment, Loader, Menu, Message, Container, Button, Icon, Grid, Card } from 'semantic-ui-react'
import { Segment, Loader, Menu, Message, Container, Button, Icon, Grid, Card, Image } from 'semantic-ui-react'
import { Outlet, Link, useParams } from 'react-router-dom'
import { useDispatch, useSelector } from 'react-redux'
import { toast } from 'react-toastify'
@ -15,10 +15,7 @@ function Group () {
const { id } = useParams()
const dispatch = useDispatch()
const { user, group, loading, errorMessage, requests, myRequests, invitations } = useSelector(state => {
let group
state.groups.groups.forEach((g) => {
if (g._id === id) group = g
})
const group = state.groups.groups.filter(g => g._id === id)[0]
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0]
const requests = state.invitations.invitations.filter(i => i.recipientGroup === group?._id)
const myRequests = state.invitations.invitations.filter(i => i.recipientGroup === group?._id && i.user === user?._id)
@ -44,9 +41,6 @@ function Group () {
}, err => toast.error(err.message))
}, () => {})
}
const toggleEmailSub = (key, enable) => {
if (enable) { api.users.createEmailSubscription(user.username, key, ({ subscriptions }) => dispatch(actions.users.updateSubscriptions(user._id, subscriptions)), err => toast.error(err.message)) } else { api.users.deleteEmailSubscription(user.username, key, ({ subscriptions }) => dispatch(actions.users.updateSubscriptions(user._id, subscriptions)), err => toast.error(err.message)) }
}
const requestToJoin = () => {
api.groups.createJoinRequest(group._id, invitation => {
@ -89,52 +83,55 @@ function Group () {
<Grid stackable>
<Grid.Column computer={4}>
<Card fluid color='yellow'>
{group.imageUrl &&
<Image src={group.imageUrl} wrapped ui={false} />}
{group.description &&
<Card.Content>{group.description}</Card.Content>}
{group.closed &&
<Card.Content>
<Card.Meta>
<h4 style={{ marginBottom: 2 }}><Icon name='user secret' /> This is a closed group</h4>
<p>Members can only join if they are approved or invited by an admin.</p>
{requests?.length > 0 && utils.isGroupAdmin(user, group) &&
<Button as={Link} to={`/groups/${group._id}/members`} size='tiny' fluid color='teal' icon='user plus' content='Manage join requests' />}
{utils.isGroupAdmin(user, group) && !utils.hasSubscription(user, 'groups.joinRequested') &&
<Button as={Link} to='/settings/notifications' icon='envelope' size='tiny' fluid basic content='Email me join requests' />}
</Card.Meta>
</Card.Content>}
<Card.Content>
<h3>Admins</h3>
{group.adminUsers && group.adminUsers.map(a =>
<UserChip user={a} key={a._id} />
)}
</Card.Content>
<Card.Content extra>
{utils.isInGroup(user, group._id) &&
<div>
<Button color='yellow' basic size='tiny' fluid icon='check' content='Member' onClick={leave} style={{ marginBottom: 5 }} />
{utils.hasEmailSubscription(user, `groupFeed-${group._id}`)
? <Button color='blue' basic size='tiny' fluid icon='rss' content='Subscribed' onClick={e => toggleEmailSub(`groupFeed-${group._id}`, false)} data-tooltip="We'll send you emails when people post in this group" />
: <Button color='blue' size='tiny' fluid icon='rss' content='Subscribe to updates' onClick={e => toggleEmailSub(`groupFeed-${group._id}`, true)} />}
</div>}
{!utils.isInGroup(user, group._id) &&
(group.closed
? <Button disabled={myRequests?.length > 0} color='yellow' size='tiny' fluid icon='user plus' content='Request to join' onClick={requestToJoin} />
: <Button color='yellow' size='tiny' fluid icon='user plus' content='Join group' onClick={join} />
)}
</Card.Content>
<Card.Meta>
{requests?.length > 0 && utils.isGroupAdmin(user, group) &&
<Button as={Link} to={`/groups/${group._id}/members`} size='tiny' fluid color='teal' icon='user plus' content='Manage join requests' />}
{utils.isGroupAdmin(user, group) && !utils.hasSubscription(user, 'groups.joinRequested') &&
<Button as={Link} to='/settings/notifications' icon='envelope' size='tiny' fluid basic content='Email me join requests' />}
</Card.Meta>
</Card.Content>}
</Card>
{utils.isInGroup(user, group._id) &&
<Menu fluid vertical>
<Menu.Item active={utils.activePath('^/groups/[a-zA-Z0-9]+$')} as={Link} to={`/groups/${group._id}`} icon='chat' content='Notice Board' />
{utils.isInGroup(user, group._id) &&
<Menu.Item active={utils.activePath('members')} as={Link} to={`/groups/${group._id}/members`} icon='user' content='Members' label='3' />}
{utils.isInGroup(user, group._id) &&
<>
{utils.hasGroupPermission(user, group, 'viewMembers') &&
<Menu.Item active={utils.activePath('members')} as={Link} to={`/groups/${group._id}/members`} icon='user' content='Members' />}
{utils.hasGroupPermission(user, group, 'viewProjects') &&
<Menu.Item active={utils.activePath('projects')} as={Link} to={`/groups/${group._id}/projects`} icon='book' content='Projects' />}
{utils.isGroupAdmin(user, group) &&
<Menu.Item active={utils.activePath('settings')} as={Link} to={`/groups/${group._id}/settings`} icon='settings' content='Settings' />}
</>}
</Menu>}
<Card fluid color='yellow'>
<Card.Content>
<h3>Admins</h3>
<div style={{ flexWrap: 'wrap', display: 'flex' }}>
{group?.adminUsers?.map(a =>
<div style={{ marginRight: 8, marginBottom: 4 }} key={a._id}><UserChip user={a} /></div>
)}
</div>
</Card.Content>
</Card>
<HelpLink link='/docs/groups#a-tour-around-your-new-group' />
</Grid.Column>

View File

@ -14,6 +14,7 @@ import UserSearch from '../../includes/UserSearch'
function Members () {
const [invitations, setInvitations] = useState([])
const [search, setSearch] = useState('')
const joinLinkRef = useRef(null)
const { id } = useParams()
const dispatch = useDispatch()
@ -54,6 +55,19 @@ function Members () {
}, err => toast.error(err.message))
}, () => {})
}
const makeUserAdmin = (member, promoting) => {
if (promoting) {
api.groups.createAdmin(group._id, member._id, () => {
toast.success('User is now an admin')
dispatch(actions.groups.updateGroup(group._id, { admins: group.admins.concat(member._id), adminUsers: group.adminUsers.concat(member) }))
}, err => toast.error(err.message))
} else {
api.groups.deleteAdmin(group._id, member._id, () => {
toast.success('Admin status revoked')
dispatch(actions.groups.updateGroup(group._id, { admins: group.admins.filter(a => a !== member._id), adminUsers: group.adminUsers.filter(a => a._id !== member._id) }))
}, err => toast.error(err.message))
}
}
const sendInvitation = (user) => {
api.groups.createInvitation(group._id, user._id, invitation => {
const newInvitations = Object.assign([], invitations)
@ -81,6 +95,9 @@ function Members () {
err => toast.error(err.message))
}
const memberList = search ? members.filter(m => m?.username?.toLowerCase().includes(search.trim().toLowerCase())) : members
const invitationList = search ? invitations.filter(i => i?.recipientUser?.username?.toLowerCase().includes(search.trim().toLowerCase())) : invitations
return (
<div>
{loading && (!members || !members.length) && <Loader active inline='centered' />}
@ -126,9 +143,11 @@ function Members () {
</Table>
</Segment>}
<Input style={{ marginBottom: 20 }} placeholder='Search by username...' value={search} onChange={e => setSearch(e.target.value)} />
<Card.Group itemsPerRow={3} doubling stackable>
{invitations && invitations.map(i =>
<Card key={i._id}>
{invitationList?.map(i =>
<Card key={i._id} style={{ opacity: 0.6 }}>
<Card.Content>
<UserChip user={i.recipientUser} />
</Card.Content>
@ -145,7 +164,7 @@ function Members () {
</Card.Content>
</Card>
)}
{members && members.map(m =>
{memberList?.map(m =>
<Card key={m._id}>
<Card.Content>
<UserChip user={m} />
@ -158,7 +177,11 @@ function Members () {
{utils.isGroupAdmin(user, group) && user._id !== m._id &&
<Dropdown text='Options'>
<Dropdown.Menu>
<Dropdown.Item icon='ban' content='Kick' onClick={e => kickUser(m._id)} />
<Dropdown.Item icon='ban' content='Remove from group' onClick={e => kickUser(m._id)} />
{!utils.isGroupAdmin(m, group) &&
<Dropdown.Item icon='rocket' content='Make admin' onClick={e => makeUserAdmin(m, true)} />}
{utils.isGroupAdmin(m, group) &&
<Dropdown.Item icon='ban' content='Revoke admin' onClick={e => makeUserAdmin(m, false)} />}
</Dropdown.Menu>
</Dropdown>}
</div>

View File

@ -1,11 +1,19 @@
import React from 'react'
import { Header, Button, Divider, Segment, Form } from 'semantic-ui-react'
import { Header, Button, Divider, Segment, Form, Checkbox } from 'semantic-ui-react'
import { useNavigate, useParams } from 'react-router-dom'
import { useSelector, useDispatch } from 'react-redux'
import { toast } from 'react-toastify'
import utils from '../../../utils/utils.js'
import actions from '../../../actions'
import api from '../../../api'
import FileChooser from '../../includes/FileChooser'
const PERMISSIONS = [
{ name: 'viewMembers', label: 'Allow members to view other members' },
{ name: 'viewNoticeboard', label: 'Allow members to view the noticeboard' },
{ name: 'postNoticeboard', label: 'Allow members to post to the noticeboard' },
{ name: 'viewProjects', label: 'Allow members to view projects linked to the group' }
]
function Settings () {
const navigate = useNavigate()
@ -29,6 +37,22 @@ function Settings () {
dispatch(actions.groups.request(false))
})
}
const savePermission = (permissionName, enabled) => {
const permissions = group.memberPermissions || []
const index = permissions.indexOf(permissionName)
if (enabled && index === -1) {
permissions.push(permissionName)
} else if (!enabled && index !== -1) {
permissions.splice(index, 1)
}
api.groups.update(group._id, { memberPermissions: permissions }, g => {
dispatch(actions.groups.updateGroup(group._id, { memberPermissions: g.memberPermissions }))
}, err => {
toast.error(err.message)
})
}
const deleteGroup = () => {
utils.confirm('Really delete this group?', 'You\'ll lose all entries in the group feed and anything else you\'ve added to it.').then(() => {
api.groups.delete(group._id, () => {
@ -39,6 +63,16 @@ function Settings () {
}, () => {})
}
const updatePicture = (image) => {
api.groups.update(group._id, { image }, g => {
if (!image) { // Needed to ensure the avatar is immediately unset
g.image = null
g.imageUrl = null
}
dispatch(actions.groups.updateGroup(group._id, { image: g.image, imageUrl: g.imageUrl }))
})
}
return (
<div>
<Segment color='blue'>
@ -48,12 +82,40 @@ function Settings () {
<Form.TextArea label='Group description' value={group.description} onChange={e => dispatch(actions.groups.updateGroup(group._id, { description: e.target.value }))} />
<Form.Checkbox toggle checked={group.closed} label='Closed group' onChange={(e, c) => dispatch(actions.groups.updateGroup(group._id, { closed: c.checked }))} />
<div style={{ marginLeft: 63, color: 'rgb(150,150,150)' }}>
<p>Closed groups are more restrictive and new members must be invited or approved to join. Members can join non-closed groups without being invited.</p>
<p>In a closed group, new members must be invited or approved to join.</p>
</div>
<Divider hidden />
<Form.Button loading={loading} color='teal' icon='check' content='Save changes' onClick={saveGroup} />
</Form>
<Header>Group picture</Header>
<p>Upload a picture to represent your group. This will be shown on the group page and may be viewable to both members and non-members.</p>
{group.imageUrl &&
<div style={{ marginBottom: 10 }}>
<img src={group.imageUrl} alt='Group' style={{ maxWidth: 200, borderRadius: 5 }} />
</div>}
<div style={{ display: 'flex', alignItems: 'center' }}>
{group.imageUrl &&
<Button basic color='gray' icon='times' content='Remove image' onClick={() => updatePicture(null)} />}
<FileChooser
forType='group' forObject={group}
trigger={<Button basic color='yellow' icon='image' content='Choose an image' />}
accept='image/*' onComplete={f => updatePicture(f.storedName)}
/>
</div>
</Segment>
<Segment color='blue'>
<Header>Permissions</Header>
<p>Group admins can always perform all group actions.</p>
{PERMISSIONS.map(p => (
<div key={p.name} style={{ marginBottom: 10 }}>
<Checkbox toggle label={p.label} onChange={(e, c) => savePermission(p.name, c.checked)} checked={group.memberPermissions?.includes(p.name)} />
</div>
))}
</Segment>
<Segment color='red'>

View File

@ -35,6 +35,9 @@ const utils = {
isGroupAdmin (user, group) {
return group?.admins?.indexOf(user?._id) > -1 || user?.roles?.indexOf('root') > -1
},
hasGroupPermission (user, group, permission) {
return utils.isInGroup(user, group._id) && (utils.isGroupAdmin(user, group) || group?.memberPermissions?.indexOf(permission) > -1)
},
ensureHttp (s) {
if (s && s.toLowerCase().indexOf('http') === -1) return `http://${s}`
return s