diff --git a/README.md b/README.md index 45006581c..f0688f820 100644 --- a/README.md +++ b/README.md @@ -65,11 +65,18 @@ Change into the new folder. $ cd Ocelot-Social ``` +## Live Demo And Developer Logins + +**Try out our deployed [development environment](https://stage.ocelot.social).** + +Visit our staging networks: + +* central staging network: [stage.ocelot.social](https://stage.ocelot.social) + + ### Login - - -Logins in the browser after the following installations: +Logins for the live demos and developers (local developers after the following installations) in the browser: | email | password | role | | :--- | :--- | :--- | diff --git a/backend/src/middleware/excerptMiddleware.js b/backend/src/middleware/excerptMiddleware.js index 68eea9a74..28b30fb4f 100644 --- a/backend/src/middleware/excerptMiddleware.js +++ b/backend/src/middleware/excerptMiddleware.js @@ -8,7 +8,8 @@ export default { return resolve(root, args, context, info) }, UpdateGroup: async (resolve, root, args, context, info) => { - args.descriptionExcerpt = trunc(args.description, DESCRIPTION_EXCERPT_HTML_LENGTH).html + if (args.description) + args.descriptionExcerpt = trunc(args.description, DESCRIPTION_EXCERPT_HTML_LENGTH).html return resolve(root, args, context, info) }, CreatePost: async (resolve, root, args, context, info) => { diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 728a248fb..906285d12 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -249,6 +249,40 @@ const isMemberOfGroup = rule({ } }) +const canCommentPost = rule({ + cache: 'no_cache', +})(async (_parent, args, { user, driver }) => { + if (!(user && user.id)) return false + const { postId } = args + const userId = user.id + const session = driver.session() + const readTxPromise = session.readTransaction(async (transaction) => { + const transactionResponse = await transaction.run( + ` + MATCH (post:Post { id: $postId }) + OPTIONAL MATCH (post)-[:IN]->(group:Group) + OPTIONAL MATCH (user:User { id: $userId })-[membership:MEMBER_OF]->(group) + RETURN group AS group, membership AS membership + `, + { postId, userId }, + ) + return { + group: transactionResponse.records.map((record) => record.get('group'))[0], + membership: transactionResponse.records.map((record) => record.get('membership'))[0], + } + }) + try { + const { group, membership } = await readTxPromise + return ( + !group || (membership && ['usual', 'admin', 'owner'].includes(membership.properties.role)) + ) + } catch (error) { + throw new Error(error) + } finally { + session.close() + } +}) + const isAuthor = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { @@ -361,7 +395,7 @@ export default shield( unshout: isAuthenticated, changePassword: isAuthenticated, review: isModerator, - CreateComment: isAuthenticated, + CreateComment: and(isAuthenticated, canCommentPost), UpdateComment: isAuthor, DeleteComment: isAuthor, DeleteUser: or(isDeletingOwnAccount, isAdmin), diff --git a/backend/src/middleware/sluggifyMiddleware.js b/backend/src/middleware/sluggifyMiddleware.js index 5ef2944be..bbe47c9aa 100644 --- a/backend/src/middleware/sluggifyMiddleware.js +++ b/backend/src/middleware/sluggifyMiddleware.js @@ -30,18 +30,12 @@ export default { args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Group'))) return resolve(root, args, context, info) }, - UpdateGroup: async (resolve, root, args, context, info) => { - if (args.name) { - args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Group'))) - } - return resolve(root, args, context, info) - }, CreatePost: async (resolve, root, args, context, info) => { args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post'))) return resolve(root, args, context, info) }, UpdatePost: async (resolve, root, args, context, info) => { - // TODO: is this absolutely correct, see condition in 'UpdateGroup' above? may it works accidentally, because args.slug is always send? + // TODO: is this absolutely correct? what happens if "args.title" is not defined? may it works accidentally, because "args.title" or "args.slug" is always send? args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post'))) return resolve(root, args, context, info) }, diff --git a/backend/src/middleware/slugifyMiddleware.spec.js b/backend/src/middleware/slugifyMiddleware.spec.js index 0b022fb53..1fdfb7364 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.js +++ b/backend/src/middleware/slugifyMiddleware.spec.js @@ -213,33 +213,6 @@ describe('slugifyMiddleware', () => { describe('if group exists', () => { describe('if new slug not(!) exists', () => { - describe('setting slug by group name', () => { - it('has the new slug', async () => { - await expect( - mutate({ - mutation: updateGroupMutation(), - variables: { - id: createGroupResult.data.CreateGroup.id, - name: 'My Best Group', - }, - }), - ).resolves.toMatchObject({ - data: { - UpdateGroup: { - name: 'My Best Group', - slug: 'my-best-group', - about: 'Some about', - description: 'Some description' + descriptionAdditional100, - groupType: 'closed', - actionRadius: 'national', - myRole: 'owner', - }, - }, - errors: undefined, - }) - }) - }) - describe('setting slug explicitly', () => { it('has the new slug', async () => { await expect( @@ -284,33 +257,6 @@ describe('slugifyMiddleware', () => { }) }) - describe('setting slug by group name', () => { - it('has unique slug "*-1"', async () => { - await expect( - mutate({ - mutation: updateGroupMutation(), - variables: { - id: createGroupResult.data.CreateGroup.id, - name: 'Pre-Existing Group', - }, - }), - ).resolves.toMatchObject({ - data: { - UpdateGroup: { - name: 'Pre-Existing Group', - slug: 'pre-existing-group-1', - about: 'Some about', - description: 'Some description' + descriptionAdditional100, - groupType: 'closed', - actionRadius: 'national', - myRole: 'owner', - }, - }, - errors: undefined, - }) - }) - }) - describe('setting slug explicitly', () => { it('rejects UpdateGroup', async (done) => { try { diff --git a/backend/src/schema/resolvers/groups.spec.js b/backend/src/schema/resolvers/groups.spec.js index d707440a4..3be8c0f90 100644 --- a/backend/src/schema/resolvers/groups.spec.js +++ b/backend/src/schema/resolvers/groups.spec.js @@ -2714,7 +2714,7 @@ describe('in mode', () => { UpdateGroup: { id: 'my-group', name: 'The New Group For Our Country', - slug: 'the-new-group-for-our-country', // changing the slug is tested in the slugifyMiddleware + slug: 'the-best-group', // changing the slug is tested in the slugifyMiddleware about: 'We will change the land!', description: 'Some country relevant description' + descriptionAdditional100, descriptionExcerpt: diff --git a/backend/src/schema/resolvers/postsInGroups.spec.js b/backend/src/schema/resolvers/postsInGroups.spec.js index d17c928ec..2c1e88b62 100644 --- a/backend/src/schema/resolvers/postsInGroups.spec.js +++ b/backend/src/schema/resolvers/postsInGroups.spec.js @@ -14,6 +14,7 @@ import { profilePagePosts, searchPosts, } from '../../db/graphql/posts' +import { createCommentMutation } from '../../db/graphql/comments' // eslint-disable-next-line no-unused-vars import { DESCRIPTION_WITHOUT_HTML_LENGTH_MIN } from '../../constants/groups' import CONFIG from '../../config' @@ -378,6 +379,170 @@ describe('Posts in Groups', () => { }) }) + describe('commenting posts in groups', () => { + describe('without membership of group', () => { + beforeAll(async () => { + authenticatedUser = await anyUser.toJson() + }) + + it('throws an error for public groups', async () => { + await expect( + mutate({ + mutation: createCommentMutation, + variables: { + postId: 'post-to-public-group', + content: + 'I am commenting a post in a public group without being a member of the group', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([expect.objectContaining({ message: 'Not Authorized!' })]), + }) + }) + + it('throws an error for closed groups', async () => { + await expect( + mutate({ + mutation: createCommentMutation, + variables: { + postId: 'post-to-closed-group', + content: + 'I am commenting a post in a closed group without being a member of the group', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([expect.objectContaining({ message: 'Not Authorized!' })]), + }) + }) + + it('throws an error for hidden groups', async () => { + await expect( + mutate({ + mutation: createCommentMutation, + variables: { + postId: 'post-to-hidden-group', + content: + 'I am commenting a post in a hidden group without being a member of the group', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([expect.objectContaining({ message: 'Not Authorized!' })]), + }) + }) + }) + + describe('as a pending member of group', () => { + beforeAll(async () => { + authenticatedUser = await pendingUser.toJson() + }) + + it('throws an error for public groups', async () => { + await expect( + mutate({ + mutation: createCommentMutation, + variables: { + postId: 'post-to-public-group', + content: 'I am commenting a post in a public group as a pending member of the group', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([expect.objectContaining({ message: 'Not Authorized!' })]), + }) + }) + + it('throws an error for closed groups', async () => { + await expect( + mutate({ + mutation: createCommentMutation, + variables: { + postId: 'post-to-closed-group', + content: 'I am commenting a post in a closed group as a pending member of the group', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([expect.objectContaining({ message: 'Not Authorized!' })]), + }) + }) + + it('throws an error for hidden groups', async () => { + await expect( + mutate({ + mutation: createCommentMutation, + variables: { + postId: 'post-to-hidden-group', + content: 'I am commenting a post in a hidden group as a pending member of the group', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([expect.objectContaining({ message: 'Not Authorized!' })]), + }) + }) + }) + + describe('as a member of group', () => { + beforeAll(async () => { + authenticatedUser = await allGroupsUser.toJson() + }) + + it('comments a post in a public group', async () => { + await expect( + mutate({ + mutation: createCommentMutation, + variables: { + postId: 'post-to-public-group', + content: 'I am commenting a post in a public group as a member of the group', + }, + }), + ).resolves.toMatchObject({ + data: { + CreateComment: { + id: expect.any(String), + }, + }, + errors: undefined, + }) + }) + + it('comments a post in a closed group', async () => { + await expect( + mutate({ + mutation: createCommentMutation, + variables: { + postId: 'post-to-closed-group', + content: 'I am commenting a post in a closed group as a member of the group', + }, + }), + ).resolves.toMatchObject({ + data: { + CreateComment: { + id: expect.any(String), + }, + }, + errors: undefined, + }) + }) + + it('comments a post in a hidden group', async () => { + await expect( + mutate({ + mutation: createCommentMutation, + variables: { + postId: 'post-to-hidden-group', + content: 'I am commenting a post in a hidden group as a member of the group', + }, + }), + ).resolves.toMatchObject({ + data: { + CreateComment: { + id: expect.any(String), + }, + }, + errors: undefined, + }) + }) + }) + }) + describe('visibility of posts', () => { describe('query post by ID', () => { describe('without authentication', () => { diff --git a/webapp/components/Group/GroupForm.vue b/webapp/components/Group/GroupForm.vue index 6604a56d5..7b0f7840c 100644 --- a/webapp/components/Group/GroupForm.vue +++ b/webapp/components/Group/GroupForm.vue @@ -333,10 +333,10 @@ export default { this.$refs.groupForm.update('description', value) }, submit() { - const { name, about, description, groupType, actionRadius, /* locationName, */ categoryIds } = - this.formData + const { name, slug, about, description, groupType, actionRadius, categoryIds } = this.formData const variables = { name, + slug, about, description, groupType, diff --git a/webapp/components/HeaderMenu/HeaderMenu.vue b/webapp/components/HeaderMenu/HeaderMenu.vue index 6fb8ca635..518c36414 100644 --- a/webapp/components/HeaderMenu/HeaderMenu.vue +++ b/webapp/components/HeaderMenu/HeaderMenu.vue @@ -7,7 +7,13 @@ @@ -91,7 +97,23 @@ - + + + + diff --git a/webapp/components/ProgressBar/ProgressBar.vue b/webapp/components/ProgressBar/ProgressBar.vue index dd904e5b1..41183b798 100644 --- a/webapp/components/ProgressBar/ProgressBar.vue +++ b/webapp/components/ProgressBar/ProgressBar.vue @@ -2,7 +2,10 @@
-
+
{{ label }}
@@ -14,6 +17,8 @@ @@ -66,15 +76,22 @@ export default { left: 0px; height: 26px; // styleguide-button-size max-width: 100%; - background: repeating-linear-gradient( - 120deg, - $color-primary 0px, - $color-primary 30px, - $color-primary-light 50px, - $color-primary-light 75px, - $color-primary 95px - ); border-radius: $border-radius-base; + + &.color-uni { + background: $color-primary-light; + } + + &.color-repeating-linear-gradient { + background: repeating-linear-gradient( + 120deg, + $color-primary 0px, + $color-primary 30px, + $color-primary-light 50px, + $color-primary-light 75px, + $color-primary 95px + ); + } } .progress-bar__border { diff --git a/webapp/components/Registration/RegistrationSlideCreate.vue b/webapp/components/Registration/RegistrationSlideCreate.vue index 35bba6614..0906fc8a4 100644 --- a/webapp/components/Registration/RegistrationSlideCreate.vue +++ b/webapp/components/Registration/RegistrationSlideCreate.vue @@ -6,6 +6,7 @@ {{ $t('components.registration.create-user-account.success') }} +
@@ -21,6 +22,7 @@ {{ $t('site.back-to-login') }} +
diff --git a/webapp/components/Registration/RegistrationSlideEmail.vue b/webapp/components/Registration/RegistrationSlideEmail.vue index 6ba443996..6d6454ac9 100644 --- a/webapp/components/Registration/RegistrationSlideEmail.vue +++ b/webapp/components/Registration/RegistrationSlideEmail.vue @@ -24,6 +24,7 @@ {{ $t('components.registration.email.form.sendEmailAgain') }} + diff --git a/webapp/components/Registration/RegistrationSlideInvite.vue b/webapp/components/Registration/RegistrationSlideInvite.vue index 48c62d412..723071510 100644 --- a/webapp/components/Registration/RegistrationSlideInvite.vue +++ b/webapp/components/Registration/RegistrationSlideInvite.vue @@ -17,6 +17,7 @@ {{ $t('components.registration.invite-code.form.description') }} + diff --git a/webapp/components/Registration/RegistrationSlideNoPublic.vue b/webapp/components/Registration/RegistrationSlideNoPublic.vue index 1d26cc8e2..c5f815ef4 100644 --- a/webapp/components/Registration/RegistrationSlideNoPublic.vue +++ b/webapp/components/Registration/RegistrationSlideNoPublic.vue @@ -2,6 +2,7 @@ + diff --git a/webapp/components/Registration/RegistrationSlideNonce.vue b/webapp/components/Registration/RegistrationSlideNonce.vue index b9de7afd1..604d9b132 100644 --- a/webapp/components/Registration/RegistrationSlideNonce.vue +++ b/webapp/components/Registration/RegistrationSlideNonce.vue @@ -19,6 +19,7 @@ {{ $t('components.registration.email-nonce.form.description') }} + diff --git a/webapp/constants/donation.js b/webapp/constants/donation.js new file mode 100644 index 000000000..3e36ae9a8 --- /dev/null +++ b/webapp/constants/donation.js @@ -0,0 +1 @@ +export const PROGRESS_BAR_COLOR_TYPE = 'gradient' // 'uni' is the other option diff --git a/webapp/graphql/groups.js b/webapp/graphql/groups.js index e3510c74d..72e49ebe3 100644 --- a/webapp/graphql/groups.js +++ b/webapp/graphql/groups.js @@ -93,7 +93,9 @@ export const updateGroupMutation = () => { name icon } - # avatar # test this as result + avatar { + url + } locationName myRole }