Sign in with Apple (#11793)

* add date check

* achievements modal polishing

* refresh private-messages page when you are already on it

* add countbadge knob to change the example

* fix lint

* typos

* typos

* typos

* add toggle for achievements categories

* typo

* fix test

* fix edit avatar modal cannot be closed

* WIP(settings): subscriber page improvements

* WIP(subscriptions): more design build-out

* fix(css): disabled button styles

* fix(css): better Amazon targeting

* fix hide tooltip + align header correctly

* disable perfect scroll

* load messages on refresh event

* fix header label + conversation actions not breaking layout on hover

* WIP(g1g1): notif

* WIP(g1g1): notif cont'd

* fix(test): snowball change

* fix(event): feature NYE card

* chore(sprites): compile

* fix(bgs): include TT required field

* add gifting banner to the max height calculation

* chore(event): enable winter customizations

* WIP(gifting): partial modal implementation

* feat(gifting): select giftee modal

* fix(gifting): notification order, modal dismiss

* Begin implementing sign in with apple

# Conflicts:
#	package-lock.json
#	website/common/script/constants.js
#	website/server/libs/auth/social.js
#	website/server/models/user/schema.js

* Add apple sign in button to website

* fix lint errors

* fix config json

* fix(modals): correct some repops

* fix(gifting): style updates

* fix(buy): modal style changes

* fix(modals): also clean out "prev"

* Attempt workaround for sign in with apple on android

* temporarily log everything as error

* refactor(modals): hide in dismiss event

* fix temporary test failure

* changes to sign in with apple

* fix: first batch of layout issues for private messages + auto sizing textarea

* fix(modals): new dismiss logic

* fix(modals): new dismiss no go??

* Only use email scope

* print debugging

* .

* ..

* ...

* username second line - open profile on face-avatar/conversation name - fix textarea height

* temporarily disable apple auth and just return data for debugging

* Hopefully this works

* .....

* WIP(subscription): unsubscribed state

* .

* ..

* MAYBE THIS ACTUALLY WORKS???

* Implement apple sign in

* fix some urls

* fix urls

* fix redirect and auth

* attempt to also request name

* fix lint error

* WIP(subscription): partial subscribed

* chore(sprites): compile

* Change approach so that it actually works

* fix config error

* fix lint errors

* Fix

* fix lint error

* lint error

* WIP(subscription): finish subscribed

* refresh on sync

* new "you dont have any messages" style + changed min textarea height

* new conversationItem style / layout

* reset message unread on reload

* chore(npm): update package-locks

* fix styles / textarea height

* feat(subscription): revised sub page RC

* list optOut / chatRevoked informations for each conversation + show why its disabled

* Improve apple redirect view

* Fix apple icon on group task registration page

* WIP(adventure): prereqs

* Block / Unblock - correct disabled states - $gray-200 instead of 300/400

* canReceive not checking chatRevoked

* fix: faceAvatar / userLink open the selected conversation user

* check if the target user is blocking the logged-in user

* fix(subs): style tweaks

* fix(profiles): short circuit contributor
Attempted fix for #11830

* chore(sprites): compile

* fix(content): missing potion data

* fix(content): missing string

* WIP(drops): new modal

* fix(subs): moar style tweaks

* check if blocks is undefined

* max-height instead of height

* fix "no messages" state + canReceive on a new conversation

* WIP(adventure): analytics fixes etc

* Improve apple signin handling

* fixed conversations width (280px on max 768 width page)

* feat(adventure): random egg+potion on 2nd task

* fix(lint): noworkies

* fix(modal): correctly construct classes

* fix(tests): expectations and escape

* Fix typo

* use base url from env variables

* fix lint

* call autosize after message is sent

* fix urls

* always verify token

* throw error when social auth could not retrieve id

* Store emails correctly for apple auth

* Retrieve name when authenticating through apple

* Fix lint errors

* fix all lint errors

* fix(content): missing strings

* Revert "always verify token"

This reverts commit 8ac40c76bf.

# Conflicts:
#	website/server/libs/auth/social.js

* Correctly load name

* remove extra changes

* remove extra logger call

* reset package and package-lock

* add back missing packages

* use name from apple

* add support for multiple apple public keys

* add some unit and integration tests

* add apple auth integration test

* tweak social signup buttons

* pixel pushing

Co-authored-by: Matteo Pagliazzi <matteopagliazzi@gmail.com>
Co-authored-by: Sabe Jones <sabrecat@gmail.com>
Co-authored-by: negue <eugen.bolz@gmail.com>
Co-authored-by: Phillip Thelen <phillip@habitica.com>
This commit is contained in:
Phillip Thelen
2020-04-08 18:44:30 +02:00
committed by GitHub
parent 1d98929453
commit 9d16ab7dba
29 changed files with 1129 additions and 173 deletions

1
.gitignore vendored
View File

@@ -37,6 +37,7 @@ yarn.lock
.elasticbeanstalk/*
!.elasticbeanstalk/*.cfg.yml
!.elasticbeanstalk/*.global.yml
/.vscode
# webstorm fake webpack for path intellisense
webpack.webstorm.config

View File

@@ -9,3 +9,4 @@ test/**
*.swp
*.swx
website/raw_sprites/**
content_cache/**

View File

@@ -75,5 +75,9 @@
"WEB_CONCURRENCY": 1,
"SKIP_SSL_CHECK_KEY": "key",
"ENABLE_STACKDRIVER_TRACING": "false",
"APPLE_AUTH_PRIVATE_KEY": "",
"APPLE_TEAM_ID": "",
"APPLE_AUTH_CLIENT_ID": "",
"APPLE_AUTH_KEY_ID": "",
"BLOCKED_IPS": ""
}

436
package-lock.json generated
View File

@@ -1472,6 +1472,15 @@
"defer-to-connect": "^2.0.0"
}
},
"@types/body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==",
"requires": {
"@types/connect": "*",
"@types/node": "*"
}
},
"@types/cacheable-request": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz",
@@ -1489,11 +1498,56 @@
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
"dev": true
},
"@types/connect": {
"version": "3.4.33",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz",
"integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==",
"requires": {
"@types/node": "*"
}
},
"@types/events": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g=="
},
"@types/express": {
"version": "4.17.4",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.4.tgz",
"integrity": "sha512-DO1L53rGqIDUEvOjJKmbMEQ5Z+BM2cIEPy/eV3En+s166Gz+FeuzRerxcab757u/U4v4XF4RYrZPmqKa+aY/2w==",
"requires": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "*",
"@types/qs": "*",
"@types/serve-static": "*"
}
},
"@types/express-jwt": {
"version": "0.0.42",
"resolved": "https://registry.npmjs.org/@types/express-jwt/-/express-jwt-0.0.42.tgz",
"integrity": "sha512-WszgUddvM1t5dPpJ3LhWNH8kfNN8GPIBrAGxgIYXVCEGx6Bx4A036aAuf/r5WH9DIEdlmp7gHOYvSM6U87B0ag==",
"requires": {
"@types/express": "*",
"@types/express-unless": "*"
}
},
"@types/express-serve-static-core": {
"version": "4.17.3",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.3.tgz",
"integrity": "sha512-sHEsvEzjqN+zLbqP+8OXTipc10yH1QLR+hnr5uw29gi9AhCAAAdri8ClNV7iMdrJrIzXIQtlkPvq8tJGhj3QJQ==",
"requires": {
"@types/node": "*",
"@types/range-parser": "*"
}
},
"@types/express-unless": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.1.tgz",
"integrity": "sha512-5fuvg7C69lemNgl0+v+CUxDYWVPSfXHhJPst4yTLcqi4zKJpORCxnDrnnilk3k0DTq/WrAUdvXFs01+vUqUZHw==",
"requires": {
"@types/express": "*"
}
},
"@types/form-data": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.5.0.tgz",
@@ -1533,6 +1587,11 @@
"@types/node": "*"
}
},
"@types/mime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz",
"integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw=="
},
"@types/minimatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@@ -1570,6 +1629,16 @@
"integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==",
"optional": true
},
"@types/qs": {
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.1.tgz",
"integrity": "sha512-lhbQXx9HKZAPgBkISrBcmAcMpZsmpe/Cd/hY7LGZS5OfkySUBItnPZHgQPssWYUET8elF+yCFBbP1Q0RZPTdaw=="
},
"@types/range-parser": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
"integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA=="
},
"@types/responselike": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
@@ -1583,6 +1652,15 @@
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="
},
"@types/serve-static": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz",
"integrity": "sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==",
"requires": {
"@types/express-serve-static-core": "*",
"@types/mime": "*"
}
},
"@types/ws": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-5.1.2.tgz",
@@ -2029,6 +2107,16 @@
"default-require-extensions": "^1.0.0"
}
},
"apple-auth": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/apple-auth/-/apple-auth-1.0.5.tgz",
"integrity": "sha512-EbMm0rqWyUANxHJfiNGrvLkS3acdBfAj0TJ1hnA/jxq7+l9GTd2br5ajgjBjcHT5bbN6CsvuZFAdYofzn1J/6A==",
"requires": {
"axios": "^0.19.0",
"express": "^4.17.1",
"jsonwebtoken": "^8.5.1"
}
},
"aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
@@ -2353,7 +2441,6 @@
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
"integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
"dev": true,
"requires": {
"follow-redirects": "1.5.10"
}
@@ -6056,22 +6143,26 @@
"dependencies": {
"abbrev": {
"version": "1.1.1",
"bundled": true,
"resolved": false,
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"optional": true
},
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"resolved": false,
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"optional": true
},
"aproba": {
"version": "1.2.0",
"bundled": true,
"resolved": false,
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"optional": true
},
"are-we-there-yet": {
"version": "1.1.5",
"bundled": true,
"resolved": false,
"integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
"optional": true,
"requires": {
"delegates": "^1.0.0",
@@ -6080,12 +6171,14 @@
},
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"resolved": false,
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"resolved": false,
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
@@ -6094,32 +6187,38 @@
},
"chownr": {
"version": "1.1.1",
"bundled": true,
"resolved": false,
"integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==",
"optional": true
},
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"resolved": false,
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"resolved": false,
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"resolved": false,
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
"optional": true
},
"core-util-is": {
"version": "1.0.2",
"bundled": true,
"resolved": false,
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"optional": true
},
"debug": {
"version": "4.1.1",
"bundled": true,
"resolved": false,
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"optional": true,
"requires": {
"ms": "^2.1.1"
@@ -6127,22 +6226,26 @@
},
"deep-extend": {
"version": "0.6.0",
"bundled": true,
"resolved": false,
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"optional": true
},
"delegates": {
"version": "1.0.0",
"bundled": true,
"resolved": false,
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
"optional": true
},
"detect-libc": {
"version": "1.0.3",
"bundled": true,
"resolved": false,
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
"optional": true
},
"fs-minipass": {
"version": "1.2.5",
"bundled": true,
"resolved": false,
"integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==",
"optional": true,
"requires": {
"minipass": "^2.2.1"
@@ -6150,12 +6253,14 @@
},
"fs.realpath": {
"version": "1.0.0",
"bundled": true,
"resolved": false,
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"optional": true
},
"gauge": {
"version": "2.7.4",
"bundled": true,
"resolved": false,
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
"optional": true,
"requires": {
"aproba": "^1.0.3",
@@ -6170,7 +6275,8 @@
},
"glob": {
"version": "7.1.3",
"bundled": true,
"resolved": false,
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
"optional": true,
"requires": {
"fs.realpath": "^1.0.0",
@@ -6183,12 +6289,14 @@
},
"has-unicode": {
"version": "2.0.1",
"bundled": true,
"resolved": false,
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
"optional": true
},
"iconv-lite": {
"version": "0.4.24",
"bundled": true,
"resolved": false,
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"optional": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
@@ -6196,7 +6304,8 @@
},
"ignore-walk": {
"version": "3.0.1",
"bundled": true,
"resolved": false,
"integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==",
"optional": true,
"requires": {
"minimatch": "^3.0.4"
@@ -6204,7 +6313,8 @@
},
"inflight": {
"version": "1.0.6",
"bundled": true,
"resolved": false,
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"optional": true,
"requires": {
"once": "^1.3.0",
@@ -6213,17 +6323,20 @@
},
"inherits": {
"version": "2.0.3",
"bundled": true,
"resolved": false,
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"optional": true
},
"ini": {
"version": "1.3.5",
"bundled": true,
"resolved": false,
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
"optional": true
},
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"resolved": false,
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
@@ -6231,12 +6344,14 @@
},
"isarray": {
"version": "1.0.0",
"bundled": true,
"resolved": false,
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"optional": true
},
"minimatch": {
"version": "3.0.4",
"bundled": true,
"resolved": false,
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
@@ -6244,12 +6359,14 @@
},
"minimist": {
"version": "0.0.8",
"bundled": true,
"resolved": false,
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"optional": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"resolved": false,
"integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
@@ -6258,7 +6375,8 @@
},
"minizlib": {
"version": "1.2.1",
"bundled": true,
"resolved": false,
"integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==",
"optional": true,
"requires": {
"minipass": "^2.2.1"
@@ -6266,7 +6384,8 @@
},
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"resolved": false,
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"optional": true,
"requires": {
"minimist": "0.0.8"
@@ -6274,12 +6393,14 @@
},
"ms": {
"version": "2.1.1",
"bundled": true,
"resolved": false,
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
"optional": true
},
"needle": {
"version": "2.3.0",
"bundled": true,
"resolved": false,
"integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==",
"optional": true,
"requires": {
"debug": "^4.1.0",
@@ -6289,7 +6410,8 @@
},
"node-pre-gyp": {
"version": "0.12.0",
"bundled": true,
"resolved": false,
"integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==",
"optional": true,
"requires": {
"detect-libc": "^1.0.2",
@@ -6306,7 +6428,8 @@
},
"nopt": {
"version": "4.0.1",
"bundled": true,
"resolved": false,
"integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
"optional": true,
"requires": {
"abbrev": "1",
@@ -6315,12 +6438,14 @@
},
"npm-bundled": {
"version": "1.0.6",
"bundled": true,
"resolved": false,
"integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==",
"optional": true
},
"npm-packlist": {
"version": "1.4.1",
"bundled": true,
"resolved": false,
"integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==",
"optional": true,
"requires": {
"ignore-walk": "^3.0.1",
@@ -6329,7 +6454,8 @@
},
"npmlog": {
"version": "4.1.2",
"bundled": true,
"resolved": false,
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"optional": true,
"requires": {
"are-we-there-yet": "~1.1.2",
@@ -6340,17 +6466,20 @@
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"resolved": false,
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"optional": true
},
"object-assign": {
"version": "4.1.1",
"bundled": true,
"resolved": false,
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"optional": true
},
"once": {
"version": "1.4.0",
"bundled": true,
"resolved": false,
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"optional": true,
"requires": {
"wrappy": "1"
@@ -6358,17 +6487,20 @@
},
"os-homedir": {
"version": "1.0.2",
"bundled": true,
"resolved": false,
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
"optional": true
},
"os-tmpdir": {
"version": "1.0.2",
"bundled": true,
"resolved": false,
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
"optional": true
},
"osenv": {
"version": "0.1.5",
"bundled": true,
"resolved": false,
"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
"optional": true,
"requires": {
"os-homedir": "^1.0.0",
@@ -6377,17 +6509,20 @@
},
"path-is-absolute": {
"version": "1.0.1",
"bundled": true,
"resolved": false,
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"optional": true
},
"process-nextick-args": {
"version": "2.0.0",
"bundled": true,
"resolved": false,
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
"optional": true
},
"rc": {
"version": "1.2.8",
"bundled": true,
"resolved": false,
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"optional": true,
"requires": {
"deep-extend": "^0.6.0",
@@ -6398,14 +6533,16 @@
"dependencies": {
"minimist": {
"version": "1.2.0",
"bundled": true,
"resolved": false,
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"optional": true
}
}
},
"readable-stream": {
"version": "2.3.6",
"bundled": true,
"resolved": false,
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"optional": true,
"requires": {
"core-util-is": "~1.0.0",
@@ -6419,7 +6556,8 @@
},
"rimraf": {
"version": "2.6.3",
"bundled": true,
"resolved": false,
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
"optional": true,
"requires": {
"glob": "^7.1.3"
@@ -6427,37 +6565,44 @@
},
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"resolved": false,
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
"bundled": true,
"resolved": false,
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"optional": true
},
"sax": {
"version": "1.2.4",
"bundled": true,
"resolved": false,
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"optional": true
},
"semver": {
"version": "5.7.0",
"bundled": true,
"resolved": false,
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
"optional": true
},
"set-blocking": {
"version": "2.0.0",
"bundled": true,
"resolved": false,
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
"optional": true
},
"signal-exit": {
"version": "3.0.2",
"bundled": true,
"resolved": false,
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
"optional": true
},
"string-width": {
"version": "1.0.2",
"bundled": true,
"resolved": false,
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
@@ -6467,7 +6612,8 @@
},
"string_decoder": {
"version": "1.1.1",
"bundled": true,
"resolved": false,
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"optional": true,
"requires": {
"safe-buffer": "~5.1.0"
@@ -6475,7 +6621,8 @@
},
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
"resolved": false,
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
@@ -6483,12 +6630,14 @@
},
"strip-json-comments": {
"version": "2.0.1",
"bundled": true,
"resolved": false,
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
"optional": true
},
"tar": {
"version": "4.4.8",
"bundled": true,
"resolved": false,
"integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==",
"optional": true,
"requires": {
"chownr": "^1.1.1",
@@ -6502,12 +6651,14 @@
},
"util-deprecate": {
"version": "1.0.2",
"bundled": true,
"resolved": false,
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"optional": true
},
"wide-align": {
"version": "1.1.3",
"bundled": true,
"resolved": false,
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
"optional": true,
"requires": {
"string-width": "^1.0.2 || 2"
@@ -6515,12 +6666,14 @@
},
"wrappy": {
"version": "1.0.2",
"bundled": true,
"resolved": false,
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"optional": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
"resolved": false,
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
"optional": true
}
}
@@ -8658,6 +8811,125 @@
"safe-buffer": "^5.0.1"
}
},
"jwks-rsa": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-1.7.0.tgz",
"integrity": "sha512-tq7DVJt9J6wTvl9+AQfwZIiPSuY2Vf0F+MovfRTFuBqLB1xgDVhegD33ChEAQ6yBv9zFvUIyj4aiwrSA5VehUw==",
"requires": {
"@types/express-jwt": "0.0.42",
"debug": "^4.1.0",
"jsonwebtoken": "^8.5.1",
"limiter": "^1.1.4",
"lru-memoizer": "^2.0.1",
"ms": "^2.1.2",
"request": "^2.88.0"
},
"dependencies": {
"assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
},
"aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
},
"caseless": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
},
"form-data": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
}
},
"har-validator": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
"integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
"requires": {
"ajv": "^6.5.5",
"har-schema": "^2.0.0"
}
},
"http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
"integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
"requires": {
"assert-plus": "^1.0.0",
"jsprim": "^1.2.2",
"sshpk": "^1.7.0"
}
},
"oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
},
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
},
"qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
},
"request": {
"version": "2.88.2",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
"integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
"requires": {
"aws-sign2": "~0.7.0",
"aws4": "^1.8.0",
"caseless": "~0.12.0",
"combined-stream": "~1.0.6",
"extend": "~3.0.2",
"forever-agent": "~0.6.1",
"form-data": "~2.3.2",
"har-validator": "~5.1.3",
"http-signature": "~1.2.0",
"is-typedarray": "~1.0.0",
"isstream": "~0.1.2",
"json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19",
"oauth-sign": "~0.9.0",
"performance-now": "^2.1.0",
"qs": "~6.5.2",
"safe-buffer": "^5.1.2",
"tough-cookie": "~2.5.0",
"tunnel-agent": "^0.6.0",
"uuid": "^3.3.2"
}
},
"tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
"requires": {
"psl": "^1.1.28",
"punycode": "^2.1.1"
}
},
"tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
"requires": {
"safe-buffer": "^5.0.1"
}
}
}
},
"jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
@@ -8807,6 +9079,11 @@
"resolve": "^1.1.7"
}
},
"limiter": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
"integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="
},
"linkify-it": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz",
@@ -8988,6 +9265,31 @@
"yallist": "^3.0.2"
}
},
"lru-memoizer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.2.tgz",
"integrity": "sha512-N5L5xlnVcbIinNn/TJ17vHBZwBMt9t7aJDz2n97moWubjNl6VO9Ao2XuAGBBddkYdjrwR9HfzXbT6NfMZXAZ/A==",
"requires": {
"lodash.clonedeep": "^4.5.0",
"lru-cache": "~4.0.0"
},
"dependencies": {
"lru-cache": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz",
"integrity": "sha1-HRdnnAac2l0ECZGgnbwsDbN35V4=",
"requires": {
"pseudomap": "^1.0.1",
"yallist": "^2.0.0"
}
},
"yallist": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
}
}
},
"make-dir": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",

View File

@@ -14,6 +14,7 @@
"amplitude": "^3.5.0",
"apidoc": "^0.17.5",
"apn": "^2.2.0",
"apple-auth": "^1.0.5",
"aws-sdk": "^2.653.0",
"bcrypt": "^3.0.8",
"body-parser": "^1.18.3",
@@ -41,6 +42,8 @@
"image-size": "^0.8.3",
"in-app-purchase": "^1.11.3",
"js2xmlparser": "^4.0.1",
"jsonwebtoken": "^8.5.1",
"jwks-rsa": "^1.7.0",
"lodash": "^4.17.15",
"merge-stream": "^2.0.0",
"method-override": "^3.0.0",

View File

@@ -18,6 +18,16 @@ function getUser () {
value: 'email@facebook',
}],
},
google: {
emails: [{
value: 'email@google',
}],
},
apple: {
emails: [{
value: 'email@apple',
}],
},
},
profile: {
name: 'profile name',
@@ -58,6 +68,8 @@ describe('emails', () => {
const user = getUser();
delete user.profile.name;
delete user.auth.local.email;
delete user.auth.google.emails;
delete user.auth.apple.emails;
const data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
@@ -67,12 +79,48 @@ describe('emails', () => {
expect(data).to.have.property('canSend', true);
});
it('returns correct user data [google users]', () => {
const attachEmail = requireAgain(pathToEmailLib);
const { getUserInfo } = attachEmail;
const user = getUser();
delete user.profile.name;
delete user.auth.local.email;
delete user.auth.facebook.emails;
delete user.auth.apple.emails;
const data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
expect(data).to.have.property('name', user.auth.local.username);
expect(data).to.have.property('email', user.auth.google.emails[0].value);
expect(data).to.have.property('_id', user._id);
expect(data).to.have.property('canSend', true);
});
it('returns correct user data [apple users]', () => {
const attachEmail = requireAgain(pathToEmailLib);
const { getUserInfo } = attachEmail;
const user = getUser();
delete user.profile.name;
delete user.auth.local.email;
delete user.auth.google.emails;
delete user.auth.facebook.emails;
const data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
expect(data).to.have.property('name', user.auth.local.username);
expect(data).to.have.property('email', user.auth.apple.emails[0].value);
expect(data).to.have.property('_id', user._id);
expect(data).to.have.property('canSend', true);
});
it('has fallbacks for missing data', () => {
const attachEmail = requireAgain(pathToEmailLib);
const { getUserInfo } = attachEmail;
const user = getUser();
delete user.auth.local.email;
delete user.auth.facebook;
delete user.auth.google;
delete user.auth.apple;
const data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);

View File

@@ -346,4 +346,23 @@ describe('DELETE /user', () => {
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
});
});
context('user with Apple auth', async () => {
beforeEach(async () => {
user = await generateUser({
auth: {
apple: {
id: 'apple-id',
},
},
});
});
it('deletes a Apple user', async () => {
await user.del('/user', {
password: DELETE_CONFIRMATION,
});
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
});
});
});

View File

@@ -95,4 +95,42 @@ describe('DELETE social registration', () => {
expect(user.auth.goodl).to.be.undefined;
});
});
context('Apple', () => {
it('fails if user does not have an alternative registration method', async () => {
await user.update({
'auth.apple.id': 'some-apple-id',
'auth.local': { ok: true },
});
await expect(user.del('/user/auth/social/apple')).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cantDetachSocial'),
});
});
it('succeeds if user has a local registration', async () => {
await user.update({
'auth.apple.id': 'some-apple-id',
});
const response = await user.del('/user/auth/social/apple');
expect(response).to.eql({});
await user.sync();
expect(user.auth.apple).to.be.undefined;
});
it('succeeds if user has a facebook registration', async () => {
await user.update({
'auth.apple.id': 'some-apple-id',
'auth.facebook.id': 'some-facebook-id',
'auth.local': { ok: true },
});
const response = await user.del('/user/auth/social/apple');
expect(response).to.eql({});
await user.sync();
expect(user.auth.goodl).to.be.undefined;
});
});
});

View File

@@ -0,0 +1,50 @@
import {
generateUser,
requester,
getProperty,
} from '../../../../../helpers/api-integration/v3';
import * as appleAuth from '../../../../../../website/server/libs/auth/apple';
describe('GET /user/auth/apple', () => {
let api;
let user;
const appleEndpoint = '/user/auth/apple';
before(async () => {
const expectedResult = { id: 'appleId', name: 'an apple user' };
sandbox.stub(appleAuth, 'appleProfile').returns(Promise.resolve(expectedResult));
});
beforeEach(async () => {
api = requester();
user = await generateUser();
});
it('registers a new user', async () => {
const response = await api.get(appleEndpoint);
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.newUser).to.be.true;
await expect(getProperty('users', response.id, 'auth.apple.id')).to.eventually.equal('appleId');
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('an apple user');
});
it('logs an existing user in', async () => {
const registerResponse = await api.get(appleEndpoint);
const response = await api.get(appleEndpoint);
expect(response.apiToken).to.eql(registerResponse.apiToken);
expect(response.id).to.eql(registerResponse.id);
expect(response.newUser).to.be.false;
});
it('add social auth to an existing user', async () => {
const response = await user.get(appleEndpoint);
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.newUser).to.be.false;
});
});

View File

@@ -492,6 +492,74 @@ describe('POST /user/auth/local/register', () => {
});
});
context('attach to google user', () => {
let user;
const email = 'some@email-google.net';
const username = 'some-username-google';
const password = 'some-password';
beforeEach(async () => {
user = await generateUser();
});
it('checks onlySocialAttachLocal', async () => {
await expect(user.post('/user/auth/local/register', {
email,
username,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlySocialAttachLocal'),
});
});
it('succeeds', async () => {
await user.update({ 'auth.google.id': 'some-google-id', 'auth.local': { ok: true } });
await user.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
await user.sync();
expect(user.auth.local.username).to.eql(username);
expect(user.auth.local.email).to.eql(email);
});
});
context('attach to apple user', () => {
let user;
const email = 'some@email-apple.net';
const username = 'some-username-apple';
const password = 'some-password';
beforeEach(async () => {
user = await generateUser();
});
it('checks onlySocialAttachLocal', async () => {
await expect(user.post('/user/auth/local/register', {
email,
username,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlySocialAttachLocal'),
});
});
it('succeeds', async () => {
await user.update({ 'auth.apple.id': 'some-apple-id', 'auth.local': { ok: true } });
await user.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
await user.sync();
expect(user.auth.local.username).to.eql(username);
expect(user.auth.local.email).to.eql(email);
});
});
context('login is already taken', () => {
let username; let email; let
api;

View File

@@ -51,6 +51,7 @@ describe('POST /user/auth/social', () => {
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a facebook user');
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.exist;
await expect(getProperty('users', response.id, 'auth.facebook.id')).to.eventually.equal(facebookId);
});
it('logs an existing user in', async () => {
@@ -106,6 +107,7 @@ describe('POST /user/auth/social', () => {
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.newUser).to.be.true;
await expect(getProperty('users', response.id, 'auth.google.id')).to.eventually.equal(googleId);
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user');
});

View File

@@ -457,6 +457,74 @@ describe('POST /user/auth/local/register', () => {
});
});
context('attach to google user', () => {
let user;
const email = 'some-google@email.net';
const username = 'some-username-google';
const password = 'some-password';
beforeEach(async () => {
user = await generateUser();
});
it('checks onlySocialAttachLocal', async () => {
await expect(user.post('/user/auth/local/register', {
email,
username,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlySocialAttachLocal'),
});
});
it('succeeds', async () => {
await user.update({ 'auth.google.id': 'some-google-id', 'auth.local': { ok: true } });
await user.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
await user.sync();
expect(user.auth.local.username).to.eql(username);
expect(user.auth.local.email).to.eql(email);
});
});
context('attach to apple user', () => {
let user;
const email = 'some-apple@email.net';
const username = 'some-username-apple';
const password = 'some-password';
beforeEach(async () => {
user = await generateUser();
});
it('checks onlySocialAttachLocal', async () => {
await expect(user.post('/user/auth/local/register', {
email,
username,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlySocialAttachLocal'),
});
});
it('succeeds', async () => {
await user.update({ 'auth.apple.id': 'some-apple-id', 'auth.local': { ok: true } });
await user.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
await user.sync();
expect(user.auth.local.username).to.eql(username);
expect(user.auth.local.email).to.eql(email);
});
});
context('login is already taken', () => {
let username; let email; let
api;

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="170px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 170 170" version="1.1" height="170px">
<path d="m150.37 130.25c-2.45 5.66-5.35 10.87-8.71 15.66-4.58 6.53-8.33 11.05-11.22 13.56-4.48 4.12-9.28 6.23-14.42 6.35-3.69 0-8.14-1.05-13.32-3.18-5.197-2.12-9.973-3.17-14.34-3.17-4.58 0-9.492 1.05-14.746 3.17-5.262 2.13-9.501 3.24-12.742 3.35-4.929 0.21-9.842-1.96-14.746-6.52-3.13-2.73-7.045-7.41-11.735-14.04-5.032-7.08-9.169-15.29-12.41-24.65-3.471-10.11-5.211-19.9-5.211-29.378 0-10.857 2.346-20.221 7.045-28.068 3.693-6.303 8.606-11.275 14.755-14.925s12.793-5.51 19.948-5.629c3.915 0 9.049 1.211 15.429 3.591 6.362 2.388 10.447 3.599 12.238 3.599 1.339 0 5.877-1.416 13.57-4.239 7.275-2.618 13.415-3.702 18.445-3.275 13.63 1.1 23.87 6.473 30.68 16.153-12.19 7.386-18.22 17.731-18.1 31.002 0.11 10.337 3.86 18.939 11.23 25.769 3.34 3.17 7.07 5.62 11.22 7.36-0.9 2.61-1.85 5.11-2.86 7.51zm-31.26-123.01c0 8.1021-2.96 15.667-8.86 22.669-7.12 8.324-15.732 13.134-25.071 12.375-0.119-0.972-0.188-1.995-0.188-3.07 0-7.778 3.386-16.102 9.399-22.908 3.002-3.446 6.82-6.3113 11.45-8.597 4.62-2.2516 8.99-3.4968 13.1-3.71 0.12 1.0831 0.17 2.1663 0.17 3.2409z" fill="#FFF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1000"
viewBox="0 0 1000 1187.198"
version="1.1"
height="1187.198"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="Apple_1998.svg">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="705"
id="namedview6"
showgrid="false"
inkscape:zoom="0.1767767"
inkscape:cx="-1066.5045"
inkscape:cy="964.94669"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
d="m 979.04184,925.18785 c -17.95397,41.47737 -39.20563,79.65705 -63.82824,114.75895 -33.56298,47.8528 -61.04356,80.9761 -82.22194,99.3698 -32.83013,30.192 -68.00529,45.6544 -105.67203,46.5338 -27.04089,0 -59.6512,-7.6946 -97.61105,-23.3035 -38.08442,-15.5358 -73.08371,-23.2303 -105.08578,-23.2303 -33.56296,0 -69.55888,7.6945 -108.06101,23.2303 -38.5608,15.6089 -69.62484,23.7432 -93.37541,24.5493 -36.12049,1.5389 -72.1237,-14.3632 -108.06101,-47.7796 -22.93711,-20.0059 -51.62684,-54.3017 -85.99592,-102.8874 C 92.254176,984.54592 61.937588,924.38175 38.187028,855.7902 12.750995,781.70252 0,709.95986 0,640.50361 0,560.94181 17.191859,492.32094 51.626869,434.81688 78.689754,388.62753 114.69299,352.19192 159.75381,325.44413 c 45.06086,-26.74775 93.74914,-40.37812 146.18212,-41.25019 28.68971,0 66.3125,8.8744 113.06613,26.31542 46.62174,17.49964 76.55727,26.37404 89.68198,26.37404 9.8124,0 43.06758,-10.37669 99.4431,-31.06405 53.31237,-19.18512 98.30724,-27.12887 135.16787,-23.99975 99.8828,8.06098 174.92313,47.43518 224.82789,118.37174 -89.33023,54.12578 -133.51903,129.93556 -132.63966,227.18753 0.8061,75.75115 28.28668,138.78795 82.2952,188.8393 24.47603,23.23022 51.81008,41.18421 82.22186,53.93522 -6.59525,19.12648 -13.557,37.44688 -20.95846,55.03446 z M 749.96366,23.751237 c 0,59.37343 -21.69138,114.810233 -64.92748,166.121963 -52.17652,60.99961 -115.28658,96.24803 -183.72426,90.68597 -0.87204,-7.12298 -1.37769,-14.61967 -1.37769,-22.49743 0,-56.99843 24.81315,-117.99801 68.87738,-167.873453 21.99909,-25.25281 49.978,-46.25018 83.90738,-63.00018 C 686.57507,10.688027 718.59913,1.5631274 748.71783,5.2734376e-4 749.59727,7.9378274 749.96366,15.875627 749.96366,23.750467 Z"
id="path4"
inkscape:connector-curvature="0" />
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -1,7 +1,7 @@
<template>
<div class="form">
<div class="form-group row text-center">
<div class="col-12 col-md-6">
<div class="col-12">
<div
class="btn btn-secondary social-button"
@click="socialAuth('facebook')"
@@ -15,7 +15,9 @@
: $t('loginWithSocial', {social: 'Facebook'}) }}</span>
</div>
</div>
<div class="col-12 col-md-6">
</div>
<div class="form-group row text-center">
<div class="col-12">
<div
class="btn btn-secondary social-button"
@click="socialAuth('google')"
@@ -30,6 +32,22 @@
</div>
</div>
</div>
<div class="form-group row text-center">
<div class="col-12">
<div
class="btn btn-secondary social-button"
@click="socialAuth('apple')"
>
<div
class="svg-icon social-icon apple-icon"
v-html="icons.appleIcon"
></div>
<span>{{ registering
? $t('signUpWithSocial', {social: 'Apple'})
: $t('loginWithSocial', {social: 'Apple'}) }}</span>
</div>
</div>
</div>
<div
v-if="registering"
class="form-group"
@@ -199,7 +217,11 @@
height: 18px;
display: inline-block;
vertical-align: top;
margin-top: .2em;
margin-top: .1em;
}
.apple-icon {
margin-top: -1px;
}
small.form-text {
@@ -219,10 +241,11 @@
import hello from 'hellojs';
import debounce from 'lodash/debounce';
import isEmail from 'validator/lib/isEmail';
import { setUpAxios } from '@/libs/auth';
import { setUpAxios, buildAppleAuthUrl } from '@/libs/auth';
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
import facebookSquareIcon from '@/assets/svg/facebook-square.svg';
import googleIcon from '@/assets/svg/google.svg';
import appleIcon from '@/assets/svg/apple_black.svg';
export default {
name: 'AuthForm',
@@ -239,6 +262,7 @@ export default {
data.icons = Object.freeze({
facebookIcon: facebookSquareIcon,
googleIcon,
appleIcon,
});
return data;
@@ -307,6 +331,9 @@ export default {
}, 500),
// @TODO: Abstract hello in to action or lib
async socialAuth (network) {
if (network === 'apple') {
window.location.href = buildAppleAuthUrl();
} else {
try {
await hello(network).logout();
} catch (e) {} // eslint-disable-line
@@ -329,6 +356,7 @@ export default {
await hello(network).logout();
this.socialAuth(network); // login again
}
}
},
async register () {
if (!this.email) {

View File

@@ -36,7 +36,7 @@
</div>
</div>
<div class="form-group row text-center">
<div class="col-12 col-md-6">
<div class="col-12 col-md-12">
<div
class="btn btn-secondary social-button"
@click="socialAuth('facebook')"
@@ -54,7 +54,9 @@
</div>
</div>
</div>
<div class="col-12 col-md-6">
</div>
<div class="form-group row text-center">
<div class="col-12 col-md-12">
<div
class="btn btn-secondary social-button"
@click="socialAuth('google')"
@@ -73,6 +75,29 @@
</div>
</div>
</div>
<div class="form-group row text-center">
<div class="col-12 col-md-12">
<div
class="btn btn-secondary social-button"
@click="socialAuth('apple')"
>
<div
class="svg-icon social-icon"
v-html="icons.appleIcon"
></div>
<div
class="text"
>
{{ registering
? $t('signUpWithSocial', {social: 'Apple'})
: $t('loginWithSocial', {social: 'Apple'}) }}
</div>
</div>
</div>
</div>
<div class="strike">
<span>{{ $t('or') }}</span>
</div>
<div
v-if="registering"
class="form-group"
@@ -496,12 +521,13 @@
}
.social-icon {
margin-left: 1em;
margin-right: 1em;
width: 18px;
height: 18px;
display: inline-block;
vertical-align: top;
margin-top: .2em;
margin-top: .1em;
}
}
@@ -581,6 +607,42 @@
.exclamation {
width: 2px;
}
.strike {
display: block;
text-align: center;
overflow: hidden;
white-space: nowrap;
margin-top: 1.5em;
margin-bottom: 1.5em;
}
.strike > span {
position: relative;
display: inline-block;
line-height: 1.14;
color: #fff;
}
.strike > span:before,
.strike > span:after {
content: "";
position: absolute;
top: 50%;
width: 9999px;
height: 1px;
background: #fff;
}
.strike > span:before {
right: 100%;
margin-right: 15px;
}
.strike > span:after {
left: 100%;
margin-left: 15px;
}
</style>
<script>
@@ -589,6 +651,7 @@ import hello from 'hellojs';
import moment from 'moment';
import debounce from 'lodash/debounce';
import isEmail from 'validator/lib/isEmail';
import { buildAppleAuthUrl } from '../../libs/auth';
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
import exclamation from '@/assets/svg/exclamation.svg';
@@ -596,6 +659,7 @@ import gryphon from '@/assets/svg/gryphon.svg';
import habiticaIcon from '@/assets/svg/habitica-logo.svg';
import facebookSquareIcon from '@/assets/svg/facebook-square.svg';
import googleIcon from '@/assets/svg/google.svg';
import appleIcon from '@/assets/svg/apple_black.svg';
export default {
data () {
@@ -618,6 +682,7 @@ export default {
habiticaIcon,
facebookIcon: facebookSquareIcon,
googleIcon,
appleIcon,
});
return data;
@@ -799,6 +864,9 @@ export default {
},
// @TODO: Abstract hello in to action or lib
async socialAuth (network) {
if (network === 'apple') {
window.location.href = buildAppleAuthUrl();
} else {
try {
await hello(network).logout();
} catch (e) {} // eslint-disable-line
@@ -828,6 +896,7 @@ export default {
// ALSO it's the only way to make sure language data
// is reloaded and correct for the logged in user
window.location.href = redirectTo;
}
},
handleSubmit () {
if (this.registering) {

View File

@@ -561,6 +561,8 @@ import { SUPPORTED_SOCIAL_NETWORKS } from '@/../../common/script/constants';
import changeClass from '@/../../common/script/ops/changeClass';
import notificationsMixin from '../../mixins/notifications';
import sounds from '../../libs/sounds';
import { buildAppleAuthUrl } from '../../libs/auth';
// @TODO: this needs our window.env fix
// import { availableLanguages } from '../../../server/libs/i18n';
@@ -837,6 +839,9 @@ export default {
this.text(this.$t('detachedSocial', { network: network.name }));
},
async socialAuth (network) {
if (network === 'apple') {
window.location.href = buildAppleAuthUrl();
} else {
const auth = await hello(network).login({ scope: 'email' });
await this.$store.dispatch('auth:socialAuth', {
@@ -844,6 +849,7 @@ export default {
});
window.location.href = '/';
}
},
async changeClassForUser (confirmationNeeded) {
if (confirmationNeeded && !window.confirm(this.$t('changeClassConfirmCost'))) return;

View File

@@ -0,0 +1,35 @@
<template>
<div class="static-view">
<p>Redirecting...</p>
</div>
</template>
<style lang='scss'>
@import '~@/assets/scss/static.scss';
.static-view {
height: 400px;
text-align: center;
}
.static-view p {
padding-top: 100px;
font-size: 2em
}
</style>
<script>
export default {
async mounted () {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const reqParams = { code: urlParams.get('code') };
if (urlParams.has('name')) {
reqParams.name = urlParams.get('name');
}
await this.$store.dispatch('auth:appleAuth', reqParams);
window.location.href = '/';
},
};
</script>

View File

@@ -28,31 +28,6 @@
<h3 class="text-center">
{{ $t('singUpForFree') }}
</h3>
<div class="text-center">
<button
class="social-button"
@click="socialAuth('facebook')"
>
<div
class="svg-icon social-icon"
v-html="icons.facebookIcon"
></div>
<span>{{ $t('signUpWithSocial', {social: 'Facebook'}) }}</span>
</button>
<button
class="social-button"
@click="socialAuth('google')"
>
<div
class="svg-icon social-icon"
v-html="icons.googleIcon"
></div>
<span>{{ $t('signUpWithSocial', {social: 'Google'}) }}</span>
</button>
</div>
<div class="strike">
<span>{{ $t('or') }}</span>
</div>
<form
class="form"
@submit.prevent.stop="register()"
@@ -127,6 +102,41 @@
{{ $t('signup') }}
</button>
</form>
<div class="strike">
<span>{{ $t('or') }}</span>
</div>
<div class="text-center">
<button
class="social-button"
@click="socialAuth('facebook')"
>
<div
class="svg-icon social-icon"
v-html="icons.facebookIcon"
></div>
<span>{{ $t('signUpWithSocial', {social: 'Facebook'}) }}</span>
</button>
<button
class="social-button"
@click="socialAuth('google')"
>
<div
class="svg-icon social-icon"
v-html="icons.googleIcon"
></div>
<span>{{ $t('signUpWithSocial', {social: 'Google'}) }}</span>
</button>
<button
class="social-button"
@click="socialAuth('apple')"
>
<div
class="svg-icon social-icon apple-icon"
v-html="icons.appleIcon"
></div>
<span>{{ $t('signUpWithSocial', {social: 'Apple'}) }}</span>
</button>
</div>
</div>
<div class="col-12">
<div
@@ -450,17 +460,17 @@
}
h3 {
font-size: 24px;
font-size: 32px;
}
.social-button {
border-radius: 2px;
border: solid 2px #bda8ff;
width: 48%;
width: 100%;
min-height: 40px;
padding: .5em;
background: transparent;
margin-right: .5em;
margin-bottom: .5em;
color: #bda8ff;
transition: .5s;
@@ -481,7 +491,11 @@
height: 18px;
display: inline-block;
vertical-align: top;
margin-top: .2em;
margin-top: .1em;
}
.apple-icon {
margin-top: -1px;
}
.strike {
@@ -781,6 +795,7 @@
import hello from 'hellojs';
import debounce from 'lodash/debounce';
import isEmail from 'validator/lib/isEmail';
import { buildAppleAuthUrl } from '../../libs/auth';
import googlePlay from '@/assets/images/home/google-play-badge.svg';
import iosAppStore from '@/assets/images/home/ios-app-store.svg';
import iphones from '@/assets/images/home/iphones.svg';
@@ -790,6 +805,7 @@ import pixelHorizontal2 from '@/assets/images/home/pixel-horizontal-2.svg';
import pixelHorizontal3 from '@/assets/images/home/pixel-horizontal-3.svg';
import facebookSquareIcon from '@/assets/svg/facebook-square.svg';
import googleIcon from '@/assets/svg/google.svg';
import appleIcon from '@/assets/svg/apple.svg';
import cnet from '@/assets/svg/cnet.svg';
import fastCompany from '@/assets/svg/fast-company.svg';
import discover from '@/assets/images/home/discover.svg';
@@ -814,6 +830,7 @@ export default {
pixelHorizontal3,
facebookIcon: facebookSquareIcon,
googleIcon,
appleIcon,
cnet,
fastCompany,
discover,
@@ -946,6 +963,9 @@ export default {
},
// @TODO: Abstract hello in to action or lib
async socialAuth (network) {
if (network === 'apple') {
window.location.href = buildAppleAuthUrl();
} else {
try {
await hello(network).logout();
} catch (e) {} // eslint-disable-line
@@ -962,6 +982,7 @@ export default {
});
window.location.href = '/';
}
},
},
};

View File

@@ -21,3 +21,8 @@ export function setUpAxios (AUTH_SETTINGS) { // eslint-disable-line import/prefe
return false;
}
export function buildAppleAuthUrl () {
const redirectUrl = `${window.location.protocol}//${window.location.host}/api/v4/user/auth/apple`;
return `https://appleid.apple.com/auth/authorize?response_mode=form_post&scope=name%20email&response_type=code&version=2&redirect_uri=${redirectUrl}&client_id=${process.env.APPLE_AUTH_CLIENT_ID}`;
}

View File

@@ -11,6 +11,7 @@ const StaticWrapper = () => import(/* webpackChunkName: "entry" */'@/components/
const HomePage = () => import(/* webpackChunkName: "entry" */'@/components/static/home');
const AppPage = () => import(/* webpackChunkName: "static" */'@/components/static/app');
const AppleRedirectPage = () => import(/* webpackChunkName: "static" */'@/components/static/appleRedirect');
const ClearBrowserDataPage = () => import(/* webpackChunkName: "static" */'@/components/static/clearBrowserData');
const CommunityGuidelinesPage = () => import(/* webpackChunkName: "static" */'@/components/static/communityGuidelines');
const ContactPage = () => import(/* webpackChunkName: "static" */'@/components/static/contact');
@@ -272,6 +273,9 @@ const router = new VueRouter({
{
name: 'app', path: 'app', component: AppPage, meta: { requiresLogin: false },
},
{
name: 'appleRedirect', path: 'apple-redirect', component: AppleRedirectPage, meta: { requiresLogin: false },
},
{
name: 'clearBrowserData', path: 'clear-browser-data', component: ClearBrowserDataPage, meta: { requiresLogin: false },
},

View File

@@ -82,6 +82,27 @@ export async function socialAuth (store, params) {
localStorage.setItem(LOCALSTORAGE_AUTH_KEY, userLocalData);
}
export async function appleAuth (store, params) {
const url = '/api/v4/user/auth/apple';
const result = await axios.get(url, {
params: {
code: params.code,
name: params.name,
},
});
const user = result.data.data;
const userLocalData = JSON.stringify({
auth: {
apiId: user.id,
apiToken: user.apiToken,
},
});
localStorage.setItem(LOCALSTORAGE_AUTH_KEY, userLocalData);
}
export function logout (store, options = {}) {
localStorage.clear();
const query = options.redirectToLogin === true ? '?redirectToLogin=true' : '';

View File

@@ -25,6 +25,7 @@ const envVars = [
'STRIPE_PUB_KEY',
'FACEBOOK_KEY',
'GOOGLE_CLIENT_ID',
'APPLE_AUTH_CLIENT_ID',
'AMPLITUDE_KEY',
'LOGGLY_CLIENT_TOKEN',
// TODO necessary? if yes how not to mess up with vue cli? 'NODE_ENV'

View File

@@ -20,6 +20,7 @@ export const CHAT_FLAG_FROM_SHADOW_MUTE = 10;
export const SUPPORTED_SOCIAL_NETWORKS = [
{ key: 'facebook', name: 'Facebook' },
{ key: 'google', name: 'Google' },
{ key: 'apple', name: 'Apple' },
];
export const GUILDS_PER_PAGE = 30; // number of guilds to return per page when using pagination
@@ -27,6 +28,7 @@ export const GUILDS_PER_PAGE = 30; // number of guilds to return per page when u
export const PARTY_LIMIT_MEMBERS = 30;
export const MINIMUM_PASSWORD_LENGTH = 8;
export const TRANSFORMATION_DEBUFFS_LIST = {
snowball: 'salt',
spookySparkles: 'opaquePotion',

View File

@@ -146,6 +146,41 @@ api.loginSocial = {
},
};
// Called by apple for web authentication.
api.redirectApple = {
method: 'POST',
middlewares: [authWithHeaders({
optional: true,
})],
url: '/user/auth/apple',
async handler (req, res) {
if (req.body.id_token) {
req.body.network = 'apple';
return loginSocial(req, res);
}
let url = `/static/apple-redirect?code=${req.body.code}`;
if (req.body.user) {
const { name } = JSON.parse(req.body.user);
url += `&name=${name.firstName} ${name.lastName}`;
}
return res.redirect(303, url);
},
};
// Called as a callback by Apple. Internal route
// Can be passed `code` and `name` as query parameters
api.loginApple = {
method: 'GET',
middlewares: [authWithHeaders({
optional: true,
})],
url: '/user/auth/apple',
async handler (req, res) {
req.body.network = 'apple';
return loginSocial(req, res);
},
};
/**
* @api {put} /api/v3/user/auth/update-username Update username
* @apiDescription Update and verify the user's username

View File

@@ -0,0 +1,52 @@
import AppleAuth from 'apple-auth';
import nconf from 'nconf';
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
import util from 'util';
const APPLE_PRIVATE_KEY = nconf.get('APPLE_AUTH_PRIVATE_KEY');
const APPLE_AUTH_CLIENT_ID = nconf.get('APPLE_AUTH_CLIENT_ID');
const APPLE_TEAM_ID = nconf.get('APPLE_TEAM_ID');
const APPLE_AUTH_KEY_ID = nconf.get('APPLE_AUTH_KEY_ID');
const BASE_URL = nconf.get('BASE_URL');
const appleAuth = new AppleAuth(JSON.stringify({
client_id: APPLE_AUTH_CLIENT_ID, // eslint-disable-line camelcase
team_id: APPLE_TEAM_ID, // eslint-disable-line camelcase
key_id: APPLE_AUTH_KEY_ID, // eslint-disable-line camelcase
redirect_uri: `${BASE_URL}/api/v4/user/auth/apple`, // eslint-disable-line camelcase
scope: 'name email',
}), APPLE_PRIVATE_KEY, 'text');
const APPLE_PUBLIC_KEYS_URL = 'https://appleid.apple.com/auth/keys';
const appleJwksClient = jwksClient({
jwksUri: APPLE_PUBLIC_KEYS_URL,
});
const getAppleSigningKey = util.promisify(appleJwksClient.getSigningKey);
export async function appleProfile (req) {
const code = req.body.code ? req.body.code : req.query.code;
const passedToken = req.body.id_token ? req.body.id_token : req.query.id_token;
let idToken;
if (code) {
const response = await appleAuth.accessToken(code);
idToken = response.id_token;
} else if (passedToken) {
idToken = passedToken;
}
const decodedToken = jwt.decode(idToken, { complete: true });
const signingKey = await getAppleSigningKey(decodedToken.header.kid);
const applePublicKey = signingKey.getPublicKey();
const verifiedPayload = await jwt.verify(idToken, applePublicKey, { algorithms: 'RS256' });
return {
id: verifiedPayload.sub,
emails: [{ value: verifiedPayload.email }],
name: verifiedPayload.name || req.body.name || req.query.name,
};
}

View File

@@ -6,6 +6,7 @@ import {
generateUsername,
loginRes,
} from './utils';
import { appleProfile } from './apple';
import { model as User } from '../../models/user';
import { model as EmailUnsubscription } from '../../models/emailUnsubscription';
import { sendTxn as sendTxnEmail } from '../email';
@@ -24,14 +25,21 @@ function _passportProfile (network, accessToken) {
export async function loginSocial (req, res) { // eslint-disable-line import/prefer-default-export
const existingUser = res.locals.user;
const accessToken = req.body.authResponse.access_token;
const { network } = req.body;
const isSupportedNetwork = common.constants.SUPPORTED_SOCIAL_NETWORKS
.find(supportedNetwork => supportedNetwork.key === network);
if (!isSupportedNetwork) throw new BadRequest(res.t('unsupportedNetwork'));
const profile = await _passportProfile(network, accessToken);
let profile = {};
if (network === 'apple') {
profile = await appleProfile(req);
} else {
const accessToken = req.body.authResponse.access_token;
profile = await _passportProfile(network, accessToken);
}
if (!profile.id) throw new BadRequest(res.t('invalidData'));
let user = await User.findOne({
[`auth.${network}.id`]: profile.id,
@@ -80,7 +88,7 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre
user.newUser = true;
}
loginRes(user, req, res);
const response = loginRes(user, req, res);
// Clean previous email preferences
if (
@@ -114,5 +122,5 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre
});
}
return null;
return response;
}

View File

@@ -1,5 +1,6 @@
import nconf from 'nconf';
import shortid from 'short-uuid';
import url from 'url';
import { NotAuthorized } from '../errors';
@@ -18,6 +19,11 @@ export function loginRes (user, req, res) {
{ communityManagerEmail: COMMUNITY_MANAGER_EMAIL, userId: user._id },
));
}
const urlPath = url.parse(req.url).pathname;
if (req.headers['x-client'] === 'habitica-android' && urlPath.includes('apple')) {
// This is a workaround for android not being able to handle sign in with apple better.
return res.redirect(`/?id=${user._id}&key=${user.apiToken}&newUser=${user.newUser || false}`);
}
const responseData = {
id: user._id,
@@ -25,6 +31,5 @@ export function loginRes (user, req, res) {
newUser: user.newUser || false,
username: user.auth.local.username,
};
return res.respond(200, responseData);
}

View File

@@ -31,6 +31,7 @@ export default new Schema({
$type: Schema.Types.Mixed,
default: () => ({}),
},
apple: { $type: Schema.Types.Mixed, default: () => ({}) },
local: {
email: {
$type: String,