diff --git a/backend/package-lock.json b/backend/package-lock.json index f74a71505..693f055a6 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -10,9 +10,11 @@ "license": "ISC", "dependencies": { "@godaddy/terminus": "^4.11.2", + "@octokit/rest": "^19.0.5", "@sentry/node": "^7.14.0", "@sentry/tracing": "^7.19.0", "@types/crypto-js": "^4.1.1", + "@types/libsodium-wrappers": "^0.7.10", "axios": "^1.1.3", "bigint-conversion": "^2.2.2", "cookie-parser": "^1.4.6", @@ -26,6 +28,7 @@ "helmet": "^5.1.1", "jsonwebtoken": "^8.5.1", "jsrp": "^0.2.4", + "libsodium-wrappers": "^0.7.10", "mongoose": "^6.7.2", "nodemailer": "^6.8.0", "posthog-node": "^2.1.0", @@ -2590,6 +2593,153 @@ "node": ">= 8" } }, + "node_modules/@octokit/auth-token": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.2.tgz", + "integrity": "sha512-pq7CwIMV1kmzkFTimdwjAINCXKTajZErLB4wMLYapR2nuB/Jpr66+05wOTZMSCBXP6n4DdDWT2W19Bm17vU69Q==", + "dependencies": { + "@octokit/types": "^8.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/core": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.1.0.tgz", + "integrity": "sha512-Czz/59VefU+kKDy+ZfDwtOIYIkFjExOKf+HA92aiTZJ6EfWpFzYQWw0l54ji8bVmyhc+mGaLUbSUmXazG7z5OQ==", + "dependencies": { + "@octokit/auth-token": "^3.0.0", + "@octokit/graphql": "^5.0.0", + "@octokit/request": "^6.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^8.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/endpoint": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.3.tgz", + "integrity": "sha512-57gRlb28bwTsdNXq+O3JTQ7ERmBTuik9+LelgcLIVfYwf235VHbN9QNo4kXExtp/h8T423cR5iJThKtFYxC7Lw==", + "dependencies": { + "@octokit/types": "^8.0.0", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/graphql": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.4.tgz", + "integrity": "sha512-amO1M5QUQgYQo09aStR/XO7KAl13xpigcy/kI8/N1PnZYSS69fgte+xA4+c2DISKqUZfsh0wwjc2FaCt99L41A==", + "dependencies": { + "@octokit/request": "^6.0.0", + "@octokit/types": "^8.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-14.0.0.tgz", + "integrity": "sha512-HNWisMYlR8VCnNurDU6os2ikx0s0VyEjDYHNS/h4cgb8DeOxQ0n72HyinUtdDVxJhFy3FWLGl0DJhfEWk3P5Iw==" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-5.0.1.tgz", + "integrity": "sha512-7A+rEkS70pH36Z6JivSlR7Zqepz3KVucEFVDnSrgHXzG7WLAzYwcHZbKdfTXHwuTHbkT1vKvz7dHl1+HNf6Qyw==", + "dependencies": { + "@octokit/types": "^8.0.0" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "@octokit/core": ">=4" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", + "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-6.7.0.tgz", + "integrity": "sha512-orxQ0fAHA7IpYhG2flD2AygztPlGYNAdlzYz8yrD8NDgelPfOYoRPROfEyIe035PlxvbYrgkfUZIhSBKju/Cvw==", + "dependencies": { + "@octokit/types": "^8.0.0", + "deprecation": "^2.3.1" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/request": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.2.tgz", + "integrity": "sha512-6VDqgj0HMc2FUX2awIs+sM6OwLgwHvAi4KCK3mT2H2IKRt6oH9d0fej5LluF5mck1lRR/rFWN0YIDSYXYSylbw==", + "dependencies": { + "@octokit/endpoint": "^7.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^8.0.0", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/request-error": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.2.tgz", + "integrity": "sha512-WMNOFYrSaX8zXWoJg9u/pKgWPo94JXilMLb2VManNOby9EZxrQaBe/QSC4a1TzpAlpxofg2X/jMnCyZgL6y7eg==", + "dependencies": { + "@octokit/types": "^8.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/rest": { + "version": "19.0.5", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-19.0.5.tgz", + "integrity": "sha512-+4qdrUFq2lk7Va+Qff3ofREQWGBeoTKNqlJO+FGjFP35ZahP+nBenhZiGdu8USSgmq4Ky3IJ/i4u0xbLqHaeow==", + "dependencies": { + "@octokit/core": "^4.1.0", + "@octokit/plugin-paginate-rest": "^5.0.0", + "@octokit/plugin-request-log": "^1.0.4", + "@octokit/plugin-rest-endpoint-methods": "^6.7.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/types": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-8.0.0.tgz", + "integrity": "sha512-65/TPpOJP1i3K4lBJMnWqPUJ6zuOtzhtagDvydAWbEXpbFYA0oMKKyLb95NFZZP0lSh/4b6K+DQlzvYQJQQePg==", + "dependencies": { + "@octokit/openapi-types": "^14.0.0" + } + }, "node_modules/@posthog/plugin-scaffold": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/@posthog/plugin-scaffold/-/plugin-scaffold-1.3.4.tgz", @@ -2895,6 +3045,11 @@ "@types/node": "*" } }, + "node_modules/@types/libsodium-wrappers": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", + "integrity": "sha512-BqI9B92u+cM3ccp8mpHf+HzJ8fBlRwdmyd6+fz3p99m3V6ifT5O3zmOMi612PGkpeFeG/G6loxUnzlDNhfjPSA==" + }, "node_modules/@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -3485,6 +3640,11 @@ } ] }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + }, "node_modules/bigint-conversion": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/bigint-conversion/-/bigint-conversion-2.2.2.tgz", @@ -4054,6 +4214,11 @@ "node": ">= 0.8" } }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -5297,6 +5462,14 @@ "node": ">=8" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -6122,6 +6295,19 @@ "node": ">= 0.8.0" } }, + "node_modules/libsodium": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.10.tgz", + "integrity": "sha512-eY+z7hDrDKxkAK+QKZVNv92A5KYkxfvIshtBJkmg5TSiCnYqZP3i9OO9whE79Pwgm4jGaoHgkM4ao/b9Cyu4zQ==" + }, + "node_modules/libsodium-wrappers": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", + "integrity": "sha512-pO3F1Q9NPLB/MWIhehim42b/Fwb30JNScCNh8TcQ/kIc+qGLQch8ag8wb0keK3EP5kbGakk1H8Wwo7v+36rNQg==", + "dependencies": { + "libsodium": "^0.7.0" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -6508,6 +6694,44 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -10622,6 +10846,11 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "node_modules/universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -13131,6 +13360,118 @@ "fastq": "^1.6.0" } }, + "@octokit/auth-token": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.2.tgz", + "integrity": "sha512-pq7CwIMV1kmzkFTimdwjAINCXKTajZErLB4wMLYapR2nuB/Jpr66+05wOTZMSCBXP6n4DdDWT2W19Bm17vU69Q==", + "requires": { + "@octokit/types": "^8.0.0" + } + }, + "@octokit/core": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.1.0.tgz", + "integrity": "sha512-Czz/59VefU+kKDy+ZfDwtOIYIkFjExOKf+HA92aiTZJ6EfWpFzYQWw0l54ji8bVmyhc+mGaLUbSUmXazG7z5OQ==", + "requires": { + "@octokit/auth-token": "^3.0.0", + "@octokit/graphql": "^5.0.0", + "@octokit/request": "^6.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^8.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/endpoint": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.3.tgz", + "integrity": "sha512-57gRlb28bwTsdNXq+O3JTQ7ERmBTuik9+LelgcLIVfYwf235VHbN9QNo4kXExtp/h8T423cR5iJThKtFYxC7Lw==", + "requires": { + "@octokit/types": "^8.0.0", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/graphql": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.4.tgz", + "integrity": "sha512-amO1M5QUQgYQo09aStR/XO7KAl13xpigcy/kI8/N1PnZYSS69fgte+xA4+c2DISKqUZfsh0wwjc2FaCt99L41A==", + "requires": { + "@octokit/request": "^6.0.0", + "@octokit/types": "^8.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/openapi-types": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-14.0.0.tgz", + "integrity": "sha512-HNWisMYlR8VCnNurDU6os2ikx0s0VyEjDYHNS/h4cgb8DeOxQ0n72HyinUtdDVxJhFy3FWLGl0DJhfEWk3P5Iw==" + }, + "@octokit/plugin-paginate-rest": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-5.0.1.tgz", + "integrity": "sha512-7A+rEkS70pH36Z6JivSlR7Zqepz3KVucEFVDnSrgHXzG7WLAzYwcHZbKdfTXHwuTHbkT1vKvz7dHl1+HNf6Qyw==", + "requires": { + "@octokit/types": "^8.0.0" + } + }, + "@octokit/plugin-request-log": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", + "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", + "requires": {} + }, + "@octokit/plugin-rest-endpoint-methods": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-6.7.0.tgz", + "integrity": "sha512-orxQ0fAHA7IpYhG2flD2AygztPlGYNAdlzYz8yrD8NDgelPfOYoRPROfEyIe035PlxvbYrgkfUZIhSBKju/Cvw==", + "requires": { + "@octokit/types": "^8.0.0", + "deprecation": "^2.3.1" + } + }, + "@octokit/request": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.2.tgz", + "integrity": "sha512-6VDqgj0HMc2FUX2awIs+sM6OwLgwHvAi4KCK3mT2H2IKRt6oH9d0fej5LluF5mck1lRR/rFWN0YIDSYXYSylbw==", + "requires": { + "@octokit/endpoint": "^7.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^8.0.0", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/request-error": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.2.tgz", + "integrity": "sha512-WMNOFYrSaX8zXWoJg9u/pKgWPo94JXilMLb2VManNOby9EZxrQaBe/QSC4a1TzpAlpxofg2X/jMnCyZgL6y7eg==", + "requires": { + "@octokit/types": "^8.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "@octokit/rest": { + "version": "19.0.5", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-19.0.5.tgz", + "integrity": "sha512-+4qdrUFq2lk7Va+Qff3ofREQWGBeoTKNqlJO+FGjFP35ZahP+nBenhZiGdu8USSgmq4Ky3IJ/i4u0xbLqHaeow==", + "requires": { + "@octokit/core": "^4.1.0", + "@octokit/plugin-paginate-rest": "^5.0.0", + "@octokit/plugin-request-log": "^1.0.4", + "@octokit/plugin-rest-endpoint-methods": "^6.7.0" + } + }, + "@octokit/types": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-8.0.0.tgz", + "integrity": "sha512-65/TPpOJP1i3K4lBJMnWqPUJ6zuOtzhtagDvydAWbEXpbFYA0oMKKyLb95NFZZP0lSh/4b6K+DQlzvYQJQQePg==", + "requires": { + "@octokit/openapi-types": "^14.0.0" + } + }, "@posthog/plugin-scaffold": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/@posthog/plugin-scaffold/-/plugin-scaffold-1.3.4.tgz", @@ -13416,6 +13757,11 @@ "@types/node": "*" } }, + "@types/libsodium-wrappers": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", + "integrity": "sha512-BqI9B92u+cM3ccp8mpHf+HzJ8fBlRwdmyd6+fz3p99m3V6ifT5O3zmOMi612PGkpeFeG/G6loxUnzlDNhfjPSA==" + }, "@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -13833,6 +14179,11 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, + "before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + }, "bigint-conversion": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/bigint-conversion/-/bigint-conversion-2.2.2.tgz", @@ -14253,6 +14604,11 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" }, + "deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, "destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -15163,6 +15519,11 @@ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + }, "is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -15808,6 +16169,19 @@ "type-check": "~0.4.0" } }, + "libsodium": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.10.tgz", + "integrity": "sha512-eY+z7hDrDKxkAK+QKZVNv92A5KYkxfvIshtBJkmg5TSiCnYqZP3i9OO9whE79Pwgm4jGaoHgkM4ao/b9Cyu4zQ==" + }, + "libsodium-wrappers": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", + "integrity": "sha512-pO3F1Q9NPLB/MWIhehim42b/Fwb30JNScCNh8TcQ/kIc+qGLQch8ag8wb0keK3EP5kbGakk1H8Wwo7v+36rNQg==", + "requires": { + "libsodium": "^0.7.0" + } + }, "lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -16110,6 +16484,35 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + }, + "dependencies": { + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -18917,6 +19320,11 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index bad2908d9..119a79073 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,9 +1,11 @@ { "dependencies": { "@godaddy/terminus": "^4.11.2", + "@octokit/rest": "^19.0.5", "@sentry/node": "^7.14.0", "@sentry/tracing": "^7.19.0", "@types/crypto-js": "^4.1.1", + "@types/libsodium-wrappers": "^0.7.10", "axios": "^1.1.3", "bigint-conversion": "^2.2.2", "cookie-parser": "^1.4.6", @@ -17,6 +19,7 @@ "helmet": "^5.1.1", "jsonwebtoken": "^8.5.1", "jsrp": "^0.2.4", + "libsodium-wrappers": "^0.7.10", "mongoose": "^6.7.2", "nodemailer": "^6.8.0", "posthog-node": "^2.1.0", diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index e3d62c42c..99c72ab3d 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -14,8 +14,11 @@ const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!; const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!; const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!; const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!; +const CLIENT_ID_GITHUB = + process.env.CLIENT_ID_GITHUB! || 'e787fc24bcec43ecd5d5'; const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!; const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!; +const CLIENT_SECRET_GITHUB = process.env.CLIENT_SECRET_GITHUB! || '407f32da788f63559abd662c6de08bb2911ca8ae'; const CLIENT_SLUG_VERCEL= process.env.CLIENT_SLUG_VERCEL!; const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com'; const POSTHOG_PROJECT_API_KEY = @@ -54,9 +57,11 @@ export { CLIENT_ID_HEROKU, CLIENT_ID_VERCEL, CLIENT_ID_NETLIFY, + CLIENT_ID_GITHUB, CLIENT_SECRET_HEROKU, CLIENT_SECRET_VERCEL, CLIENT_SECRET_NETLIFY, + CLIENT_SECRET_GITHUB, CLIENT_SLUG_VERCEL, POSTHOG_HOST, POSTHOG_PROJECT_API_KEY, diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index 70680ef7d..c4e53c1f8 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -1,15 +1,16 @@ import axios from 'axios'; import * as Sentry from '@sentry/node'; +import { Octokit } from '@octokit/rest'; +import { IIntegrationAuth } from '../models'; import { - IIntegrationAuth -} from '../models'; -import { - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_HEROKU_API_URL, - INTEGRATION_VERCEL_API_URL, - INTEGRATION_NETLIFY_API_URL + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_HEROKU_API_URL, + INTEGRATION_VERCEL_API_URL, + INTEGRATION_NETLIFY_API_URL, + INTEGRATION_GITHUB_API_URL } from '../variables'; /** @@ -21,47 +22,51 @@ import { * @returns {String} apps.name - name of integration app */ const getApps = async ({ - integrationAuth, - accessToken + integrationAuth, + accessToken }: { - integrationAuth: IIntegrationAuth; - accessToken: string; + integrationAuth: IIntegrationAuth; + accessToken: string; }) => { - - interface App { - name: string; - siteId?: string; - } + interface App { + name: string; + siteId?: string; + } - let apps: App[]; // TODO: add type and define payloads for apps - try { - switch (integrationAuth.integration) { - case INTEGRATION_HEROKU: - apps = await getAppsHeroku({ - accessToken - }); - break; - case INTEGRATION_VERCEL: - apps = await getAppsVercel({ - accessToken - }); - break; - case INTEGRATION_NETLIFY: - apps = await getAppsNetlify({ - integrationAuth, - accessToken - }); - break; - } - - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to get integration apps'); + let apps: App[]; // TODO: add type and define payloads for apps + try { + switch (integrationAuth.integration) { + case INTEGRATION_HEROKU: + apps = await getAppsHeroku({ + accessToken + }); + break; + case INTEGRATION_VERCEL: + apps = await getAppsVercel({ + accessToken + }); + break; + case INTEGRATION_NETLIFY: + apps = await getAppsNetlify({ + integrationAuth, + accessToken + }); + break; + case INTEGRATION_GITHUB: + apps = await getAppsGithub({ + integrationAuth, + accessToken + }); + break; } - - return apps; -} + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to get integration apps'); + } + + return apps; +}; /** * Return list of names of apps for Heroku integration @@ -70,31 +75,29 @@ const getApps = async ({ * @returns {Object[]} apps - names of Heroku apps * @returns {String} apps.name - name of Heroku app */ -const getAppsHeroku = async ({ - accessToken -}: { - accessToken: string; -}) => { - let apps; - try { - const res = (await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, { - headers: { - Accept: 'application/vnd.heroku+json; version=3', - Authorization: `Bearer ${accessToken}` - } - })).data; - - apps = res.map((a: any) => ({ - name: a.name - })); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to get Heroku integration apps'); - } - - return apps; -} +const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => { + let apps; + try { + const res = ( + await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, { + headers: { + Accept: 'application/vnd.heroku+json; version=3', + Authorization: `Bearer ${accessToken}` + } + }) + ).data; + + apps = res.map((a: any) => ({ + name: a.name + })); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to get Heroku integration apps'); + } + + return apps; +}; /** * Return list of names of apps for Vercel integration @@ -103,30 +106,28 @@ const getAppsHeroku = async ({ * @returns {Object[]} apps - names of Vercel apps * @returns {String} apps.name - name of Vercel app */ -const getAppsVercel = async ({ - accessToken -}: { - accessToken: string; -}) => { - let apps; - try { - const res = (await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, { - headers: { - Authorization: `Bearer ${accessToken}` - } - })).data; - - apps = res.projects.map((a: any) => ({ - name: a.name - })); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to get Vercel integration apps'); - } - - return apps; -} +const getAppsVercel = async ({ accessToken }: { accessToken: string }) => { + let apps; + try { + const res = ( + await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }) + ).data; + + apps = res.projects.map((a: any) => ({ + name: a.name + })); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to get Vercel integration apps'); + } + + return apps; +}; /** * Return list of names of sites for Netlify integration @@ -136,34 +137,69 @@ const getAppsVercel = async ({ * @returns {String} apps.name - name of Netlify site */ const getAppsNetlify = async ({ - integrationAuth, - accessToken + integrationAuth, + accessToken }: { - integrationAuth: IIntegrationAuth; - accessToken: string; + integrationAuth: IIntegrationAuth; + accessToken: string; }) => { - let apps; - try { - const res = (await axios.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, { - headers: { - Authorization: `Bearer ${accessToken}` - } - })).data; - - apps = res.map((a: any) => ({ - name: a.name, - siteId: a.site_id - })); - - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to get Netlify integration apps'); - } - - return apps; -} + let apps; + try { + const res = ( + await axios.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }) + ).data; -export { - getApps -} \ No newline at end of file + apps = res.map((a: any) => ({ + name: a.name, + siteId: a.site_id + })); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to get Netlify integration apps'); + } + + return apps; +}; + +/** + * Return list of names of repositories for Github integration + * @param {Object} obj + * @param {String} obj.accessToken - access token for Netlify API + * @returns {Object[]} apps - names of Netlify sites + * @returns {String} apps.name - name of Netlify site + */ +const getAppsGithub = async ({ + integrationAuth, + accessToken +}: { + integrationAuth: IIntegrationAuth; + accessToken: string; +}) => { + let apps; + try { + const octokit = new Octokit({ + auth: accessToken + }); + + const repos = await octokit.request( + 'GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}', + {} + ); + apps = repos.map((a: any) => { + a.name; + }); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to get Github repos'); + } + + return apps; +}; + +export { getApps }; diff --git a/backend/src/integrations/exchange.ts b/backend/src/integrations/exchange.ts index 26e6521a1..3b7526200 100644 --- a/backend/src/integrations/exchange.ts +++ b/backend/src/integrations/exchange.ts @@ -1,46 +1,57 @@ import axios from 'axios'; import * as Sentry from '@sentry/node'; import { - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_HEROKU_TOKEN_URL, - INTEGRATION_VERCEL_TOKEN_URL, - INTEGRATION_NETLIFY_TOKEN_URL, - ACTION_PUSH_TO_HEROKU + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_HEROKU_TOKEN_URL, + INTEGRATION_VERCEL_TOKEN_URL, + INTEGRATION_NETLIFY_TOKEN_URL, + INTEGRATION_GITHUB_TOKEN_URL, + ACTION_PUSH_TO_HEROKU } from '../variables'; -import { - SITE_URL, - CLIENT_SECRET_HEROKU, - CLIENT_ID_VERCEL, - CLIENT_ID_NETLIFY, - CLIENT_SECRET_VERCEL, - CLIENT_SECRET_NETLIFY + +import { + SITE_URL, + CLIENT_ID_VERCEL, + CLIENT_ID_NETLIFY, + CLIENT_ID_GITHUB, + CLIENT_SECRET_VERCEL, + CLIENT_SECRET_NETLIFY, + CLIENT_SECRET_GITHUB } from '../config'; +import { user } from '../routes'; interface ExchangeCodeHerokuResponse { - token_type: string; - access_token: string; - expires_in: number; - refresh_token: string; - user_id: string; - session_nonce?: string; + token_type: string; + access_token: string; + expires_in: number; + refresh_token: string; + user_id: string; + session_nonce?: string; } interface ExchangeCodeVercelResponse { - token_type: string; - access_token: string; - installation_id: string; - user_id: string; - team_id?: string; + token_type: string; + access_token: string; + installation_id: string; + user_id: string; + team_id?: string; } interface ExchangeCodeNetlifyResponse { - access_token: string; - token_type: string; - refresh_token: string; - scope: string; - created_at: number; + access_token: string; + token_type: string; + refresh_token: string; + scope: string; + created_at: number; +} + +interface ExchangeCodeGithubResponse { + access_token: string; + scope: string; + token_type: string; } /** @@ -56,40 +67,45 @@ interface ExchangeCodeNetlifyResponse { * @returns {String} obj.action - integration action for bot sequence */ const exchangeCode = async ({ - integration, - code -}: { - integration: string; - code: string; + integration, + code +}: { + integration: string; + code: string; }) => { - let obj = {} as any; - - try { - switch (integration) { - case INTEGRATION_HEROKU: - obj = await exchangeCodeHeroku({ - code - }); - break; - case INTEGRATION_VERCEL: - obj = await exchangeCodeVercel({ - code - }); - break; - case INTEGRATION_NETLIFY: - obj = await exchangeCodeNetlify({ - code - }); - break; - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed OAuth2 code-token exchange'); + let obj = {} as any; + + try { + switch (integration) { + case INTEGRATION_HEROKU: + obj = await exchangeCodeHeroku({ + code + }); + break; + case INTEGRATION_VERCEL: + obj = await exchangeCodeVercel({ + code + }); + break; + case INTEGRATION_NETLIFY: + obj = await exchangeCodeNetlify({ + code + }); + break; + case INTEGRATION_GITHUB: + obj = await exchangeCodeGithub({ + code + }); + break; } - - return obj; -} + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed OAuth2 code-token exchange'); + } + + return obj; +}; /** * Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku @@ -101,6 +117,36 @@ const exchangeCode = async ({ * @returns {String} obj2.refreshToken - refresh token for Heroku API * @returns {Date} obj2.accessExpiresAt - date of expiration for access token */ +<<<<<<< HEAD +const exchangeCodeHeroku = async ({ code }: { code: string }) => { + let res: ExchangeCodeHerokuResponse; + const accessExpiresAt = new Date(); + try { + res = ( + await axios.post( + INTEGRATION_HEROKU_TOKEN_URL, + new URLSearchParams({ + grant_type: 'authorization_code', + code: code, + client_secret: OAUTH_CLIENT_SECRET_HEROKU + } as any) + ) + ).data; + + accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed OAuth2 code-token exchange with Heroku'); + } + + return { + accessToken: res.access_token, + refreshToken: res.refresh_token, + accessExpiresAt + }; +}; +======= const exchangeCodeHeroku = async ({ code }: { @@ -133,6 +179,7 @@ const exchangeCodeHeroku = async ({ accessExpiresAt }); } +>>>>>>> 5444382d5ae1fabf1107434a856b58b9f09c67f6 /** * Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel @@ -144,35 +191,33 @@ const exchangeCodeHeroku = async ({ * @returns {String} obj2.refreshToken - refresh token for Heroku API * @returns {Date} obj2.accessExpiresAt - date of expiration for access token */ -const exchangeCodeVercel = async ({ - code -}: { - code: string; -}) => { - let res: ExchangeCodeVercelResponse; - try { - res = (await axios.post( - INTEGRATION_VERCEL_TOKEN_URL, - new URLSearchParams({ - code: code, - client_id: CLIENT_ID_VERCEL, - client_secret: CLIENT_SECRET_VERCEL, - redirect_uri: `${SITE_URL}/vercel` - } as any) - )).data; - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed OAuth2 code-token exchange with Vercel'); - } - - return ({ - accessToken: res.access_token, - refreshToken: null, - accessExpiresAt: null, - teamId: res.team_id - }); -} +const exchangeCodeVercel = async ({ code }: { code: string }) => { + let res: ExchangeCodeVercelResponse; + try { + res = ( + await axios.post( + INTEGRATION_VERCEL_TOKEN_URL, + new URLSearchParams({ + code: code, + client_id: CLIENT_ID_VERCEL, + client_secret: CLIENT_SECRET_VERCEL, + redirect_uri: `${SITE_URL}/vercel` + } as any) + ) + ).data; + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed OAuth2 code-token exchange with Vercel'); + } + + return { + accessToken: res.access_token, + refreshToken: null, + accessExpiresAt: null, + teamId: res.team_id + }; +}; /** * Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel @@ -184,58 +229,98 @@ const exchangeCodeVercel = async ({ * @returns {String} obj2.refreshToken - refresh token for Heroku API * @returns {Date} obj2.accessExpiresAt - date of expiration for access token */ -const exchangeCodeNetlify = async ({ - code -}: { - code: string; -}) => { - let res: ExchangeCodeNetlifyResponse; - let accountId; - try { - res = (await axios.post( - INTEGRATION_NETLIFY_TOKEN_URL, - new URLSearchParams({ - grant_type: 'authorization_code', - code: code, - client_id: CLIENT_ID_NETLIFY, - client_secret: CLIENT_SECRET_NETLIFY, - redirect_uri: `${SITE_URL}/netlify` - } as any) - )).data; +const exchangeCodeNetlify = async ({ code }: { code: string }) => { + let res: ExchangeCodeNetlifyResponse; + let accountId; + try { + res = ( + await axios.post( + INTEGRATION_NETLIFY_TOKEN_URL, + new URLSearchParams({ + grant_type: 'authorization_code', + code: code, + client_id: CLIENT_ID_NETLIFY, + client_secret: CLIENT_SECRET_NETLIFY, + redirect_uri: `${SITE_URL}/netlify` + } as any) + ) + ).data; - const res2 = await axios.get( - 'https://api.netlify.com/api/v1/sites', - { - headers: { - Authorization: `Bearer ${res.access_token}` - } - } - ); - - const res3 = (await axios.get( - 'https://api.netlify.com/api/v1/accounts', - { - headers: { - Authorization: `Bearer ${res.access_token}` - } - } - )).data; - - accountId = res3[0].id; - - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed OAuth2 code-token exchange with Netlify'); - } - - return ({ - accessToken: res.access_token, - refreshToken: res.refresh_token, - accountId + const res2 = await axios.get('https://api.netlify.com/api/v1/sites', { + headers: { + Authorization: `Bearer ${res.access_token}` + } }); -} -export { - exchangeCode -} \ No newline at end of file + const res3 = ( + await axios.get('https://api.netlify.com/api/v1/accounts', { + headers: { + Authorization: `Bearer ${res.access_token}` + } + }) + ).data; + + accountId = res3[0].id; + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed OAuth2 code-token exchange with Netlify'); + } + + return { + accessToken: res.access_token, + refreshToken: res.refresh_token, + accountId + }; +}; + +/** + * Return [accessToken], [accessExpiresAt], and [refreshToken] for Github + * code-token exchange + * @param {Object} obj1 + * @param {Object} obj1.code - code for code-token exchange + * @returns {Object} obj2 + * @returns {String} obj2.accessToken - access token for Github API + * @returns {String} obj2.refreshToken - refresh token for Github API + * @returns {Date} obj2.accessExpiresAt - date of expiration for access token + */ +const exchangeCodeGithub = async ({ code }: { code: string }) => { + let res: ExchangeCodeGithubResponse; + let res2; + try { + res = ( + await axios.get(INTEGRATION_GITHUB_TOKEN_URL, { + params: { + client_id: CLIENT_ID_GITHUB, + client_secret: CLIENT_SECRET_GITHUB, + code: code, + redirect_uri: `${SITE_URL}/github` + } + }) + ).data; + + res2 = ( + await axios.get(INTEGRATION_GITHUB_TOKEN_URL, { + params: { + Authorization: `Bearer ${res.access_token}` + } + }) + ).data; + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed OAuth2 code-token exchange with Github'); + } + + // TODO: Check actual response and fix next line + const accountId = res2.user; + + return { + accessToken: res.access_token, + user: accountId, + refreshToken: null, + accessExpiresAt: null + }; +}; + +export { exchangeCode }; diff --git a/backend/src/integrations/refresh.ts b/backend/src/integrations/refresh.ts index 16870944d..382d3ca24 100644 --- a/backend/src/integrations/refresh.ts +++ b/backend/src/integrations/refresh.ts @@ -13,45 +13,57 @@ import { * named [integration] * @param {Object} obj * @param {String} obj.integration - name of integration - * @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku + * @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku */ const exchangeRefresh = async ({ - integration, - refreshToken + integration, + refreshToken }: { - integration: string; - refreshToken: string; + integration: string; + refreshToken: string; }) => { - let accessToken; - try { - switch (integration) { - case INTEGRATION_HEROKU: - accessToken = await exchangeRefreshHeroku({ - refreshToken - }); - break; - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to get new OAuth2 access token'); + let accessToken; + try { + switch (integration) { + case INTEGRATION_HEROKU: + accessToken = await exchangeRefreshHeroku({ + refreshToken + }); + break; } - - return accessToken; -} + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to get new OAuth2 access token'); + } + + return accessToken; +}; /** * Return new access token by exchanging refresh token [refreshToken] for the * Heroku integration * @param {Object} obj * @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku - * @returns + * @returns */ const exchangeRefreshHeroku = async ({ - refreshToken + refreshToken }: { - refreshToken: string; + refreshToken: string; }) => { +<<<<<<< HEAD + let accessToken; + try { + const res = await axios.post( + INTEGRATION_HEROKU_TOKEN_URL, + new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_secret: OAUTH_CLIENT_SECRET_HEROKU + } as any) + ); +======= let accessToken; try { const res = await axios.post( @@ -62,17 +74,16 @@ const exchangeRefreshHeroku = async ({ client_secret: CLIENT_SECRET_HEROKU } as any) ); +>>>>>>> 5444382d5ae1fabf1107434a856b58b9f09c67f6 - accessToken = res.data.access_token; - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to get new OAuth2 access token for Heroku'); - } - - return accessToken; -} + accessToken = res.data.access_token; + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to get new OAuth2 access token for Heroku'); + } -export { - exchangeRefresh -} \ No newline at end of file + return accessToken; +}; + +export { exchangeRefresh }; diff --git a/backend/src/integrations/revoke.ts b/backend/src/integrations/revoke.ts index 833e6c88a..483486343 100644 --- a/backend/src/integrations/revoke.ts +++ b/backend/src/integrations/revoke.ts @@ -1,50 +1,47 @@ import axios from 'axios'; import * as Sentry from '@sentry/node'; +import { IIntegrationAuth, IntegrationAuth, Integration } from '../models'; import { - IIntegrationAuth, - IntegrationAuth, - Integration -} from '../models'; -import { - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB } from '../variables'; const revokeAccess = async ({ - integrationAuth, - accessToken + integrationAuth, + accessToken }: { - integrationAuth: IIntegrationAuth, - accessToken: String + integrationAuth: IIntegrationAuth; + accessToken: string; }) => { - try { - // add any integration-specific revocation logic - switch (integrationAuth.integration) { - case INTEGRATION_HEROKU: - break; - case INTEGRATION_VERCEL: - break; - case INTEGRATION_NETLIFY: - break; - } - - const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({ - _id: integrationAuth._id - }); - - if (deletedIntegrationAuth) { - await Integration.deleteMany({ - integrationAuth: deletedIntegrationAuth._id - }); - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to delete integration authorization'); + try { + // add any integration-specific revocation logic + switch (integrationAuth.integration) { + case INTEGRATION_HEROKU: + break; + case INTEGRATION_VERCEL: + break; + case INTEGRATION_NETLIFY: + break; + case INTEGRATION_GITHUB: + break; } -} -export { - revokeAccess -} \ No newline at end of file + const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({ + _id: integrationAuth._id + }); + + if (deletedIntegrationAuth) { + await Integration.deleteMany({ + integrationAuth: deletedIntegrationAuth._id + }); + } + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to delete integration authorization'); + } +}; + +export { revokeAccess }; diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index b26bdee74..9c6b66674 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -1,16 +1,19 @@ import axios from 'axios'; import * as Sentry from '@sentry/node'; +import { Octokit } from '@octokit/rest'; +import * as sodium from 'libsodium-wrappers'; +import { IIntegration, IIntegrationAuth } from '../models'; import { - IIntegration, IIntegrationAuth -} from '../models'; -import { - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_HEROKU_API_URL, - INTEGRATION_VERCEL_API_URL, - INTEGRATION_NETLIFY_API_URL + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_HEROKU_API_URL, + INTEGRATION_VERCEL_API_URL, + INTEGRATION_NETLIFY_API_URL, + INTEGRATION_GITHUB_API_URL } from '../variables'; +import { access, appendFile } from 'fs'; // TODO: need a helper function in the future to handle integration // envar priorities (i.e. prioritize secrets within integration or those on Infisical) @@ -26,49 +29,54 @@ import { * @param {String} obj.accessToken - access token for integration */ const syncSecrets = async ({ - integration, - integrationAuth, - secrets, - accessToken, + integration, + integrationAuth, + secrets, + accessToken }: { - integration: IIntegration; - integrationAuth: IIntegrationAuth; - secrets: any; - accessToken: string; + integration: IIntegration; + integrationAuth: IIntegrationAuth; + secrets: any; + accessToken: string; }) => { - try { - switch (integration.integration) { - case INTEGRATION_HEROKU: - await syncSecretsHeroku({ - integration, - secrets, - accessToken - }); - break; - case INTEGRATION_VERCEL: - await syncSecretsVercel({ - integration, - secrets, - accessToken - }); - break; - case INTEGRATION_NETLIFY: - await syncSecretsNetlify({ - integration, - integrationAuth, - secrets, - accessToken - }); - break; - } - - // TODO: set integration to inactive if it was not synced correctly (send alert?) - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to sync secrets to integration'); - } -} + try { + switch (integration.integration) { + case INTEGRATION_HEROKU: + await syncSecretsHeroku({ + integration, + secrets, + accessToken + }); + break; + case INTEGRATION_VERCEL: + await syncSecretsVercel({ + integration, + secrets, + accessToken + }); + break; + case INTEGRATION_NETLIFY: + await syncSecretsNetlify({ + integration, + integrationAuth, + secrets, + accessToken + }); + break; + case INTEGRATION_GITHUB: + await syncSecretsGitHub({ + integration, + integrationAuth, + secrets, + accessToken + }); + break; + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to sync secrets to integration'); + } +}; /** * Sync/push [secrets] to Heroku [app] @@ -77,47 +85,49 @@ const syncSecrets = async ({ * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) */ const syncSecretsHeroku = async ({ - integration, - secrets, - accessToken + integration, + secrets, + accessToken }: { - integration: IIntegration, - secrets: any; - accessToken: string; + integration: IIntegration; + secrets: any; + accessToken: string; }) => { - try { - const herokuSecrets = (await axios.get( - `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, - { - headers: { - Accept: 'application/vnd.heroku+json; version=3', - Authorization: `Bearer ${accessToken}` - } - } - )).data; - - Object.keys(herokuSecrets).forEach(key => { - if (!(key in secrets)) { - secrets[key] = null; - } - }); + try { + const herokuSecrets = ( + await axios.get( + `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, + { + headers: { + Accept: 'application/vnd.heroku+json; version=3', + Authorization: `Bearer ${accessToken}` + } + } + ) + ).data; - await axios.patch( - `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, - secrets, - { - headers: { - Accept: 'application/vnd.heroku+json; version=3', - Authorization: `Bearer ${accessToken}` - } - } - ); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to sync secrets to Heroku'); - } -} + Object.keys(herokuSecrets).forEach((key) => { + if (!(key in secrets)) { + secrets[key] = null; + } + }); + + await axios.patch( + `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, + secrets, + { + headers: { + Accept: 'application/vnd.heroku+json; version=3', + Authorization: `Bearer ${accessToken}` + } + } + ); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to sync secrets to Heroku'); + } +}; /** * Sync/push [secrets] to Heroku [app] @@ -474,8 +484,116 @@ const syncSecretsNetlify = async ({ throw new Error('Failed to sync secrets to Heroku'); } } - -export { - syncSecrets -} \ No newline at end of file +/** + * Sync/push [secrets] to GitHub site [app] + * @param {Object} obj + * @param {IIntegration} obj.integration - integration details + * @param {IIntegrationAuth} obj.integrationAuth - integration auth details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + */ +const syncSecretsGitHub = async ({ + integration, + integrationAuth, + secrets, + accessToken +}: { + integration: IIntegration; + integrationAuth: IIntegrationAuth; + secrets: any; + accessToken: string; +}) => { + try { + const deleteSecrets: Array = []; + + const octokit = new Octokit({ + auth: accessToken + }); + + const loggedInUser = await octokit.request('GET /user', {}); + // TODO: Check loggedInUser.login == repo owner + const repoPublicKey = await octokit.request( + 'GET /repos/{owner}/{repo}/actions/secrets/public-key', + { + owner: loggedInUser.login, + repo: integration.app + } + ).key; + + const userRepos = await octokit.request('GET /user/repos', {}); + + // Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key + const encryptedSecrets = await octokit.request( + 'GET /repos/{owner}/{repo}/actions/secrets', + { + owner: loggedInUser.name, + repo: integration.app + } + ); + + Object.keys(secrets).map((key) => { + if (!(key in encryptedSecrets)) { + deleteSecrets.push(key); + } + }); + + if (!Object.values(userRepos).includes(integration.app)) { + if (deleteSecrets.length == 0) { + throw new Error('Failed to sync secrets to Github'); + } + } + + // Sync/push all secrets + for (const i in secrets) { + let encryptedSecret; + sodium.ready.then(() => { + // Convert Secret & Base64 key to Uint8Array. + const binkey = sodium.from_base64( + repoPublicKey, + sodium.base64_variants.ORIGINAL + ); + const binsec = sodium.from_string(secrets[i]); + + //Encrypt the secret using LibSodium + const encBytes = sodium.crypto_box_seal(binsec, binkey); + + // Convert encrypted Uint8Array to Base64 + encryptedSecret = sodium.to_base64( + encBytes, + sodium.base64_variants.ORIGINAL + ); + }); + + const res = await octokit.request( + 'PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}', + { + owner: loggedInUser.login, + repo: integration.app, + secret_name: Object.keys(secrets[i]), + encrypted_value: encryptedSecret, + key_id: '' //TODO: Not sure if we need this? https://docs.github.com/en/rest/actions/secrets?apiVersion=2022-11-28#create-or-update-a-repository-secret + } + ); + } + + // Delete secrets + if (deleteSecrets.length > 0) { + for (const i in deleteSecrets) { + const res = await octokit.request( + 'DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}', + { + owner: loggedInUser.login, + repo: integration.app, + secret_name: secrets[i] + } + ); + } + } + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to sync secrets to Github'); + } +}; + +export { syncSecrets }; diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index edbe0234e..6da699216 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -1,77 +1,83 @@ import { Schema, model, Types } from 'mongoose'; import { - ENV_DEV, - ENV_TESTING, - ENV_STAGING, - ENV_PROD, - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY + ENV_DEV, + ENV_TESTING, + ENV_STAGING, + ENV_PROD, + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB } from '../variables'; export interface IIntegration { - _id: Types.ObjectId; - workspace: Types.ObjectId; - environment: 'dev' | 'test' | 'staging' | 'prod'; - isActive: boolean; - app: string; - target: string; - context: string; - siteId: string; - integration: 'heroku' | 'vercel' | 'netlify'; - integrationAuth: Types.ObjectId; + _id: Types.ObjectId; + workspace: Types.ObjectId; + environment: 'dev' | 'test' | 'staging' | 'prod'; + isActive: boolean; + app: string; + target: string; + context: string; + siteId: string; + integration: 'heroku' | 'vercel' | 'netlify' | 'github'; + integrationAuth: Types.ObjectId; } const integrationSchema = new Schema( - { - workspace: { - type: Schema.Types.ObjectId, - ref: 'Workspace', - required: true - }, - environment: { - type: String, - enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD], - required: true - }, - isActive: { - type: Boolean, - required: true - }, - app: { // name of app in provider - type: String, - default: null - }, - target: { // vercel-specific target (environment) - type: String, - default: null - }, - context: { // netlify-specific context (deploy) - type: String, - default: null - }, - siteId: { // netlify-specific site (app) id - type: String, - default: null - }, - integration: { - type: String, - enum: [ - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY - ], - required: true - }, - integrationAuth: { - type: Schema.Types.ObjectId, - ref: 'IntegrationAuth', - required: true - } - }, - { - timestamps: true - } + { + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace', + required: true + }, + environment: { + type: String, + enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD], + required: true + }, + isActive: { + type: Boolean, + required: true + }, + app: { + // name of app in provider + type: String, + default: null + }, + target: { + // vercel-specific target (environment) + type: String, + default: null + }, + context: { + // netlify-specific context (deploy) + type: String, + default: null + }, + siteId: { + // netlify-specific site (app) id + type: String, + default: null + }, + integration: { + type: String, + enum: [ + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB + ], + required: true + }, + integrationAuth: { + type: Schema.Types.ObjectId, + ref: 'IntegrationAuth', + required: true + } + }, + { + timestamps: true + } ); const Integration = model('Integration', integrationSchema); diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index 0da3eb0d8..231416588 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -1,83 +1,87 @@ import { Schema, model, Types } from 'mongoose'; -import { - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY +import { + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB } from '../variables'; export interface IIntegrationAuth { - _id: Types.ObjectId; - workspace: Types.ObjectId; - integration: 'heroku' | 'vercel' | 'netlify'; - teamId: string; - accountId: string; - refreshCiphertext?: string; - refreshIV?: string; - refreshTag?: string; - accessCiphertext?: string; - accessIV?: string; - accessTag?: string; - accessExpiresAt?: Date; + _id: Types.ObjectId; + workspace: Types.ObjectId; + integration: 'heroku' | 'vercel' | 'netlify' | 'github'; + teamId: string; + accountId: string; + refreshCiphertext?: string; + refreshIV?: string; + refreshTag?: string; + accessCiphertext?: string; + accessIV?: string; + accessTag?: string; + accessExpiresAt?: Date; } const integrationAuthSchema = new Schema( - { - workspace: { - type: Schema.Types.ObjectId, - required: true - }, - integration: { - type: String, - enum: [ - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY - ], - required: true - }, - teamId: { // vercel-specific integration param - type: String - }, - accountId: { // netlify-specific integration param - type: String - }, - refreshCiphertext: { - type: String, - select: false - }, - refreshIV: { - type: String, - select: false - }, - refreshTag: { - type: String, - select: false - }, - accessCiphertext: { - type: String, - select: false - }, - accessIV: { - type: String, - select: false - }, - accessTag: { - type: String, - select: false - }, - accessExpiresAt: { - type: Date, - select: false - } - }, - { - timestamps: true - } + { + workspace: { + type: Schema.Types.ObjectId, + required: true + }, + integration: { + type: String, + enum: [ + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB + ], + required: true + }, + teamId: { + // vercel-specific integration param + type: String + }, + accountId: { + // netlify-specific integration param + type: String + }, + refreshCiphertext: { + type: String, + select: false + }, + refreshIV: { + type: String, + select: false + }, + refreshTag: { + type: String, + select: false + }, + accessCiphertext: { + type: String, + select: false + }, + accessIV: { + type: String, + select: false + }, + accessTag: { + type: String, + select: false + }, + accessExpiresAt: { + type: Date, + select: false + } + }, + { + timestamps: true + } ); const IntegrationAuth = model( - 'IntegrationAuth', - integrationAuthSchema + 'IntegrationAuth', + integrationAuthSchema ); export default IntegrationAuth; diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts index b21324423..c69ed8176 100644 --- a/backend/src/variables/index.ts +++ b/backend/src/variables/index.ts @@ -1,79 +1,74 @@ import { - ENV_DEV, - ENV_TESTING, - ENV_STAGING, - ENV_PROD, - ENV_SET + ENV_DEV, + ENV_TESTING, + ENV_STAGING, + ENV_PROD, + ENV_SET } from './environment'; import { - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_SET, - INTEGRATION_OAUTH2, - INTEGRATION_HEROKU_TOKEN_URL, - INTEGRATION_VERCEL_TOKEN_URL, - INTEGRATION_NETLIFY_TOKEN_URL, - INTEGRATION_HEROKU_API_URL, - INTEGRATION_VERCEL_API_URL, - INTEGRATION_NETLIFY_API_URL, - INTEGRATION_OPTIONS + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_SET, + INTEGRATION_OAUTH2, + INTEGRATION_HEROKU_TOKEN_URL, + INTEGRATION_VERCEL_TOKEN_URL, + INTEGRATION_NETLIFY_TOKEN_URL, + INTEGRATION_GITHUB_TOKEN_URL, + INTEGRATION_HEROKU_API_URL, + INTEGRATION_VERCEL_API_URL, + INTEGRATION_NETLIFY_API_URL, + INTEGRATION_GITHUB_API_URL, + INTEGRATION_OPTIONS } from './integration'; import { - OWNER, - ADMIN, - MEMBER, - INVITED, - ACCEPTED, - COMPLETED, - GRANTED + OWNER, + ADMIN, + MEMBER, + INVITED, + ACCEPTED, + COMPLETED, + GRANTED } from './organization'; -import { - SECRET_SHARED, - SECRET_PERSONAL -} from './secret'; -import { - PLAN_STARTER, - PLAN_PRO -} from './stripe'; -import { - EVENT_PUSH_SECRETS, - EVENT_PULL_SECRETS -} from './event'; -import { - ACTION_PUSH_TO_HEROKU -} from './action'; +import { SECRET_SHARED, SECRET_PERSONAL } from './secret'; +import { PLAN_STARTER, PLAN_PRO } from './stripe'; +import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event'; +import { ACTION_PUSH_TO_HEROKU } from './action'; export { - OWNER, - ADMIN, - MEMBER, - INVITED, - ACCEPTED, - COMPLETED, - GRANTED, - PLAN_STARTER, - PLAN_PRO, - SECRET_SHARED, - SECRET_PERSONAL, - ENV_DEV, - ENV_TESTING, - ENV_STAGING, - ENV_PROD, - ENV_SET, - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_SET, - INTEGRATION_OAUTH2, - INTEGRATION_HEROKU_TOKEN_URL, - INTEGRATION_VERCEL_TOKEN_URL, - INTEGRATION_NETLIFY_TOKEN_URL, - INTEGRATION_HEROKU_API_URL, - INTEGRATION_VERCEL_API_URL, - INTEGRATION_NETLIFY_API_URL, - EVENT_PUSH_SECRETS, - EVENT_PULL_SECRETS, - ACTION_PUSH_TO_HEROKU, - INTEGRATION_OPTIONS -}; \ No newline at end of file + OWNER, + ADMIN, + MEMBER, + INVITED, + ACCEPTED, + COMPLETED, + GRANTED, + PLAN_STARTER, + PLAN_PRO, + SECRET_SHARED, + SECRET_PERSONAL, + ENV_DEV, + ENV_TESTING, + ENV_STAGING, + ENV_PROD, + ENV_SET, + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_SET, + INTEGRATION_OAUTH2, + INTEGRATION_HEROKU_TOKEN_URL, + INTEGRATION_VERCEL_TOKEN_URL, + INTEGRATION_NETLIFY_TOKEN_URL, + INTEGRATION_GITHUB_TOKEN_URL, + INTEGRATION_HEROKU_API_URL, + INTEGRATION_VERCEL_API_URL, + INTEGRATION_NETLIFY_API_URL, + INTEGRATION_GITHUB_API_URL, + EVENT_PUSH_SECRETS, + EVENT_PULL_SECRETS, + ACTION_PUSH_TO_HEROKU, + INTEGRATION_OPTIONS +}; diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index 55ae271a1..2e0a8182b 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -8,10 +8,12 @@ import { const INTEGRATION_HEROKU = 'heroku'; const INTEGRATION_VERCEL = 'vercel'; const INTEGRATION_NETLIFY = 'netlify'; +const INTEGRATION_GITHUB = 'github'; const INTEGRATION_SET = new Set([ - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB ]); // integration types @@ -19,13 +21,17 @@ const INTEGRATION_OAUTH2 = 'oauth2'; // integration oauth endpoints const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token'; -const INTEGRATION_VERCEL_TOKEN_URL = 'https://api.vercel.com/v2/oauth/access_token'; +const INTEGRATION_VERCEL_TOKEN_URL = + 'https://api.vercel.com/v2/oauth/access_token'; const INTEGRATION_NETLIFY_TOKEN_URL = 'https://api.netlify.com/oauth/token'; +const INTEGRATION_GITHUB_TOKEN_URL = + 'https://github.com/login/oauth/access_token'; // integration apps endpoints const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com'; const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com'; const INTEGRATION_NETLIFY_API_URL = 'https://api.netlify.com'; +const INTEGRATION_GITHUB_API_URL = ' https://api.github.com/'; const INTEGRATION_OPTIONS = [ { @@ -104,16 +110,19 @@ const INTEGRATION_OPTIONS = [ ] export { - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_SET, - INTEGRATION_OAUTH2, - INTEGRATION_HEROKU_TOKEN_URL, - INTEGRATION_VERCEL_TOKEN_URL, - INTEGRATION_NETLIFY_TOKEN_URL, - INTEGRATION_HEROKU_API_URL, - INTEGRATION_VERCEL_API_URL, - INTEGRATION_NETLIFY_API_URL, - INTEGRATION_OPTIONS -} \ No newline at end of file + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_SET, + INTEGRATION_OAUTH2, + INTEGRATION_HEROKU_TOKEN_URL, + INTEGRATION_VERCEL_TOKEN_URL, + INTEGRATION_NETLIFY_TOKEN_URL, + INTEGRATION_GITHUB_TOKEN_URL, + INTEGRATION_HEROKU_API_URL, + INTEGRATION_VERCEL_API_URL, + INTEGRATION_NETLIFY_API_URL, + INTEGRATION_GITHUB_API_URL, + INTEGRATION_OPTIONS +}; diff --git a/frontend/pages/github.js b/frontend/pages/github.js new file mode 100644 index 000000000..af2b2b087 --- /dev/null +++ b/frontend/pages/github.js @@ -0,0 +1,37 @@ +import React, { useEffect } from "react"; +import Head from "next/head"; +import { useRouter } from "next/router"; +const queryString = require("query-string"); +import AuthorizeIntegration from "./api/integrations/authorizeIntegration"; + +export default function Github() { + const router = useRouter(); + const parsedUrl = queryString.parse(router.asPath.split("?")[1]); + const code = parsedUrl.code; + const state = parsedUrl.state; + + /** + * Here we forward to the default workspace if a user opens this url + */ + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(async () => { + try { + if (state === localStorage.getItem('latestCSRFToken')) { + localStorage.removeItem('latestCSRFToken'); + await AuthorizeIntegration({ + workspaceId: localStorage.getItem('projectData.id'), + code, + integration: "github", + }); + router.push("/integrations/" + localStorage.getItem("projectData.id")); + } + } catch (error) { + console.error('Github integration error: ', error); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return
; +} + +Github.requireAuth = true; diff --git a/frontend/pages/integrations/[id].js b/frontend/pages/integrations/[id].js index a7b5f6b13..c47822c80 100644 --- a/frontend/pages/integrations/[id].js +++ b/frontend/pages/integrations/[id].js @@ -137,6 +137,14 @@ export default function Integrations() { window.location = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`; break; case 'Netlify': + window.location = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${integrationOption.redirectURL}&state=${state}`; + break; + case 'Github': + window.location = `https://github.com.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${integrationOption.redirectURL}&state=${state}`; + break; + case 'Fly.io': + console.log('fly.io'); + setIntegrationAccessTokenDialogOpen(true); window.location = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/netlify`; break; // case 'Fly.io':