web/backend support for group member permissions

This commit is contained in:
Will Webberley 2024-10-23 20:12:56 +01:00
parent 980a5bb14b
commit a8a000ae55
6 changed files with 76 additions and 41 deletions
api/api
web/src

View File

@ -9,6 +9,17 @@ from api import uploads
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:
@ -95,11 +106,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,
@ -163,11 +176,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)
)
@ -216,7 +231,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})
@ -224,6 +239,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,
@ -351,11 +368,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}
@ -393,11 +412,13 @@ def delete_member(user, id, 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

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

@ -34,26 +34,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') &&
<>
{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))} />}
<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

@ -44,9 +44,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 => {
@ -95,47 +92,46 @@ function Group () {
<Card.Content>{group.description}</Card.Content>}
{group.closed &&
<Card.Content>
{utils.isInGroup(user, group._id) &&
<div>
<Button color='yellow' basic size='tiny' fluid icon='check' content='Member' onClick={leave} style={{ marginBottom: 5 }} />
</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.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>
{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) &&
<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' />}
<>
{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>
{group.adminUsers && group.adminUsers.map(a =>
<UserChip user={a} key={a._id} />
)}
</Card.Content>
</Card>
<HelpLink link='/docs/groups#a-tour-around-your-new-group' />

View File

@ -13,7 +13,6 @@ const PERMISSIONS = [
{ 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' },
{ name: 'postProjects', label: 'Allow members to link projects to the group' }
]
function Settings () {

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