Merge pull request #624 from mirumee/ref/auth

Refactor authorization
This commit is contained in:
Dominik Żegleń 2020-07-30 11:47:02 +02:00 committed by GitHub
commit 5062d35270
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 3843 additions and 474 deletions

View file

@ -13,6 +13,7 @@ All notable, unreleased changes to this project will be documented in this file.
- Add order invoices management - #570 by @orzechdev - Add order invoices management - #570 by @orzechdev
- Add Cypress e2e runner - #584 by @krzysztofwolski - Add Cypress e2e runner - #584 by @krzysztofwolski
- create Apps - #599 by @AlicjaSzu - create Apps - #599 by @AlicjaSzu
- Refactor authorization - #624 by @dominik-zeglen
## 2.10.1 ## 2.10.1

2545
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -83,6 +83,9 @@
"@babel/preset-react": "^7.7.4", "@babel/preset-react": "^7.7.4",
"@babel/preset-typescript": "^7.7.4", "@babel/preset-typescript": "^7.7.4",
"@babel/runtime": "^7.7.6", "@babel/runtime": "^7.7.6",
"@pollyjs/adapter-node-http": "^4.3.0",
"@pollyjs/core": "^4.3.0",
"@pollyjs/persister-fs": "^4.3.0",
"@storybook/addon-storyshots": "^5.2.8", "@storybook/addon-storyshots": "^5.2.8",
"@storybook/react": "^5.1.9", "@storybook/react": "^5.1.9",
"@testing-library/react-hooks": "^1.1.0", "@testing-library/react-hooks": "^1.1.0",
@ -93,6 +96,9 @@
"@types/jest": "^24.0.24", "@types/jest": "^24.0.24",
"@types/lodash-es": "^4.17.3", "@types/lodash-es": "^4.17.3",
"@types/moment-timezone": "^0.5.12", "@types/moment-timezone": "^0.5.12",
"@types/node-fetch": "^2.5.7",
"@types/pollyjs__adapter-node-http": "^2.0.0",
"@types/pollyjs__persister-fs": "^2.0.0",
"@types/react": "^16.9.16", "@types/react": "^16.9.16",
"@types/react-dom": "^16.8.5", "@types/react-dom": "^16.8.5",
"@types/react-dropzone": "^4.2.2", "@types/react-dropzone": "^4.2.2",
@ -103,6 +109,7 @@
"@types/react-sortable-tree": "^0.3.6", "@types/react-sortable-tree": "^0.3.6",
"@types/react-test-renderer": "^16.8.2", "@types/react-test-renderer": "^16.8.2",
"@types/semver-compare": "^1.0.1", "@types/semver-compare": "^1.0.1",
"@types/setup-polly-jest": "^0.5.0",
"@types/storybook__addon-storyshots": "^3.4.9", "@types/storybook__addon-storyshots": "^3.4.9",
"@types/storybook__react": "^4.0.2", "@types/storybook__react": "^4.0.2",
"@types/url-join": "^4.0.0", "@types/url-join": "^4.0.0",
@ -133,8 +140,10 @@
"husky": "^3.0.8", "husky": "^3.0.8",
"jest": "^24.8.0", "jest": "^24.8.0",
"jest-file": "^1.0.0", "jest-file": "^1.0.0",
"jest-localstorage-mock": "^2.4.3",
"lint-staged": "^9.4.2", "lint-staged": "^9.4.2",
"mock-apollo-client": "^0.4.0", "mock-apollo-client": "^0.4.0",
"node-fetch": "^2.6.0",
"prettier": "^1.19.1", "prettier": "^1.19.1",
"react-intl-translations-manager": "^5.0.3", "react-intl-translations-manager": "^5.0.3",
"react-test-renderer": "^16.12.0", "react-test-renderer": "^16.12.0",
@ -142,6 +151,8 @@
"require-context.macro": "^1.1.1", "require-context.macro": "^1.1.1",
"rimraf": "^3.0.0", "rimraf": "^3.0.0",
"start-server-and-test": "^1.11.0", "start-server-and-test": "^1.11.0",
"setup-polly-jest": "^0.8.0",
"testcafe": "^1.3.3",
"ts-jest": "^24.2.0", "ts-jest": "^24.2.0",
"tsconfig-paths-webpack-plugin": "^3.2.0", "tsconfig-paths-webpack-plugin": "^3.2.0",
"webpack": "^4.35.3", "webpack": "^4.35.3",
@ -152,6 +163,9 @@
"fsevents": "^1.2.9" "fsevents": "^1.2.9"
}, },
"jest": { "jest": {
"setupFiles": [
"jest-localstorage-mock"
],
"transform": { "transform": {
"^.+\\.(jsx?|tsx?)$": "babel-jest", "^.+\\.(jsx?|tsx?)$": "babel-jest",
"^.+\\.(png|svg|jpe?g)$": "jest-file" "^.+\\.(png|svg|jpe?g)$": "jest-file"

View file

@ -0,0 +1,128 @@
{
"log": {
"_recordingName": "User/will be logged if has valid token",
"creator": {
"comment": "persister:fs",
"name": "Polly.JS",
"version": "4.3.0"
},
"entries": [
{
"_id": "f515e15cbc83df73e5bd41437971c2e6",
"_order": 0,
"cache": {},
"request": {
"bodySize": 691,
"cookies": [],
"headers": [
{
"_fromType": "array",
"name": "accept",
"value": "*/*"
},
{
"_fromType": "array",
"name": "content-type",
"value": "application/json"
},
{
"_fromType": "array",
"name": "content-length",
"value": "691"
},
{
"_fromType": "array",
"name": "user-agent",
"value": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)"
},
{
"_fromType": "array",
"name": "accept-encoding",
"value": "gzip,deflate"
},
{
"_fromType": "array",
"name": "connection",
"value": "close"
},
{
"name": "host",
"value": "localhost:8000"
}
],
"headersSize": 254,
"httpVersion": "HTTP/1.1",
"method": "POST",
"postData": {
"mimeType": "application/json",
"params": [],
"text": "[{\"operationName\":\"VerifyToken\",\"variables\":{\"token\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTYwMjgyMTgsImV4cCI6MTU5NjAyODUxOCwidG9rZW4iOiJDM1NrMmtMUlZ1UEEiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6ImFjY2VzcyIsInVzZXJfaWQiOiJWWE5sY2pveU1RPT0iLCJpc19zdGFmZiI6dHJ1ZX0.eo8_Ew98HICB4cFQN2U7mCJ8ydGVOvQLGRT4CnkufMc\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation VerifyToken($token: String!) {\\n tokenVerify(token: $token) {\\n payload\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]"
},
"queryString": [],
"url": "http://localhost:8000/graphql/"
},
"response": {
"bodySize": 1619,
"content": {
"mimeType": "application/json",
"size": 1619,
"text": "[{\"data\": {\"tokenVerify\": {\"payload\": {\"iat\": 1596028218, \"exp\": 1596028518, \"token\": \"C3Sk2kLRVuPA\", \"email\": \"admin@example.com\", \"type\": \"access\", \"user_id\": \"VXNlcjoyMQ==\", \"is_staff\": true}, \"user\": {\"id\": \"VXNlcjoyMQ==\", \"email\": \"admin@example.com\", \"firstName\": \"\", \"lastName\": \"\", \"userPermissions\": [{\"code\": \"MANAGE_APPS\", \"name\": \"Manage apps\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_CHECKOUTS\", \"name\": \"Manage checkouts\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_DISCOUNTS\", \"name\": \"Manage sales and vouchers.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_GIFT_CARD\", \"name\": \"Manage gift cards.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_MENUS\", \"name\": \"Manage navigation.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_ORDERS\", \"name\": \"Manage orders.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PAGES\", \"name\": \"Manage pages.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PLUGINS\", \"name\": \"Manage plugins\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PRODUCTS\", \"name\": \"Manage products.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SETTINGS\", \"name\": \"Manage settings.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SHIPPING\", \"name\": \"Manage shipping.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_STAFF\", \"name\": \"Manage staff.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_TRANSLATIONS\", \"name\": \"Manage translations.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_USERS\", \"name\": \"Manage customers.\", \"__typename\": \"UserPermission\"}], \"avatar\": null, \"__typename\": \"User\"}, \"__typename\": \"VerifyToken\"}}}]"
},
"cookies": [],
"headers": [
{
"name": "date",
"value": "Wed, 29 Jul 2020 13:10:18 GMT"
},
{
"name": "server",
"value": "WSGIServer/0.2 CPython/3.8.1"
},
{
"name": "content-type",
"value": "application/json"
},
{
"name": "access-control-allow-origin",
"value": "http://localhost:9000"
},
{
"name": "access-control-allow-methods",
"value": "POST, OPTIONS"
},
{
"name": "access-control-allow-headers",
"value": "Origin, Content-Type, Accept, Authorization"
},
{
"name": "content-length",
"value": "1619"
},
{
"name": "x-content-type-options",
"value": "nosniff"
}
],
"headersSize": 336,
"httpVersion": "HTTP/1.1",
"redirectURL": "",
"status": 200,
"statusText": "OK"
},
"startedDateTime": "2020-07-29T13:10:18.327Z",
"time": 23,
"timings": {
"blocked": -1,
"connect": -1,
"dns": -1,
"receive": 0,
"send": 0,
"ssl": -1,
"wait": 23
}
}
],
"pages": [],
"version": "1.2"
}
}

View file

@ -0,0 +1,141 @@
{
"log": {
"_recordingName": "User/will be logged in if has valid credentials",
"creator": {
"comment": "persister:fs",
"name": "Polly.JS",
"version": "4.3.0"
},
"entries": [
{
"_id": "7c460842cac4a92c188d5451dfc533a2",
"_order": 0,
"cache": {},
"request": {
"bodySize": 587,
"cookies": [],
"headers": [
{
"_fromType": "array",
"name": "accept",
"value": "*/*"
},
{
"_fromType": "array",
"name": "content-type",
"value": "application/json"
},
{
"_fromType": "array",
"name": "content-length",
"value": "587"
},
{
"_fromType": "array",
"name": "user-agent",
"value": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)"
},
{
"_fromType": "array",
"name": "accept-encoding",
"value": "gzip,deflate"
},
{
"_fromType": "array",
"name": "connection",
"value": "close"
},
{
"name": "host",
"value": "localhost:8000"
}
],
"headersSize": 254,
"httpVersion": "HTTP/1.1",
"method": "POST",
"postData": {
"mimeType": "application/json",
"params": [],
"text": "[{\"operationName\":\"TokenAuth\",\"variables\":{\"email\":\"admin@example.com\",\"password\":\"admin\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation TokenAuth($email: String!, $password: String!) {\\n tokenCreate(email: $email, password: $password) {\\n errors: accountErrors {\\n field\\n message\\n __typename\\n }\\n csrfToken\\n token\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]"
},
"queryString": [],
"url": "http://localhost:8000/graphql/"
},
"response": {
"bodySize": 1830,
"content": {
"mimeType": "application/json",
"size": 1830,
"text": "[{\"data\": {\"tokenCreate\": {\"errors\": [], \"csrfToken\": \"rLPNMGNYKXH8VY4UNEWl4nEOFMseocljioigPl36IM2CqbdmOTEpNwvdHBAJ1ZWQ\", \"token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTYwMjgyMTgsImV4cCI6MTU5NjAyODUxOCwidG9rZW4iOiJDM1NrMmtMUlZ1UEEiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6ImFjY2VzcyIsInVzZXJfaWQiOiJWWE5sY2pveU1RPT0iLCJpc19zdGFmZiI6dHJ1ZX0.eo8_Ew98HICB4cFQN2U7mCJ8ydGVOvQLGRT4CnkufMc\", \"user\": {\"id\": \"VXNlcjoyMQ==\", \"email\": \"admin@example.com\", \"firstName\": \"\", \"lastName\": \"\", \"userPermissions\": [{\"code\": \"MANAGE_APPS\", \"name\": \"Manage apps\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_CHECKOUTS\", \"name\": \"Manage checkouts\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_DISCOUNTS\", \"name\": \"Manage sales and vouchers.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_GIFT_CARD\", \"name\": \"Manage gift cards.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_MENUS\", \"name\": \"Manage navigation.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_ORDERS\", \"name\": \"Manage orders.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PAGES\", \"name\": \"Manage pages.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PLUGINS\", \"name\": \"Manage plugins\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PRODUCTS\", \"name\": \"Manage products.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SETTINGS\", \"name\": \"Manage settings.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SHIPPING\", \"name\": \"Manage shipping.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_STAFF\", \"name\": \"Manage staff.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_TRANSLATIONS\", \"name\": \"Manage translations.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_USERS\", \"name\": \"Manage customers.\", \"__typename\": \"UserPermission\"}], \"avatar\": null, \"__typename\": \"User\"}, \"__typename\": \"CreateToken\"}}}]"
},
"cookies": [
{
"httpOnly": true,
"name": "refreshToken",
"path": "/",
"secure": true,
"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTYwMjgyMTgsImV4cCI6MTU5ODYyMDIxOCwidG9rZW4iOiJDM1NrMmtMUlZ1UEEiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6InJlZnJlc2giLCJ1c2VyX2lkIjoiVlhObGNqb3lNUT09IiwiaXNfc3RhZmYiOnRydWUsImNzcmZUb2tlbiI6InJMUE5NR05ZS1hIOFZZNFVORVdsNG5FT0ZNc2VvY2xqaW9pZ1BsMzZJTTJDcWJkbU9URXBOd3ZkSEJBSjFaV1EifQ.boD8G4pkSnZF-PLl5oOg85Uj-mqTiAzOkua9aAG3Bz4"
}
],
"headers": [
{
"name": "date",
"value": "Wed, 29 Jul 2020 13:10:18 GMT"
},
{
"name": "server",
"value": "WSGIServer/0.2 CPython/3.8.1"
},
{
"name": "content-type",
"value": "application/json"
},
{
"name": "access-control-allow-origin",
"value": "http://localhost:9000"
},
{
"name": "access-control-allow-methods",
"value": "POST, OPTIONS"
},
{
"name": "access-control-allow-headers",
"value": "Origin, Content-Type, Accept, Authorization"
},
{
"name": "content-length",
"value": "1830"
},
{
"name": "x-content-type-options",
"value": "nosniff"
},
{
"_fromType": "array",
"name": "set-cookie",
"value": "refreshToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTYwMjgyMTgsImV4cCI6MTU5ODYyMDIxOCwidG9rZW4iOiJDM1NrMmtMUlZ1UEEiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6InJlZnJlc2giLCJ1c2VyX2lkIjoiVlhObGNqb3lNUT09IiwiaXNfc3RhZmYiOnRydWUsImNzcmZUb2tlbiI6InJMUE5NR05ZS1hIOFZZNFVORVdsNG5FT0ZNc2VvY2xqaW9pZ1BsMzZJTTJDcWJkbU9URXBOd3ZkSEJBSjFaV1EifQ.boD8G4pkSnZF-PLl5oOg85Uj-mqTiAzOkua9aAG3Bz4; HttpOnly; Path=/; Secure"
}
],
"headersSize": 768,
"httpVersion": "HTTP/1.1",
"redirectURL": "",
"status": 200,
"statusText": "OK"
},
"startedDateTime": "2020-07-29T13:10:18.064Z",
"time": 118,
"timings": {
"blocked": -1,
"connect": -1,
"dns": -1,
"receive": 0,
"send": 0,
"ssl": -1,
"wait": 118
}
}
],
"pages": [],
"version": "1.2"
}
}

View file

@ -0,0 +1,128 @@
{
"log": {
"_recordingName": "User/will not be logged if has expired token",
"creator": {
"comment": "persister:fs",
"name": "Polly.JS",
"version": "4.3.0"
},
"entries": [
{
"_id": "414f6b24b58b132c92e9a6ea67613a15",
"_order": 0,
"cache": {},
"request": {
"bodySize": 691,
"cookies": [],
"headers": [
{
"_fromType": "array",
"name": "accept",
"value": "*/*"
},
{
"_fromType": "array",
"name": "content-type",
"value": "application/json"
},
{
"_fromType": "array",
"name": "content-length",
"value": "691"
},
{
"_fromType": "array",
"name": "user-agent",
"value": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)"
},
{
"_fromType": "array",
"name": "accept-encoding",
"value": "gzip,deflate"
},
{
"_fromType": "array",
"name": "connection",
"value": "close"
},
{
"name": "host",
"value": "localhost:8000"
}
],
"headersSize": 254,
"httpVersion": "HTTP/1.1",
"method": "POST",
"postData": {
"mimeType": "application/json",
"params": [],
"text": "[{\"operationName\":\"VerifyToken\",\"variables\":{\"token\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTUyMzk4OTcsImV4cCI6MTU5NTI0MDE5NywidG9rZW4iOiJxQ1Jia0dOMnpOT28iLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6ImFjY2VzcyIsInVzZXJfaWQiOiJWWE5sY2pveU1RPT0iLCJpc19zdGFmZiI6dHJ1ZX0.l-FnFDVmi5fASo7Uae2Emewu2pKyO2qLz7ZQl1fSzo4\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation VerifyToken($token: String!) {\\n tokenVerify(token: $token) {\\n payload\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]"
},
"queryString": [],
"url": "http://localhost:8000/graphql/"
},
"response": {
"bodySize": 89,
"content": {
"mimeType": "application/json",
"size": 89,
"text": "[{\"data\": {\"tokenVerify\": {\"payload\": null, \"user\": null, \"__typename\": \"VerifyToken\"}}}]"
},
"cookies": [],
"headers": [
{
"name": "date",
"value": "Tue, 21 Jul 2020 13:43:50 GMT"
},
{
"name": "server",
"value": "WSGIServer/0.2 CPython/3.8.1"
},
{
"name": "content-type",
"value": "application/json"
},
{
"name": "access-control-allow-origin",
"value": "*"
},
{
"name": "access-control-allow-methods",
"value": "POST, OPTIONS"
},
{
"name": "access-control-allow-headers",
"value": "Origin, Content-Type, Accept, Authorization"
},
{
"name": "content-length",
"value": "89"
},
{
"name": "x-content-type-options",
"value": "nosniff"
}
],
"headersSize": 314,
"httpVersion": "HTTP/1.1",
"redirectURL": "",
"status": 200,
"statusText": "OK"
},
"startedDateTime": "2020-07-21T13:43:50.249Z",
"time": 17,
"timings": {
"blocked": -1,
"connect": -1,
"dns": -1,
"receive": 0,
"send": 0,
"ssl": -1,
"wait": 17
}
}
],
"pages": [],
"version": "1.2"
}
}

View file

@ -0,0 +1,128 @@
{
"log": {
"_recordingName": "User/will not be logged if has invalid token",
"creator": {
"comment": "persister:fs",
"name": "Polly.JS",
"version": "4.3.0"
},
"entries": [
{
"_id": "4836098613648775386c1e10728424dd",
"_order": 0,
"cache": {},
"request": {
"bodySize": 428,
"cookies": [],
"headers": [
{
"_fromType": "array",
"name": "accept",
"value": "*/*"
},
{
"_fromType": "array",
"name": "content-type",
"value": "application/json"
},
{
"_fromType": "array",
"name": "content-length",
"value": "428"
},
{
"_fromType": "array",
"name": "user-agent",
"value": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)"
},
{
"_fromType": "array",
"name": "accept-encoding",
"value": "gzip,deflate"
},
{
"_fromType": "array",
"name": "connection",
"value": "close"
},
{
"name": "host",
"value": "localhost:8000"
}
],
"headersSize": 254,
"httpVersion": "HTTP/1.1",
"method": "POST",
"postData": {
"mimeType": "application/json",
"params": [],
"text": "[{\"operationName\":\"VerifyToken\",\"variables\":{\"token\":\"NotAToken\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation VerifyToken($token: String!) {\\n tokenVerify(token: $token) {\\n payload\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]"
},
"queryString": [],
"url": "http://localhost:8000/graphql/"
},
"response": {
"bodySize": 89,
"content": {
"mimeType": "application/json",
"size": 89,
"text": "[{\"data\": {\"tokenVerify\": {\"payload\": null, \"user\": null, \"__typename\": \"VerifyToken\"}}}]"
},
"cookies": [],
"headers": [
{
"name": "date",
"value": "Wed, 29 Jul 2020 13:10:18 GMT"
},
{
"name": "server",
"value": "WSGIServer/0.2 CPython/3.8.1"
},
{
"name": "content-type",
"value": "application/json"
},
{
"name": "access-control-allow-origin",
"value": "http://localhost:9000"
},
{
"name": "access-control-allow-methods",
"value": "POST, OPTIONS"
},
{
"name": "access-control-allow-headers",
"value": "Origin, Content-Type, Accept, Authorization"
},
{
"name": "content-length",
"value": "89"
},
{
"name": "x-content-type-options",
"value": "nosniff"
}
],
"headersSize": 334,
"httpVersion": "HTTP/1.1",
"redirectURL": "",
"status": 200,
"statusText": "OK"
},
"startedDateTime": "2020-07-29T13:10:18.368Z",
"time": 6,
"timings": {
"blocked": -1,
"connect": -1,
"dns": -1,
"receive": 0,
"send": 0,
"ssl": -1,
"wait": 6
}
}
],
"pages": [],
"version": "1.2"
}
}

View file

@ -0,0 +1,128 @@
{
"log": {
"_recordingName": "User/will not be logged in if doesn't have valid credentials",
"creator": {
"comment": "persister:fs",
"name": "Polly.JS",
"version": "4.3.0"
},
"entries": [
{
"_id": "86487093ff8b070d496fcdc566e01adf",
"_order": 0,
"cache": {},
"request": {
"bodySize": 603,
"cookies": [],
"headers": [
{
"_fromType": "array",
"name": "accept",
"value": "*/*"
},
{
"_fromType": "array",
"name": "content-type",
"value": "application/json"
},
{
"_fromType": "array",
"name": "content-length",
"value": "603"
},
{
"_fromType": "array",
"name": "user-agent",
"value": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)"
},
{
"_fromType": "array",
"name": "accept-encoding",
"value": "gzip,deflate"
},
{
"_fromType": "array",
"name": "connection",
"value": "close"
},
{
"name": "host",
"value": "localhost:8000"
}
],
"headersSize": 254,
"httpVersion": "HTTP/1.1",
"method": "POST",
"postData": {
"mimeType": "application/json",
"params": [],
"text": "[{\"operationName\":\"TokenAuth\",\"variables\":{\"email\":\"admin@example.com\",\"password\":\"NotAValidPassword123!\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation TokenAuth($email: String!, $password: String!) {\\n tokenCreate(email: $email, password: $password) {\\n errors: accountErrors {\\n field\\n message\\n __typename\\n }\\n csrfToken\\n token\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]"
},
"queryString": [],
"url": "http://localhost:8000/graphql/"
},
"response": {
"bodySize": 214,
"content": {
"mimeType": "application/json",
"size": 214,
"text": "[{\"data\": {\"tokenCreate\": {\"errors\": [{\"field\": \"email\", \"message\": \"Please, enter valid credentials\", \"__typename\": \"AccountError\"}], \"csrfToken\": null, \"token\": null, \"user\": null, \"__typename\": \"CreateToken\"}}}]"
},
"cookies": [],
"headers": [
{
"name": "date",
"value": "Wed, 29 Jul 2020 13:10:18 GMT"
},
{
"name": "server",
"value": "WSGIServer/0.2 CPython/3.8.1"
},
{
"name": "content-type",
"value": "application/json"
},
{
"name": "access-control-allow-origin",
"value": "http://localhost:9000"
},
{
"name": "access-control-allow-methods",
"value": "POST, OPTIONS"
},
{
"name": "access-control-allow-headers",
"value": "Origin, Content-Type, Accept, Authorization"
},
{
"name": "content-length",
"value": "214"
},
{
"name": "x-content-type-options",
"value": "nosniff"
}
],
"headersSize": 335,
"httpVersion": "HTTP/1.1",
"redirectURL": "",
"status": 200,
"statusText": "OK"
},
"startedDateTime": "2020-07-29T13:10:18.208Z",
"time": 99,
"timings": {
"blocked": -1,
"connect": -1,
"dns": -1,
"receive": 0,
"send": 0,
"ssl": -1,
"wait": 99
}
}
],
"pages": [],
"version": "1.2"
}
}

View file

@ -0,0 +1,77 @@
import setupApi from "@test/api";
import { act, renderHook } from "@testing-library/react-hooks";
import ApolloClient from "apollo-client";
import { useAuthProvider } from "./AuthProvider";
import { getTokens, setAuthToken } from "./utils";
const apolloClient = setupApi();
function renderAuthProvider(apolloClient: ApolloClient<any>) {
const intl = {
formatMessage: ({ defaultMessage }) => defaultMessage
};
const notify = jest.fn();
const { result } = renderHook(() =>
useAuthProvider(intl as any, notify, apolloClient)
);
return result;
}
const credentials = {
email: "admin@example.com",
password: "admin",
token: null
};
beforeEach(() => {
localStorage.clear();
sessionStorage.clear();
});
describe("User", () => {
it("will be logged in if has valid credentials", async done => {
const hook = renderAuthProvider(apolloClient);
await act(() =>
hook.current.login(credentials.email, credentials.password)
);
expect(hook.current.userContext.email).toBe(credentials.email);
credentials.token = getTokens().auth;
done();
});
it("will not be logged in if doesn't have valid credentials", async done => {
const hook = renderAuthProvider(apolloClient);
await act(() =>
hook.current.login(credentials.email, "NotAValidPassword123!")
);
expect(hook.current.userContext).toBe(null);
done();
});
it("will be logged if has valid token", async done => {
setAuthToken(credentials.token, false);
const hook = renderAuthProvider(apolloClient);
await act(() => hook.current.autologinPromise.current);
expect(hook.current.userContext.email).toBe(credentials.email);
done();
});
it("will not be logged if has invalid token", async done => {
setAuthToken("NotAToken", false);
const hook = renderAuthProvider(apolloClient);
await act(() => hook.current.autologinPromise.current);
expect(hook.current.userContext).toBe(undefined);
done();
});
});

View file

@ -1,228 +1,227 @@
import { IMessageContext } from "@saleor/components/messages";
import { DEMO_MODE } from "@saleor/config"; import { DEMO_MODE } from "@saleor/config";
import { User } from "@saleor/fragments/types/User"; import { User } from "@saleor/fragments/types/User";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import { maybe } from "@saleor/misc"; import { getMutationStatus } from "@saleor/misc";
import { import {
isSupported as isCredentialsManagementAPISupported, isSupported as isCredentialsManagementAPISupported,
login as loginWithCredentialsManagementAPI, login as loginWithCredentialsManagementAPI,
saveCredentials saveCredentials
} from "@saleor/utils/credentialsManagement"; } from "@saleor/utils/credentialsManagement";
import React from "react"; import ApolloClient from "apollo-client";
import { MutationFunction, MutationResult } from "react-apollo"; import React, { useContext, useEffect, useRef, useState } from "react";
import { useIntl } from "react-intl"; import { useApolloClient, useMutation } from "react-apollo";
import { IntlShape, useIntl } from "react-intl";
import { UserContext } from "./"; import { UserContext } from "./";
import { import {
TokenRefreshMutation, tokenAuthMutation,
TypedTokenAuthMutation, tokenRefreshMutation,
TypedVerifyTokenMutation tokenVerifyMutation
} from "./mutations"; } from "./mutations";
import { RefreshToken, RefreshTokenVariables } from "./types/RefreshToken"; import { RefreshToken, RefreshTokenVariables } from "./types/RefreshToken";
import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth"; import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth";
import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken"; import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken";
import { import {
displayDemoMessage, displayDemoMessage,
getAuthToken, getTokens,
removeAuthToken, removeTokens,
setAuthToken setAuthToken,
setTokens
} from "./utils"; } from "./utils";
interface AuthProviderOperationsProps { const persistToken = false;
children: (props: {
hasToken: boolean;
isAuthenticated: boolean;
tokenAuthLoading: boolean;
tokenVerifyLoading: boolean;
user: User;
}) => React.ReactNode;
}
const AuthProviderOperations: React.FC<AuthProviderOperationsProps> = ({
children
}) => {
const intl = useIntl();
const notify = useNotifier();
const handleLogin = () => { export function useAuthProvider(
intl: IntlShape,
notify: IMessageContext,
apolloClient: ApolloClient<any>
) {
const [userContext, setUserContext] = useState<undefined | User>(undefined);
const autologinPromise = useRef<Promise<any>>();
const refreshPromise = useRef<Promise<boolean>>();
const logout = () => {
setUserContext(undefined);
if (isCredentialsManagementAPISupported) {
navigator.credentials.preventSilentAccess();
}
removeTokens();
};
const [tokenAuth, tokenAuthResult] = useMutation<
TokenAuth,
TokenAuthVariables
>(tokenAuthMutation, {
client: apolloClient,
onCompleted: result => {
if (result.tokenCreate.errors.length > 0) {
logout();
}
const user = result.tokenCreate.user;
// FIXME: Now we set state also when auth fails and returned user is
// `null`, because the LoginView uses this `null` to display error.
setUserContext(user);
if (user) {
setTokens(
result.tokenCreate.token,
result.tokenCreate.csrfToken,
persistToken
);
}
},
onError: logout
});
const [tokenRefresh] = useMutation<RefreshToken, RefreshTokenVariables>(
tokenRefreshMutation,
{
client: apolloClient,
onError: logout
}
);
const [tokenVerify, tokenVerifyResult] = useMutation<
VerifyToken,
VerifyTokenVariables
>(tokenVerifyMutation, {
client: apolloClient,
onCompleted: result => {
if (result.tokenVerify === null) {
logout();
} else {
const user = result.tokenVerify?.user;
if (!!user) {
setUserContext(user);
}
}
},
onError: logout
});
const tokenAuthOpts = {
...tokenAuthResult,
status: getMutationStatus(tokenAuthResult)
};
const tokenVerifyOpts = {
...tokenVerifyResult,
status: getMutationStatus(tokenVerifyResult)
};
const onLogin = () => {
if (DEMO_MODE) { if (DEMO_MODE) {
displayDemoMessage(intl, notify); displayDemoMessage(intl, notify);
} }
}; };
return ( useEffect(() => {
<TypedTokenAuthMutation> const token = getTokens().auth;
{(...tokenAuth) => ( if (!!token && !userContext) {
<TypedVerifyTokenMutation> autologinPromise.current = tokenVerify({ variables: { token } });
{(...tokenVerify) => (
<TokenRefreshMutation>
{(...tokenRefresh) => (
<AuthProvider
tokenAuth={tokenAuth}
tokenVerify={tokenVerify}
tokenRefresh={tokenRefresh}
onLogin={handleLogin}
>
{children}
</AuthProvider>
)}
</TokenRefreshMutation>
)}
</TypedVerifyTokenMutation>
)}
</TypedTokenAuthMutation>
);
};
interface AuthProviderProps {
children: (props: {
hasToken: boolean;
isAuthenticated: boolean;
tokenAuthLoading: boolean;
tokenVerifyLoading: boolean;
user: User;
}) => React.ReactNode;
tokenAuth: [
MutationFunction<TokenAuth, TokenAuthVariables>,
MutationResult<TokenAuth>
];
tokenVerify: [
MutationFunction<VerifyToken, VerifyTokenVariables>,
MutationResult<VerifyToken>
];
tokenRefresh: [
MutationFunction<RefreshToken, RefreshTokenVariables>,
MutationResult<RefreshToken>
];
onLogin?: () => void;
}
interface AuthProviderState {
user: User;
persistToken: boolean;
}
class AuthProvider extends React.Component<
AuthProviderProps,
AuthProviderState
> {
constructor(props) {
super(props);
this.state = { persistToken: false, user: undefined };
}
componentWillReceiveProps(props: AuthProviderProps) {
const { tokenAuth, tokenVerify } = props;
const tokenAuthOpts = tokenAuth[1];
const tokenVerifyOpts = tokenVerify[1];
if (tokenAuthOpts.error || tokenVerifyOpts.error) {
this.logout();
}
if (tokenAuthOpts.data) {
const user = tokenAuthOpts.data.tokenCreate.user;
// FIXME: Now we set state also when auth fails and returned user is
// `null`, because the LoginView uses this `null` to display error.
this.setState({ user });
if (user) {
setAuthToken(
tokenAuthOpts.data.tokenCreate.token,
this.state.persistToken
);
}
} else { } else {
if (maybe(() => tokenVerifyOpts.data.tokenVerify === null)) { autologinPromise.current = loginWithCredentialsManagementAPI(login);
this.logout();
} else {
const user = maybe(() => tokenVerifyOpts.data.tokenVerify.user);
if (!!user) {
this.setState({ user });
}
}
}
} }
}, []);
componentDidMount() { const login = async (email: string, password: string) => {
const { user } = this.state; const result = await tokenAuth({ variables: { email, password } });
const token = getAuthToken();
if (!!token && !user) {
this.verifyToken(token);
} else {
loginWithCredentialsManagementAPI(this.login);
}
}
login = async (email: string, password: string) => {
const { tokenAuth, onLogin } = this.props;
const [tokenAuthFn] = tokenAuth;
tokenAuthFn({ variables: { email, password } }).then(result => {
if (result && !result.data.tokenCreate.errors.length) { if (result && !result.data.tokenCreate.errors.length) {
if (!!onLogin) { if (!!onLogin) {
onLogin(); onLogin();
} }
saveCredentials(result.data.tokenCreate.user, password); saveCredentials(result.data.tokenCreate.user, password);
return result.data.tokenCreate.user;
} }
return null;
};
const loginByToken = (auth: string, refresh: string, user: User) => {
setUserContext(user);
setTokens(auth, refresh, persistToken);
};
const refreshToken = (): Promise<boolean> => {
if (!!refreshPromise.current) {
return refreshPromise.current;
}
return new Promise(resolve => {
const token = getTokens().refresh;
return tokenRefresh({ variables: { token } }).then(refreshData => {
if (!!refreshData.data.tokenRefresh?.token) {
setAuthToken(refreshData.data.tokenRefresh.token, persistToken);
return resolve(true);
}
return resolve(false);
});
}); });
}; };
loginByToken = (token: string, user: User) => { return {
this.setState({ user }); autologinPromise,
setAuthToken(token, this.state.persistToken); login,
loginByToken,
logout,
refreshToken,
tokenAuthOpts,
tokenVerifyOpts,
userContext
}; };
logout = () => {
this.setState({ user: undefined });
if (isCredentialsManagementAPISupported) {
navigator.credentials.preventSilentAccess();
} }
removeAuthToken();
};
verifyToken = (token: string) => { interface AuthProviderProps {
const { tokenVerify } = this.props; children: React.ReactNode;
const [tokenVerifyFn] = tokenVerify; }
return tokenVerifyFn({ variables: { token } }); const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
}; const apolloClient = useApolloClient();
const intl = useIntl();
const notify = useNotifier();
refreshToken = async () => { const {
const { tokenRefresh } = this.props; login,
const [tokenRefreshFn] = tokenRefresh; loginByToken,
const token = getAuthToken(); logout,
tokenAuthOpts,
const refreshData = await tokenRefreshFn({ variables: { token } }); refreshToken,
tokenVerifyOpts,
setAuthToken(refreshData.data.tokenRefresh.token, this.state.persistToken); userContext
}; } = useAuthProvider(intl, notify, apolloClient);
render() {
const { children, tokenAuth, tokenVerify } = this.props;
const tokenAuthOpts = tokenAuth[1];
const tokenVerifyOpts = tokenVerify[1];
const { user } = this.state;
const isAuthenticated = !!user;
return ( return (
<UserContext.Provider <UserContext.Provider
value={{ value={{
login: this.login, login,
loginByToken: this.loginByToken, loginByToken,
logout: this.logout, logout,
tokenAuthLoading: tokenAuthOpts.loading, tokenAuthLoading: tokenAuthOpts.loading,
tokenRefresh: this.refreshToken, tokenRefresh: refreshToken,
tokenVerifyLoading: tokenVerifyOpts.loading, tokenVerifyLoading: tokenVerifyOpts.loading,
user user: userContext
}} }}
> >
{children({ {children}
hasToken: !!getAuthToken(),
isAuthenticated,
tokenAuthLoading: tokenAuthOpts.loading,
tokenVerifyLoading: tokenVerifyOpts.loading,
user
})}
</UserContext.Provider> </UserContext.Provider>
); );
} };
}
export default AuthProviderOperations; export const useAuth = () => {
const user = useContext(UserContext);
const isAuthenticated = !!user.user;
return {
hasToken: !!getTokens(),
isAuthenticated,
tokenAuthLoading: user.tokenAuthLoading,
tokenVerifyLoading: user.tokenVerifyLoading,
user: user.user
};
};
export default AuthProvider;

View file

@ -2,8 +2,9 @@ import { findValueInEnum } from "@saleor/misc";
import { GraphQLError } from "graphql"; import { GraphQLError } from "graphql";
export enum JWTError { export enum JWTError {
invalid = "JSONWebTokenError", invalid = "InvalidTokenError",
expired = "JSONWebTokenExpired" invalidSignature = "InvalidSignatureError",
expired = "ExpiredSignatureError"
} }
export function isJwtError(error: GraphQLError): boolean { export function isJwtError(error: GraphQLError): boolean {

View file

@ -3,7 +3,6 @@ import React from "react";
import { Route, Switch } from "react-router-dom"; import { Route, Switch } from "react-router-dom";
import Layout from "./components/Layout"; import Layout from "./components/Layout";
import LoginLoading from "./components/LoginLoading";
import { import {
newPasswordPath, newPasswordPath,
passwordResetPath, passwordResetPath,
@ -16,10 +15,10 @@ import ResetPasswordSuccess from "./views/ResetPasswordSuccess";
interface UserContext { interface UserContext {
login: (username: string, password: string) => void; login: (username: string, password: string) => void;
loginByToken: (token: string, user: User) => void; loginByToken: (auth: string, csrf: string, user: User) => void;
logout: () => void; logout: () => void;
tokenAuthLoading: boolean; tokenAuthLoading: boolean;
tokenRefresh: () => Promise<void>; tokenRefresh: () => Promise<boolean>;
tokenVerifyLoading: boolean; tokenVerifyLoading: boolean;
user?: User; user?: User;
} }
@ -33,20 +32,12 @@ export const UserContext = React.createContext<UserContext>({
tokenVerifyLoading: false tokenVerifyLoading: false
}); });
interface AuthRouterProps { const AuthRouter: React.FC = () => (
hasToken: boolean;
}
const AuthRouter: React.FC<AuthRouterProps> = ({ hasToken }) => (
<Layout> <Layout>
<Switch> <Switch>
<Route path={passwordResetSuccessPath} component={ResetPasswordSuccess} /> <Route path={passwordResetSuccessPath} component={ResetPasswordSuccess} />
<Route path={passwordResetPath} component={ResetPassword} /> <Route path={passwordResetPath} component={ResetPassword} />
{!hasToken ? (
<Route path={newPasswordPath} component={NewPassword} /> <Route path={newPasswordPath} component={NewPassword} />
) : (
<LoginLoading />
)}
<Route component={LoginView} /> <Route component={LoginView} />
</Switch> </Switch>
</Layout> </Layout>

39
src/auth/link.ts Normal file
View file

@ -0,0 +1,39 @@
import { setContext } from "apollo-link-context";
import { ErrorResponse, onError } from "apollo-link-error";
import { getTokens, removeTokens } from "./";
import { isJwtError, JWTError } from "./errors";
interface ResponseError extends ErrorResponse {
networkError?: Error & {
statusCode?: number;
bodyText?: string;
};
}
export const invalidateTokenLink = onError((error: ResponseError) => {
if (
(error.networkError && error.networkError.statusCode === 401) ||
error.graphQLErrors?.some(isJwtError)
) {
if (error.graphQLErrors[0].extensions.code !== JWTError.expired) {
removeTokens();
}
}
});
export const tokenLink = setContext((_, context) => {
const authToken = getTokens().auth;
return {
...context,
headers: {
...context.headers,
Authorization: authToken ? `JWT ${authToken}` : null
}
};
});
const link = invalidateTokenLink.concat(tokenLink);
export default link;

View file

@ -3,24 +3,22 @@ import { accountErrorFragment } from "@saleor/fragments/errors";
import gql from "graphql-tag"; import gql from "graphql-tag";
import { TypedMutation } from "../mutations"; import { TypedMutation } from "../mutations";
import { RefreshToken, RefreshTokenVariables } from "./types/RefreshToken";
import { import {
RequestPasswordReset, RequestPasswordReset,
RequestPasswordResetVariables RequestPasswordResetVariables
} from "./types/RequestPasswordReset"; } from "./types/RequestPasswordReset";
import { SetPassword, SetPasswordVariables } from "./types/SetPassword"; import { SetPassword, SetPasswordVariables } from "./types/SetPassword";
import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth";
import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken";
export const tokenAuthMutation = gql` export const tokenAuthMutation = gql`
${fragmentUser} ${fragmentUser}
mutation TokenAuth($email: String!, $password: String!) { mutation TokenAuth($email: String!, $password: String!) {
tokenCreate(email: $email, password: $password) { tokenCreate(email: $email, password: $password) {
token errors: accountErrors {
errors {
field field
message message
} }
csrfToken
token
user { user {
...User ...User
} }
@ -28,11 +26,6 @@ export const tokenAuthMutation = gql`
} }
`; `;
export const TypedTokenAuthMutation = TypedMutation<
TokenAuth,
TokenAuthVariables
>(tokenAuthMutation);
export const tokenVerifyMutation = gql` export const tokenVerifyMutation = gql`
${fragmentUser} ${fragmentUser}
mutation VerifyToken($token: String!) { mutation VerifyToken($token: String!) {
@ -45,10 +38,13 @@ export const tokenVerifyMutation = gql`
} }
`; `;
export const TypedVerifyTokenMutation = TypedMutation< export const tokenRefreshMutation = gql`
VerifyToken, mutation RefreshToken($token: String!) {
VerifyTokenVariables tokenRefresh(csrfToken: $token) {
>(tokenVerifyMutation); token
}
}
`;
export const requestPasswordReset = gql` export const requestPasswordReset = gql`
${accountErrorFragment} ${accountErrorFragment}
@ -73,6 +69,8 @@ export const setPassword = gql`
errors: accountErrors { errors: accountErrors {
...AccountErrorFragment ...AccountErrorFragment
} }
csrfToken
refreshToken
token token
user { user {
...User ...User
@ -84,15 +82,3 @@ export const SetPasswordMutation = TypedMutation<
SetPassword, SetPassword,
SetPasswordVariables SetPasswordVariables
>(setPassword); >(setPassword);
const refreshToken = gql`
mutation RefreshToken($token: String!) {
tokenRefresh(csrfToken: $token) {
token
}
}
`;
export const TokenRefreshMutation = TypedMutation<
RefreshToken,
RefreshTokenVariables
>(refreshToken);

View file

@ -38,6 +38,8 @@ export interface SetPassword_setPassword_user {
export interface SetPassword_setPassword { export interface SetPassword_setPassword {
__typename: "SetPassword"; __typename: "SetPassword";
errors: SetPassword_setPassword_errors[]; errors: SetPassword_setPassword_errors[];
csrfToken: string | null;
refreshToken: string | null;
token: string | null; token: string | null;
user: SetPassword_setPassword_user | null; user: SetPassword_setPassword_user | null;
} }

View file

@ -9,7 +9,7 @@ import { PermissionEnum } from "./../../types/globalTypes";
// ==================================================== // ====================================================
export interface TokenAuth_tokenCreate_errors { export interface TokenAuth_tokenCreate_errors {
__typename: "Error"; __typename: "AccountError";
field: string | null; field: string | null;
message: string | null; message: string | null;
} }
@ -37,8 +37,9 @@ export interface TokenAuth_tokenCreate_user {
export interface TokenAuth_tokenCreate { export interface TokenAuth_tokenCreate {
__typename: "CreateToken"; __typename: "CreateToken";
token: string | null;
errors: TokenAuth_tokenCreate_errors[]; errors: TokenAuth_tokenCreate_errors[];
csrfToken: string | null;
token: string | null;
user: TokenAuth_tokenCreate_user | null; user: TokenAuth_tokenCreate_user | null;
} }

View file

@ -1,21 +1,46 @@
import { IMessageContext } from "@saleor/components/messages";
import { UseNotifierResult } from "@saleor/hooks/useNotifier"; import { UseNotifierResult } from "@saleor/hooks/useNotifier";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { ApolloError } from "apollo-client";
import { IntlShape } from "react-intl"; import { IntlShape } from "react-intl";
const TOKEN_STORAGE_KEY = "dashboardAuth"; import { isJwtError, isTokenExpired } from "./errors";
export const getAuthToken = () => export enum TOKEN_STORAGE_KEY {
localStorage.getItem(TOKEN_STORAGE_KEY) || AUTH = "auth",
sessionStorage.getItem(TOKEN_STORAGE_KEY); CSRF = "csrf"
}
export const setAuthToken = (token: string, persist: boolean) => export const getTokens = () => ({
persist auth:
? localStorage.setItem(TOKEN_STORAGE_KEY, token) localStorage.getItem(TOKEN_STORAGE_KEY.AUTH) ||
: sessionStorage.setItem(TOKEN_STORAGE_KEY, token); sessionStorage.getItem(TOKEN_STORAGE_KEY.AUTH),
refresh:
localStorage.getItem(TOKEN_STORAGE_KEY.CSRF) ||
sessionStorage.getItem(TOKEN_STORAGE_KEY.CSRF)
});
export const removeAuthToken = () => { export const setTokens = (auth: string, csrf: string, persist: boolean) => {
localStorage.removeItem(TOKEN_STORAGE_KEY); if (persist) {
sessionStorage.removeItem(TOKEN_STORAGE_KEY); localStorage.setItem(TOKEN_STORAGE_KEY.AUTH, auth);
localStorage.setItem(TOKEN_STORAGE_KEY.CSRF, csrf);
} else {
sessionStorage.setItem(TOKEN_STORAGE_KEY.AUTH, auth);
sessionStorage.setItem(TOKEN_STORAGE_KEY.CSRF, csrf);
}
};
export const setAuthToken = (auth: string, persist: boolean) => {
if (persist) {
localStorage.setItem(TOKEN_STORAGE_KEY.AUTH, auth);
} else {
sessionStorage.setItem(TOKEN_STORAGE_KEY.AUTH, auth);
}
};
export const removeTokens = () => {
localStorage.removeItem(TOKEN_STORAGE_KEY.AUTH);
sessionStorage.removeItem(TOKEN_STORAGE_KEY.AUTH);
}; };
export const displayDemoMessage = ( export const displayDemoMessage = (
@ -26,3 +51,40 @@ export const displayDemoMessage = (
text: intl.formatMessage(commonMessages.demo) text: intl.formatMessage(commonMessages.demo)
}); });
}; };
export async function handleQueryAuthError(
error: ApolloError,
notify: IMessageContext,
tokenRefresh: () => Promise<boolean>,
logout: () => void,
intl: IntlShape
) {
if (error.graphQLErrors.some(isJwtError)) {
if (error.graphQLErrors.every(isTokenExpired)) {
const success = await tokenRefresh();
if (!success) {
logout();
notify({
status: "error",
text: intl.formatMessage(commonMessages.sessionExpired)
});
}
} else {
logout();
notify({
status: "error",
text: intl.formatMessage(commonMessages.somethingWentWrong)
});
}
} else if (
!error.graphQLErrors.every(
err => err.extensions?.exception?.code === "PermissionDenied"
)
) {
notify({
status: "error",
text: intl.formatMessage(commonMessages.somethingWentWrong)
});
}
}

View file

@ -19,7 +19,11 @@ const NewPassword: React.FC<RouteComponentProps> = ({ location }) => {
const handleSetPassword = async (data: SetPassword) => { const handleSetPassword = async (data: SetPassword) => {
if (data.setPassword.errors.length === 0) { if (data.setPassword.errors.length === 0) {
loginByToken(data.setPassword.token, data.setPassword.user); loginByToken(
data.setPassword.token,
data.setPassword.csrfToken,
data.setPassword.user
);
navigate("/", true); navigate("/", true);
} }
}; };

View file

@ -1,6 +1,5 @@
import { isJwtError } from "@saleor/auth/errors"; import { handleQueryAuthError } from "@saleor/auth";
import { commonMessages } from "@saleor/intl"; import { RequireAtLeastOne } from "@saleor/misc";
import { maybe, RequireAtLeastOne } from "@saleor/misc";
import { ApolloQueryResult } from "apollo-client"; import { ApolloQueryResult } from "apollo-client";
import { DocumentNode } from "graphql"; import { DocumentNode } from "graphql";
import { useEffect } from "react"; import { useEffect } from "react";
@ -48,6 +47,14 @@ function makeQuery<TData, TVariables>(
}, },
errorPolicy: "all", errorPolicy: "all",
fetchPolicy: "cache-and-network", fetchPolicy: "cache-and-network",
onError: error =>
handleQueryAuthError(
error,
notify,
user.tokenRefresh,
user.logout,
intl
),
skip, skip,
variables variables
}); });
@ -63,26 +70,6 @@ function makeQuery<TData, TVariables>(
} }
}, [queryData.loading]); }, [queryData.loading]);
if (queryData.error) {
if (queryData.error.graphQLErrors.some(isJwtError)) {
user.logout();
notify({
status: "error",
text: intl.formatMessage(commonMessages.sessionExpired)
});
} else if (
!queryData.error.graphQLErrors.every(
err =>
maybe(() => err.extensions.exception.code) === "PermissionDenied"
)
) {
notify({
status: "error",
text: intl.formatMessage(commonMessages.somethingWentWrong)
});
}
}
const loadMore = ( const loadMore = (
mergeFunc: (previousResults: TData, fetchMoreResult: TData) => TData, mergeFunc: (previousResults: TData, fetchMoreResult: TData) => TData,
extraVariables: RequireAtLeastOne<TVariables> extraVariables: RequireAtLeastOne<TVariables>

View file

@ -4,8 +4,6 @@ import { defaultDataIdFromObject, InMemoryCache } from "apollo-cache-inmemory";
import { ApolloClient } from "apollo-client"; import { ApolloClient } from "apollo-client";
import { ApolloLink } from "apollo-link"; import { ApolloLink } from "apollo-link";
import { BatchHttpLink } from "apollo-link-batch-http"; import { BatchHttpLink } from "apollo-link-batch-http";
import { setContext } from "apollo-link-context";
import { ErrorResponse, onError } from "apollo-link-error";
import { createUploadLink } from "apollo-upload-client"; import { createUploadLink } from "apollo-upload-client";
import React from "react"; import React from "react";
import { ApolloProvider } from "react-apollo"; import { ApolloProvider } from "react-apollo";
@ -19,11 +17,11 @@ import AppsSection from "./apps";
import { appsSection } from "./apps/urls"; import { appsSection } from "./apps/urls";
import AttributeSection from "./attributes"; import AttributeSection from "./attributes";
import { attributeSection } from "./attributes/urls"; import { attributeSection } from "./attributes/urls";
import Auth, { getAuthToken, removeAuthToken } from "./auth"; import Auth from "./auth";
import AuthProvider from "./auth/AuthProvider"; import AuthProvider, { useAuth } from "./auth/AuthProvider";
import LoginLoading from "./auth/components/LoginLoading/LoginLoading"; import LoginLoading from "./auth/components/LoginLoading/LoginLoading";
import SectionRoute from "./auth/components/SectionRoute"; import SectionRoute from "./auth/components/SectionRoute";
import { isJwtError } from "./auth/errors"; import authLink from "./auth/link";
import { hasPermission } from "./auth/misc"; import { hasPermission } from "./auth/misc";
import CategorySection from "./categories"; import CategorySection from "./categories";
import CollectionSection from "./collections"; import CollectionSection from "./collections";
@ -60,43 +58,15 @@ import { PermissionEnum } from "./types/globalTypes";
import WarehouseSection from "./warehouses"; import WarehouseSection from "./warehouses";
import { warehouseSection } from "./warehouses/urls"; import { warehouseSection } from "./warehouses/urls";
interface ResponseError extends ErrorResponse {
networkError?: Error & {
statusCode?: number;
bodyText?: string;
};
}
if (process.env.GTM_ID !== undefined) { if (process.env.GTM_ID !== undefined) {
TagManager.initialize({ gtmId: GTM_ID }); TagManager.initialize({ gtmId: GTM_ID });
} }
const invalidTokenLink = onError((error: ResponseError) => {
if (
(error.networkError && error.networkError.statusCode === 401) ||
error.graphQLErrors?.some(isJwtError)
) {
removeAuthToken();
}
});
const authLink = setContext((_, context) => {
const authToken = getAuthToken();
return {
...context,
headers: {
...context.headers,
Authorization: authToken ? `JWT ${authToken}` : null
}
};
});
// DON'T TOUCH THIS // DON'T TOUCH THIS
// These are separate clients and do not share configs between themselves // These are separate clients and do not share configs between themselves
// so we need to explicitly set them // so we need to explicitly set them
const linkOptions = { const linkOptions = {
credentials: "same-origin", credentials: "include",
uri: API_URI uri: API_URI
}; };
const uploadLink = createUploadLink(linkOptions); const uploadLink = createUploadLink(linkOptions);
@ -122,7 +92,7 @@ const apolloClient = new ApolloClient({
return defaultDataIdFromObject(obj); return defaultDataIdFromObject(obj);
} }
}), }),
link: invalidTokenLink.concat(authLink.concat(link)) link: authLink.concat(link)
}); });
const App: React.FC = () => { const App: React.FC = () => {
@ -138,7 +108,9 @@ const App: React.FC = () => {
<BackgroundTasksProvider> <BackgroundTasksProvider>
<AppStateProvider> <AppStateProvider>
<ShopProvider> <ShopProvider>
<AuthProvider>
<Routes /> <Routes />
</AuthProvider>
</ShopProvider> </ShopProvider>
</AppStateProvider> </AppStateProvider>
</BackgroundTasksProvider> </BackgroundTasksProvider>
@ -154,19 +126,18 @@ const App: React.FC = () => {
const Routes: React.FC = () => { const Routes: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const [, dispatchAppState] = useAppState(); const [, dispatchAppState] = useAppState();
const {
return (
<>
<WindowTitle title={intl.formatMessage(commonMessages.dashboard)} />
<AuthProvider>
{({
hasToken, hasToken,
isAuthenticated, isAuthenticated,
tokenAuthLoading, tokenAuthLoading,
tokenVerifyLoading, tokenVerifyLoading,
user user
}) => } = useAuth();
isAuthenticated && !tokenAuthLoading && !tokenVerifyLoading ? (
return (
<>
<WindowTitle title={intl.formatMessage(commonMessages.dashboard)} />
{isAuthenticated && !tokenAuthLoading && !tokenVerifyLoading ? (
<AppLayout> <AppLayout>
<Navigator /> <Navigator />
<ErrorBoundary <ErrorBoundary
@ -277,9 +248,7 @@ const Routes: React.FC = () => {
component={WarehouseSection} component={WarehouseSection}
/> />
{createConfigurationMenu(intl).filter(menu => {createConfigurationMenu(intl).filter(menu =>
menu.menuItems.map(item => menu.menuItems.map(item => hasPermission(item.permission, user))
hasPermission(item.permission, user)
)
).length > 0 && ( ).length > 0 && (
<SectionRoute <SectionRoute
exact exact
@ -294,10 +263,8 @@ const Routes: React.FC = () => {
) : hasToken && tokenVerifyLoading ? ( ) : hasToken && tokenVerifyLoading ? (
<LoginLoading /> <LoginLoading />
) : ( ) : (
<Auth hasToken={hasToken} /> <Auth />
) )}
}
</AuthProvider>
</> </>
); );
}; };

View file

@ -100,8 +100,8 @@ const getEventMessage = (event: OrderDetails_order_events, intl: IntlShape) => {
description: "order history message" description: "order history message"
}, },
{ {
invoiceNumber: event.invoiceNumber, generatedBy: event.user ? event.user.email : null,
generatedBy: event.user ? event.user.email : null invoiceNumber: event.invoiceNumber
} }
); );
case OrderEventsEnum.INVOICE_UPDATED: case OrderEventsEnum.INVOICE_UPDATED:

View file

@ -4,12 +4,11 @@ import React from "react";
import { Query, QueryResult } from "react-apollo"; import { Query, QueryResult } from "react-apollo";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { isJwtError } from "./auth/errors"; import { handleQueryAuthError } from "./auth";
import useAppState from "./hooks/useAppState"; import useAppState from "./hooks/useAppState";
import useNotifier from "./hooks/useNotifier"; import useNotifier from "./hooks/useNotifier";
import useUser from "./hooks/useUser"; import useUser from "./hooks/useUser";
import { commonMessages } from "./intl"; import { RequireAtLeastOne } from "./misc";
import { maybe, RequireAtLeastOne } from "./misc";
export interface LoadMore<TData, TVariables> { export interface LoadMore<TData, TVariables> {
loadMore: ( loadMore: (
@ -79,29 +78,17 @@ export function TypedQuery<TData, TVariables>(
skip={skip} skip={skip}
context={{ useBatching: true }} context={{ useBatching: true }}
errorPolicy="all" errorPolicy="all"
onError={error =>
handleQueryAuthError(
error,
notify,
user.tokenRefresh,
user.logout,
intl
)
}
> >
{(queryData: QueryResult<TData, TVariables>) => { {(queryData: QueryResult<TData, TVariables>) => {
if (queryData.error) {
if (queryData.error.graphQLErrors.some(isJwtError)) {
user.logout();
notify({
status: "error",
text: intl.formatMessage(commonMessages.sessionExpired)
});
} else if (
!queryData.error.graphQLErrors.every(
err =>
maybe(() => err.extensions.exception.code) ===
"PermissionDenied"
)
) {
notify({
status: "error",
text: intl.formatMessage(commonMessages.somethingWentWrong)
});
}
}
const loadMore = ( const loadMore = (
mergeFunc: ( mergeFunc: (
previousResults: TData, previousResults: TData,

View file

@ -3,14 +3,16 @@ import { User } from "@saleor/fragments/types/User";
export const isSupported = export const isSupported =
navigator.credentials && navigator.credentials.preventSilentAccess; navigator.credentials && navigator.credentials.preventSilentAccess;
export function login(loginFn: (id: string, password: string) => void) { export function login<T>(loginFn: (id: string, password: string) => T): T {
if (isSupported) { if (isSupported) {
navigator.credentials.get({ password: true }).then(credential => { navigator.credentials.get({ password: true }).then(credential => {
if (credential instanceof PasswordCredential) { if (credential instanceof PasswordCredential) {
loginFn(credential.id, credential.password); return loginFn(credential.id, credential.password);
} }
}); });
} }
return null;
} }
export function saveCredentials(user: User, password: string) { export function saveCredentials(user: User, password: string) {

51
testUtils/api.ts Normal file
View file

@ -0,0 +1,51 @@
import NodeHttpAdapter from "@pollyjs/adapter-node-http";
import { Polly } from "@pollyjs/core";
import FSPersister from "@pollyjs/persister-fs";
import { InMemoryCache } from "apollo-cache-inmemory";
import ApolloClient from "apollo-client";
import { BatchHttpLink } from "apollo-link-batch-http";
import fetch from "node-fetch";
import path from "path";
import { setupPolly } from "setup-polly-jest";
Polly.register(NodeHttpAdapter);
Polly.register(FSPersister);
function setupApi() {
setupPolly({
adapters: ["node-http"],
matchRequestsBy: {
headers: false,
url: {
hash: false,
hostname: false,
password: false,
pathname: false,
port: false,
protocol: false,
query: false,
username: false
}
},
persister: "fs",
persisterOptions: {
fs: {
recordingsDir: path.resolve(__dirname, "../recordings")
}
}
});
const cache = new InMemoryCache();
const link = new BatchHttpLink({
// @ts-ignore
fetch,
uri: process.env.API_URI || "http://localhost:8000/graphql/"
});
const apolloClient = new ApolloClient({
cache,
link
});
return apolloClient;
}
export default setupApi;