mirror of
https://github.com/Myxelium/Bridge-Multi.git
synced 2026-04-09 05:09:39 +00:00
Google authentication
This commit is contained in:
384
package-lock.json
generated
384
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bridge",
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -599,6 +599,17 @@
|
||||
"graceful-fs": "^4.1.2",
|
||||
"jsonfile": "^4.0.0",
|
||||
"universalify": "^0.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"jsonfile": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"get-caller-file": {
|
||||
@@ -2180,6 +2191,15 @@
|
||||
"integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/jsonfile": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.0.0.tgz",
|
||||
"integrity": "sha512-mUHbRieyluPtL3c466K7oUGua1lAVlz45PV4U3bHs5CXdBlDIeXJI5xQXa6IZYnrgmcJzJp/CiTZB4zfShAi6w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/minimatch": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
|
||||
@@ -2578,6 +2598,14 @@
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
|
||||
},
|
||||
"abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"requires": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"accepts": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
|
||||
@@ -3322,8 +3350,7 @@
|
||||
"base64-js": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
|
||||
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==",
|
||||
"dev": true
|
||||
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
|
||||
},
|
||||
"batch": {
|
||||
"version": "0.6.1",
|
||||
@@ -3388,6 +3415,11 @@
|
||||
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
|
||||
"dev": true
|
||||
},
|
||||
"bignumber.js": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz",
|
||||
"integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A=="
|
||||
},
|
||||
"binary-extensions": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
|
||||
@@ -3503,6 +3535,11 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"bottleneck": {
|
||||
"version": "2.19.5",
|
||||
"resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
|
||||
"integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="
|
||||
},
|
||||
"boxen": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz",
|
||||
@@ -3735,6 +3772,11 @@
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz",
|
||||
"integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74="
|
||||
},
|
||||
"buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
|
||||
},
|
||||
"buffer-from": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
|
||||
@@ -5686,6 +5728,14 @@
|
||||
"safer-buffer": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"requires": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"editions": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/editions/-/editions-1.3.4.tgz",
|
||||
@@ -6121,6 +6171,16 @@
|
||||
"requires": {
|
||||
"jsonfile": "^4.0.0",
|
||||
"mkdirp": "^0.5.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"jsonfile": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
|
||||
"requires": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"elliptic": {
|
||||
@@ -6609,6 +6669,11 @@
|
||||
"es5-ext": "~0.10.14"
|
||||
}
|
||||
},
|
||||
"event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
|
||||
},
|
||||
"eventemitter3": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz",
|
||||
@@ -6943,6 +7008,11 @@
|
||||
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
|
||||
"dev": true
|
||||
},
|
||||
"fast-text-encoding": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz",
|
||||
"integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig=="
|
||||
},
|
||||
"faye-websocket": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz",
|
||||
@@ -7421,6 +7491,17 @@
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^4.0.0",
|
||||
"universalify": "^0.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"jsonfile": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fs-minipass": {
|
||||
@@ -8311,6 +8392,260 @@
|
||||
"sparkles": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"googleapis": {
|
||||
"version": "59.0.0",
|
||||
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-59.0.0.tgz",
|
||||
"integrity": "sha512-GV/E4KRN89a4GxSk7D7cwUfRYgcJHR05sOgm/WGdwc/u8dxNXG5lWmz9gF5ZwFGk2yKtVxL4VZNn4zBuZ6rmGg==",
|
||||
"requires": {
|
||||
"google-auth-library": "^6.0.0",
|
||||
"googleapis-common": "^4.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"agent-base": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz",
|
||||
"integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==",
|
||||
"requires": {
|
||||
"debug": "4"
|
||||
}
|
||||
},
|
||||
"arrify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
|
||||
"integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug=="
|
||||
},
|
||||
"gaxios": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-3.1.0.tgz",
|
||||
"integrity": "sha512-DDTn3KXVJJigtz+g0J3vhcfbDbKtAroSTxauWsdnP57sM5KZ3d2c/3D9RKFJ86s43hfw6WULg6TXYw/AYiBlpA==",
|
||||
"requires": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"extend": "^3.0.2",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"is-stream": "^2.0.0",
|
||||
"node-fetch": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"gcp-metadata": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.1.4.tgz",
|
||||
"integrity": "sha512-5J/GIH0yWt/56R3dNaNWPGQ/zXsZOddYECfJaqxFWgrZ9HC2Kvc5vl9upOgUUHKzURjAVf2N+f6tEJiojqXUuA==",
|
||||
"requires": {
|
||||
"gaxios": "^3.0.0",
|
||||
"json-bigint": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"google-auth-library": {
|
||||
"version": "6.0.6",
|
||||
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.0.6.tgz",
|
||||
"integrity": "sha512-fWYdRdg55HSJoRq9k568jJA1lrhg9i2xgfhVIMJbskUmbDpJGHsbv9l41DGhCDXM21F9Kn4kUwdysgxSYBYJUw==",
|
||||
"requires": {
|
||||
"arrify": "^2.0.0",
|
||||
"base64-js": "^1.3.0",
|
||||
"ecdsa-sig-formatter": "^1.0.11",
|
||||
"fast-text-encoding": "^1.0.0",
|
||||
"gaxios": "^3.0.0",
|
||||
"gcp-metadata": "^4.1.0",
|
||||
"gtoken": "^5.0.0",
|
||||
"jws": "^4.0.0",
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"google-p12-pem": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.2.tgz",
|
||||
"integrity": "sha512-tbjzndQvSIHGBLzHnhDs3cL4RBjLbLXc2pYvGH+imGVu5b4RMAttUTdnmW2UH0t11QeBTXZ7wlXPS7hrypO/tg==",
|
||||
"requires": {
|
||||
"node-forge": "^0.9.0"
|
||||
}
|
||||
},
|
||||
"gtoken": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.0.3.tgz",
|
||||
"integrity": "sha512-Nyd1wZCMRc2dj/mAD0LlfQLcAO06uKdpKJXvK85SGrF5+5+Bpfil9u/2aw35ltvEHjvl0h5FMKN5knEU+9JrOg==",
|
||||
"requires": {
|
||||
"gaxios": "^3.0.0",
|
||||
"google-p12-pem": "^3.0.0",
|
||||
"jws": "^4.0.0",
|
||||
"mime": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"https-proxy-agent": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
|
||||
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
|
||||
"requires": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
}
|
||||
},
|
||||
"is-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw=="
|
||||
},
|
||||
"json-bigint": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
|
||||
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
|
||||
"requires": {
|
||||
"bignumber.js": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"mime": {
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz",
|
||||
"integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA=="
|
||||
},
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"googleapis-common": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-4.4.0.tgz",
|
||||
"integrity": "sha512-Bgrs8/1OZQFFIfVuX38L9t48rPAkVUXttZy6NzhhXxFOEMSHgfFIjxou7RIXOkBHxmx2pVwct9WjKkbnqMYImQ==",
|
||||
"requires": {
|
||||
"extend": "^3.0.2",
|
||||
"gaxios": "^3.0.0",
|
||||
"google-auth-library": "^6.0.0",
|
||||
"qs": "^6.7.0",
|
||||
"url-template": "^2.0.8",
|
||||
"uuid": "^8.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"agent-base": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz",
|
||||
"integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==",
|
||||
"requires": {
|
||||
"debug": "4"
|
||||
}
|
||||
},
|
||||
"arrify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
|
||||
"integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug=="
|
||||
},
|
||||
"gaxios": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-3.1.0.tgz",
|
||||
"integrity": "sha512-DDTn3KXVJJigtz+g0J3vhcfbDbKtAroSTxauWsdnP57sM5KZ3d2c/3D9RKFJ86s43hfw6WULg6TXYw/AYiBlpA==",
|
||||
"requires": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"extend": "^3.0.2",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"is-stream": "^2.0.0",
|
||||
"node-fetch": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"gcp-metadata": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.1.4.tgz",
|
||||
"integrity": "sha512-5J/GIH0yWt/56R3dNaNWPGQ/zXsZOddYECfJaqxFWgrZ9HC2Kvc5vl9upOgUUHKzURjAVf2N+f6tEJiojqXUuA==",
|
||||
"requires": {
|
||||
"gaxios": "^3.0.0",
|
||||
"json-bigint": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"google-auth-library": {
|
||||
"version": "6.0.6",
|
||||
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.0.6.tgz",
|
||||
"integrity": "sha512-fWYdRdg55HSJoRq9k568jJA1lrhg9i2xgfhVIMJbskUmbDpJGHsbv9l41DGhCDXM21F9Kn4kUwdysgxSYBYJUw==",
|
||||
"requires": {
|
||||
"arrify": "^2.0.0",
|
||||
"base64-js": "^1.3.0",
|
||||
"ecdsa-sig-formatter": "^1.0.11",
|
||||
"fast-text-encoding": "^1.0.0",
|
||||
"gaxios": "^3.0.0",
|
||||
"gcp-metadata": "^4.1.0",
|
||||
"gtoken": "^5.0.0",
|
||||
"jws": "^4.0.0",
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"google-p12-pem": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.2.tgz",
|
||||
"integrity": "sha512-tbjzndQvSIHGBLzHnhDs3cL4RBjLbLXc2pYvGH+imGVu5b4RMAttUTdnmW2UH0t11QeBTXZ7wlXPS7hrypO/tg==",
|
||||
"requires": {
|
||||
"node-forge": "^0.9.0"
|
||||
}
|
||||
},
|
||||
"gtoken": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.0.3.tgz",
|
||||
"integrity": "sha512-Nyd1wZCMRc2dj/mAD0LlfQLcAO06uKdpKJXvK85SGrF5+5+Bpfil9u/2aw35ltvEHjvl0h5FMKN5knEU+9JrOg==",
|
||||
"requires": {
|
||||
"gaxios": "^3.0.0",
|
||||
"google-p12-pem": "^3.0.0",
|
||||
"jws": "^4.0.0",
|
||||
"mime": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"https-proxy-agent": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
|
||||
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
|
||||
"requires": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
}
|
||||
},
|
||||
"is-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw=="
|
||||
},
|
||||
"json-bigint": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
|
||||
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
|
||||
"requires": {
|
||||
"bignumber.js": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"mime": {
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz",
|
||||
"integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA=="
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.9.4",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz",
|
||||
"integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ=="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz",
|
||||
"integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ=="
|
||||
},
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"got": {
|
||||
"version": "9.6.0",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz",
|
||||
@@ -10536,11 +10871,19 @@
|
||||
}
|
||||
},
|
||||
"jsonfile": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz",
|
||||
"integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==",
|
||||
"requires": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
"graceful-fs": "^4.1.6",
|
||||
"universalify": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"universalify": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
|
||||
"integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"jsonparse": {
|
||||
@@ -10565,6 +10908,25 @@
|
||||
"resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz",
|
||||
"integrity": "sha1-h/zPrv/AtozRnVX2cilD+SnqNeo="
|
||||
},
|
||||
"jwa": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
|
||||
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
|
||||
"requires": {
|
||||
"buffer-equal-constant-time": "1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"jws": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
||||
"requires": {
|
||||
"jwa": "^2.0.0",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"karma-source-map-support": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz",
|
||||
@@ -11962,8 +12324,7 @@
|
||||
"node-forge": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz",
|
||||
"integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ=="
|
||||
},
|
||||
"node-libs-browser": {
|
||||
"version": "2.2.1",
|
||||
@@ -17489,6 +17850,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"url-template": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
|
||||
"integrity": "sha1-/FZaPMy/93MMd19WQflVV5FDnyE="
|
||||
},
|
||||
"use": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
||||
|
||||
@@ -34,13 +34,16 @@
|
||||
"@angular/platform-browser": "~9.1.4",
|
||||
"@angular/platform-browser-dynamic": "~9.1.4",
|
||||
"@angular/router": "~9.1.4",
|
||||
"bottleneck": "^2.19.5",
|
||||
"cli-color": "^2.0.0",
|
||||
"comparators": "^3.0.2",
|
||||
"electron-unhandled": "^3.0.2",
|
||||
"electron-updater": "^4.3.1",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"fomantic-ui": "^2.8.3",
|
||||
"googleapis": "^59.0.0",
|
||||
"jquery": "^3.5.1",
|
||||
"jsonfile": "^6.0.1",
|
||||
"mv": "^2.1.1",
|
||||
"needle": "^2.3.2",
|
||||
"node-7z": "^2.0.5",
|
||||
@@ -61,6 +64,7 @@
|
||||
"@angular/language-service": "~9.1.4",
|
||||
"@types/cli-color": "^2.0.0",
|
||||
"@types/electron-window-state": "^2.0.33",
|
||||
"@types/jsonfile": "^6.0.0",
|
||||
"@types/mv": "^2.1.0",
|
||||
"@types/needle": "^2.0.4",
|
||||
"@types/node": "^12.11.1",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<h3 class="ui header">Downloads</h3>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<div *ngIf="loginAvailable" class="field">
|
||||
<label>Google rate limit delay</label>
|
||||
<div id="rateLimitInput" class="ui right labeled input">
|
||||
<input type="number" [value]="settingsService.rateLimitDelay" (input)="changeRateLimit($event)">
|
||||
@@ -28,12 +28,21 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="loginAvailable" class="field">
|
||||
<div class="ui button" data-tooltip="Removes rate limit delay" data-position="right center" (click)="googleLogin()">
|
||||
<i class="google icon"></i>Sign in with Google
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="!loginAvailable" class="field">
|
||||
<div class="ui button" (click)="googleLogout()">
|
||||
<i class="google icon"></i>Sign out
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="settingsService.rateLimitDelay < 30" class="ui warning message">
|
||||
<i class="exclamation circle icon"></i>
|
||||
<b>Warning:</b> downloading files from Google with a delay less than about 30 seconds will eventually cause Google to
|
||||
refuse download requests from this program for a few hours. This can be avoided by authenticating with your Google account.
|
||||
(this will be possible in a future update to Bridge)
|
||||
</div>
|
||||
|
||||
<!-- <h3 class="ui header">Theme</h3>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, ViewChild, ElementRef, AfterViewInit, ChangeDetectorRef } from '@angular/core'
|
||||
import { Component, OnInit, AfterViewInit, ChangeDetectorRef } from '@angular/core'
|
||||
import { ElectronService } from 'src/app/core/services/electron.service'
|
||||
import { SettingsService } from 'src/app/core/services/settings.service'
|
||||
|
||||
@@ -12,6 +12,8 @@ export class SettingsComponent implements OnInit, AfterViewInit {
|
||||
|
||||
cacheSize = 'Calculating...'
|
||||
updateAvailable = false
|
||||
loginAvailable = true
|
||||
loginClicked = false
|
||||
downloadUpdateText = 'Update available'
|
||||
updateDownloading = false
|
||||
updateDownloaded = false
|
||||
@@ -35,6 +37,10 @@ export class SettingsComponent implements OnInit, AfterViewInit {
|
||||
this.updateAvailable = isAvailable
|
||||
this.ref.detectChanges()
|
||||
})
|
||||
this.electronService.invoke('get-auth-status', undefined).then(isAuthenticated => {
|
||||
this.loginAvailable = !isAuthenticated
|
||||
this.ref.detectChanges()
|
||||
})
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
@@ -64,6 +70,19 @@ export class SettingsComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
async googleLogin() {
|
||||
if (this.loginClicked) { return }
|
||||
this.loginClicked = true
|
||||
const isAuthenticated = await this.electronService.invoke('google-login', undefined)
|
||||
this.loginAvailable = !isAuthenticated
|
||||
this.loginClicked = false
|
||||
}
|
||||
|
||||
async googleLogout() {
|
||||
this.loginAvailable = true
|
||||
await this.electronService.invoke('google-logout', undefined)
|
||||
}
|
||||
|
||||
openLibraryDirectory() {
|
||||
this.electronService.openFolder(this.settingsService.libraryDirectory)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FileDownloader } from './FileDownloader'
|
||||
import { FileDownloader, getDownloader } from './FileDownloader'
|
||||
import { join, parse } from 'path'
|
||||
import { FileExtractor } from './FileExtractor'
|
||||
import { sanitizeFilename, interpolate } from '../../shared/UtilFunctions'
|
||||
@@ -140,7 +140,10 @@ export class ChartDownload {
|
||||
// DOWNLOAD FILES
|
||||
for (let i = 0; i < this.files.length; i++) {
|
||||
if (this.files[i].name == 'ch.dat') { continue }
|
||||
const downloader = new FileDownloader(this.files[i].webContentLink, join(this.tempPath, this.files[i].name))
|
||||
let wasCanceled = false
|
||||
this.cancelFn = () => { wasCanceled = true }
|
||||
const downloader = await getDownloader(this.files[i].webContentLink, join(this.tempPath, this.files[i].name))
|
||||
if (wasCanceled) { return }
|
||||
this.cancelFn = () => downloader.cancelDownload()
|
||||
|
||||
const downloadComplete = this.addDownloadEventListeners(downloader, i)
|
||||
@@ -194,23 +197,25 @@ export class ChartDownload {
|
||||
*/
|
||||
private addDownloadEventListeners(downloader: FileDownloader, fileIndex: number) {
|
||||
let downloadHeader = `[${this.files[fileIndex].name}] (file ${fileIndex + 1}/${this.files.length})`
|
||||
let downloadStartPoint = 0 // How far into the individual file progress portion the download progress starts
|
||||
let fileProgress = 0
|
||||
|
||||
downloader.on('waitProgress', (remainingSeconds: number, totalSeconds: number) => {
|
||||
downloadStartPoint = this.individualFileProgressPortion / 2
|
||||
this.percent = this._allFilesProgress + interpolate(remainingSeconds, totalSeconds, 0, 0, this.individualFileProgressPortion / 2)
|
||||
this.updateGUI(downloadHeader, `Waiting for Google rate limit... (${remainingSeconds}s)`, 'good')
|
||||
})
|
||||
|
||||
downloader.on('requestSent', () => {
|
||||
fileProgress = this.individualFileProgressPortion / 2
|
||||
fileProgress = downloadStartPoint
|
||||
this.percent = this._allFilesProgress + fileProgress
|
||||
this.updateGUI(downloadHeader, 'Sending request...', 'good')
|
||||
})
|
||||
|
||||
downloader.on('downloadProgress', (bytesDownloaded) => {
|
||||
downloader.on('downloadProgress', (bytesDownloaded: number) => {
|
||||
downloadHeader = `[${this.files[fileIndex].name}] (file ${fileIndex + 1}/${this.files.length})`
|
||||
const size = Number(this.files[fileIndex].size)
|
||||
fileProgress = interpolate(bytesDownloaded, 0, size, this.individualFileProgressPortion / 2, this.individualFileProgressPortion)
|
||||
fileProgress = interpolate(bytesDownloaded, 0, size, downloadStartPoint, this.individualFileProgressPortion)
|
||||
this.percent = this._allFilesProgress + fileProgress
|
||||
this.updateGUI(downloadHeader, `Downloading... (${Math.round(1000 * bytesDownloaded / size) / 10}%)`, 'fastUpdate')
|
||||
})
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import { AnyFunction } from '../../shared/UtilFunctions'
|
||||
import { createWriteStream } from 'fs'
|
||||
import * as needle from 'needle'
|
||||
import { Readable } from 'stream'
|
||||
// TODO: replace needle with got (for cancel() method) (if before-headers event is possible?)
|
||||
import { googleTimer } from './GoogleTimer'
|
||||
import { DownloadError } from './ChartDownload'
|
||||
import { googleAuth } from '../google/GoogleAuth'
|
||||
import { google } from 'googleapis'
|
||||
import Bottleneck from 'bottleneck'
|
||||
const drive = google.drive('v3')
|
||||
const limiter = new Bottleneck({
|
||||
minTime: 200 // Wait 200 ms between API requests
|
||||
})
|
||||
|
||||
const RETRY_MAX = 2
|
||||
|
||||
type EventCallback = {
|
||||
'waitProgress': (remainingSeconds: number, totalSeconds: number) => void
|
||||
@@ -15,12 +25,157 @@ type EventCallback = {
|
||||
'complete': () => void
|
||||
}
|
||||
type Callbacks = { [E in keyof EventCallback]: EventCallback[E] }
|
||||
export type FileDownloader = APIFileDownloader | SlowFileDownloader
|
||||
|
||||
const downloadErrors = {
|
||||
timeout: (type: string) => { return { header: 'Timeout', body: `The download server could not be reached. (type=${type})` } },
|
||||
connectionError: (err: Error) => { return { header: 'Connection Error', body: `${err.name}: ${err.message}` } },
|
||||
responseError: (statusCode: number) => { return { header: 'Connection failed', body: `Server returned status code: ${statusCode}` } },
|
||||
htmlError: () => { return { header: 'Invalid response', body: 'Download server returned HTML instead of a file.' } }
|
||||
htmlError: () => { return { header: 'Invalid response', body: 'Download server returned HTML instead of a file.' } },
|
||||
linkError: (url: string) => { return { header: 'Invalid link', body: `The download link is not formatted correctly: ${url}` } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a file from `url` to `fullPath`.
|
||||
* Will handle google drive virus scan warnings. Provides event listeners for download progress.
|
||||
* On error, provides the ability to retry.
|
||||
* Will only send download requests once every `getSettings().rateLimitDelay` seconds if a Google account has not been authenticated.
|
||||
* @param url The download link.
|
||||
* @param fullPath The full path to where this file should be stored (including the filename).
|
||||
*/
|
||||
export async function getDownloader(url: string, fullPath: string): Promise<FileDownloader> {
|
||||
if (await googleAuth.attemptToAuthenticate()) {
|
||||
return new APIFileDownloader(url, fullPath)
|
||||
} else {
|
||||
return new SlowFileDownloader(url, fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a file from `url` to `fullPath`.
|
||||
* On error, provides the ability to retry.
|
||||
*/
|
||||
class APIFileDownloader {
|
||||
private readonly URL_REGEX = /uc\?id=([^&]*)&export=download/u
|
||||
|
||||
private callbacks = {} as Callbacks
|
||||
private retryCount: number
|
||||
private wasCanceled = false
|
||||
private fileID: string
|
||||
private downloadStream: Readable
|
||||
|
||||
/**
|
||||
* @param url The download link.
|
||||
* @param fullPath The full path to where this file should be stored (including the filename).
|
||||
*/
|
||||
constructor(private url: string, private fullPath: string) {
|
||||
// url looks like: "https://drive.google.com/uc?id=1TlxtOZlVgRgX7-1tyW0d5QzXVfL-MC3Q&export=download"
|
||||
this.fileID = this.URL_REGEX.exec(url)[1]
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls `callback` when `event` fires. (no events will be fired after `this.cancelDownload()` is called)
|
||||
*/
|
||||
on<E extends keyof EventCallback>(event: E, callback: EventCallback[E]) {
|
||||
this.callbacks[event] = callback
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the file after waiting for the google rate limit.
|
||||
*/
|
||||
beginDownload() {
|
||||
if (this.fileID == undefined) {
|
||||
this.failDownload(downloadErrors.linkError(this.url))
|
||||
}
|
||||
|
||||
this.startDownloadStream()
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the Google Drive API to start a download stream for the file with `this.fileID`.
|
||||
*/
|
||||
private startDownloadStream() {
|
||||
limiter.schedule(this.cancelable(async () => {
|
||||
this.callbacks.requestSent()
|
||||
try {
|
||||
this.downloadStream = (await drive.files.get({
|
||||
fileId: this.fileID,
|
||||
alt: 'media'
|
||||
}, {
|
||||
responseType: 'stream'
|
||||
})).data
|
||||
|
||||
if (this.wasCanceled) { return }
|
||||
|
||||
this.handleDownloadResponse()
|
||||
} catch (err) {
|
||||
this.retryCount++
|
||||
if (this.retryCount <= RETRY_MAX) {
|
||||
console.log(`Failed to get file: Retry attempt ${this.retryCount}...`)
|
||||
if (this.wasCanceled) { return }
|
||||
this.startDownloadStream()
|
||||
} else {
|
||||
console.log(err)
|
||||
this.failDownload(downloadErrors.responseError(err ? (err.code ?? 'unknown') : 'unknown'))
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Pipes the data from a download response to `this.fullPath`.
|
||||
* @param req The download request.
|
||||
*/
|
||||
private handleDownloadResponse() {
|
||||
this.callbacks.downloadProgress(0)
|
||||
let downloadedSize = 0
|
||||
try {
|
||||
this.downloadStream.pipe(createWriteStream(this.fullPath))
|
||||
} catch (err) {
|
||||
this.failDownload(downloadErrors.connectionError(err))
|
||||
}
|
||||
|
||||
this.downloadStream.on('data', this.cancelable((chunk: Buffer) => {
|
||||
downloadedSize += chunk.length
|
||||
this.callbacks.downloadProgress(downloadedSize)
|
||||
}))
|
||||
|
||||
this.downloadStream.on('error', this.cancelable((err: Error) => {
|
||||
this.failDownload(downloadErrors.connectionError(err))
|
||||
}))
|
||||
|
||||
this.downloadStream.on('end', this.cancelable(() => {
|
||||
this.callbacks.complete()
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Display an error message and provide a function to retry the download.
|
||||
*/
|
||||
private failDownload(error: DownloadError) {
|
||||
this.callbacks.error(error, this.cancelable(() => this.beginDownload()))
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the process of downloading the file. (no more events will be fired after this is called)
|
||||
*/
|
||||
cancelDownload() {
|
||||
this.wasCanceled = true
|
||||
googleTimer.cancelTimer() // Prevents timer from trying to activate a download and resetting
|
||||
if (this.downloadStream) {
|
||||
this.downloadStream.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a function that is able to be prevented if `this.cancelDownload()` was called.
|
||||
*/
|
||||
private cancelable<F extends AnyFunction>(fn: F) {
|
||||
return (...args: Parameters<F>): ReturnType<F> => {
|
||||
if (this.wasCanceled) { return }
|
||||
return fn(...Array.from(args))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,8 +184,7 @@ const downloadErrors = {
|
||||
* On error, provides the ability to retry.
|
||||
* Will only send download requests once every `getSettings().rateLimitDelay` seconds.
|
||||
*/
|
||||
export class FileDownloader {
|
||||
private readonly RETRY_MAX = 2
|
||||
class SlowFileDownloader {
|
||||
|
||||
private callbacks = {} as Callbacks
|
||||
private retryCount: number
|
||||
@@ -82,7 +236,7 @@ export class FileDownloader {
|
||||
|
||||
this.req.on('timeout', this.cancelable((type: string) => {
|
||||
this.retryCount++
|
||||
if (this.retryCount <= this.RETRY_MAX) {
|
||||
if (this.retryCount <= RETRY_MAX) {
|
||||
console.log(`TIMEOUT: Retry attempt ${this.retryCount}...`)
|
||||
this.requestDownload(cookieHeader)
|
||||
} else {
|
||||
|
||||
62
src/electron/ipc/google/AuthServer.ts
Normal file
62
src/electron/ipc/google/AuthServer.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import * as http from 'http'
|
||||
import { URL } from 'url'
|
||||
import { REDIRECT_PATH, REDIRECT_BASE, SERVER_PORT } from '../../shared/Paths'
|
||||
|
||||
type EventCallback = {
|
||||
'listening': () => void
|
||||
'authCode': (authCode: string) => Promise<void>
|
||||
}
|
||||
type Callbacks = { [E in keyof EventCallback]: EventCallback[E] }
|
||||
|
||||
class AuthServer {
|
||||
|
||||
private server: http.Server
|
||||
private callbacks = {} as Callbacks
|
||||
private connections = {}
|
||||
|
||||
/**
|
||||
* Calls `callback` when `event` fires. (no events will be fired after `this.cancelDownload()` is called)
|
||||
*/
|
||||
on<E extends keyof EventCallback>(event: E, callback: EventCallback[E]) {
|
||||
this.callbacks[event] = callback
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts listening on `SERVER_PORT` for the authentication callback.
|
||||
* Emits the 'listening' event when the server is ready to listen.
|
||||
* Emits the 'authCode' event when the callback request provides the authentication code.
|
||||
*/
|
||||
startServer() {
|
||||
if (this.server != null) {
|
||||
this.callbacks.listening()
|
||||
} else {
|
||||
this.server = http.createServer(this.requestListener.bind(this))
|
||||
this.server.on('connection', (conn) => {
|
||||
const key = conn.remoteAddress + ':' + conn.remotePort
|
||||
this.connections[key] = conn
|
||||
conn.on('close', () => delete this.connections[key])
|
||||
})
|
||||
|
||||
this.server.listen(SERVER_PORT, () => this.callbacks.listening())
|
||||
}
|
||||
}
|
||||
|
||||
private requestListener(req: http.IncomingMessage, res: http.ServerResponse) {
|
||||
if (req.url.includes(REDIRECT_PATH)) {
|
||||
const searchParams = new URL(req.url, REDIRECT_BASE).searchParams
|
||||
res.end()
|
||||
this.destroyServer()
|
||||
this.callbacks.authCode(searchParams.get('code'))
|
||||
}
|
||||
}
|
||||
|
||||
private destroyServer() {
|
||||
this.server.close()
|
||||
for (const key in this.connections) {
|
||||
this.connections[key].destroy()
|
||||
}
|
||||
this.server = null
|
||||
}
|
||||
}
|
||||
|
||||
export const authServer = new AuthServer()
|
||||
134
src/electron/ipc/google/GoogleAuth.ts
Normal file
134
src/electron/ipc/google/GoogleAuth.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import { dataPath, CLIENT_ID, CLIENT_SECRET, REDIRECT_URI } from '../../shared/Paths'
|
||||
import { mainWindow } from '../../main'
|
||||
import { join } from 'path'
|
||||
import { readFile, writeFile } from 'jsonfile'
|
||||
import { google } from 'googleapis'
|
||||
import { authServer } from './AuthServer'
|
||||
import { BrowserWindow } from 'electron'
|
||||
import * as fs from 'fs'
|
||||
import { promisify } from 'util'
|
||||
|
||||
const unlink = promisify(fs.unlink)
|
||||
|
||||
const TOKEN_PATH = join(dataPath, 'token.json')
|
||||
|
||||
export class GoogleAuth {
|
||||
|
||||
private hasTriedTokenFile = false
|
||||
private hasAuthenticated = false
|
||||
|
||||
/**
|
||||
* Attempts to authenticate the googleapis library using the token stored at `TOKEN_PATH`.
|
||||
* @returns `true` if the user is authenticated, and `false` otherwise.
|
||||
*/
|
||||
async attemptToAuthenticate() {
|
||||
if (this.hasTriedTokenFile) {
|
||||
return this.hasAuthenticated
|
||||
}
|
||||
|
||||
const token = await this.getStoredToken()
|
||||
if (token != null) {
|
||||
// Token has been restored from a previous session
|
||||
const oAuth2Client = new google.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI)
|
||||
oAuth2Client.setCredentials(token)
|
||||
google.options({ auth: oAuth2Client })
|
||||
this.hasAuthenticated = true
|
||||
return true
|
||||
} else {
|
||||
// Token doesn't exist; user has not authenticated
|
||||
this.hasAuthenticated = false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async generateAuthToken() {
|
||||
|
||||
if (await this.getStoredToken() != null) { return true }
|
||||
|
||||
if (this.hasTriedTokenFile == false) {
|
||||
// Token exists but couldn't be read
|
||||
console.log('Auth token exists but could not be loaded. Check file permissions.')
|
||||
return false
|
||||
}
|
||||
|
||||
const oAuth2Client = new google.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI)
|
||||
let popupWindow: BrowserWindow
|
||||
let gotAuthCode = false
|
||||
|
||||
return new Promise<boolean>(resolve => {
|
||||
authServer.on('listening', () => {
|
||||
const authUrl = oAuth2Client.generateAuthUrl({
|
||||
access_type: 'offline',
|
||||
scope: ['profile', 'email', 'https://www.googleapis.com/auth/drive.readonly'],
|
||||
redirect_uri: REDIRECT_URI
|
||||
})
|
||||
|
||||
popupWindow = new BrowserWindow({
|
||||
fullscreenable: false,
|
||||
modal: true,
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
show: false,
|
||||
parent: mainWindow,
|
||||
autoHideMenuBar: true,
|
||||
center: true,
|
||||
thickFrame: true,
|
||||
useContentSize: true,
|
||||
width: 400,
|
||||
|
||||
})
|
||||
popupWindow.loadURL(authUrl, { userAgent: 'Chrome' })
|
||||
popupWindow.on('ready-to-show', () => popupWindow.show())
|
||||
popupWindow.on('closed', () => resolve(gotAuthCode))
|
||||
})
|
||||
|
||||
authServer.on('authCode', async (authCode) => {
|
||||
const { tokens } = await oAuth2Client.getToken(authCode)
|
||||
oAuth2Client.setCredentials(tokens)
|
||||
google.options({ auth: oAuth2Client })
|
||||
await writeFile(TOKEN_PATH, tokens)
|
||||
this.hasTriedTokenFile = false
|
||||
gotAuthCode = true
|
||||
popupWindow.close()
|
||||
})
|
||||
|
||||
authServer.startServer()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @returns the previously stored auth token, or `null` if it doesn't exist or can't be accessed.
|
||||
*/
|
||||
private async getStoredToken() {
|
||||
this.hasTriedTokenFile = true
|
||||
try {
|
||||
return await readFile(TOKEN_PATH)
|
||||
} catch (err) {
|
||||
if (err && err.code && err.code != 'ENOENT') {
|
||||
// Failed to access the file; next attempt should try again
|
||||
this.hasTriedTokenFile = false
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* removes the previously stored auth token.
|
||||
*/
|
||||
async deleteStoredToken() {
|
||||
this.hasTriedTokenFile = false
|
||||
this.hasAuthenticated = false
|
||||
try {
|
||||
await unlink(TOKEN_PATH)
|
||||
} catch (err) {
|
||||
console.log('Failed to delete token.')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const googleAuth = new GoogleAuth()
|
||||
56
src/electron/ipc/google/GoogleLoginHandler.ipc.ts
Normal file
56
src/electron/ipc/google/GoogleLoginHandler.ipc.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { IPCInvokeHandler } from '../../shared/IPCHandler'
|
||||
import { googleAuth } from './GoogleAuth'
|
||||
|
||||
/**
|
||||
* Handles the 'google-login' event.
|
||||
*/
|
||||
class GoogleLoginHandler implements IPCInvokeHandler<'google-login'> {
|
||||
event: 'google-login' = 'google-login'
|
||||
|
||||
/**
|
||||
* @returns `true` if the user has been authenticated.
|
||||
*/
|
||||
async handler() {
|
||||
return new Promise<boolean>(resolve => {
|
||||
googleAuth.generateAuthToken().then((isLoggedIn) => resolve(isLoggedIn))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const googleLoginHandler = new GoogleLoginHandler()
|
||||
|
||||
/**
|
||||
* Handles the 'google-login' event.
|
||||
*/
|
||||
class GoogleLogoutHandler implements IPCInvokeHandler<'google-logout'> {
|
||||
event: 'google-logout' = 'google-logout'
|
||||
|
||||
/**
|
||||
* @returns `true` if the user has been authenticated.
|
||||
*/
|
||||
async handler() {
|
||||
return new Promise<undefined>(resolve => {
|
||||
googleAuth.deleteStoredToken().then(() => resolve())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const googleLogoutHandler = new GoogleLogoutHandler()
|
||||
|
||||
/**
|
||||
* Handles the 'get-auth-status' event.
|
||||
*/
|
||||
class GetAuthStatusHandler implements IPCInvokeHandler<'get-auth-status'> {
|
||||
event: 'get-auth-status' = 'get-auth-status'
|
||||
|
||||
/**
|
||||
* @returns `true` if the user is authenticated with Google.
|
||||
*/
|
||||
handler() {
|
||||
return new Promise<boolean>(resolve => {
|
||||
googleAuth.attemptToAuthenticate().then(isAuthenticated => resolve(isAuthenticated))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const getAuthStatusHandler = new GetAuthStatusHandler()
|
||||
@@ -10,7 +10,7 @@ import { getIPCInvokeHandlers, getIPCEmitHandlers, IPCEmitEvents } from './share
|
||||
import { getSettingsHandler } from './ipc/SettingsHandler.ipc'
|
||||
import { dataPath } from './shared/Paths'
|
||||
|
||||
let mainWindow: BrowserWindow
|
||||
export let mainWindow: BrowserWindow
|
||||
const args = process.argv.slice(1)
|
||||
const isDevBuild = args.some(val => val == '--dev')
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { downloadHandler } from '../ipc/download/DownloadHandler'
|
||||
import { Settings } from './Settings'
|
||||
import { batchSongDetailsHandler } from '../ipc/browse/BatchSongDetailsHandler.ipc'
|
||||
import { getSettingsHandler, setSettingsHandler } from '../ipc/SettingsHandler.ipc'
|
||||
import { googleLoginHandler, getAuthStatusHandler, googleLogoutHandler } from '../ipc/google/GoogleLoginHandler.ipc'
|
||||
import { UpdateProgress, getCurrentVersionHandler, downloadUpdateHandler, quitAndInstallHandler, getUpdateAvailableHandler } from '../ipc/UpdateHandler.ipc'
|
||||
import { UpdateInfo } from 'electron-updater'
|
||||
|
||||
@@ -27,7 +28,10 @@ export function getIPCInvokeHandlers(): IPCInvokeHandler<keyof IPCInvokeEvents>[
|
||||
batchSongDetailsHandler,
|
||||
albumArtHandler,
|
||||
getCurrentVersionHandler,
|
||||
getUpdateAvailableHandler
|
||||
getUpdateAvailableHandler,
|
||||
googleLoginHandler,
|
||||
googleLogoutHandler,
|
||||
getAuthStatusHandler
|
||||
]
|
||||
}
|
||||
|
||||
@@ -63,6 +67,18 @@ export type IPCInvokeEvents = {
|
||||
input: undefined
|
||||
output: boolean
|
||||
}
|
||||
'google-login': {
|
||||
input: undefined
|
||||
output: boolean
|
||||
}
|
||||
'google-logout': {
|
||||
input: undefined
|
||||
output: undefined
|
||||
}
|
||||
'get-auth-status': {
|
||||
input: undefined
|
||||
output: boolean
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,4 +9,12 @@ export const tempPath = join(dataPath, 'temp')
|
||||
export const themesPath = join(dataPath, 'themes')
|
||||
|
||||
// URL
|
||||
export const serverURL = 'bridge-db.net'
|
||||
export const serverURL = 'bridge-db.net'
|
||||
|
||||
// Google Project ID (More info on why these are here: https://developers.google.com/identity/protocols/oauth2#installed)
|
||||
export const CLIENT_ID = '668064259105-vkm77i5lcoo2oumk2eulik7bae8k5agf.apps.googleusercontent.com'
|
||||
export const CLIENT_SECRET = 'RU69Ubr9CidGcI0Z23Ttn2ZV'
|
||||
export const SERVER_PORT = 42813
|
||||
export const REDIRECT_BASE = `http://127.0.0.1:${SERVER_PORT}`
|
||||
export const REDIRECT_PATH = `/oauth2callback`
|
||||
export const REDIRECT_URI = `${REDIRECT_BASE}${REDIRECT_PATH}`
|
||||
Reference in New Issue
Block a user