diff --git a/backend/package.json b/backend/package.json index a3f03f1fb..beb46da10 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,8 +10,8 @@ "dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js -e js,graphql", "lint": "eslint src --config .eslintrc.js", "test": "run-s test:jest test:cucumber", - "test:before:server": "cross-env GRAPHQL_URI=http://localhost:4123 GRAPHQL_PORT=4123 yarn run dev", - "test:before:seeder": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 DISABLED_MIDDLEWARES=permissions,activityPub yarn run dev", + "test:before:server": "cross-env GRAPHQL_URI=http://localhost:4123 GRAPHQL_PORT=4123 yarn run dev 2> /dev/null", + "test:before:seeder": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 DISABLED_MIDDLEWARES=permissions,activityPub yarn run dev 2> /dev/null", "test:jest:cmd": "wait-on tcp:4001 tcp:4123 && jest --forceExit --detectOpenHandles --runInBand", "test:cucumber:cmd": "wait-on tcp:4001 tcp:4123 && cucumber-js --require-module @babel/register --exit test/", "test:jest:cmd:debug": "wait-on tcp:4001 tcp:4123 && node --inspect-brk ./node_modules/.bin/jest -i --forceExit --detectOpenHandles --runInBand", @@ -94,4 +94,4 @@ "nodemon": "~1.18.11", "supertest": "~4.0.2" } -} \ No newline at end of file +} diff --git a/backend/src/graphql-schema.js b/backend/src/graphql-schema.js index c17b967d2..1e13c95f4 100644 --- a/backend/src/graphql-schema.js +++ b/backend/src/graphql-schema.js @@ -7,6 +7,7 @@ import reports from './resolvers/reports.js' import posts from './resolvers/posts.js' import moderation from './resolvers/moderation.js' import rewards from './resolvers/rewards.js' +import socialMedia from './resolvers/socialMedia.js' import notifications from './resolvers/notifications' export const typeDefs = fs @@ -27,6 +28,7 @@ export const resolvers = { ...posts.Mutation, ...moderation.Mutation, ...rewards.Mutation, + ...socialMedia.Mutation, ...notifications.Mutation } } diff --git a/backend/src/middleware/index.js b/backend/src/middleware/index.js index 8d893a78b..e6759e8ff 100644 --- a/backend/src/middleware/index.js +++ b/backend/src/middleware/index.js @@ -10,12 +10,14 @@ import permissionsMiddleware from './permissionsMiddleware' import userMiddleware from './userMiddleware' import includedFieldsMiddleware from './includedFieldsMiddleware' import orderByMiddleware from './orderByMiddleware' +import validUrlMiddleware from './validUrlMiddleware' import notificationsMiddleware from './notificationsMiddleware' export default schema => { let middleware = [ passwordMiddleware, dateTimeMiddleware, + validUrlMiddleware, sluggifyMiddleware, excerptMiddleware, xssMiddleware, diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 549499dcd..3ac43a6e2 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -74,6 +74,7 @@ const permissions = shield({ UpdateBadge: isAdmin, DeleteBadge: isAdmin, AddUserBadges: isAdmin, + CreateSocialMedia: isAuthenticated, // AddBadgeRewarded: isAdmin, // RemoveBadgeRewarded: isAdmin, reward: isAdmin, diff --git a/backend/src/middleware/validUrlMiddleware.js b/backend/src/middleware/validUrlMiddleware.js new file mode 100644 index 000000000..37f06199c --- /dev/null +++ b/backend/src/middleware/validUrlMiddleware.js @@ -0,0 +1,18 @@ +const validURL = str => { + const isValid = str.match(/^(?:https?:\/\/)(?:[^@\n])?(?:www\.)?([^:/\n?]+)/g) + return !!isValid +} + +export default { + Mutation: { + CreateSocialMedia: async (resolve, root, args, context, info) => { + let socialMedia + if (validURL(args.url)) { + socialMedia = await resolve(root, args, context, info) + } else { + throw Error('Input is not a URL') + } + return socialMedia + } + } +} diff --git a/backend/src/resolvers/socialMedia.js b/backend/src/resolvers/socialMedia.js new file mode 100644 index 000000000..3adf0e2d0 --- /dev/null +++ b/backend/src/resolvers/socialMedia.js @@ -0,0 +1,21 @@ +import { neo4jgraphql } from 'neo4j-graphql-js' + +export default { + Mutation: { + CreateSocialMedia: async (object, params, context, resolveInfo) => { + const socialMedia = await neo4jgraphql(object, params, context, resolveInfo, true) + const session = context.driver.session() + await session.run( + `MATCH (owner:User {id: $userId}), (socialMedia:SocialMedia {id: $socialMediaId}) + MERGE (socialMedia)<-[:OWNED]-(owner) + RETURN owner`, { + userId: context.user.id, + socialMediaId: socialMedia.id + } + ) + session.close() + + return socialMedia + } + } +} diff --git a/backend/src/resolvers/socialMedia.spec.js b/backend/src/resolvers/socialMedia.spec.js new file mode 100644 index 000000000..b97316543 --- /dev/null +++ b/backend/src/resolvers/socialMedia.spec.js @@ -0,0 +1,49 @@ +import Factory from '../seed/factories' +import { GraphQLClient } from 'graphql-request' +import { host, login } from '../jest/helpers' + +const factory = Factory() + +describe('CreateSocialMedia', () => { + let client + let headers + const mutation = ` + mutation($url: String!) { + CreateSocialMedia(url: $url) { + url + } + } + ` + beforeEach(async () => { + await factory.create('User', { + avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg', + id: 'acb2d923-f3af-479e-9f00-61b12e864666', + name: 'Matilde Hermiston', + slug: 'matilde-hermiston', + role: 'user', + email: 'test@example.org', + password: '1234' + }) + }) + + afterEach(async () => { + await factory.cleanDatabase() + }) + + describe('authenticated', () => { + beforeEach(async () => { + headers = await login({ email: 'test@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('rejects empty string', async () => { + const variables = { url: '' } + await expect(client.request(mutation, variables)).rejects.toThrow('Input is not a URL') + }) + + it('validates URLs', async () => { + const variables = { url: 'not-a-url' } + await expect(client.request(mutation, variables)).rejects.toThrow('Input is not a URL') + }) + }) +}) diff --git a/backend/src/schema.graphql b/backend/src/schema.graphql index db468471a..94e28d0d7 100644 --- a/backend/src/schema.graphql +++ b/backend/src/schema.graphql @@ -128,6 +128,7 @@ type User { location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") locationName: String about: String + socialMedia: [SocialMedia]! @relation(name: "OWNED", direction: "OUT") createdAt: String updatedAt: String @@ -318,3 +319,10 @@ type SharedInboxEndpoint { id: ID! uri: String } + +type SocialMedia { + id: ID! + url: String + ownedBy: [User]! @relation(name: "OWNED", direction: "IN") +} + diff --git a/cypress/integration/common/settings.js b/cypress/integration/common/settings.js index 3aa6022a8..e1f3cc5a8 100644 --- a/cypress/integration/common/settings.js +++ b/cypress/integration/common/settings.js @@ -61,3 +61,52 @@ Then( 'I can see my new name {string} when I click on my profile picture in the top right', name => matchNameInUserMenu(name) ) + +When('I click on the {string} link', link => { + cy.get('a') + .contains(link) + .click() +}) + +Then('I should be on the {string} page', page => { + cy.location() + .should(loc => { + expect(loc.pathname).to.eq(page) + }) + .get('h3') + .should('contain', 'Social media') +}) + +Then('I add a social media link', () => { + cy.get("input[name='social-media']") + .type('https://freeradical.zone/peter-pan') + .get('button') + .contains('Add link') + .click() +}) + +Then('it gets saved successfully', () => { + cy.get('.iziToast-message') + .should('contain', 'Updated user') +}) + +Then('the new social media link shows up on the page', () => { + cy.get('a[href="https://freeradical.zone/peter-pan"]') + .should('have.length', 1) +}) + +Given('I have added a social media link', () => { + cy.openPage('/settings/my-social-media') + .get("input[name='social-media']") + .type('https://freeradical.zone/peter-pan') + .get('button') + .contains('Add link') + .click() +}) + +Then('they should be able to see my social media links', () => { + cy.get('.ds-card-content') + .contains('Where else can I find Peter Pan?') + .get('a[href="https://freeradical.zone/peter-pan"]') + .should('have.length', 1) +}) diff --git a/cypress/integration/user_profile/SocialMedia.feature b/cypress/integration/user_profile/SocialMedia.feature new file mode 100644 index 000000000..988923c17 --- /dev/null +++ b/cypress/integration/user_profile/SocialMedia.feature @@ -0,0 +1,21 @@ +Feature: List Social Media Accounts + As a User + I'd like to enter my social media + So I can show them to other users to get in contact + + Background: + Given I have a user account + And I am logged in + + Scenario: Adding Social Media + Given I am on the "settings" page + And I click on the "Social media" link + Then I should be on the "/settings/my-social-media" page + When I add a social media link + Then it gets saved successfully + And the new social media link shows up on the page + + Scenario: Other user's viewing my Social Media + Given I have added a social media link + When people visit my profile page + Then they should be able to see my social media links diff --git a/webapp/graphql/UserProfileQuery.js b/webapp/graphql/UserProfileQuery.js index f0d7720ae..16e7e1440 100644 --- a/webapp/graphql/UserProfileQuery.js +++ b/webapp/graphql/UserProfileQuery.js @@ -98,6 +98,10 @@ export default app => { } } } + socialMedia { + id + url + } } } `) diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 6e47d7122..94ca1ac1b 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -15,7 +15,8 @@ "followers": "Folgen", "following": "Folgt", "shouted": "Empfohlen", - "commented": "Kommentiert" + "commented": "Kommentiert", + "socialMedia": "Wo sonst finde ich" }, "search": { "placeholder": "Suchen", @@ -51,6 +52,11 @@ }, "languages": { "name": "Sprachen" + }, + "social-media": { + "name": "Soziale Medien", + "submit": "Link hinzufügen", + "success": "Profil aktualisiert" } }, "admin": { diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 62c8f3e19..40cc766d0 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -15,7 +15,8 @@ "followers": "Followers", "following": "Following", "shouted": "Shouted", - "commented": "Commented" + "commented": "Commented", + "socialMedia": "Where else can I find" }, "search": { "placeholder": "Search", @@ -51,6 +52,11 @@ }, "languages": { "name": "Languages" + }, + "social-media": { + "name": "Social media", + "submit": "Add link", + "success": "Updated user profile" } }, "admin": { diff --git a/webapp/pages/profile/_id/_slug.vue b/webapp/pages/profile/_id/_slug.vue index 2eed6ac04..a70058b06 100644 --- a/webapp/pages/profile/_id/_slug.vue +++ b/webapp/pages/profile/_id/_slug.vue @@ -202,6 +202,37 @@
+