diff --git a/.gitignore b/.gitignore index cafaf74a0..7d30b9530 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .env .idea +*.iml .vscode .DS_Store npm-debug.log* diff --git a/backend/package.json b/backend/package.json index 2833f3e18..6bd5db20f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,7 +14,7 @@ "lint": "eslint src --config .eslintrc.js", "test": "nyc --reporter=text-lcov yarn test:jest", "test:cypress": "run-p --race test:before:*", - "test:before:server": "cross-env GRAPHQL_URI=http://localhost:4123 GRAPHQL_PORT=4123 babel-node src/ 2> /dev/null", + "test:before:server": "cross-env CLIENT_URI=http://localhost:4123 GRAPHQL_URI=http://localhost:4123 GRAPHQL_PORT=4123 babel-node src/ 2> /dev/null", "test:before:seeder": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 PERMISSIONS=disabled babel-node src/ 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/", @@ -58,7 +58,7 @@ "graphql-shield": "~5.3.1", "graphql-tag": "~2.10.1", "graphql-yoga": "~1.17.4", - "helmet": "~3.15.1", + "helmet": "~3.16.0", "jsonwebtoken": "~8.5.1", "linkifyjs": "~2.1.8", "lodash": "~4.17.11", @@ -76,7 +76,7 @@ }, "devDependencies": { "@babel/cli": "~7.2.3", - "@babel/core": "~7.3.4", + "@babel/core": "~7.4.0", "@babel/node": "~7.2.2", "@babel/plugin-proposal-throw-expressions": "^7.2.0", "@babel/preset-env": "~7.4.2", diff --git a/backend/src/activitypub/ActivityPub.js b/backend/src/activitypub/ActivityPub.js index bcebf4d49..6c3fb4082 100644 --- a/backend/src/activitypub/ActivityPub.js +++ b/backend/src/activitypub/ActivityPub.js @@ -22,9 +22,8 @@ let activityPub = null export { activityPub } export default class ActivityPub { - constructor (domain, port, uri) { - if (domain === 'localhost') { this.domain = `${domain}:${port}` } else { this.domain = domain } - this.port = port + constructor (host, uri) { + this.host = host this.dataSource = new NitroDataSource(uri) this.collections = new Collections(this.dataSource) } @@ -32,8 +31,8 @@ export default class ActivityPub { static init (server) { if (!activityPub) { dotenv.config() - const url = new URL(process.env.GRAPHQL_URI) - activityPub = new ActivityPub(url.hostname || 'localhost', url.port || 4000, url.origin) + const url = new URL(process.env.CLIENT_URI) + activityPub = new ActivityPub(url.host || 'localhost:4000', url.origin) // integrate into running graphql express server server.express.set('ap', activityPub) @@ -59,7 +58,7 @@ export default class ActivityPub { } }, async (err, response, toActorObject) => { if (err) return reject(err) - debug(`name = ${toActorName}@${this.domain}`) + debug(`name = ${toActorName}@${this.host}`) // save shared inbox toActorObject = JSON.parse(toActorObject) await this.dataSource.addSharedInboxEndpoint(toActorObject.endpoints.sharedInbox) @@ -184,7 +183,7 @@ export default class ActivityPub { } generateStatusId (slug) { - return `http://${this.domain}/activitypub/users/${slug}/status/${uuid()}` + return `https://${this.host}/activitypub/users/${slug}/status/${uuid()}` } async sendActivity (activity) { diff --git a/backend/src/activitypub/utils/activity.js b/backend/src/activitypub/utils/activity.js index 53bcd37ea..a8c525302 100644 --- a/backend/src/activitypub/utils/activity.js +++ b/backend/src/activitypub/utils/activity.js @@ -11,14 +11,14 @@ export function createNoteObject (text, name, id, published) { return { '@context': 'https://www.w3.org/ns/activitystreams', - 'id': `https://${activityPub.domain}/activitypub/users/${name}/status/${createUuid}`, + 'id': `https://${activityPub.host}/activitypub/users/${name}/status/${createUuid}`, 'type': 'Create', - 'actor': `https://${activityPub.domain}/activitypub/users/${name}`, + 'actor': `https://${activityPub.host}/activitypub/users/${name}`, 'object': { - 'id': `https://${activityPub.domain}/activitypub/users/${name}/status/${id}`, + 'id': `https://${activityPub.host}/activitypub/users/${name}/status/${id}`, 'type': 'Note', 'published': published, - 'attributedTo': `https://${activityPub.domain}/activitypub/users/${name}`, + 'attributedTo': `https://${activityPub.host}/activitypub/users/${name}`, 'content': text, 'to': 'https://www.w3.org/ns/activitystreams#Public' } @@ -64,8 +64,8 @@ export async function getActorId (name) { export function sendAcceptActivity (theBody, name, targetDomain, url) { as.accept() - .id(`https://${activityPub.domain}/activitypub/users/${name}/status/` + crypto.randomBytes(16).toString('hex')) - .actor(`https://${activityPub.domain}/activitypub/users/${name}`) + .id(`https://${activityPub.host}/activitypub/users/${name}/status/` + crypto.randomBytes(16).toString('hex')) + .actor(`https://${activityPub.host}/activitypub/users/${name}`) .object(theBody) .prettyWrite((err, doc) => { if (!err) { @@ -79,8 +79,8 @@ export function sendAcceptActivity (theBody, name, targetDomain, url) { export function sendRejectActivity (theBody, name, targetDomain, url) { as.reject() - .id(`https://${activityPub.domain}/activitypub/users/${name}/status/` + crypto.randomBytes(16).toString('hex')) - .actor(`https://${activityPub.domain}/activitypub/users/${name}`) + .id(`https://${activityPub.host}/activitypub/users/${name}/status/` + crypto.randomBytes(16).toString('hex')) + .actor(`https://${activityPub.host}/activitypub/users/${name}`) .object(theBody) .prettyWrite((err, doc) => { if (!err) { diff --git a/backend/src/activitypub/utils/actor.js b/backend/src/activitypub/utils/actor.js index 0b8cc7454..0a3884023 100644 --- a/backend/src/activitypub/utils/actor.js +++ b/backend/src/activitypub/utils/actor.js @@ -6,21 +6,21 @@ export function createActor (name, pubkey) { 'https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1' ], - 'id': `https://${activityPub.domain}/activitypub/users/${name}`, + 'id': `https://${activityPub.host}/activitypub/users/${name}`, 'type': 'Person', 'preferredUsername': `${name}`, 'name': `${name}`, - 'following': `https://${activityPub.domain}/activitypub/users/${name}/following`, - 'followers': `https://${activityPub.domain}/activitypub/users/${name}/followers`, - 'inbox': `https://${activityPub.domain}/activitypub/users/${name}/inbox`, - 'outbox': `https://${activityPub.domain}/activitypub/users/${name}/outbox`, - 'url': `https://${activityPub.domain}/activitypub/@${name}`, + 'following': `https://${activityPub.host}/activitypub/users/${name}/following`, + 'followers': `https://${activityPub.host}/activitypub/users/${name}/followers`, + 'inbox': `https://${activityPub.host}/activitypub/users/${name}/inbox`, + 'outbox': `https://${activityPub.host}/activitypub/users/${name}/outbox`, + 'url': `https://${activityPub.host}/activitypub/@${name}`, 'endpoints': { - 'sharedInbox': `https://${activityPub.domain}/activitypub/inbox` + 'sharedInbox': `https://${activityPub.host}/activitypub/inbox` }, 'publicKey': { - 'id': `https://${activityPub.domain}/activitypub/users/${name}#main-key`, - 'owner': `https://${activityPub.domain}/activitypub/users/${name}`, + 'id': `https://${activityPub.host}/activitypub/users/${name}#main-key`, + 'owner': `https://${activityPub.host}/activitypub/users/${name}`, 'publicKeyPem': pubkey } } @@ -28,12 +28,12 @@ export function createActor (name, pubkey) { export function createWebFinger (name) { return { - 'subject': `acct:${name}@${activityPub.domain}`, + 'subject': `acct:${name}@${activityPub.host}`, 'links': [ { 'rel': 'self', 'type': 'application/activity+json', - 'href': `https://${activityPub.domain}/users/${name}` + 'href': `https://${activityPub.host}/activitypub/users/${name}` } ] } diff --git a/backend/src/activitypub/utils/collection.js b/backend/src/activitypub/utils/collection.js index 4c46adbde..61670bb47 100644 --- a/backend/src/activitypub/utils/collection.js +++ b/backend/src/activitypub/utils/collection.js @@ -5,10 +5,10 @@ const debug = require('debug')('ea:utils:collections') export function createOrderedCollection (name, collectionName) { return { '@context': 'https://www.w3.org/ns/activitystreams', - 'id': `https://${activityPub.domain}/activitypub/users/${name}/${collectionName}`, + 'id': `https://${activityPub.host}/activitypub/users/${name}/${collectionName}`, 'summary': `${name}s ${collectionName} collection`, 'type': 'OrderedCollection', - 'first': `https://${activityPub.domain}/activitypub/users/${name}/${collectionName}?page=true`, + 'first': `https://${activityPub.host}/activitypub/users/${name}/${collectionName}?page=true`, 'totalItems': 0 } } @@ -16,11 +16,11 @@ export function createOrderedCollection (name, collectionName) { export function createOrderedCollectionPage (name, collectionName) { return { '@context': 'https://www.w3.org/ns/activitystreams', - 'id': `https://${activityPub.domain}/activitypub/users/${name}/${collectionName}?page=true`, + 'id': `https://${activityPub.host}/activitypub/users/${name}/${collectionName}?page=true`, 'summary': `${name}s ${collectionName} collection`, 'type': 'OrderedCollectionPage', 'totalItems': 0, - 'partOf': `https://${activityPub.domain}/activitypub/users/${name}/${collectionName}`, + 'partOf': `https://${activityPub.host}/activitypub/users/${name}/${collectionName}`, 'orderedItems': [] } } diff --git a/backend/src/activitypub/utils/index.js b/backend/src/activitypub/utils/index.js index c48e15e3d..4ae2b073f 100644 --- a/backend/src/activitypub/utils/index.js +++ b/backend/src/activitypub/utils/index.js @@ -20,12 +20,12 @@ export function extractIdFromActivityId (uri) { return splitted[splitted.indexOf('status') + 1] } -export function constructIdFromName (name, fromDomain = activityPub.domain) { +export function constructIdFromName (name, fromDomain = activityPub.host) { return `http://${fromDomain}/activitypub/users/${name}` } export function extractDomainFromUrl (url) { - return new URL(url).hostname + return new URL(url).host } export function throwErrorIfApolloErrorOccurred (result) { @@ -76,7 +76,7 @@ export function signAndSend (activity, fromName, targetDomain, url) { 'Host': targetDomain, 'Date': date, 'Signature': createSignature({ privateKey, - keyId: `http://${activityPub.domain}/activitypub/users/${fromName}#main-key`, + keyId: `http://${activityPub.host}/activitypub/users/${fromName}#main-key`, url, headers: { 'Host': targetDomain, diff --git a/backend/src/index.js b/backend/src/index.js index c91f3e9be..7ed7bf70a 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -12,6 +12,6 @@ const serverConfig = { const server = createServer() server.start(serverConfig, options => { /* eslint-disable-next-line no-console */ - console.log(`Server ready at ${process.env.GRAPHQL_URI} 🚀`) + console.log(`Server ready at ${process.env.CLIENT_URI} 🚀`) ActivityPub.init(server) }) diff --git a/backend/src/middleware/activityPubMiddleware.js b/backend/src/middleware/activityPubMiddleware.js index 6c737faff..dcb5ae93c 100644 --- a/backend/src/middleware/activityPubMiddleware.js +++ b/backend/src/middleware/activityPubMiddleware.js @@ -49,7 +49,7 @@ export default { CreateUser: async (resolve, root, args, context, info) => { const keys = generateRsaKeyPair() Object.assign(args, keys) - args.actorId = `${process.env.GRAPHQL_URI}/activitypub/users/${args.slug}` + args.actorId = `${activityPub.host}/activitypub/users/${args.slug}` return resolve(root, args, context, info) } } diff --git a/backend/test/features/webfinger.feature b/backend/test/features/webfinger.feature index e7d1ace04..c9f9e587b 100644 --- a/backend/test/features/webfinger.feature +++ b/backend/test/features/webfinger.feature @@ -19,7 +19,7 @@ Feature: Webfinger discovery { "rel": "self", "type": "application/activity+json", - "href": "https://localhost:4123/users/peter-lustiger" + "href": "https://localhost:4123/activitypub/users/peter-lustiger" } ] } diff --git a/backend/yarn.lock b/backend/yarn.lock index 7341dbc83..8df148093 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -38,18 +38,18 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/core@^7.1.0": - version "7.3.3" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.3.3.tgz#d090d157b7c5060d05a05acaebc048bd2b037947" - integrity sha512-w445QGI2qd0E0GlSnq6huRZWPMmQGCp5gd5ZWS4hagn0EiwzxD5QMFkpchyusAyVC1n27OKXzQ0/88aVU9n4xQ== +"@babel/core@^7.1.0", "@babel/core@~7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.0.tgz#248fd6874b7d755010bfe61f557461d4f446d9e9" + integrity sha512-Dzl7U0/T69DFOTwqz/FJdnOSWS57NpjNfCwMKHABr589Lg8uX1RrlBIJ7L5Dubt/xkLsx0xH5EBFzlBVes1ayA== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.3.3" - "@babel/helpers" "^7.2.0" - "@babel/parser" "^7.3.3" - "@babel/template" "^7.2.2" - "@babel/traverse" "^7.2.2" - "@babel/types" "^7.3.3" + "@babel/generator" "^7.4.0" + "@babel/helpers" "^7.4.0" + "@babel/parser" "^7.4.0" + "@babel/template" "^7.4.0" + "@babel/traverse" "^7.4.0" + "@babel/types" "^7.4.0" convert-source-map "^1.1.0" debug "^4.1.0" json5 "^2.1.0" @@ -58,49 +58,7 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/core@~7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.3.4.tgz#921a5a13746c21e32445bf0798680e9d11a6530b" - integrity sha512-jRsuseXBo9pN197KnDwhhaaBzyZr2oIcLHHTt2oDdQrej5Qp57dCCJafWx5ivU8/alEYDpssYqv1MUqcxwQlrA== - dependencies: - "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.3.4" - "@babel/helpers" "^7.2.0" - "@babel/parser" "^7.3.4" - "@babel/template" "^7.2.2" - "@babel/traverse" "^7.3.4" - "@babel/types" "^7.3.4" - convert-source-map "^1.1.0" - debug "^4.1.0" - json5 "^2.1.0" - lodash "^4.17.11" - resolve "^1.3.2" - semver "^5.4.1" - source-map "^0.5.0" - -"@babel/generator@^7.0.0", "@babel/generator@^7.2.2", "@babel/generator@^7.3.3": - version "7.3.3" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.3.tgz#185962ade59a52e00ca2bdfcfd1d58e528d4e39e" - integrity sha512-aEADYwRRZjJyMnKN7llGIlircxTCofm3dtV5pmY6ob18MSIuipHpA2yZWkPlycwu5HJcx/pADS3zssd8eY7/6A== - dependencies: - "@babel/types" "^7.3.3" - jsesc "^2.5.1" - lodash "^4.17.11" - source-map "^0.5.0" - trim-right "^1.0.1" - -"@babel/generator@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.4.tgz#9aa48c1989257877a9d971296e5b73bfe72e446e" - integrity sha512-8EXhHRFqlVVWXPezBW5keTiQi/rJMQTg/Y9uVCEZ0CAF3PKtCCaVRnp64Ii1ujhkoDhhF1fVsImoN4yJ2uz4Wg== - dependencies: - "@babel/types" "^7.3.4" - jsesc "^2.5.1" - lodash "^4.17.11" - source-map "^0.5.0" - trim-right "^1.0.1" - -"@babel/generator@^7.4.0": +"@babel/generator@^7.0.0", "@babel/generator@^7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.4.0.tgz#c230e79589ae7a729fd4631b9ded4dc220418196" integrity sha512-/v5I+a1jhGSKLgZDcmAUZ4K/VePi43eRkUs3yePW1HB1iANOD5tqJXwGSG4BZhSksP8J9ejSlwGeTiiOFZOrXQ== @@ -283,14 +241,14 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.2.0" -"@babel/helpers@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.2.0.tgz#8335f3140f3144270dc63c4732a4f8b0a50b7a21" - integrity sha512-Fr07N+ea0dMcMN8nFpuK6dUIT7/ivt9yKQdEEnjVS83tG2pHwPi03gYmk/tyuwONnZ+sY+GFFPlWGgCtW1hF9A== +"@babel/helpers@^7.4.0": + version "7.4.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.4.2.tgz#3bdfa46a552ca77ef5a0f8551be5f0845ae989be" + integrity sha512-gQR1eQeroDzFBikhrCccm5Gs2xBjZ57DNjGbqTaHo911IpmSxflOQWMAHPw/TXk8L3isv7s9lYzUkexOeTQUYg== dependencies: - "@babel/template" "^7.1.2" - "@babel/traverse" "^7.1.5" - "@babel/types" "^7.2.0" + "@babel/template" "^7.4.0" + "@babel/traverse" "^7.4.0" + "@babel/types" "^7.4.0" "@babel/highlight@^7.0.0": version "7.0.0" @@ -312,17 +270,7 @@ lodash "^4.17.10" v8flags "^3.1.1" -"@babel/parser@^7.0.0", "@babel/parser@^7.2.2", "@babel/parser@^7.2.3", "@babel/parser@^7.3.3": - version "7.3.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.3.tgz#092d450db02bdb6ccb1ca8ffd47d8774a91aef87" - integrity sha512-xsH1CJoln2r74hR+y7cg2B5JCPaTh+Hd+EbBRk9nWGSNspuo6krjhX0Om6RnRQuIvFq8wVXCLKH3kwKDYhanSg== - -"@babel/parser@^7.1.0", "@babel/parser@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.4.tgz#a43357e4bbf4b92a437fb9e465c192848287f27c" - integrity sha512-tXZCqWtlOOP4wgCp6RjRvLmfuhnqTLy9VHwRochJBCP2nDm27JnnuFEnXFASVyQNHk36jD1tAammsCEEqgscIQ== - -"@babel/parser@^7.4.0": +"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.4.0": version "7.4.2" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.2.tgz#b4521a400cb5a871eab3890787b4bc1326d38d91" integrity sha512-9fJTDipQFvlfSVdD/JBtkiY0br9BtfvW2R8wo6CX/Ej2eMuV0gWPk1M67Mt3eggQvBqYW1FCEk8BN7WvGm/g5g== @@ -720,46 +668,16 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.1.2", "@babel/template@^7.2.2": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907" - integrity sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g== +"@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.0.tgz#12474e9c077bae585c5d835a95c0b0b790c25c8b" + integrity sha512-SOWwxxClTTh5NdbbYZ0BmaBVzxzTh2tO/TeLTbF6MO6EzVhHTnff8CdBXx3mEtazFBoysmEM6GU/wF+SuSx4Fw== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/parser" "^7.2.2" - "@babel/types" "^7.2.2" + "@babel/parser" "^7.4.0" + "@babel/types" "^7.4.0" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.2.2": - version "7.2.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.2.3.tgz#7ff50cefa9c7c0bd2d81231fdac122f3957748d8" - integrity sha512-Z31oUD/fJvEWVR0lNZtfgvVt512ForCTNKYcJBGbPb1QZfve4WGH8Wsy7+Mev33/45fhP/hwQtvgusNdcCMgSw== - dependencies: - "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.2.2" - "@babel/helper-function-name" "^7.1.0" - "@babel/helper-split-export-declaration" "^7.0.0" - "@babel/parser" "^7.2.3" - "@babel/types" "^7.2.2" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.10" - -"@babel/traverse@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.3.4.tgz#1330aab72234f8dea091b08c4f8b9d05c7119e06" - integrity sha512-TvTHKp6471OYEcE/91uWmhR6PrrYywQntCHSaZ8CM8Vmp+pjAusal4nGB2WCCQd0rvI7nOMKn9GnbcvTUz3/ZQ== - dependencies: - "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.3.4" - "@babel/helper-function-name" "^7.1.0" - "@babel/helper-split-export-declaration" "^7.0.0" - "@babel/parser" "^7.3.4" - "@babel/types" "^7.3.4" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.11" - -"@babel/traverse@^7.4.0": +"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.0.tgz#14006967dd1d2b3494cdd650c686db9daf0ddada" integrity sha512-/DtIHKfyg2bBKnIN+BItaIlEg5pjAnzHOIQe5w+rHAw/rg9g0V7T4rqPX8BJPfW11kt3koyjAnTNwCzb28Y1PA== @@ -774,7 +692,7 @@ globals "^11.1.0" lodash "^4.17.11" -"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.3.4", "@babel/types@^7.4.0": +"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.0.tgz#670724f77d24cce6cc7d8cf64599d511d164894c" integrity sha512-aPvkXyU2SPOnztlgo8n9cEiXW755mgyvueUPcpStqdzoSPm0fjO0vQBjLkt3JKJW7ufikfcnMTTPsN1xaTsBPA== @@ -4011,10 +3929,10 @@ helmet-csp@2.7.1: dasherize "2.0.0" platform "1.3.5" -helmet@~3.15.1: - version "3.15.1" - resolved "https://registry.yarnpkg.com/helmet/-/helmet-3.15.1.tgz#2c80d1a59138b6f23929605afca4b1c88b3298ec" - integrity sha512-hgoNe/sjKlKNvJ3g9Gz149H14BjMMWOCmW/DTXl7IfyKGtIK37GePwZrHNfr4aPXdKVyXcTj26RgRFbPKDy9lw== +helmet@~3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/helmet/-/helmet-3.16.0.tgz#7df41a4bfe4c83d90147c1e30d70893f92a9d97c" + integrity sha512-rsTKRogc5OYGlvSHuq5QsmOsOzF6uDoMqpfh+Np8r23+QxDq+SUx90Rf8HyIKQVl7H6NswZEwfcykinbAeZ6UQ== dependencies: depd "2.0.0" dns-prefetch-control "0.1.0" @@ -4026,8 +3944,8 @@ helmet@~3.15.1: helmet-csp "2.7.1" hide-powered-by "1.0.0" hpkp "2.0.0" - hsts "2.1.0" - ienoopen "1.0.0" + hsts "2.2.0" + ienoopen "1.1.0" nocache "2.0.0" referrer-policy "1.1.0" x-xss-protection "1.1.0" @@ -4064,10 +3982,12 @@ hpkp@2.0.0: resolved "https://registry.yarnpkg.com/hpkp/-/hpkp-2.0.0.tgz#10e142264e76215a5d30c44ec43de64dee6d1672" integrity sha1-EOFCJk52IVpdMMROxD3mTe5tFnI= -hsts@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/hsts/-/hsts-2.1.0.tgz#cbd6c918a2385fee1dd5680bfb2b3a194c0121cc" - integrity sha512-zXhh/DqgrTXJ7erTN6Fh5k/xjMhDGXCqdYN3wvxUvGUQvnxcFfUd8E+6vLg/nk3ss1TYMb+DhRl25fYABioTvA== +hsts@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/hsts/-/hsts-2.2.0.tgz#09119d42f7a8587035d027dda4522366fe75d964" + integrity sha512-ToaTnQ2TbJkochoVcdXYm4HOCliNozlviNsg+X2XQLQvZNI/kCHR9rZxVYpJB3UPcHz80PgxRyWQ7PdU1r+VBQ== + dependencies: + depd "2.0.0" html-encoding-sniffer@^1.0.2: version "1.0.2" @@ -4142,10 +4062,10 @@ ieee754@^1.1.4: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b" integrity sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA== -ienoopen@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/ienoopen/-/ienoopen-1.0.0.tgz#346a428f474aac8f50cf3784ea2d0f16f62bda6b" - integrity sha1-NGpCj0dKrI9QzzeE6i0PFvYr2ms= +ienoopen@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ienoopen/-/ienoopen-1.1.0.tgz#411e5d530c982287dbdc3bb31e7a9c9e32630974" + integrity sha512-MFs36e/ca6ohEKtinTJ5VvAJ6oDRAYFdYXweUnGY9L9vcoqFOU4n2ZhmJ0C4z/cwGZ3YIQRSB3XZ1+ghZkY5NQ== ignore-by-default@^1.0.1: version "1.0.1" diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index eeb3a49d3..1a9891a7a 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -5,7 +5,7 @@ import { getLangByName } from '../../support/helpers' let lastPost = {} -const loginCredentials = { +let loginCredentials = { email: 'peterpan@example.org', password: '1234' } @@ -244,3 +244,48 @@ Then( cy.get('.error').should('contain', message) } ) + +Given('my user account has the following login credentials:', table => { + loginCredentials = table.hashes()[0] + cy.debug() + cy.factory().create('User', loginCredentials) +}) + +When('I fill the password form with:', table => { + table = table.rowsHash() + cy.get('input[id=oldPassword]') + .type(table['Your old password']) + .get('input[id=newPassword]') + .type(table['Your new passsword']) + .get('input[id=confirmPassword]') + .type(table['Confirm new password']) +}) + +When('submit the form', () => { + cy.get('form').submit() +}) + +Then('I cannot login anymore with password {string}', password => { + cy.reload() + const { email } = loginCredentials + cy.visit(`/login`) + cy.get('input[name=email]') + .trigger('focus') + .type(email) + cy.get('input[name=password]') + .trigger('focus') + .type(password) + cy.get('button[name=submit]') + .as('submitButton') + .click() + cy.get('.iziToast-wrapper').should('contain', 'Incorrect email address or password.') +}) + +Then('I can login successfully with password {string}', password => { + cy.reload() + cy.login({ + ...loginCredentials, + ...{password} + }) + cy.get('.iziToast-wrapper').should('contain', "You are logged in!") +}) diff --git a/cypress/integration/settings/ChangePassword.feature b/cypress/integration/settings/ChangePassword.feature new file mode 100644 index 000000000..44e4e5483 --- /dev/null +++ b/cypress/integration/settings/ChangePassword.feature @@ -0,0 +1,31 @@ +Feature: Change password + As a user + I want to change my password in my settings + For security, e.g. if I exposed my password by accident + + Login via email and password is a well-known authentication procedure and you + can assure to the server that you are who you claim to be. Either if you + exposed your password by acccident and you want to invalidate the exposed + password or just out of an good habit, you want to change your password. + + Background: + Given my user account has the following login credentials: + | email | password | + | user@example.org | exposed | + And I am logged in + + Scenario: Change my password + Given I am on the "settings" page + And I click on "Security" + When I fill the password form with: + | Your old password | exposed | + | Your new passsword | secure | + | Confirm new password | secure | + And submit the form + And I see a success message: + """ + Password successfully changed! + """ + And I log out through the menu in the top right corner + Then I cannot login anymore with password "exposed" + But I can login successfully with password "secure" diff --git a/deployment/human-connection/deployment-backend.yaml b/deployment/human-connection/deployment-backend.yaml index 13cc7f7ed..29992ef7e 100644 --- a/deployment/human-connection/deployment-backend.yaml +++ b/deployment/human-connection/deployment-backend.yaml @@ -4,8 +4,6 @@ metadata: name: nitro-backend namespace: human-connection - labels: - commit: "COMMIT" spec: replicas: 1 minReadySeconds: 15 @@ -20,6 +18,7 @@ template: metadata: labels: + human-connection.org/commit: COMMIT human-connection.org/selector: deployment-human-connection-backend name: "nitro-backend" spec: diff --git a/deployment/human-connection/deployment-web.yaml b/deployment/human-connection/deployment-web.yaml index d69ebf617..885762e0a 100644 --- a/deployment/human-connection/deployment-web.yaml +++ b/deployment/human-connection/deployment-web.yaml @@ -3,8 +3,6 @@ kind: Deployment metadata: name: nitro-web namespace: human-connection - labels: - commit: "COMMIT" spec: replicas: 2 minReadySeconds: 15 @@ -15,6 +13,7 @@ spec: template: metadata: labels: + human-connection.org/commit: COMMIT human-connection.org/selector: deployment-human-connection-web name: nitro-web spec: diff --git a/scripts/patch-deployment.yaml b/scripts/patch-deployment.yaml index 05afe9b31..c229b8e7c 100644 --- a/scripts/patch-deployment.yaml +++ b/scripts/patch-deployment.yaml @@ -1,3 +1,5 @@ -metadata: - labels: - commit: +spec: + template: + metadata: + labels: + human-connection.org/commit: diff --git a/webapp/components/ChangePassword.spec.js b/webapp/components/ChangePassword.spec.js new file mode 100644 index 000000000..98a66da72 --- /dev/null +++ b/webapp/components/ChangePassword.spec.js @@ -0,0 +1,154 @@ +import { mount, createLocalVue } from '@vue/test-utils' +import ChangePassword from './ChangePassword.vue' +import Vue from 'vue' +import Styleguide from '@human-connection/styleguide' + +const localVue = createLocalVue() + +localVue.use(Styleguide) + +describe('ChangePassword.vue', () => { + let mocks + let wrapper + + beforeEach(() => { + mocks = { + validate: jest.fn(), + $toast: { + error: jest.fn(), + success: jest.fn() + }, + $t: jest.fn(), + $store: { + commit: jest.fn() + }, + $apollo: { + mutate: jest + .fn() + .mockRejectedValue({ message: 'Ouch!' }) + .mockResolvedValueOnce({ data: { changePassword: 'NEWTOKEN' } }) + } + } + }) + + describe('mount', () => { + let wrapper + const Wrapper = () => { + return mount(ChangePassword, { mocks, localVue }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders three input fields', () => { + expect(wrapper.findAll('input')).toHaveLength(3) + }) + + describe('validations', () => { + it('invalid', () => { + expect(wrapper.vm.disabled).toBe(true) + }) + + describe('old password and new password', () => { + describe('match', () => { + beforeEach(() => { + wrapper.find('input#oldPassword').setValue('some secret') + wrapper.find('input#newPassword').setValue('some secret') + }) + + it('invalid', () => { + expect(wrapper.vm.disabled).toBe(true) + }) + + it.skip('displays a warning', () => { + const calls = mocks.validate.mock.calls + const expected = [ + ['change-password.validations.old-and-new-password-match'] + ] + expect(calls).toEqual(expect.arrayContaining(expected)) + }) + }) + }) + + describe('new password and confirmation', () => { + describe('mismatch', () => { + it.todo('invalid') + it.todo('displays a warning') + }) + + describe('match', () => { + describe('and old password mismatch', () => { + it.todo('valid') + }) + + describe('clicked', () => { + it.todo('sets loading') + }) + }) + }) + }) + + describe('given valid input', () => { + beforeEach(() => { + wrapper.find('input#oldPassword').setValue('supersecret') + wrapper.find('input#newPassword').setValue('superdupersecret') + wrapper.find('input#confirmPassword').setValue('superdupersecret') + }) + + describe('submit form', () => { + beforeEach(() => { + wrapper.find('form').trigger('submit') + }) + + it('calls changePassword mutation', () => { + expect(mocks.$apollo.mutate).toHaveBeenCalled() + }) + + it('passes form data as variables', () => { + expect(mocks.$apollo.mutate.mock.calls[0][0]).toEqual( + expect.objectContaining({ + variables: { + oldPassword: 'supersecret', + newPassword: 'superdupersecret', + confirmPassword: 'superdupersecret' + } + }) + ) + }) + + describe('mutation resolves', () => { + beforeEach(() => { + mocks.$apollo.mutate = jest.fn().mockResolvedValue() + wrapper = Wrapper() + }) + + it('calls auth/SET_TOKEN with response', () => { + expect(mocks.$store.commit).toHaveBeenCalledWith( + 'auth/SET_TOKEN', + 'NEWTOKEN' + ) + }) + + it('displays success message', () => { + expect(mocks.$t).toHaveBeenCalledWith( + 'settings.security.change-password.success' + ) + expect(mocks.$toast.success).toHaveBeenCalled() + }) + }) + + describe('mutation rejects', () => { + beforeEach(() => { + // second call will reject + wrapper.find('form').trigger('submit') + }) + + it('displays error message', () => { + expect(mocks.$toast.error).toHaveBeenCalledWith('Ouch!') + }) + }) + }) + }) + }) +}) diff --git a/webapp/components/ChangePassword.vue b/webapp/components/ChangePassword.vue new file mode 100644 index 000000000..dc8cc09da --- /dev/null +++ b/webapp/components/ChangePassword.vue @@ -0,0 +1,83 @@ + + + diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 2ec3bac9f..6e47d7122 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -31,7 +31,11 @@ "labelBio": "Über dich" }, "security": { - "name": "Sicherheit" + "name": "Sicherheit", + "change-password": { + "button": "Passwort ändern", + "success": "Passwort erfolgreich geändert!" + } }, "invites": { "name": "Einladungen" diff --git a/webapp/locales/en.json b/webapp/locales/en.json index b3e2909ca..0c341b765 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -31,7 +31,11 @@ "labelBio": "About You" }, "security": { - "name": "Security" + "name": "Security", + "change-password": { + "button": "Change password", + "success": "Password successfully changed!" + } }, "invites": { "name": "Invites" diff --git a/webapp/nuxt.config.js b/webapp/nuxt.config.js index 70ab6333d..80b17a26c 100644 --- a/webapp/nuxt.config.js +++ b/webapp/nuxt.config.js @@ -121,12 +121,21 @@ module.exports = { proxy: true }, proxy: { + '/.well-known/webfinger': { + target: process.env.GRAPHQL_URI || 'http://localhost:4000', + toProxy: true // cloudflare needs that + }, + '/activityPub': { + // make this configurable (nuxt-dotenv) + target: process.env.GRAPHQL_URI || 'http://localhost:4000', + pathRewrite: { '^/activityPub': '' }, + toProxy: true // cloudflare needs that + }, '/api': { // make this configurable (nuxt-dotenv) target: process.env.GRAPHQL_URI || 'http://localhost:4000', pathRewrite: { '^/api': '' }, toProxy: true, // cloudflare needs that - changeOrigin: true, headers: { Accept: 'application/json', 'X-UI-Request': true, diff --git a/webapp/pages/settings/security.vue b/webapp/pages/settings/security.vue index 937aac9dd..376f104e5 100644 --- a/webapp/pages/settings/security.vue +++ b/webapp/pages/settings/security.vue @@ -1,18 +1,16 @@