Use Auth SDK (#1474)
* Use Auth SDK * Update auth provider hook * Update sdk module mapping * Update setting password * Fix no user details on first login * Update auth tests * Cleanups * Update SDK Update SDK Update SDK Update test recordings Update SDK * Implement SDK External Auth Update new password view Hnalde external logout Update SDK Fix logout external redirect * Fix login page style * Update SDK * Auth Provider cleanups Update and refactor auth Auth types cleanups and refactor * Update channel context provider * Fix login error handling * Logout immidiatelly non-staff user * Update test snapshots * Trigger CI * Update to SDK v0.4, remove duplicated UserContext hook * Handle server errors during login * Fix wrong login page form submition handling * Update login error messages Co-authored-by: Jakub Majorek <majorek.jakub@gmail.com>
This commit is contained in:
parent
866bb91177
commit
4880093f63
59 changed files with 1005 additions and 1610 deletions
|
@ -1238,20 +1238,26 @@
|
|||
"context": "link",
|
||||
"string": "Use this link to recover it"
|
||||
},
|
||||
"src_dot_auth_dot_components_dot_LoginPage_dot_3476994590": {
|
||||
"string": "Sorry, your username and/or password are incorrect. Please try again."
|
||||
},
|
||||
"src_dot_auth_dot_components_dot_LoginPage_dot_3762459576": {
|
||||
"context": "description",
|
||||
"string": "or login using"
|
||||
},
|
||||
"src_dot_auth_dot_components_dot_LoginPage_dot_534894384": {
|
||||
"string": "Sorry, login went wrong. Please try again."
|
||||
},
|
||||
"src_dot_auth_dot_components_dot_LoginPage_dot_599516345": {
|
||||
"context": "description",
|
||||
"string": "Forgot password? {resetPasswordLink}"
|
||||
},
|
||||
"src_dot_auth_dot_components_dot_LoginPage_dot_externalLoginError": {
|
||||
"context": "error message",
|
||||
"string": "Sorry, login went wrong. Please try again."
|
||||
},
|
||||
"src_dot_auth_dot_components_dot_LoginPage_dot_loginError": {
|
||||
"context": "error message",
|
||||
"string": "Sorry, your username and/or password are incorrect. Please try again."
|
||||
},
|
||||
"src_dot_auth_dot_components_dot_LoginPage_dot_serverError": {
|
||||
"context": "error message",
|
||||
"string": "Saleor is unavailable, please check your network connection and try again."
|
||||
},
|
||||
"src_dot_auth_dot_components_dot_NewPasswordPage_dot_1254879564": {
|
||||
"string": "New Password"
|
||||
},
|
||||
|
|
120
package-lock.json
generated
120
package-lock.json
generated
|
@ -4,6 +4,92 @@
|
|||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": {
|
||||
"version": "3.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.4.15.tgz",
|
||||
"integrity": "sha512-CnlT9i7TgHagkKQNvti81A9KcbIMqgpUPGJJL6bg5spTsB2R/5J6E7qiPcMvXuuXwR2xe4FmE4Ey4HizStb8Hg==",
|
||||
"requires": {
|
||||
"@graphql-typed-document-node/core": "^3.0.0",
|
||||
"@wry/context": "^0.6.0",
|
||||
"@wry/equality": "^0.5.0",
|
||||
"@wry/trie": "^0.3.0",
|
||||
"graphql-tag": "^2.12.3",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"optimism": "^0.16.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"symbol-observable": "^4.0.0",
|
||||
"ts-invariant": "^0.9.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zen-observable-ts": "~1.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/zen-observable": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.3.tgz",
|
||||
"integrity": "sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw=="
|
||||
},
|
||||
"@wry/context": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@wry/context/-/context-0.6.1.tgz",
|
||||
"integrity": "sha512-LOmVnY1iTU2D8tv4Xf6MVMZZ+juIJ87Kt/plMijjN20NMAXGmH4u8bS1t0uT74cZ5gwpocYueV58YwyI8y+GKw==",
|
||||
"requires": {
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"@wry/equality": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.2.tgz",
|
||||
"integrity": "sha512-oVMxbUXL48EV/C0/M7gLVsoK6qRHPS85x8zECofEZOVvxGmIPLA9o5Z27cc2PoAyZz1S2VoM2A7FLAnpfGlneA==",
|
||||
"requires": {
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"graphql-tag": {
|
||||
"version": "2.12.5",
|
||||
"resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.5.tgz",
|
||||
"integrity": "sha512-5xNhP4063d16Pz3HBtKprutsPrmHZi5IdUGOWRxA2B6VF7BIRGOHZ5WQvDmJXZuPcBg7rYwaFxvQYjqkSdR3TQ==",
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"optimism": {
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/optimism/-/optimism-0.16.1.tgz",
|
||||
"integrity": "sha512-64i+Uw3otrndfq5kaoGNoY7pvOhSsjFEN4bdEFh80MWVk/dbgJfMv7VFDeCT8LxNAlEVhQmdVEbfE7X2nWNIIg==",
|
||||
"requires": {
|
||||
"@wry/context": "^0.6.0",
|
||||
"@wry/trie": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"symbol-observable": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
|
||||
"integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ=="
|
||||
},
|
||||
"ts-invariant": {
|
||||
"version": "0.9.3",
|
||||
"resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.9.3.tgz",
|
||||
"integrity": "sha512-HinBlTbFslQI0OHP07JLsSXPibSegec6r9ai5xxq/qHYCsIQbzpymLpDhAUsnXcSrDEcd0L62L8vsOEdzM0qlA==",
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"tslib": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||
},
|
||||
"zen-observable-ts": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.1.0.tgz",
|
||||
"integrity": "sha512-1h4zlLSqI2cRLPJUHJFL8bCWHhkpuXkF+dbGkRaWjgDIG26DmzyshUMrdV/rL3UnR+mhaX4fRq8LPouq0MYYIA==",
|
||||
"requires": {
|
||||
"@types/zen-observable": "0.8.3",
|
||||
"zen-observable": "0.8.15"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@apollo/federation": {
|
||||
"version": "0.20.7",
|
||||
"resolved": "https://registry.npmjs.org/@apollo/federation/-/federation-0.20.7.tgz",
|
||||
|
@ -3069,6 +3155,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"@graphql-typed-document-node/core": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.0.tgz",
|
||||
"integrity": "sha512-wYn6r8zVZyQJ6rQaALBEln5B1pzxb9shV5Ef97kTvn6yVGrqyXVnDqnU24MXnFubR+rZjBY9NWuxX3FB2sTsjg=="
|
||||
},
|
||||
"@hapi/address": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz",
|
||||
|
@ -5187,6 +5278,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"@saleor/sdk": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@saleor/sdk/-/sdk-0.4.0.tgz",
|
||||
"integrity": "sha512-1wJ1WC73uAX7OBcZ4qRDJBEFVKrc2yXbqlbkFTjdRcRT+0k/T9RUPHZNIgFP0mj4/v6y6fyd9lgkknnF5vJnxw==",
|
||||
"requires": {
|
||||
"cross-fetch": "^3.1.4",
|
||||
"jwt-decode": "^3.1.2"
|
||||
}
|
||||
},
|
||||
"@samverschueren/stream-to-observable": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz",
|
||||
|
@ -7414,6 +7514,21 @@
|
|||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@wry/trie": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.3.1.tgz",
|
||||
"integrity": "sha512-WwB53ikYudh9pIorgxrkHKrQZcCqNM/Q/bDzZBffEaGUKGuHrRb3zZUT9Sh2qw9yogC7SsdRmQ1ER0pqvd3bfw==",
|
||||
"requires": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@xtuc/ieee754": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
||||
|
@ -20067,6 +20182,11 @@
|
|||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"jwt-decode": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
|
||||
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
|
||||
},
|
||||
"keycode": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.0.tgz",
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
"npm": ">=6.11.0 <7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.4.15",
|
||||
"@editorjs/editorjs": "^2.19.3",
|
||||
"@editorjs/header": "^2.6.1",
|
||||
"@editorjs/image": "^2.6.0",
|
||||
|
@ -28,6 +29,7 @@
|
|||
"@material-ui/lab": "^4.0.0-alpha.58",
|
||||
"@material-ui/styles": "^4.11.4",
|
||||
"@saleor/macaw-ui": "^0.2.7",
|
||||
"@saleor/sdk": "^0.4.0",
|
||||
"@sentry/react": "^6.0.0",
|
||||
"@types/faker": "^5.1.6",
|
||||
"@uiw/react-color-hue": "0.0.34",
|
||||
|
@ -118,7 +120,6 @@
|
|||
"@types/fuzzaldrin": "^2.1.2",
|
||||
"@types/jest": "^24.0.24",
|
||||
"@types/lodash-es": "^4.17.3",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/pollyjs__adapter-node-http": "^2.0.1",
|
||||
"@types/pollyjs__persister-fs": "^2.0.1",
|
||||
"@types/react": "^16.9.16",
|
||||
|
@ -169,7 +170,6 @@
|
|||
"jest-localstorage-mock": "^2.4.3",
|
||||
"lint-staged": "^10.5.1",
|
||||
"mock-apollo-client": "^0.4.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"prettier": "^1.19.1",
|
||||
"react-intl-translations-manager": "^5.0.3",
|
||||
"react-test-renderer": "^16.12.0",
|
||||
|
@ -220,7 +220,7 @@
|
|||
"moduleNameMapper": {
|
||||
"@assets(.*)$": "<rootDir>/assets/$1",
|
||||
"@locale(.*)$": "<rootDir>/locale/$1",
|
||||
"@saleor(?!.*macaw)(.*)$": "<rootDir>/src/$1",
|
||||
"@saleor(?!.*macaw)(?!.*sdk)(.*)$": "<rootDir>/src/$1",
|
||||
"@test/(.*)$": "<rootDir>/testUtils/$1",
|
||||
"^lodash-es(.*)$": "lodash/$1",
|
||||
"^@material-ui/core$": "<rootDir>/node_modules/@material-ui/core",
|
||||
|
|
|
@ -1,120 +0,0 @@
|
|||
{
|
||||
"log": {
|
||||
"_recordingName": "User/will be logged if has valid token",
|
||||
"creator": {
|
||||
"comment": "persister:fs",
|
||||
"name": "Polly.JS",
|
||||
"version": "4.3.0"
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"_id": "a3088678db2635ada66ab049f76c9722",
|
||||
"_order": 0,
|
||||
"cache": {},
|
||||
"request": {
|
||||
"bodySize": 702,
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
"_fromType": "array",
|
||||
"name": "accept",
|
||||
"value": "*/*"
|
||||
},
|
||||
{
|
||||
"_fromType": "array",
|
||||
"name": "content-type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"_fromType": "array",
|
||||
"name": "content-length",
|
||||
"value": "702"
|
||||
},
|
||||
{
|
||||
"_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.eyJpYXQiOjE2MTA2MzI1NDQsImV4cCI6MTYxMDYzMjg0NCwidG9rZW4iOiJrc0VWTXZnZzZCZmkiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6ImFjY2VzcyIsInVzZXJfaWQiOiJWWE5sY2pveU5BPT0iLCJpc19zdGFmZiI6dHJ1ZX0.QDp4vlm1tKhk8iFnY2MnREvO-IubI5j8g_Wylb1XJqc\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n isStaff\\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": 1765,
|
||||
"content": {
|
||||
"mimeType": "application/json",
|
||||
"size": 1765,
|
||||
"text": "[{\"data\": {\"tokenVerify\": {\"payload\": {\"iat\": 1610632544, \"exp\": 1610632844, \"token\": \"ksEVMvgg6Bfi\", \"email\": \"admin@example.com\", \"type\": \"access\", \"user_id\": \"VXNlcjoyNA==\", \"is_staff\": true}, \"user\": {\"id\": \"VXNlcjoyNA==\", \"email\": \"admin@example.com\", \"firstName\": \"\", \"lastName\": \"\", \"isStaff\": true, \"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_PRODUCT_TYPES_AND_ATTRIBUTES\", \"name\": \"Manage product types and attributes.\", \"__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": "Thu, 14 Jan 2021 14:10:40 GMT"
|
||||
},
|
||||
{
|
||||
"name": "server",
|
||||
"value": "WSGIServer/0.2 CPython/3.8.7"
|
||||
},
|
||||
{
|
||||
"name": "content-type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"name": "content-length",
|
||||
"value": "1765"
|
||||
},
|
||||
{
|
||||
"name": "x-content-type-options",
|
||||
"value": "nosniff"
|
||||
},
|
||||
{
|
||||
"name": "referrer-policy",
|
||||
"value": "same-origin"
|
||||
}
|
||||
],
|
||||
"headersSize": 194,
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"redirectURL": "",
|
||||
"status": 200,
|
||||
"statusText": "OK"
|
||||
},
|
||||
"startedDateTime": "2021-01-14T14:10:40.434Z",
|
||||
"time": 155,
|
||||
"timings": {
|
||||
"blocked": -1,
|
||||
"connect": -1,
|
||||
"dns": -1,
|
||||
"receive": 0,
|
||||
"send": 0,
|
||||
"ssl": -1,
|
||||
"wait": 155
|
||||
}
|
||||
}
|
||||
],
|
||||
"pages": [],
|
||||
"version": "1.2"
|
||||
}
|
||||
}
|
|
@ -8,11 +8,11 @@
|
|||
},
|
||||
"entries": [
|
||||
{
|
||||
"_id": "4062f0ffdf1fab89a50cd1b667a889ef",
|
||||
"_id": "2c067f705383db903cdaf88cb881c922",
|
||||
"_order": 0,
|
||||
"cache": {},
|
||||
"request": {
|
||||
"bodySize": 583,
|
||||
"bodySize": 1154,
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
|
@ -28,7 +28,7 @@
|
|||
{
|
||||
"_fromType": "array",
|
||||
"name": "content-length",
|
||||
"value": "583"
|
||||
"value": "1154"
|
||||
},
|
||||
{
|
||||
"_fromType": "array",
|
||||
|
@ -50,40 +50,43 @@
|
|||
"value": "localhost:8000"
|
||||
}
|
||||
],
|
||||
"headersSize": 281,
|
||||
"headersSize": 255,
|
||||
"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 isStaff\\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 {\\n field\\n message\\n __typename\\n }\\n csrfToken\\n token\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]"
|
||||
"text": "{\"operationName\":\"login\",\"variables\":{\"email\":\"admin@example.com\",\"password\":\"admin\"},\"query\":\"fragment AccountErrorFragment on AccountError {\\n code\\n field\\n message\\n __typename\\n}\\n\\nfragment AddressFragment on Address {\\n id\\n firstName\\n lastName\\n companyName\\n streetAddress1\\n streetAddress2\\n city\\n cityArea\\n postalCode\\n country {\\n code\\n country\\n __typename\\n }\\n countryArea\\n phone\\n isDefaultBillingAddress\\n isDefaultShippingAddress\\n __typename\\n}\\n\\nfragment UserFragment on User {\\n id\\n email\\n firstName\\n lastName\\n isStaff\\n metadata {\\n key\\n value\\n __typename\\n }\\n defaultShippingAddress {\\n ...AddressFragment\\n __typename\\n }\\n defaultBillingAddress {\\n ...AddressFragment\\n __typename\\n }\\n addresses {\\n ...AddressFragment\\n __typename\\n }\\n __typename\\n}\\n\\nmutation login($email: String!, $password: String!) {\\n tokenCreate(email: $email, password: $password) {\\n csrfToken\\n token\\n errors {\\n ...AccountErrorFragment\\n __typename\\n }\\n user {\\n ...UserFragment\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}"
|
||||
},
|
||||
"queryString": [],
|
||||
"url": "http://localhost:8000/graphql/"
|
||||
},
|
||||
"response": {
|
||||
"bodySize": 1976,
|
||||
"bodySize": 679,
|
||||
"content": {
|
||||
"mimeType": "application/json",
|
||||
"size": 1976,
|
||||
"text": "[{\"data\": {\"tokenCreate\": {\"errors\": [], \"csrfToken\": \"UIzzJSFalS8pplfM1j5QNIUNiXb0VFH3kbe6kfTddYvLjJ9DhMasCtHJKXoGDfbw\", \"token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTA2MzI1NDQsImV4cCI6MTYxMDYzMjg0NCwidG9rZW4iOiJrc0VWTXZnZzZCZmkiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6ImFjY2VzcyIsInVzZXJfaWQiOiJWWE5sY2pveU5BPT0iLCJpc19zdGFmZiI6dHJ1ZX0.QDp4vlm1tKhk8iFnY2MnREvO-IubI5j8g_Wylb1XJqc\", \"user\": {\"id\": \"VXNlcjoyNA==\", \"email\": \"admin@example.com\", \"firstName\": \"\", \"lastName\": \"\", \"isStaff\": true, \"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_PRODUCT_TYPES_AND_ATTRIBUTES\", \"name\": \"Manage product types and attributes.\", \"__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\"}}}]"
|
||||
"size": 679,
|
||||
"text": "{\"data\": {\"tokenCreate\": {\"csrfToken\": \"yDcn3nGgfW2AXuCc62pvaLGtL0dKs7n513jhL30DqmDirUDXL4zwEJYQfji8L2yP\", \"token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MzM5NTEwNzMsIm93bmVyIjoic2FsZW9yIiwiZXhwIjoxNjMzOTUxMzczLCJ0b2tlbiI6ImdtRU95YlM5TDhmaiIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5jb20iLCJ0eXBlIjoiYWNjZXNzIiwidXNlcl9pZCI6IlZYTmxjam94IiwiaXNfc3RhZmYiOnRydWV9.hf105U1yvWzf-X_bKit4jh6vU_EnCC52XupS9GVlKaw\", \"errors\": [], \"user\": {\"id\": \"VXNlcjox\", \"email\": \"admin@example.com\", \"firstName\": \"\", \"lastName\": \"\", \"isStaff\": true, \"metadata\": [], \"defaultShippingAddress\": null, \"defaultBillingAddress\": null, \"addresses\": [], \"__typename\": \"User\"}, \"__typename\": \"CreateToken\"}}}"
|
||||
},
|
||||
"cookies": [
|
||||
{
|
||||
"expires": "2021-11-10T11:17:53.000Z",
|
||||
"httpOnly": true,
|
||||
"maxAge": 2592000,
|
||||
"name": "refreshToken",
|
||||
"path": "/",
|
||||
"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTA2MzI1NDQsImV4cCI6MTYxMzIyNDU0NCwidG9rZW4iOiJrc0VWTXZnZzZCZmkiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6InJlZnJlc2giLCJ1c2VyX2lkIjoiVlhObGNqb3lOQT09IiwiaXNfc3RhZmYiOnRydWUsImNzcmZUb2tlbiI6IlVJenpKU0ZhbFM4cHBsZk0xajVRTklVTmlYYjBWRkgza2JlNmtmVGRkWXZMako5RGhNYXNDdEhKS1hvR0RmYncifQ.Br0GWGPPcnysyUxukjBBfXNbwCAm2qlR5OYClwFF3ZQ"
|
||||
"sameSite": "Lax",
|
||||
"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MzM5NTEwNzMsIm93bmVyIjoic2FsZW9yIiwiZXhwIjoxNjM2NTQzMDczLCJ0b2tlbiI6ImdtRU95YlM5TDhmaiIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5jb20iLCJ0eXBlIjoicmVmcmVzaCIsInVzZXJfaWQiOiJWWE5sY2pveCIsImlzX3N0YWZmIjp0cnVlLCJjc3JmVG9rZW4iOiJ5RGNuM25HZ2ZXMkFYdUNjNjJwdmFMR3RMMGRLczduNTEzamhMMzBEcW1EaXJVRFhMNHp3RUpZUWZqaThMMnlQIn0.zRkkzPiC8DPlBUvUPVcOSrEF4AzgDKcK8gpyQXSpKM4"
|
||||
}
|
||||
],
|
||||
"headers": [
|
||||
{
|
||||
"name": "date",
|
||||
"value": "Thu, 14 Jan 2021 13:55:44 GMT"
|
||||
"value": "Mon, 11 Oct 2021 11:17:53 GMT"
|
||||
},
|
||||
{
|
||||
"name": "server",
|
||||
"value": "WSGIServer/0.2 CPython/3.8.7"
|
||||
"value": "WSGIServer/0.2 CPython/3.8.3"
|
||||
},
|
||||
{
|
||||
"name": "content-type",
|
||||
|
@ -91,7 +94,7 @@
|
|||
},
|
||||
{
|
||||
"name": "content-length",
|
||||
"value": "1976"
|
||||
"value": "679"
|
||||
},
|
||||
{
|
||||
"name": "x-content-type-options",
|
||||
|
@ -104,17 +107,17 @@
|
|||
{
|
||||
"_fromType": "array",
|
||||
"name": "set-cookie",
|
||||
"value": "refreshToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTA2MzI1NDQsImV4cCI6MTYxMzIyNDU0NCwidG9rZW4iOiJrc0VWTXZnZzZCZmkiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6InJlZnJlc2giLCJ1c2VyX2lkIjoiVlhObGNqb3lOQT09IiwiaXNfc3RhZmYiOnRydWUsImNzcmZUb2tlbiI6IlVJenpKU0ZhbFM4cHBsZk0xajVRTklVTmlYYjBWRkgza2JlNmtmVGRkWXZMako5RGhNYXNDdEhKS1hvR0RmYncifQ.Br0GWGPPcnysyUxukjBBfXNbwCAm2qlR5OYClwFF3ZQ; HttpOnly; Path=/"
|
||||
"value": "refreshToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MzM5NTEwNzMsIm93bmVyIjoic2FsZW9yIiwiZXhwIjoxNjM2NTQzMDczLCJ0b2tlbiI6ImdtRU95YlM5TDhmaiIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5jb20iLCJ0eXBlIjoicmVmcmVzaCIsInVzZXJfaWQiOiJWWE5sY2pveCIsImlzX3N0YWZmIjp0cnVlLCJjc3JmVG9rZW4iOiJ5RGNuM25HZ2ZXMkFYdUNjNjJwdmFMR3RMMGRLczduNTEzamhMMzBEcW1EaXJVRFhMNHp3RUpZUWZqaThMMnlQIn0.zRkkzPiC8DPlBUvUPVcOSrEF4AzgDKcK8gpyQXSpKM4; expires=Wed, 10 Nov 2021 11:17:53 GMT; HttpOnly; Max-Age=2592000; Path=/; SameSite=Lax"
|
||||
}
|
||||
],
|
||||
"headersSize": 618,
|
||||
"headersSize": 704,
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"redirectURL": "",
|
||||
"status": 200,
|
||||
"statusText": "OK"
|
||||
},
|
||||
"startedDateTime": "2021-04-22T11:07:20.652Z",
|
||||
"time": 758,
|
||||
"startedDateTime": "2021-10-11T11:17:52.646Z",
|
||||
"time": 521,
|
||||
"timings": {
|
||||
"blocked": -1,
|
||||
"connect": -1,
|
||||
|
@ -122,7 +125,7 @@
|
|||
"receive": 0,
|
||||
"send": 0,
|
||||
"ssl": -1,
|
||||
"wait": 758
|
||||
"wait": 521
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -1,128 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
|
@ -1,120 +0,0 @@
|
|||
{
|
||||
"log": {
|
||||
"_recordingName": "User/will not be logged if has invalid token",
|
||||
"creator": {
|
||||
"comment": "persister:fs",
|
||||
"name": "Polly.JS",
|
||||
"version": "4.3.0"
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"_id": "b1557b45bbbf7aed1a4a53f5141ca324",
|
||||
"_order": 0,
|
||||
"cache": {},
|
||||
"request": {
|
||||
"bodySize": 439,
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
"_fromType": "array",
|
||||
"name": "accept",
|
||||
"value": "*/*"
|
||||
},
|
||||
{
|
||||
"_fromType": "array",
|
||||
"name": "content-type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"_fromType": "array",
|
||||
"name": "content-length",
|
||||
"value": "439"
|
||||
},
|
||||
{
|
||||
"_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 isStaff\\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": "Thu, 14 Jan 2021 14:10:40 GMT"
|
||||
},
|
||||
{
|
||||
"name": "server",
|
||||
"value": "WSGIServer/0.2 CPython/3.8.7"
|
||||
},
|
||||
{
|
||||
"name": "content-type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"name": "content-length",
|
||||
"value": "89"
|
||||
},
|
||||
{
|
||||
"name": "x-content-type-options",
|
||||
"value": "nosniff"
|
||||
},
|
||||
{
|
||||
"name": "referrer-policy",
|
||||
"value": "same-origin"
|
||||
}
|
||||
],
|
||||
"headersSize": 192,
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"redirectURL": "",
|
||||
"status": 200,
|
||||
"statusText": "OK"
|
||||
},
|
||||
"startedDateTime": "2021-01-14T14:10:40.611Z",
|
||||
"time": 25,
|
||||
"timings": {
|
||||
"blocked": -1,
|
||||
"connect": -1,
|
||||
"dns": -1,
|
||||
"receive": 0,
|
||||
"send": 0,
|
||||
"ssl": -1,
|
||||
"wait": 25
|
||||
}
|
||||
}
|
||||
],
|
||||
"pages": [],
|
||||
"version": "1.2"
|
||||
}
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
{
|
||||
"log": {
|
||||
"_recordingName": "User/will not be logged if is non-staff",
|
||||
"creator": {
|
||||
"comment": "persister:fs",
|
||||
"name": "Polly.JS",
|
||||
"version": "5.0.0"
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"_id": "0b09ec35ecae5b17a2ccda062b1d6ef5",
|
||||
"_order": 0,
|
||||
"cache": {},
|
||||
"request": {
|
||||
"bodySize": 602,
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
"_fromType": "array",
|
||||
"name": "accept",
|
||||
"value": "*/*"
|
||||
},
|
||||
{
|
||||
"_fromType": "array",
|
||||
"name": "content-type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"_fromType": "array",
|
||||
"name": "content-length",
|
||||
"value": "602"
|
||||
},
|
||||
{
|
||||
"_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\":\"client@example.com\",\"password\":\"password\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n isStaff\\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": 616,
|
||||
"content": {
|
||||
"mimeType": "application/json",
|
||||
"size": 616,
|
||||
"text": "[{\"data\": {\"tokenCreate\": {\"errors\": [], \"csrfToken\": \"Gac5v8mZt6dW0HBXp5RNt8GAWciTbVzsycpqtUKV797npCXajke5h9VoF4l9MreP\", \"token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTA2OTg2MzMsImV4cCI6MTYxMDY5ODkzMywidG9rZW4iOiJ6MDc2QndLZkNEMmYiLCJlbWFpbCI6ImNsaWVudEBleGFtcGxlLmNvbSIsInR5cGUiOiJhY2Nlc3MiLCJ1c2VyX2lkIjoiVlhObGNqb3pNQT09IiwiaXNfc3RhZmYiOmZhbHNlfQ.RVYwqQSPEZoi2E_ImC30Ml37RJ2Fu6AnSmfDkAYMcqY\", \"user\": {\"id\": \"VXNlcjozMA==\", \"email\": \"client@example.com\", \"firstName\": \"\", \"lastName\": \"\", \"isStaff\": false, \"userPermissions\": [], \"avatar\": null, \"__typename\": \"User\"}, \"__typename\": \"CreateToken\"}}}]"
|
||||
},
|
||||
"cookies": [
|
||||
{
|
||||
"httpOnly": true,
|
||||
"name": "refreshToken",
|
||||
"path": "/",
|
||||
"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTA2OTg2MzMsImV4cCI6MTYxMzI5MDYzMywidG9rZW4iOiJ6MDc2QndLZkNEMmYiLCJlbWFpbCI6ImNsaWVudEBleGFtcGxlLmNvbSIsInR5cGUiOiJyZWZyZXNoIiwidXNlcl9pZCI6IlZYTmxjam96TUE9PSIsImlzX3N0YWZmIjpmYWxzZSwiY3NyZlRva2VuIjoiR2FjNXY4bVp0NmRXMEhCWHA1Uk50OEdBV2NpVGJWenN5Y3BxdFVLVjc5N25wQ1hhamtlNWg5Vm9GNGw5TXJlUCJ9.jUF_9vvtwT8EUbQ4GM7u0YVivk7TiSoSecHDZ0jJ2MI"
|
||||
}
|
||||
],
|
||||
"headers": [
|
||||
{
|
||||
"name": "date",
|
||||
"value": "Fri, 15 Jan 2021 08:17:13 GMT"
|
||||
},
|
||||
{
|
||||
"name": "server",
|
||||
"value": "WSGIServer/0.2 CPython/3.8.7"
|
||||
},
|
||||
{
|
||||
"name": "content-type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"name": "content-length",
|
||||
"value": "616"
|
||||
},
|
||||
{
|
||||
"name": "x-content-type-options",
|
||||
"value": "nosniff"
|
||||
},
|
||||
{
|
||||
"name": "referrer-policy",
|
||||
"value": "same-origin"
|
||||
},
|
||||
{
|
||||
"_fromType": "array",
|
||||
"name": "set-cookie",
|
||||
"value": "refreshToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTA2OTg2MzMsImV4cCI6MTYxMzI5MDYzMywidG9rZW4iOiJ6MDc2QndLZkNEMmYiLCJlbWFpbCI6ImNsaWVudEBleGFtcGxlLmNvbSIsInR5cGUiOiJyZWZyZXNoIiwidXNlcl9pZCI6IlZYTmxjam96TUE9PSIsImlzX3N0YWZmIjpmYWxzZSwiY3NyZlRva2VuIjoiR2FjNXY4bVp0NmRXMEhCWHA1Uk50OEdBV2NpVGJWenN5Y3BxdFVLVjc5N25wQ1hhamtlNWg5Vm9GNGw5TXJlUCJ9.jUF_9vvtwT8EUbQ4GM7u0YVivk7TiSoSecHDZ0jJ2MI; HttpOnly; Path=/"
|
||||
}
|
||||
],
|
||||
"headersSize": 619,
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"redirectURL": "",
|
||||
"status": 200,
|
||||
"statusText": "OK"
|
||||
},
|
||||
"startedDateTime": "2021-01-15T08:17:12.850Z",
|
||||
"time": 623,
|
||||
"timings": {
|
||||
"blocked": -1,
|
||||
"connect": -1,
|
||||
"dns": -1,
|
||||
"receive": 0,
|
||||
"send": 0,
|
||||
"ssl": -1,
|
||||
"wait": 623
|
||||
}
|
||||
}
|
||||
],
|
||||
"pages": [],
|
||||
"version": "1.2"
|
||||
}
|
||||
}
|
|
@ -8,11 +8,11 @@
|
|||
},
|
||||
"entries": [
|
||||
{
|
||||
"_id": "6fed086670e88883223acb33bab4ff31",
|
||||
"_id": "f36c6d9d965b62862363338cf17d6135",
|
||||
"_order": 0,
|
||||
"cache": {},
|
||||
"request": {
|
||||
"bodySize": 599,
|
||||
"bodySize": 324,
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
|
@ -28,7 +28,7 @@
|
|||
{
|
||||
"_fromType": "array",
|
||||
"name": "content-length",
|
||||
"value": "599"
|
||||
"value": "324"
|
||||
},
|
||||
{
|
||||
"_fromType": "array",
|
||||
|
@ -50,33 +50,33 @@
|
|||
"value": "localhost:8000"
|
||||
}
|
||||
],
|
||||
"headersSize": 281,
|
||||
"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 isStaff\\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 {\\n field\\n message\\n __typename\\n }\\n csrfToken\\n token\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]"
|
||||
"text": "[{\"operationName\":\"UserDetails\",\"variables\":{},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n isStaff\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nquery UserDetails {\\n me {\\n ...User\\n __typename\\n }\\n}\\n\"}]"
|
||||
},
|
||||
"queryString": [],
|
||||
"url": "http://localhost:8000/graphql/"
|
||||
},
|
||||
"response": {
|
||||
"bodySize": 214,
|
||||
"bodySize": 24,
|
||||
"content": {
|
||||
"mimeType": "application/json",
|
||||
"size": 214,
|
||||
"text": "[{\"data\": {\"tokenCreate\": {\"errors\": [{\"field\": \"email\", \"message\": \"Please, enter valid credentials\", \"__typename\": \"Error\"}], \"csrfToken\": null, \"token\": null, \"user\": null, \"__typename\": \"CreateToken\"}}}]"
|
||||
"size": 24,
|
||||
"text": "[{\"data\": {\"me\": null}}]"
|
||||
},
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
"name": "date",
|
||||
"value": "Thu, 14 Jan 2021 13:55:45 GMT"
|
||||
"value": "Thu, 07 Oct 2021 23:09:08 GMT"
|
||||
},
|
||||
{
|
||||
"name": "server",
|
||||
"value": "WSGIServer/0.2 CPython/3.8.7"
|
||||
"value": "WSGIServer/0.2 CPython/3.8.3"
|
||||
},
|
||||
{
|
||||
"name": "content-type",
|
||||
|
@ -84,7 +84,113 @@
|
|||
},
|
||||
{
|
||||
"name": "content-length",
|
||||
"value": "214"
|
||||
"value": "24"
|
||||
},
|
||||
{
|
||||
"name": "x-content-type-options",
|
||||
"value": "nosniff"
|
||||
},
|
||||
{
|
||||
"name": "referrer-policy",
|
||||
"value": "same-origin"
|
||||
}
|
||||
],
|
||||
"headersSize": 192,
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"redirectURL": "",
|
||||
"status": 200,
|
||||
"statusText": "OK"
|
||||
},
|
||||
"startedDateTime": "2021-10-07T23:09:08.310Z",
|
||||
"time": 105,
|
||||
"timings": {
|
||||
"blocked": -1,
|
||||
"connect": -1,
|
||||
"dns": -1,
|
||||
"receive": 0,
|
||||
"send": 0,
|
||||
"ssl": -1,
|
||||
"wait": 105
|
||||
}
|
||||
},
|
||||
{
|
||||
"_id": "156d01ee9c99f8b74ab79e7482eadf9f",
|
||||
"_order": 0,
|
||||
"cache": {},
|
||||
"request": {
|
||||
"bodySize": 1170,
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
"_fromType": "array",
|
||||
"name": "accept",
|
||||
"value": "*/*"
|
||||
},
|
||||
{
|
||||
"_fromType": "array",
|
||||
"name": "content-type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"_fromType": "array",
|
||||
"name": "content-length",
|
||||
"value": "1170"
|
||||
},
|
||||
{
|
||||
"_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": 255,
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"method": "POST",
|
||||
"postData": {
|
||||
"mimeType": "application/json",
|
||||
"params": [],
|
||||
"text": "{\"operationName\":\"login\",\"variables\":{\"email\":\"admin@example.com\",\"password\":\"NotAValidPassword123!\"},\"query\":\"fragment AccountErrorFragment on AccountError {\\n code\\n field\\n message\\n __typename\\n}\\n\\nfragment AddressFragment on Address {\\n id\\n firstName\\n lastName\\n companyName\\n streetAddress1\\n streetAddress2\\n city\\n cityArea\\n postalCode\\n country {\\n code\\n country\\n __typename\\n }\\n countryArea\\n phone\\n isDefaultBillingAddress\\n isDefaultShippingAddress\\n __typename\\n}\\n\\nfragment UserFragment on User {\\n id\\n email\\n firstName\\n lastName\\n isStaff\\n metadata {\\n key\\n value\\n __typename\\n }\\n defaultShippingAddress {\\n ...AddressFragment\\n __typename\\n }\\n defaultBillingAddress {\\n ...AddressFragment\\n __typename\\n }\\n addresses {\\n ...AddressFragment\\n __typename\\n }\\n __typename\\n}\\n\\nmutation login($email: String!, $password: String!) {\\n tokenCreate(email: $email, password: $password) {\\n csrfToken\\n token\\n errors {\\n ...AccountErrorFragment\\n __typename\\n }\\n user {\\n ...UserFragment\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}"
|
||||
},
|
||||
"queryString": [],
|
||||
"url": "http://localhost:8000/graphql/"
|
||||
},
|
||||
"response": {
|
||||
"bodySize": 243,
|
||||
"content": {
|
||||
"mimeType": "application/json",
|
||||
"size": 243,
|
||||
"text": "{\"data\": {\"tokenCreate\": {\"csrfToken\": null, \"token\": null, \"errors\": [{\"code\": \"INVALID_CREDENTIALS\", \"field\": \"email\", \"message\": \"Please, enter valid credentials\", \"__typename\": \"AccountError\"}], \"user\": null, \"__typename\": \"CreateToken\"}}}"
|
||||
},
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
"name": "date",
|
||||
"value": "Mon, 11 Oct 2021 11:17:53 GMT"
|
||||
},
|
||||
{
|
||||
"name": "server",
|
||||
"value": "WSGIServer/0.2 CPython/3.8.3"
|
||||
},
|
||||
{
|
||||
"name": "content-type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"name": "content-length",
|
||||
"value": "243"
|
||||
},
|
||||
{
|
||||
"name": "x-content-type-options",
|
||||
|
@ -101,8 +207,8 @@
|
|||
"status": 200,
|
||||
"statusText": "OK"
|
||||
},
|
||||
"startedDateTime": "2021-04-22T11:09:20.963Z",
|
||||
"time": 555,
|
||||
"startedDateTime": "2021-10-11T11:17:53.202Z",
|
||||
"time": 438,
|
||||
"timings": {
|
||||
"blocked": -1,
|
||||
"connect": -1,
|
||||
|
@ -110,7 +216,7 @@
|
|||
"receive": 0,
|
||||
"send": 0,
|
||||
"ssl": -1,
|
||||
"wait": 555
|
||||
"wait": 438
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -8,11 +8,11 @@
|
|||
},
|
||||
"entries": [
|
||||
{
|
||||
"_id": "8948f5cbe2259e56b6dd03f068fbfa4d",
|
||||
"_id": "07fe7bf33b7e219c3280229514fcf39d",
|
||||
"_order": 0,
|
||||
"cache": {},
|
||||
"request": {
|
||||
"bodySize": 587,
|
||||
"bodySize": 1158,
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
|
@ -28,7 +28,7 @@
|
|||
{
|
||||
"_fromType": "array",
|
||||
"name": "content-length",
|
||||
"value": "587"
|
||||
"value": "1158"
|
||||
},
|
||||
{
|
||||
"_fromType": "array",
|
||||
|
@ -50,40 +50,33 @@
|
|||
"value": "localhost:8000"
|
||||
}
|
||||
],
|
||||
"headersSize": 281,
|
||||
"headersSize": 255,
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"method": "POST",
|
||||
"postData": {
|
||||
"mimeType": "application/json",
|
||||
"params": [],
|
||||
"text": "[{\"operationName\":\"TokenAuth\",\"variables\":{\"email\":\"client@example.com\",\"password\":\"password\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n isStaff\\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 {\\n field\\n message\\n __typename\\n }\\n csrfToken\\n token\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]"
|
||||
"text": "{\"operationName\":\"login\",\"variables\":{\"email\":\"client@example.com\",\"password\":\"password\"},\"query\":\"fragment AccountErrorFragment on AccountError {\\n code\\n field\\n message\\n __typename\\n}\\n\\nfragment AddressFragment on Address {\\n id\\n firstName\\n lastName\\n companyName\\n streetAddress1\\n streetAddress2\\n city\\n cityArea\\n postalCode\\n country {\\n code\\n country\\n __typename\\n }\\n countryArea\\n phone\\n isDefaultBillingAddress\\n isDefaultShippingAddress\\n __typename\\n}\\n\\nfragment UserFragment on User {\\n id\\n email\\n firstName\\n lastName\\n isStaff\\n metadata {\\n key\\n value\\n __typename\\n }\\n defaultShippingAddress {\\n ...AddressFragment\\n __typename\\n }\\n defaultBillingAddress {\\n ...AddressFragment\\n __typename\\n }\\n addresses {\\n ...AddressFragment\\n __typename\\n }\\n __typename\\n}\\n\\nmutation login($email: String!, $password: String!) {\\n tokenCreate(email: $email, password: $password) {\\n csrfToken\\n token\\n errors {\\n ...AccountErrorFragment\\n __typename\\n }\\n user {\\n ...UserFragment\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}"
|
||||
},
|
||||
"queryString": [],
|
||||
"url": "http://localhost:8000/graphql/"
|
||||
},
|
||||
"response": {
|
||||
"bodySize": 616,
|
||||
"bodySize": 243,
|
||||
"content": {
|
||||
"mimeType": "application/json",
|
||||
"size": 616,
|
||||
"text": "[{\"data\": {\"tokenCreate\": {\"errors\": [], \"csrfToken\": \"ztCCq2djodzVWeaynv8ifQbIIB4nrM6HyInqcZ4xqoaIEfOOKUEhAqw9rVR8Cr8L\", \"token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTEwNjY1MTcsImV4cCI6MTYxMTA2NjgxNywidG9rZW4iOiJ6MDc2QndLZkNEMmYiLCJlbWFpbCI6ImNsaWVudEBleGFtcGxlLmNvbSIsInR5cGUiOiJhY2Nlc3MiLCJ1c2VyX2lkIjoiVlhObGNqb3pNQT09IiwiaXNfc3RhZmYiOmZhbHNlfQ.lXsNnIBxZCCL843TTjn84lkWpE05o88F5q811ApjdKA\", \"user\": {\"id\": \"VXNlcjozMA==\", \"email\": \"client@example.com\", \"firstName\": \"\", \"lastName\": \"\", \"isStaff\": false, \"userPermissions\": [], \"avatar\": null, \"__typename\": \"User\"}, \"__typename\": \"CreateToken\"}}}]"
|
||||
"size": 243,
|
||||
"text": "{\"data\": {\"tokenCreate\": {\"csrfToken\": null, \"token\": null, \"errors\": [{\"code\": \"INVALID_CREDENTIALS\", \"field\": \"email\", \"message\": \"Please, enter valid credentials\", \"__typename\": \"AccountError\"}], \"user\": null, \"__typename\": \"CreateToken\"}}}"
|
||||
},
|
||||
"cookies": [
|
||||
{
|
||||
"httpOnly": true,
|
||||
"name": "refreshToken",
|
||||
"path": "/",
|
||||
"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTEwNjY1MTcsImV4cCI6MTYxMzY1ODUxNywidG9rZW4iOiJ6MDc2QndLZkNEMmYiLCJlbWFpbCI6ImNsaWVudEBleGFtcGxlLmNvbSIsInR5cGUiOiJyZWZyZXNoIiwidXNlcl9pZCI6IlZYTmxjam96TUE9PSIsImlzX3N0YWZmIjpmYWxzZSwiY3NyZlRva2VuIjoienRDQ3EyZGpvZHpWV2VheW52OGlmUWJJSUI0bnJNNkh5SW5xY1o0eHFvYUlFZk9PS1VFaEFxdzlyVlI4Q3I4TCJ9.hDMEK3HFSLol7rLd9dfaSCLTWgvetNDAFsb039L9PXQ"
|
||||
}
|
||||
],
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
"name": "date",
|
||||
"value": "Tue, 19 Jan 2021 14:28:37 GMT"
|
||||
"value": "Mon, 11 Oct 2021 11:17:53 GMT"
|
||||
},
|
||||
{
|
||||
"name": "server",
|
||||
"value": "WSGIServer/0.2 CPython/3.9.1"
|
||||
"value": "WSGIServer/0.2 CPython/3.8.3"
|
||||
},
|
||||
{
|
||||
"name": "content-type",
|
||||
|
@ -91,7 +84,7 @@
|
|||
},
|
||||
{
|
||||
"name": "content-length",
|
||||
"value": "616"
|
||||
"value": "243"
|
||||
},
|
||||
{
|
||||
"name": "x-content-type-options",
|
||||
|
@ -100,21 +93,16 @@
|
|||
{
|
||||
"name": "referrer-policy",
|
||||
"value": "same-origin"
|
||||
},
|
||||
{
|
||||
"_fromType": "array",
|
||||
"name": "set-cookie",
|
||||
"value": "refreshToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTEwNjY1MTcsImV4cCI6MTYxMzY1ODUxNywidG9rZW4iOiJ6MDc2QndLZkNEMmYiLCJlbWFpbCI6ImNsaWVudEBleGFtcGxlLmNvbSIsInR5cGUiOiJyZWZyZXNoIiwidXNlcl9pZCI6IlZYTmxjam96TUE9PSIsImlzX3N0YWZmIjpmYWxzZSwiY3NyZlRva2VuIjoienRDQ3EyZGpvZHpWV2VheW52OGlmUWJJSUI0bnJNNkh5SW5xY1o0eHFvYUlFZk9PS1VFaEFxdzlyVlI4Q3I4TCJ9.hDMEK3HFSLol7rLd9dfaSCLTWgvetNDAFsb039L9PXQ; HttpOnly; Path=/"
|
||||
}
|
||||
],
|
||||
"headersSize": 619,
|
||||
"headersSize": 193,
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"redirectURL": "",
|
||||
"status": 200,
|
||||
"statusText": "OK"
|
||||
},
|
||||
"startedDateTime": "2021-04-22T11:09:21.540Z",
|
||||
"time": 382,
|
||||
"startedDateTime": "2021-10-11T11:17:53.664Z",
|
||||
"time": 61,
|
||||
"timings": {
|
||||
"blocked": -1,
|
||||
"connect": -1,
|
||||
|
@ -122,7 +110,7 @@
|
|||
"receive": 0,
|
||||
"send": 0,
|
||||
"ssl": -1,
|
||||
"wait": 382
|
||||
"wait": 61
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -1,99 +0,0 @@
|
|||
import setupApi from "@test/api";
|
||||
import { act, renderHook } from "@testing-library/react-hooks";
|
||||
import ApolloClient from "apollo-client";
|
||||
|
||||
import { useAuthProvider } from "./hooks/useAuthProvider";
|
||||
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({ apolloClient, intl: intl as any, notify })
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const adminCredentials = {
|
||||
email: "admin@example.com",
|
||||
password: "admin",
|
||||
token: null
|
||||
};
|
||||
|
||||
const nonStaffUserCredentials = {
|
||||
email: "client@example.com",
|
||||
password: "password"
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
||||
describe("User", () => {
|
||||
it("will be logged in if has valid credentials", async done => {
|
||||
const hook = renderAuthProvider(apolloClient);
|
||||
|
||||
await act(async () => {
|
||||
await hook.current.login(
|
||||
adminCredentials.email,
|
||||
adminCredentials.password
|
||||
);
|
||||
});
|
||||
expect(hook.current.user.email).toBe(adminCredentials.email);
|
||||
adminCredentials.token = getTokens().auth;
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it("will not be logged in if doesn't have valid credentials", async done => {
|
||||
const hook = renderAuthProvider(apolloClient);
|
||||
|
||||
await act(async () => {
|
||||
await hook.current.login(adminCredentials.email, "NotAValidPassword123!");
|
||||
});
|
||||
expect(hook.current.user).toBe(null);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it("will not be logged in if is non-staff", async done => {
|
||||
const hook = renderAuthProvider(apolloClient);
|
||||
|
||||
await act(async () => {
|
||||
await hook.current.login(
|
||||
nonStaffUserCredentials.email,
|
||||
nonStaffUserCredentials.password
|
||||
);
|
||||
});
|
||||
expect(hook.current.user).toBe(undefined);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it("will be logged if has valid token", async done => {
|
||||
setAuthToken(adminCredentials.token, false);
|
||||
const hook = renderAuthProvider(apolloClient);
|
||||
|
||||
await act(() => hook.current.autologinPromise.current);
|
||||
expect(hook.current.user.email).toBe(adminCredentials.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.user).toBe(undefined);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
92
src/auth/AuthProvider.test.tsx
Normal file
92
src/auth/AuthProvider.test.tsx
Normal file
|
@ -0,0 +1,92 @@
|
|||
import { createSaleorClient, SaleorProvider } from "@saleor/sdk";
|
||||
import setupApi from "@test/api";
|
||||
import { act, renderHook } from "@testing-library/react-hooks";
|
||||
import React from "react";
|
||||
|
||||
import { useAuthProvider } from "./hooks/useAuthProvider";
|
||||
|
||||
const apolloClient = setupApi();
|
||||
|
||||
function renderAuthProvider() {
|
||||
const intl = {
|
||||
formatMessage: ({ defaultMessage }) => defaultMessage
|
||||
};
|
||||
const notify = jest.fn();
|
||||
const saleorClient = createSaleorClient({
|
||||
apiUrl: process.env.API_URI || "http://localhost:8000/graphql/",
|
||||
channel: ""
|
||||
});
|
||||
const wrapper = ({ children }) => (
|
||||
<SaleorProvider client={saleorClient}>{children}</SaleorProvider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useAuthProvider({ intl: intl as any, notify, apolloClient }),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const adminCredentials = {
|
||||
email: "admin@example.com",
|
||||
password: "admin",
|
||||
token: null
|
||||
};
|
||||
|
||||
const nonStaffUserCredentials = {
|
||||
email: "client@example.com",
|
||||
password: "password"
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
||||
describe("User", () => {
|
||||
it("will be logged in if has valid credentials", async done => {
|
||||
const hook = renderAuthProvider();
|
||||
|
||||
await act(async () => {
|
||||
const result = await hook.current.login(
|
||||
adminCredentials.email,
|
||||
adminCredentials.password
|
||||
);
|
||||
expect(result.user?.email).toBe(adminCredentials.email);
|
||||
});
|
||||
expect(hook.current.authenticated).toBe(true);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it("will not be logged in if doesn't have valid credentials", async done => {
|
||||
const hook = renderAuthProvider();
|
||||
|
||||
await act(async () => {
|
||||
const result = await hook.current.login(
|
||||
adminCredentials.email,
|
||||
"NotAValidPassword123!"
|
||||
);
|
||||
expect(result.user).toBe(null);
|
||||
});
|
||||
expect(hook.current.authenticated).toBe(false);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it("will not be logged in if is non-staff", async done => {
|
||||
const hook = renderAuthProvider();
|
||||
|
||||
await act(async () => {
|
||||
const result = await hook.current.login(
|
||||
nonStaffUserCredentials.email,
|
||||
nonStaffUserCredentials.password
|
||||
);
|
||||
expect(result.user).toBe(null);
|
||||
});
|
||||
expect(hook.current.authenticated).toBe(false);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
|
@ -1,11 +1,10 @@
|
|||
import useNotifier from "@saleor/hooks/useNotifier";
|
||||
import React, { useContext } from "react";
|
||||
import React from "react";
|
||||
import { useApolloClient } from "react-apollo";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { UserContext } from "./";
|
||||
import { useAuthProvider } from "./hooks/useAuthProvider";
|
||||
import { getTokens } from "./utils";
|
||||
|
||||
interface AuthProviderProps {
|
||||
children: React.ReactNode;
|
||||
|
@ -16,24 +15,11 @@ const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
|||
const intl = useIntl();
|
||||
const notify = useNotifier();
|
||||
|
||||
const authProvider = useAuthProvider({ apolloClient, intl, notify });
|
||||
const authProvider = useAuthProvider({ intl, notify, apolloClient });
|
||||
|
||||
return (
|
||||
<UserContext.Provider value={authProvider}>{children}</UserContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const user = useContext(UserContext);
|
||||
const isAuthenticated = !!user.user;
|
||||
|
||||
return {
|
||||
hasToken: !!getTokens().auth,
|
||||
isAuthenticated,
|
||||
tokenAuthLoading: user.tokenAuthLoading,
|
||||
tokenVerifyLoading: user.tokenVerifyLoading,
|
||||
user: user.user
|
||||
};
|
||||
};
|
||||
|
||||
export default AuthProvider;
|
||||
|
|
|
@ -7,7 +7,6 @@ import LoginPage, { LoginCardProps } from "../../../auth/components/LoginPage";
|
|||
|
||||
const props: Omit<LoginCardProps, "classes"> = {
|
||||
disabled: false,
|
||||
error: false,
|
||||
externalAuthentications: [
|
||||
{
|
||||
__typename: "ExternalAuthentication",
|
||||
|
@ -15,7 +14,6 @@ const props: Omit<LoginCardProps, "classes"> = {
|
|||
name: "Example auth plugin"
|
||||
}
|
||||
],
|
||||
externalError: false,
|
||||
loading: false,
|
||||
onExternalAuthentication: () => undefined,
|
||||
onPasswordRecovery: undefined,
|
||||
|
@ -26,6 +24,9 @@ storiesOf("Views / Authentication / Log in", module)
|
|||
.addDecorator(CardDecorator)
|
||||
.addDecorator(Decorator)
|
||||
.add("default", () => <LoginPage {...props} />)
|
||||
.add("error", () => <LoginPage {...props} error={true} />)
|
||||
.add("error login", () => <LoginPage {...props} error={"loginError"} />)
|
||||
.add("error external login", () => (
|
||||
<LoginPage {...props} error={"externalLoginError"} />
|
||||
))
|
||||
.add("disabled", () => <LoginPage {...props} disabled={true} />)
|
||||
.add("loading", () => <LoginPage {...props} loading={true} />);
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
TextField,
|
||||
Typography
|
||||
} from "@material-ui/core";
|
||||
import { UserContextError } from "@saleor/auth/types";
|
||||
import { AvailableExternalAuthentications_shop_availableExternalAuthentications } from "@saleor/auth/types/AvailableExternalAuthentications";
|
||||
import { FormSpacer } from "@saleor/components/FormSpacer";
|
||||
import { SubmitPromise } from "@saleor/hooks/useForm";
|
||||
|
@ -14,6 +15,7 @@ import React from "react";
|
|||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import LoginForm, { LoginFormData } from "./form";
|
||||
import { getErrorMessage } from "./messages";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
|
@ -49,8 +51,7 @@ const useStyles = makeStyles(
|
|||
);
|
||||
|
||||
export interface LoginCardProps {
|
||||
error: boolean;
|
||||
externalError: boolean;
|
||||
error?: UserContextError;
|
||||
disabled: boolean;
|
||||
loading: boolean;
|
||||
externalAuthentications?: AvailableExternalAuthentications_shop_availableExternalAuthentications[];
|
||||
|
@ -62,7 +63,6 @@ export interface LoginCardProps {
|
|||
const LoginCard: React.FC<LoginCardProps> = props => {
|
||||
const {
|
||||
error,
|
||||
externalError,
|
||||
disabled,
|
||||
loading,
|
||||
externalAuthentications = [],
|
||||
|
@ -84,19 +84,12 @@ const LoginCard: React.FC<LoginCardProps> = props => {
|
|||
|
||||
return (
|
||||
<LoginForm onSubmit={onSubmit}>
|
||||
{({ change: handleChange, data, submit: handleSubmit }) => (
|
||||
{({ change: handleChange, data }) => (
|
||||
<>
|
||||
{error && (
|
||||
<div className={classes.panel} data-test="loginErrorMessage">
|
||||
<Typography variant="caption">
|
||||
<FormattedMessage defaultMessage="Sorry, your username and/or password are incorrect. Please try again." />
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
{externalError && (
|
||||
<div className={classes.panel} data-test="loginErrorMessage">
|
||||
<Typography variant="caption">
|
||||
<FormattedMessage defaultMessage="Sorry, login went wrong. Please try again." />
|
||||
{getErrorMessage(error, intl)}
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
|
@ -136,7 +129,6 @@ const LoginCard: React.FC<LoginCardProps> = props => {
|
|||
color="primary"
|
||||
disabled={disabled}
|
||||
variant="contained"
|
||||
onClick={handleSubmit}
|
||||
type="submit"
|
||||
data-test="submit"
|
||||
>
|
||||
|
@ -184,7 +176,6 @@ const LoginCard: React.FC<LoginCardProps> = props => {
|
|||
color="primary"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
size="large"
|
||||
onClick={() =>
|
||||
onExternalAuthentication(externalAuthentication.id)
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ export interface UseLoginFormResult {
|
|||
change: FormChange;
|
||||
data: LoginFormData;
|
||||
hasChanged: boolean;
|
||||
submit: () => Promise<boolean>;
|
||||
submit: (event: React.FormEvent<HTMLFormElement>) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface LoginFormProps {
|
||||
|
@ -53,7 +53,11 @@ function useLoginForm(
|
|||
return errors;
|
||||
};
|
||||
|
||||
const submit = async () => handleFormSubmit(data, handleSubmit, setChanged);
|
||||
const submit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
return handleFormSubmit(data, handleSubmit, setChanged);
|
||||
};
|
||||
|
||||
return {
|
||||
change: handleChange,
|
||||
|
|
33
src/auth/components/LoginPage/messages.ts
Normal file
33
src/auth/components/LoginPage/messages.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { UserContextError } from "@saleor/auth/types";
|
||||
import { defineMessages, IntlShape } from "react-intl";
|
||||
|
||||
export const errorMessages = defineMessages({
|
||||
loginError: {
|
||||
defaultMessage:
|
||||
"Sorry, your username and/or password are incorrect. Please try again.",
|
||||
description: "error message"
|
||||
},
|
||||
externalLoginError: {
|
||||
defaultMessage: "Sorry, login went wrong. Please try again.",
|
||||
description: "error message"
|
||||
},
|
||||
serverError: {
|
||||
defaultMessage:
|
||||
"Saleor is unavailable, please check your network connection and try again.",
|
||||
description: "error message"
|
||||
}
|
||||
});
|
||||
|
||||
export function getErrorMessage(
|
||||
err: UserContextError,
|
||||
intl: IntlShape
|
||||
): string {
|
||||
switch (err) {
|
||||
case "loginError":
|
||||
return intl.formatMessage(errorMessages.loginError);
|
||||
case "externalLoginError":
|
||||
return intl.formatMessage(errorMessages.externalLoginError);
|
||||
case "serverError":
|
||||
return intl.formatMessage(errorMessages.serverError);
|
||||
}
|
||||
}
|
|
@ -21,7 +21,8 @@ storiesOf("Views / Authentication / Set up a new password", module)
|
|||
__typename: "AccountError",
|
||||
code: AccountErrorCode.PASSWORD_TOO_SHORT,
|
||||
field,
|
||||
addressType: null
|
||||
addressType: null,
|
||||
message: null
|
||||
}))}
|
||||
disabled={false}
|
||||
onSubmit={() => undefined}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Button, TextField, Typography } from "@material-ui/core";
|
||||
import { SetPassword_setPassword_errors } from "@saleor/auth/types/SetPassword";
|
||||
import Form from "@saleor/components/Form";
|
||||
import FormSpacer from "@saleor/components/FormSpacer";
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
import { SetPasswordData } from "@saleor/sdk";
|
||||
import getAccountErrorMessage from "@saleor/utils/errors/account";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
@ -33,7 +33,7 @@ export interface NewPasswordPageFormData {
|
|||
}
|
||||
export interface NewPasswordPageProps {
|
||||
disabled: boolean;
|
||||
errors: SetPassword_setPassword_errors[];
|
||||
errors: SetPasswordData["errors"];
|
||||
onSubmit: (data: NewPasswordPageFormData) => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import useUser from "@saleor/hooks/useUser";
|
||||
import React from "react";
|
||||
import { Route, RouteProps } from "react-router-dom";
|
||||
|
||||
import { useUser } from "..";
|
||||
import NotFound from "../../NotFound";
|
||||
import { PermissionEnum } from "../../types/globalTypes";
|
||||
import { hasAllPermissions, hasAnyPermissions } from "../misc";
|
||||
|
|
|
@ -1,58 +1,181 @@
|
|||
import { IMessageContext } from "@saleor/components/messages";
|
||||
import { User } from "@saleor/fragments/types/User";
|
||||
import useLocalStorage from "@saleor/hooks/useLocalStorage";
|
||||
import { APP_DEFAULT_URI, APP_MOUNT_URI, DEMO_MODE } from "@saleor/config";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import {
|
||||
GetExternalAccessTokenData,
|
||||
LoginData,
|
||||
useAuth,
|
||||
useAuthState
|
||||
} from "@saleor/sdk";
|
||||
import {
|
||||
isSupported as isCredentialsManagementAPISupported,
|
||||
login as loginWithCredentialsManagementAPI,
|
||||
saveCredentials
|
||||
} from "@saleor/utils/credentialsManagement";
|
||||
import ApolloClient from "apollo-client";
|
||||
import { MutableRefObject } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "react-apollo";
|
||||
import { IntlShape } from "react-intl";
|
||||
import urlJoin from "url-join";
|
||||
|
||||
import { useExternalAuthProvider } from "./useExternalAuthProvider";
|
||||
import { useSaleorAuthProvider } from "./useSaleorAuthProvider";
|
||||
import { userDetailsQuery } from "../queries";
|
||||
import {
|
||||
ExternalLoginInput,
|
||||
RequestExternalLoginInput,
|
||||
RequestExternalLogoutInput,
|
||||
UserContext,
|
||||
UserContextError
|
||||
} from "../types";
|
||||
import { UserDetails } from "../types/UserDetails";
|
||||
import { displayDemoMessage } from "../utils";
|
||||
|
||||
export interface UseAuthProvider {
|
||||
logout: () => void;
|
||||
tokenAuthLoading: boolean;
|
||||
tokenRefresh: () => Promise<boolean>;
|
||||
tokenVerifyLoading: boolean;
|
||||
user?: User;
|
||||
autologinPromise?: MutableRefObject<Promise<any>>;
|
||||
}
|
||||
export interface UseAuthProviderOpts {
|
||||
intl: IntlShape;
|
||||
notify: IMessageContext;
|
||||
apolloClient: ApolloClient<any>;
|
||||
}
|
||||
|
||||
export function useAuthProvider(opts: UseAuthProviderOpts) {
|
||||
const [authPlugin, setAuthPlugin] = useLocalStorage("authPlugin", undefined);
|
||||
export function useAuthProvider({
|
||||
intl,
|
||||
notify,
|
||||
apolloClient
|
||||
}: UseAuthProviderOpts): UserContext {
|
||||
const {
|
||||
login,
|
||||
getExternalAuthUrl,
|
||||
getExternalAccessToken,
|
||||
logout
|
||||
} = useAuth();
|
||||
const { authenticated, authenticating, user } = useAuthState();
|
||||
const [error, setError] = useState<UserContextError>();
|
||||
|
||||
const saleorAuth = useSaleorAuthProvider({
|
||||
authPlugin,
|
||||
setAuthPlugin,
|
||||
...opts
|
||||
useEffect(() => {
|
||||
if (authenticating && error) {
|
||||
setError(undefined);
|
||||
}
|
||||
}, [authenticating]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authenticated && !authenticating) {
|
||||
loginWithCredentialsManagementAPI(handleLogin);
|
||||
}
|
||||
}, [authenticated, authenticating]);
|
||||
|
||||
const userDetails = useQuery<UserDetails>(userDetailsQuery, {
|
||||
client: apolloClient,
|
||||
skip: !authenticated
|
||||
});
|
||||
|
||||
const externalAuth = useExternalAuthProvider({
|
||||
authPlugin,
|
||||
setAuthPlugin,
|
||||
...opts
|
||||
});
|
||||
const handleLogout = async () => {
|
||||
const result = await logout({
|
||||
input: JSON.stringify({
|
||||
returnTo: urlJoin(
|
||||
window.location.origin,
|
||||
APP_MOUNT_URI === APP_DEFAULT_URI ? "" : APP_MOUNT_URI
|
||||
)
|
||||
} as RequestExternalLogoutInput)
|
||||
});
|
||||
|
||||
const loginAuth = {
|
||||
login: saleorAuth.login,
|
||||
loginByExternalPlugin: externalAuth.loginByExternalPlugin,
|
||||
loginByToken: saleorAuth.loginByToken,
|
||||
requestLoginByExternalPlugin: externalAuth.requestLoginByExternalPlugin
|
||||
if (isCredentialsManagementAPISupported) {
|
||||
navigator.credentials.preventSilentAccess();
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const errors = result.errors || [];
|
||||
const logoutUrl = JSON.parse(
|
||||
result.data?.externalLogout?.logoutData || null
|
||||
)?.logoutUrl;
|
||||
if (!errors.length && logoutUrl) {
|
||||
window.location.href = logoutUrl;
|
||||
}
|
||||
};
|
||||
|
||||
if (authPlugin) {
|
||||
return {
|
||||
...externalAuth,
|
||||
...loginAuth
|
||||
};
|
||||
}
|
||||
const handleLogin = async (email: string, password: string) => {
|
||||
try {
|
||||
const result = await login({
|
||||
email,
|
||||
password
|
||||
});
|
||||
|
||||
if (result && !result.data.tokenCreate.errors.length) {
|
||||
if (DEMO_MODE) {
|
||||
displayDemoMessage(intl, notify);
|
||||
}
|
||||
saveCredentials(result.data.tokenCreate.user, password);
|
||||
} else {
|
||||
setError("loginError");
|
||||
}
|
||||
|
||||
await logoutNonStaffUser(result.data.tokenCreate);
|
||||
|
||||
return result.data.tokenCreate;
|
||||
} catch (error) {
|
||||
setError("serverError");
|
||||
}
|
||||
};
|
||||
|
||||
const handleRequestExternalLogin = async (
|
||||
pluginId: string,
|
||||
input: RequestExternalLoginInput
|
||||
) => {
|
||||
const result = await getExternalAuthUrl({
|
||||
pluginId,
|
||||
input: JSON.stringify(input)
|
||||
});
|
||||
|
||||
return result?.data?.externalAuthenticationUrl;
|
||||
};
|
||||
|
||||
const handleExternalLogin = async (
|
||||
pluginId: string,
|
||||
input: ExternalLoginInput
|
||||
) => {
|
||||
try {
|
||||
const result = await getExternalAccessToken({
|
||||
pluginId,
|
||||
input: JSON.stringify(input)
|
||||
});
|
||||
|
||||
if (result && !result.data?.externalObtainAccessTokens.errors.length) {
|
||||
if (DEMO_MODE) {
|
||||
displayDemoMessage(intl, notify);
|
||||
}
|
||||
} else {
|
||||
setError("externalLoginError");
|
||||
}
|
||||
|
||||
await logoutNonStaffUser(result.data.externalObtainAccessTokens);
|
||||
|
||||
return result?.data?.externalObtainAccessTokens;
|
||||
} catch (error) {
|
||||
setError("serverError");
|
||||
}
|
||||
};
|
||||
|
||||
const logoutNonStaffUser = async (
|
||||
data: LoginData | GetExternalAccessTokenData
|
||||
) => {
|
||||
if (data.user && !data.user.isStaff) {
|
||||
notify({
|
||||
status: "error",
|
||||
text: intl.formatMessage(commonMessages.unauthorizedDashboardAccess),
|
||||
title: intl.formatMessage(commonMessages.insufficientPermissions)
|
||||
});
|
||||
await handleLogout();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...saleorAuth,
|
||||
...loginAuth
|
||||
login: handleLogin,
|
||||
requestLoginByExternalPlugin: handleRequestExternalLogin,
|
||||
loginByExternalPlugin: handleExternalLogin,
|
||||
logout: handleLogout,
|
||||
authenticating: authenticating && !error,
|
||||
authenticated: authenticated && user?.isStaff,
|
||||
user: userDetails.data?.me,
|
||||
error
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,261 +0,0 @@
|
|||
import { DEMO_MODE } from "@saleor/config";
|
||||
import { User } from "@saleor/fragments/types/User";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { getFullName, getMutationStatus } from "@saleor/misc";
|
||||
import errorTracker from "@saleor/services/errorTracking";
|
||||
import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
|
||||
import { useMutation } from "react-apollo";
|
||||
|
||||
import {
|
||||
externalAuthenticationUrlMutation,
|
||||
externalObtainAccessTokensMutation,
|
||||
externalTokenRefreshMutation,
|
||||
externalTokenVerifyMutation
|
||||
} from "../mutations";
|
||||
import {
|
||||
ExternalAuthenticationUrl,
|
||||
ExternalAuthenticationUrlVariables
|
||||
} from "../types/ExternalAuthenticationUrl";
|
||||
import {
|
||||
ExternalObtainAccessTokens,
|
||||
ExternalObtainAccessTokens_externalObtainAccessTokens,
|
||||
ExternalObtainAccessTokensVariables
|
||||
} from "../types/ExternalObtainAccessTokens";
|
||||
import {
|
||||
ExternalRefreshToken,
|
||||
ExternalRefreshTokenVariables
|
||||
} from "../types/ExternalRefreshToken";
|
||||
import {
|
||||
ExternalVerifyToken,
|
||||
ExternalVerifyTokenVariables
|
||||
} from "../types/ExternalVerifyToken";
|
||||
import {
|
||||
displayDemoMessage,
|
||||
getTokens,
|
||||
removeTokens,
|
||||
setAuthToken,
|
||||
setTokens
|
||||
} from "../utils";
|
||||
import { UseAuthProvider, UseAuthProviderOpts } from "./useAuthProvider";
|
||||
|
||||
export interface RequestExternalLoginInput {
|
||||
redirectUri: string;
|
||||
}
|
||||
export interface ExternalLoginInput {
|
||||
code: string;
|
||||
state: string;
|
||||
}
|
||||
|
||||
export interface UseExternalAuthProvider extends UseAuthProvider {
|
||||
requestLoginByExternalPlugin: (
|
||||
pluginId: string,
|
||||
input: RequestExternalLoginInput
|
||||
) => Promise<void>;
|
||||
loginByExternalPlugin: (
|
||||
input: ExternalLoginInput
|
||||
) => Promise<ExternalObtainAccessTokens_externalObtainAccessTokens>;
|
||||
}
|
||||
export interface UseExternalAuthProviderOpts extends UseAuthProviderOpts {
|
||||
setAuthPlugin: Dispatch<SetStateAction<any>>;
|
||||
authPlugin: string;
|
||||
}
|
||||
|
||||
const persistToken = false;
|
||||
|
||||
export function useExternalAuthProvider({
|
||||
apolloClient,
|
||||
authPlugin,
|
||||
intl,
|
||||
notify,
|
||||
setAuthPlugin
|
||||
}: UseExternalAuthProviderOpts): UseExternalAuthProvider {
|
||||
const [userContext, setUserContext] = useState<undefined | User>(undefined);
|
||||
const autologinPromise = useRef<Promise<any>>();
|
||||
const refreshPromise = useRef<Promise<boolean>>();
|
||||
|
||||
useEffect(() => {
|
||||
const token = getTokens().auth;
|
||||
if (authPlugin && !!token && !userContext) {
|
||||
const input = JSON.stringify({
|
||||
token
|
||||
});
|
||||
autologinPromise.current = tokenVerify({
|
||||
variables: { input, pluginId: authPlugin }
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (authPlugin && userContext) {
|
||||
const { id, email } = userContext;
|
||||
errorTracker.setUserData({
|
||||
email,
|
||||
id,
|
||||
username: getFullName(userContext)
|
||||
});
|
||||
|
||||
if (!userContext.isStaff) {
|
||||
logout();
|
||||
notify({
|
||||
status: "error",
|
||||
text: intl.formatMessage(commonMessages.unauthorizedDashboardAccess),
|
||||
title: intl.formatMessage(commonMessages.insufficientPermissions)
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [userContext]);
|
||||
|
||||
const logout = () => {
|
||||
setUserContext(undefined);
|
||||
setAuthPlugin(undefined);
|
||||
removeTokens();
|
||||
};
|
||||
|
||||
const [externalAuthenticationUrl] = useMutation<
|
||||
ExternalAuthenticationUrl,
|
||||
ExternalAuthenticationUrlVariables
|
||||
>(externalAuthenticationUrlMutation, {
|
||||
client: apolloClient,
|
||||
onError: logout
|
||||
});
|
||||
const [obtainAccessTokens, obtainAccessTokensResult] = useMutation<
|
||||
ExternalObtainAccessTokens,
|
||||
ExternalObtainAccessTokensVariables
|
||||
>(externalObtainAccessTokensMutation, {
|
||||
client: apolloClient,
|
||||
onCompleted: ({ externalObtainAccessTokens }) => {
|
||||
if (externalObtainAccessTokens.errors.length > 0) {
|
||||
logout();
|
||||
}
|
||||
|
||||
const user = externalObtainAccessTokens.user;
|
||||
|
||||
setUserContext(user);
|
||||
if (user) {
|
||||
setTokens(
|
||||
externalObtainAccessTokens.token,
|
||||
externalObtainAccessTokens.csrfToken,
|
||||
persistToken
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: logout
|
||||
});
|
||||
const [tokenRefresh] = useMutation<
|
||||
ExternalRefreshToken,
|
||||
ExternalRefreshTokenVariables
|
||||
>(externalTokenRefreshMutation, {
|
||||
client: apolloClient,
|
||||
onError: logout
|
||||
});
|
||||
const [tokenVerify, tokenVerifyResult] = useMutation<
|
||||
ExternalVerifyToken,
|
||||
ExternalVerifyTokenVariables
|
||||
>(externalTokenVerifyMutation, {
|
||||
client: apolloClient,
|
||||
onCompleted: result => {
|
||||
if (result.externalVerify === null) {
|
||||
logout();
|
||||
} else {
|
||||
const user = result.externalVerify?.user;
|
||||
|
||||
if (!!user) {
|
||||
setUserContext(user);
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: logout
|
||||
});
|
||||
|
||||
const obtainAccessTokensOpts = {
|
||||
...obtainAccessTokensResult,
|
||||
status: getMutationStatus(obtainAccessTokensResult)
|
||||
};
|
||||
const tokenVerifyOpts = {
|
||||
...tokenVerifyResult,
|
||||
status: getMutationStatus(tokenVerifyResult)
|
||||
};
|
||||
|
||||
const onLogin = () => {
|
||||
if (DEMO_MODE) {
|
||||
displayDemoMessage(intl, notify);
|
||||
}
|
||||
};
|
||||
|
||||
const requestLoginByExternalPlugin = async (
|
||||
pluginId: string,
|
||||
pluginInput: RequestExternalLoginInput
|
||||
) => {
|
||||
const input = JSON.stringify(pluginInput);
|
||||
const result = await externalAuthenticationUrl({
|
||||
variables: {
|
||||
input,
|
||||
pluginId
|
||||
}
|
||||
});
|
||||
|
||||
if (result && !result.data.externalAuthenticationUrl.errors.length) {
|
||||
setAuthPlugin(pluginId);
|
||||
|
||||
const authenticationData = JSON.parse(
|
||||
result.data.externalAuthenticationUrl.authenticationData
|
||||
);
|
||||
|
||||
location.href = authenticationData.authorizationUrl;
|
||||
} else {
|
||||
setAuthPlugin(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const loginByExternalPlugin = async (loginInput: ExternalLoginInput) => {
|
||||
const input = JSON.stringify(loginInput);
|
||||
const result = await obtainAccessTokens({
|
||||
variables: { input, pluginId: authPlugin }
|
||||
});
|
||||
|
||||
if (result && !result.data?.externalObtainAccessTokens?.errors?.length) {
|
||||
if (!!onLogin) {
|
||||
onLogin();
|
||||
}
|
||||
} else {
|
||||
setAuthPlugin(undefined);
|
||||
}
|
||||
|
||||
return result?.data?.externalObtainAccessTokens;
|
||||
};
|
||||
|
||||
const refreshToken = (): Promise<boolean> => {
|
||||
if (!!refreshPromise.current) {
|
||||
return refreshPromise.current;
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
const token = getTokens().refresh;
|
||||
const input = JSON.stringify({
|
||||
refreshToken: token
|
||||
});
|
||||
|
||||
return tokenRefresh({ variables: { input, pluginId: authPlugin } }).then(
|
||||
refreshData => {
|
||||
if (!!refreshData.data.externalRefresh?.token) {
|
||||
setAuthToken(refreshData.data.externalRefresh.token, persistToken);
|
||||
return resolve(true);
|
||||
}
|
||||
|
||||
return resolve(false);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
autologinPromise,
|
||||
loginByExternalPlugin,
|
||||
logout,
|
||||
requestLoginByExternalPlugin,
|
||||
tokenAuthLoading: obtainAccessTokensOpts.loading,
|
||||
tokenRefresh: refreshToken,
|
||||
tokenVerifyLoading: tokenVerifyOpts.loading,
|
||||
user: userContext
|
||||
};
|
||||
}
|
|
@ -1,203 +0,0 @@
|
|||
import { DEMO_MODE } from "@saleor/config";
|
||||
import { User } from "@saleor/fragments/types/User";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { getFullName, getMutationStatus } from "@saleor/misc";
|
||||
import errorTracker from "@saleor/services/errorTracking";
|
||||
import {
|
||||
isSupported as isCredentialsManagementAPISupported,
|
||||
login as loginWithCredentialsManagementAPI,
|
||||
saveCredentials
|
||||
} from "@saleor/utils/credentialsManagement";
|
||||
import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
|
||||
import { useMutation } from "react-apollo";
|
||||
|
||||
import {
|
||||
tokenAuthMutation,
|
||||
tokenRefreshMutation,
|
||||
tokenVerifyMutation
|
||||
} from "../mutations";
|
||||
import { RefreshToken, RefreshTokenVariables } from "../types/RefreshToken";
|
||||
import {
|
||||
TokenAuth,
|
||||
TokenAuth_tokenCreate,
|
||||
TokenAuthVariables
|
||||
} from "../types/TokenAuth";
|
||||
import { VerifyToken, VerifyTokenVariables } from "../types/VerifyToken";
|
||||
import {
|
||||
displayDemoMessage,
|
||||
getTokens,
|
||||
removeTokens,
|
||||
setAuthToken,
|
||||
setTokens
|
||||
} from "../utils";
|
||||
import { UseAuthProvider, UseAuthProviderOpts } from "./useAuthProvider";
|
||||
|
||||
export interface UseSaleorAuthProvider extends UseAuthProvider {
|
||||
login: (username: string, password: string) => Promise<TokenAuth_tokenCreate>;
|
||||
loginByToken: (auth: string, csrf: string, user: User) => void;
|
||||
}
|
||||
export interface UseSaleorAuthProviderOpts extends UseAuthProviderOpts {
|
||||
setAuthPlugin: Dispatch<SetStateAction<any>>;
|
||||
authPlugin: string;
|
||||
}
|
||||
|
||||
const persistToken = false;
|
||||
|
||||
export function useSaleorAuthProvider({
|
||||
apolloClient,
|
||||
authPlugin,
|
||||
intl,
|
||||
notify,
|
||||
setAuthPlugin
|
||||
}: UseSaleorAuthProviderOpts): UseSaleorAuthProvider {
|
||||
const [userContext, setUserContext] = useState<undefined | User>(undefined);
|
||||
const autologinPromise = useRef<Promise<any>>();
|
||||
const refreshPromise = useRef<Promise<boolean>>();
|
||||
|
||||
useEffect(() => {
|
||||
const token = getTokens().auth;
|
||||
if (!authPlugin && !!token && !userContext) {
|
||||
autologinPromise.current = tokenVerify({ variables: { token } });
|
||||
} else if (!authPlugin) {
|
||||
autologinPromise.current = loginWithCredentialsManagementAPI(login);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authPlugin && userContext) {
|
||||
const { id, email } = userContext;
|
||||
errorTracker.setUserData({
|
||||
email,
|
||||
id,
|
||||
username: getFullName(userContext)
|
||||
});
|
||||
|
||||
if (!userContext.isStaff) {
|
||||
logout();
|
||||
notify({
|
||||
status: "error",
|
||||
text: intl.formatMessage(commonMessages.unauthorizedDashboardAccess),
|
||||
title: intl.formatMessage(commonMessages.insufficientPermissions)
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [userContext]);
|
||||
|
||||
const logout = () => {
|
||||
setUserContext(undefined);
|
||||
if (isCredentialsManagementAPISupported) {
|
||||
navigator.credentials.preventSilentAccess();
|
||||
}
|
||||
removeTokens();
|
||||
};
|
||||
|
||||
const [tokenAuth, tokenAuthResult] = useMutation<
|
||||
TokenAuth,
|
||||
TokenAuthVariables
|
||||
>(tokenAuthMutation, {
|
||||
client: apolloClient,
|
||||
onCompleted: ({ tokenCreate }) => {
|
||||
if (tokenCreate.errors.length > 0) {
|
||||
logout();
|
||||
}
|
||||
|
||||
const user = tokenCreate.user;
|
||||
|
||||
setUserContext(user);
|
||||
if (user) {
|
||||
setTokens(tokenCreate.token, 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) {
|
||||
displayDemoMessage(intl, notify);
|
||||
}
|
||||
};
|
||||
|
||||
const login = async (email: string, password: string) => {
|
||||
setAuthPlugin(undefined);
|
||||
const result = await tokenAuth({ variables: { email, password } });
|
||||
|
||||
if (result && !result.data.tokenCreate.errors.length) {
|
||||
if (!!onLogin) {
|
||||
onLogin();
|
||||
}
|
||||
saveCredentials(result.data.tokenCreate.user, password);
|
||||
}
|
||||
|
||||
return result.data.tokenCreate;
|
||||
};
|
||||
|
||||
const loginByToken = (auth: string, refresh: string, user: User) => {
|
||||
setAuthPlugin(undefined);
|
||||
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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
autologinPromise,
|
||||
login,
|
||||
loginByToken,
|
||||
logout,
|
||||
tokenAuthLoading: tokenAuthOpts.loading,
|
||||
tokenRefresh: refreshToken,
|
||||
tokenVerifyLoading: tokenVerifyOpts.loading,
|
||||
user: userContext
|
||||
};
|
||||
}
|
|
@ -1,15 +1,9 @@
|
|||
import { User } from "@saleor/fragments/types/User";
|
||||
import { parse as parseQs } from "qs";
|
||||
import React, { MutableRefObject } from "react";
|
||||
import React, { useContext } from "react";
|
||||
import { Route, RouteComponentProps, Switch } from "react-router-dom";
|
||||
|
||||
import Layout from "./components/Layout";
|
||||
import {
|
||||
ExternalLoginInput,
|
||||
RequestExternalLoginInput
|
||||
} from "./hooks/useExternalAuthProvider";
|
||||
import { ExternalObtainAccessTokens_externalObtainAccessTokens } from "./types/ExternalObtainAccessTokens";
|
||||
import { TokenAuth_tokenCreate } from "./types/TokenAuth";
|
||||
import { UserContext as Context } from "./types";
|
||||
import {
|
||||
LoginUrlQueryParams,
|
||||
newPasswordPath,
|
||||
|
@ -28,33 +22,13 @@ const LoginView: React.FC<RouteComponentProps<any>> = () => {
|
|||
return <LoginViewComponent params={params} />;
|
||||
};
|
||||
|
||||
interface UserContext {
|
||||
login: (username: string, password: string) => Promise<TokenAuth_tokenCreate>;
|
||||
loginByExternalPlugin: (
|
||||
input: ExternalLoginInput
|
||||
) => Promise<ExternalObtainAccessTokens_externalObtainAccessTokens>;
|
||||
loginByToken: (auth: string, csrf: string, user: User) => void;
|
||||
logout: () => void;
|
||||
requestLoginByExternalPlugin: (
|
||||
pluginId: string,
|
||||
input: RequestExternalLoginInput
|
||||
) => Promise<void>;
|
||||
tokenAuthLoading: boolean;
|
||||
tokenRefresh: () => Promise<boolean>;
|
||||
tokenVerifyLoading: boolean;
|
||||
user?: User;
|
||||
autologinPromise?: MutableRefObject<Promise<any>>;
|
||||
}
|
||||
|
||||
export const UserContext = React.createContext<UserContext>({
|
||||
export const UserContext = React.createContext<Context>({
|
||||
login: undefined,
|
||||
loginByExternalPlugin: undefined,
|
||||
loginByToken: undefined,
|
||||
logout: undefined,
|
||||
requestLoginByExternalPlugin: undefined,
|
||||
tokenAuthLoading: false,
|
||||
tokenRefresh: undefined,
|
||||
tokenVerifyLoading: false
|
||||
authenticating: false,
|
||||
authenticated: false
|
||||
});
|
||||
|
||||
const AuthRouter: React.FC = () => (
|
||||
|
@ -72,3 +46,4 @@ AuthRouter.displayName = "AuthRouter";
|
|||
export default AuthRouter;
|
||||
|
||||
export * from "./utils";
|
||||
export const useUser = () => useContext(UserContext);
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
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-Bearer": authToken || null
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const link = invalidateTokenLink.concat(tokenLink);
|
||||
|
||||
export default link;
|
|
@ -1,4 +1,3 @@
|
|||
import { fragmentUser } from "@saleor/fragments/auth";
|
||||
import { accountErrorFragment } from "@saleor/fragments/errors";
|
||||
import gql from "graphql-tag";
|
||||
|
||||
|
@ -7,44 +6,6 @@ import {
|
|||
RequestPasswordReset,
|
||||
RequestPasswordResetVariables
|
||||
} from "./types/RequestPasswordReset";
|
||||
import { SetPassword, SetPasswordVariables } from "./types/SetPassword";
|
||||
|
||||
export const tokenAuthMutation = gql`
|
||||
${fragmentUser}
|
||||
mutation TokenAuth($email: String!, $password: String!) {
|
||||
tokenCreate(email: $email, password: $password) {
|
||||
errors {
|
||||
field
|
||||
message
|
||||
}
|
||||
csrfToken
|
||||
token
|
||||
user {
|
||||
...User
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const tokenVerifyMutation = gql`
|
||||
${fragmentUser}
|
||||
mutation VerifyToken($token: String!) {
|
||||
tokenVerify(token: $token) {
|
||||
payload
|
||||
user {
|
||||
...User
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const tokenRefreshMutation = gql`
|
||||
mutation RefreshToken($token: String!) {
|
||||
tokenRefresh(csrfToken: $token) {
|
||||
token
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const requestPasswordReset = gql`
|
||||
${accountErrorFragment}
|
||||
|
@ -60,74 +21,3 @@ export const RequestPasswordResetMutation = TypedMutation<
|
|||
RequestPasswordReset,
|
||||
RequestPasswordResetVariables
|
||||
>(requestPasswordReset);
|
||||
|
||||
export const setPassword = gql`
|
||||
${accountErrorFragment}
|
||||
${fragmentUser}
|
||||
mutation SetPassword($email: String!, $password: String!, $token: String!) {
|
||||
setPassword(email: $email, password: $password, token: $token) {
|
||||
errors {
|
||||
...AccountErrorFragment
|
||||
}
|
||||
csrfToken
|
||||
refreshToken
|
||||
token
|
||||
user {
|
||||
...User
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const SetPasswordMutation = TypedMutation<
|
||||
SetPassword,
|
||||
SetPasswordVariables
|
||||
>(setPassword);
|
||||
|
||||
export const externalAuthenticationUrlMutation = gql`
|
||||
${accountErrorFragment}
|
||||
mutation ExternalAuthenticationUrl($pluginId: String!, $input: JSONString!) {
|
||||
externalAuthenticationUrl(pluginId: $pluginId, input: $input) {
|
||||
authenticationData
|
||||
errors {
|
||||
...AccountErrorFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const externalObtainAccessTokensMutation = gql`
|
||||
${accountErrorFragment}
|
||||
${fragmentUser}
|
||||
mutation ExternalObtainAccessTokens($pluginId: String!, $input: JSONString!) {
|
||||
externalObtainAccessTokens(pluginId: $pluginId, input: $input) {
|
||||
token
|
||||
csrfToken
|
||||
user {
|
||||
...User
|
||||
}
|
||||
errors {
|
||||
...AccountErrorFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const externalTokenRefreshMutation = gql`
|
||||
mutation ExternalRefreshToken($pluginId: String!, $input: JSONString!) {
|
||||
externalRefresh(pluginId: $pluginId, input: $input) {
|
||||
token
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const externalTokenVerifyMutation = gql`
|
||||
${fragmentUser}
|
||||
mutation ExternalVerifyToken($pluginId: String!, $input: JSONString!) {
|
||||
externalVerify(pluginId: $pluginId, input: $input) {
|
||||
verifyData
|
||||
user {
|
||||
...User
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { fragmentUser } from "@saleor/fragments/auth";
|
||||
import gql from "graphql-tag";
|
||||
|
||||
export const availableExternalAuthentications = gql`
|
||||
|
@ -10,3 +11,12 @@ export const availableExternalAuthentications = gql`
|
|||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const userDetailsQuery = gql`
|
||||
${fragmentUser}
|
||||
query UserDetails {
|
||||
me {
|
||||
...User
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
41
src/auth/types.ts
Normal file
41
src/auth/types.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { User } from "@saleor/fragments/types/User";
|
||||
import {
|
||||
GetExternalAccessTokenData,
|
||||
GetExternalAuthUrlData,
|
||||
LoginData
|
||||
} from "@saleor/sdk";
|
||||
|
||||
export interface RequestExternalLoginInput {
|
||||
redirectUri: string;
|
||||
}
|
||||
|
||||
export interface ExternalLoginInput {
|
||||
code: string;
|
||||
state: string;
|
||||
}
|
||||
|
||||
export interface RequestExternalLogoutInput {
|
||||
returnTo: string;
|
||||
}
|
||||
|
||||
export type UserContextError =
|
||||
| "loginError"
|
||||
| "externalLoginError"
|
||||
| "serverError";
|
||||
|
||||
export interface UserContext {
|
||||
login: (username: string, password: string) => Promise<LoginData>;
|
||||
loginByExternalPlugin: (
|
||||
pluginId: string,
|
||||
input: ExternalLoginInput
|
||||
) => Promise<GetExternalAccessTokenData>;
|
||||
logout: () => Promise<void>;
|
||||
requestLoginByExternalPlugin: (
|
||||
pluginId: string,
|
||||
input: RequestExternalLoginInput
|
||||
) => Promise<GetExternalAuthUrlData>;
|
||||
user?: User;
|
||||
authenticating: boolean;
|
||||
authenticated: boolean;
|
||||
error?: UserContextError;
|
||||
}
|
36
src/auth/types/UserDetails.ts
Normal file
36
src/auth/types/UserDetails.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { PermissionEnum } from "./../../types/globalTypes";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: UserDetails
|
||||
// ====================================================
|
||||
|
||||
export interface UserDetails_me_userPermissions {
|
||||
__typename: "UserPermission";
|
||||
code: PermissionEnum;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface UserDetails_me_avatar {
|
||||
__typename: "Image";
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface UserDetails_me {
|
||||
__typename: "User";
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
isStaff: boolean;
|
||||
userPermissions: (UserDetails_me_userPermissions | null)[] | null;
|
||||
avatar: UserDetails_me_avatar | null;
|
||||
}
|
||||
|
||||
export interface UserDetails {
|
||||
me: UserDetails_me | null;
|
||||
}
|
|
@ -6,43 +6,6 @@ import { IntlShape } from "react-intl";
|
|||
|
||||
import { isJwtError, isTokenExpired } from "./errors";
|
||||
|
||||
export enum TOKEN_STORAGE_KEY {
|
||||
AUTH = "auth",
|
||||
CSRF = "csrf"
|
||||
}
|
||||
|
||||
export const getTokens = () => ({
|
||||
auth:
|
||||
localStorage.getItem(TOKEN_STORAGE_KEY.AUTH) ||
|
||||
sessionStorage.getItem(TOKEN_STORAGE_KEY.AUTH),
|
||||
refresh:
|
||||
localStorage.getItem(TOKEN_STORAGE_KEY.CSRF) ||
|
||||
sessionStorage.getItem(TOKEN_STORAGE_KEY.CSRF)
|
||||
});
|
||||
|
||||
export const setTokens = (auth: string, csrf: string, persist: boolean) => {
|
||||
if (persist) {
|
||||
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 = (
|
||||
intl: IntlShape,
|
||||
notify: UseNotifierResult
|
||||
|
@ -55,23 +18,17 @@ export const displayDemoMessage = (
|
|||
export async function handleQueryAuthError(
|
||||
error: ApolloError,
|
||||
notify: IMessageContext,
|
||||
tokenRefresh: () => Promise<boolean>,
|
||||
logout: () => void,
|
||||
intl: IntlShape
|
||||
) {
|
||||
if (error.graphQLErrors.some(isJwtError)) {
|
||||
logout();
|
||||
if (error.graphQLErrors.every(isTokenExpired)) {
|
||||
const success = await tokenRefresh();
|
||||
|
||||
if (!success) {
|
||||
logout();
|
||||
notify({
|
||||
status: "error",
|
||||
text: intl.formatMessage(commonMessages.sessionExpired)
|
||||
});
|
||||
}
|
||||
notify({
|
||||
status: "error",
|
||||
text: intl.formatMessage(commonMessages.sessionExpired)
|
||||
});
|
||||
} else {
|
||||
logout();
|
||||
notify({
|
||||
status: "error",
|
||||
text: intl.formatMessage(commonMessages.somethingWentWrong)
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { APP_DEFAULT_URI, APP_MOUNT_URI } from "@saleor/config";
|
||||
import useLocalStorage from "@saleor/hooks/useLocalStorage";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import useUser from "@saleor/hooks/useUser";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import { useQuery } from "react-apollo";
|
||||
import urlJoin from "url-join";
|
||||
import useRouter from "use-react-router";
|
||||
|
||||
import { useUser } from "..";
|
||||
import LoginPage from "../components/LoginPage";
|
||||
import { LoginFormData } from "../components/LoginPage/form";
|
||||
import { availableExternalAuthentications } from "../queries";
|
||||
|
@ -27,46 +28,50 @@ const LoginView: React.FC<LoginViewProps> = ({ params }) => {
|
|||
login,
|
||||
requestLoginByExternalPlugin,
|
||||
loginByExternalPlugin,
|
||||
tokenAuthLoading
|
||||
authenticating,
|
||||
error
|
||||
} = useUser();
|
||||
const [isError, setIsError] = useState(false);
|
||||
const [isExternalError, setIsExternalError] = useState(false);
|
||||
const {
|
||||
data: externalAuthentications,
|
||||
loading: externalAuthenticationsLoading
|
||||
} = useQuery<AvailableExternalAuthentications>(
|
||||
availableExternalAuthentications
|
||||
);
|
||||
const [
|
||||
requestedExternalPluginId,
|
||||
setRequestedExternalPluginId
|
||||
] = useLocalStorage("requestedExternalPluginId", null);
|
||||
|
||||
const handleSubmit = async (data: LoginFormData) => {
|
||||
const result = await login(data.email, data.password);
|
||||
const errors = result?.errors || [];
|
||||
|
||||
setIsExternalError(false);
|
||||
setIsError(!result || errors?.length > 0);
|
||||
return errors;
|
||||
};
|
||||
|
||||
const handleRequestExternalAuthentication = (pluginId: string) =>
|
||||
requestLoginByExternalPlugin(pluginId, {
|
||||
const handleRequestExternalAuthentication = async (pluginId: string) => {
|
||||
const result = await requestLoginByExternalPlugin(pluginId, {
|
||||
redirectUri: urlJoin(
|
||||
window.location.origin,
|
||||
APP_MOUNT_URI === APP_DEFAULT_URI ? "" : APP_MOUNT_URI,
|
||||
loginCallbackPath
|
||||
)
|
||||
});
|
||||
const data = JSON.parse(result?.authenticationData || "");
|
||||
if (data && !result?.errors?.length) {
|
||||
setRequestedExternalPluginId(pluginId);
|
||||
window.location.href = data.authorizationUrl;
|
||||
}
|
||||
};
|
||||
|
||||
const handleExternalAuthentication = async (code: string, state: string) => {
|
||||
const result = await loginByExternalPlugin({ code, state });
|
||||
const errors = result?.errors || [];
|
||||
|
||||
setIsError(false);
|
||||
if (!result || errors?.length > 0) {
|
||||
setIsExternalError(true);
|
||||
} else {
|
||||
const result = await loginByExternalPlugin(requestedExternalPluginId, {
|
||||
code,
|
||||
state
|
||||
});
|
||||
if (result && !result?.errors?.length) {
|
||||
navigate(APP_DEFAULT_URI);
|
||||
}
|
||||
return errors;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -80,13 +85,12 @@ const LoginView: React.FC<LoginViewProps> = ({ params }) => {
|
|||
|
||||
return (
|
||||
<LoginPage
|
||||
error={isError}
|
||||
externalError={isExternalError}
|
||||
disabled={tokenAuthLoading}
|
||||
error={error}
|
||||
disabled={authenticating}
|
||||
externalAuthentications={
|
||||
externalAuthentications?.shop?.availableExternalAuthentications
|
||||
}
|
||||
loading={externalAuthenticationsLoading || tokenAuthLoading}
|
||||
loading={externalAuthenticationsLoading || authenticating}
|
||||
onExternalAuthentication={handleRequestExternalAuthentication}
|
||||
onPasswordRecovery={() => navigate(passwordResetUrl)}
|
||||
onSubmit={handleSubmit}
|
||||
|
|
|
@ -1,54 +1,49 @@
|
|||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import useUser from "@saleor/hooks/useUser";
|
||||
import { SetPasswordData, useAuth } from "@saleor/sdk";
|
||||
import { parse as parseQs } from "qs";
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
|
||||
import NewPasswordPage, {
|
||||
NewPasswordPageFormData
|
||||
} from "../components/NewPasswordPage";
|
||||
import { SetPasswordMutation } from "../mutations";
|
||||
import { SetPassword } from "../types/SetPassword";
|
||||
import { NewPasswordUrlQueryParams } from "../urls";
|
||||
|
||||
const NewPassword: React.FC<RouteComponentProps> = ({ location }) => {
|
||||
const navigate = useNavigator();
|
||||
const { loginByToken } = useUser();
|
||||
|
||||
const { setPassword } = useAuth();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errors, setErrors] = useState<SetPasswordData["errors"]>([]);
|
||||
|
||||
const params: NewPasswordUrlQueryParams = parseQs(location.search.substr(1));
|
||||
|
||||
const handleSetPassword = async (data: SetPassword) => {
|
||||
if (data.setPassword.errors.length === 0) {
|
||||
loginByToken(
|
||||
data.setPassword.token,
|
||||
data.setPassword.csrfToken,
|
||||
data.setPassword.user
|
||||
);
|
||||
const handleSubmit = async (data: NewPasswordPageFormData) => {
|
||||
setLoading(true);
|
||||
|
||||
const result = await setPassword({
|
||||
email: params.email,
|
||||
password: data.password,
|
||||
token: params.token
|
||||
});
|
||||
|
||||
const errors = result.data?.setPassword?.errors || [];
|
||||
|
||||
setErrors(errors);
|
||||
setLoading(false);
|
||||
|
||||
if (!errors.length) {
|
||||
navigate("/", { replace: true });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SetPasswordMutation onCompleted={handleSetPassword}>
|
||||
{(setPassword, setPasswordOpts) => {
|
||||
const handleSubmit = (data: NewPasswordPageFormData) =>
|
||||
setPassword({
|
||||
variables: {
|
||||
email: params.email,
|
||||
password: data.password,
|
||||
token: params.token
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<NewPasswordPage
|
||||
errors={setPasswordOpts.data?.setPassword.errors || []}
|
||||
disabled={setPasswordOpts.loading}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</SetPasswordMutation>
|
||||
<NewPasswordPage
|
||||
errors={errors}
|
||||
disabled={loading}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -8,9 +8,9 @@ import {
|
|||
ListItemText,
|
||||
Typography
|
||||
} from "@material-ui/core";
|
||||
import { useUser } from "@saleor/auth";
|
||||
import CardTitle from "@saleor/components/CardTitle";
|
||||
import Skeleton from "@saleor/components/Skeleton";
|
||||
import useUser from "@saleor/hooks/useUser";
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
import { PermissionData } from "@saleor/permissionGroups/components/PermissionGroupDetailsPage/PermissionGroupDetailsPage";
|
||||
import React from "react";
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { useAuth } from "@saleor/auth/AuthProvider";
|
||||
import { useUser } from "@saleor/auth";
|
||||
import { useBaseChannelsList } from "@saleor/channels/queries";
|
||||
import { BaseChannels_channels } from "@saleor/channels/types/BaseChannels";
|
||||
import { ChannelFragment } from "@saleor/fragments/types/ChannelFragment";
|
||||
import useLocalStorage from "@saleor/hooks/useLocalStorage";
|
||||
import { getById } from "@saleor/orders/components/OrderReturnPage/utils";
|
||||
import { useSaleorConfig } from "@saleor/sdk";
|
||||
import React from "react";
|
||||
|
||||
interface UseAppChannel {
|
||||
|
@ -38,10 +39,11 @@ const isValidChannel = (
|
|||
};
|
||||
|
||||
export const AppChannelProvider: React.FC = ({ children }) => {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { setChannel } = useSaleorConfig();
|
||||
const { authenticated } = useUser();
|
||||
const [selectedChannel, setSelectedChannel] = useLocalStorage("channel", "");
|
||||
const { data: channelData, refetch } = useBaseChannelsList({
|
||||
skip: !isAuthenticated
|
||||
skip: !authenticated
|
||||
});
|
||||
|
||||
const [isPickerActive, setPickerActive] = React.useState(false);
|
||||
|
@ -54,6 +56,10 @@ export const AppChannelProvider: React.FC = ({ children }) => {
|
|||
}
|
||||
}, [channelData]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setChannel(selectedChannel);
|
||||
}, [selectedChannel]);
|
||||
|
||||
const availableChannels = channelData?.channels || [];
|
||||
|
||||
const channel =
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { LinearProgress, useMediaQuery } from "@material-ui/core";
|
||||
import { useUser } from "@saleor/auth";
|
||||
import useAppState from "@saleor/hooks/useAppState";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import useUser from "@saleor/hooks/useUser";
|
||||
import {
|
||||
makeStyles,
|
||||
SaleorTheme,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Button, Card, CardContent, Typography } from "@material-ui/core";
|
||||
import { useUser } from "@saleor/auth";
|
||||
import CardTitle from "@saleor/components/CardTitle";
|
||||
import Hr from "@saleor/components/Hr";
|
||||
import RequirePermissions from "@saleor/components/RequirePermissions";
|
||||
import useUser from "@saleor/hooks/useUser";
|
||||
import { PermissionEnum } from "@saleor/types/globalTypes";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
|
|
@ -2,7 +2,7 @@ import appleTouchIcon from "@assets/favicons/apple-touch-icon.png";
|
|||
import favicon16 from "@assets/favicons/favicon-16x16.png";
|
||||
import favicon32 from "@assets/favicons/favicon-32x32.png";
|
||||
import safariPinnedTab from "@assets/favicons/safari-pinned-tab.svg";
|
||||
import { useAuth } from "@saleor/auth/AuthProvider";
|
||||
import { useUser } from "@saleor/auth";
|
||||
import React from "react";
|
||||
import Helmet from "react-helmet";
|
||||
|
||||
|
@ -14,10 +14,10 @@ type ShopContext = ShopInfo_shop;
|
|||
export const ShopContext = React.createContext<ShopContext>(undefined);
|
||||
|
||||
export const ShopProvider: React.FC = ({ children }) => {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { authenticated } = useUser();
|
||||
|
||||
return (
|
||||
<TypedShopInfoQuery skip={!isAuthenticated}>
|
||||
<TypedShopInfoQuery skip={!authenticated}>
|
||||
{({ data }) => (
|
||||
<>
|
||||
<Helmet>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { attributeListUrl } from "@saleor/attributes/urls";
|
||||
import { useUser } from "@saleor/auth";
|
||||
import { channelsListUrl } from "@saleor/channels/urls";
|
||||
import { WindowTitle } from "@saleor/components/WindowTitle";
|
||||
import { APP_VERSION as dashboardVersion } from "@saleor/config";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import useShop from "@saleor/hooks/useShop";
|
||||
import useUser from "@saleor/hooks/useUser";
|
||||
import Attributes from "@saleor/icons/Attributes";
|
||||
import Channels from "@saleor/icons/Channels";
|
||||
import Navigation from "@saleor/icons/Navigation";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useUser } from "@saleor/auth";
|
||||
import { channelsListUrl } from "@saleor/channels/urls";
|
||||
import useAppChannel from "@saleor/components/AppLayout/AppChannelContext";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import useUser from "@saleor/hooks/useUser";
|
||||
import { mapEdgesToItems } from "@saleor/utils/maps";
|
||||
import React from "react";
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { useUser } from "@saleor/auth";
|
||||
import { isJwtError } from "@saleor/auth/errors";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { getMutationStatus } from "@saleor/misc";
|
||||
|
@ -13,7 +14,6 @@ import {
|
|||
import { useIntl } from "react-intl";
|
||||
|
||||
import useNotifier from "./useNotifier";
|
||||
import useUser from "./useUser";
|
||||
|
||||
export type MutationResultWithOpts<TData> = MutationResult<TData> &
|
||||
MutationResultAdditionalProps;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { handleQueryAuthError } from "@saleor/auth";
|
||||
import { useUser } from "@saleor/auth";
|
||||
import { RequireAtLeastOne } from "@saleor/misc";
|
||||
import { ApolloQueryResult, WatchQueryFetchPolicy } from "apollo-client";
|
||||
import { DocumentNode } from "graphql";
|
||||
|
@ -11,7 +12,6 @@ import { PrefixedPermissions } from "../types/extendedTypes";
|
|||
import { PermissionEnum } from "../types/globalTypes";
|
||||
import useAppState from "./useAppState";
|
||||
import useNotifier from "./useNotifier";
|
||||
import useUser from "./useUser";
|
||||
|
||||
const getPermissionKey = (permission: string) =>
|
||||
`PERMISSION_${permission}` as PrefixedPermissions;
|
||||
|
@ -81,14 +81,7 @@ function makeQuery<TData, TVariables>(
|
|||
},
|
||||
errorPolicy: "all",
|
||||
fetchPolicy: fetchPolicy || "cache-and-network",
|
||||
onError: error =>
|
||||
handleQueryAuthError(
|
||||
error,
|
||||
notify,
|
||||
user.tokenRefresh,
|
||||
user.logout,
|
||||
intl
|
||||
),
|
||||
onError: error => handleQueryAuthError(error, notify, user.logout, intl),
|
||||
skip,
|
||||
variables: variablesWithPermissions
|
||||
});
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import { useContext } from "react";
|
||||
|
||||
import { UserContext } from "../auth";
|
||||
|
||||
function useUser() {
|
||||
const user = useContext(UserContext);
|
||||
return user;
|
||||
}
|
||||
export default useUser;
|
|
@ -1,6 +1,7 @@
|
|||
import DemoBanner from "@saleor/components/DemoBanner";
|
||||
import useAppState from "@saleor/hooks/useAppState";
|
||||
import { ThemeProvider } from "@saleor/macaw-ui";
|
||||
import { createFetch, createSaleorClient, SaleorProvider } from "@saleor/sdk";
|
||||
import { defaultDataIdFromObject, InMemoryCache } from "apollo-cache-inmemory";
|
||||
import { IntrospectionFragmentMatcher } from "apollo-cache-inmemory";
|
||||
import { ApolloClient } from "apollo-client";
|
||||
|
@ -21,11 +22,10 @@ import { ExternalAppProvider } from "./apps/components/ExternalAppContext";
|
|||
import { appsSection } from "./apps/urls";
|
||||
import AttributeSection from "./attributes";
|
||||
import { attributeSection } from "./attributes/urls";
|
||||
import Auth from "./auth";
|
||||
import AuthProvider, { useAuth } from "./auth/AuthProvider";
|
||||
import Auth, { useUser } from "./auth";
|
||||
import AuthProvider from "./auth/AuthProvider";
|
||||
import LoginLoading from "./auth/components/LoginLoading/LoginLoading";
|
||||
import SectionRoute from "./auth/components/SectionRoute";
|
||||
import authLink from "./auth/link";
|
||||
import CategorySection from "./categories";
|
||||
import ChannelsSection from "./channels";
|
||||
import { channelsSection } from "./channels/urls";
|
||||
|
@ -83,7 +83,8 @@ errorTracker.init();
|
|||
// so we need to explicitly set them
|
||||
const linkOptions = {
|
||||
credentials: "include",
|
||||
uri: API_URI
|
||||
uri: API_URI,
|
||||
fetch: createFetch()
|
||||
};
|
||||
const uploadLink = createUploadLink(linkOptions);
|
||||
const batchLink = new BatchHttpLink({
|
||||
|
@ -113,60 +114,57 @@ const apolloClient = new ApolloClient({
|
|||
return defaultDataIdFromObject(obj);
|
||||
}
|
||||
}),
|
||||
link: authLink.concat(link)
|
||||
link
|
||||
});
|
||||
|
||||
const saleorClient = createSaleorClient({
|
||||
apiUrl: API_URI,
|
||||
channel: ""
|
||||
});
|
||||
|
||||
const App: React.FC = () => (
|
||||
<ApolloProvider client={apolloClient}>
|
||||
<BrowserRouter basename={APP_MOUNT_URI}>
|
||||
<ThemeProvider overrides={themeOverrides}>
|
||||
<DateProvider>
|
||||
<LocaleProvider>
|
||||
<MessageManagerProvider>
|
||||
<ServiceWorker />
|
||||
<BackgroundTasksProvider>
|
||||
<AppStateProvider>
|
||||
<AuthProvider>
|
||||
<ShopProvider>
|
||||
<AppChannelProvider>
|
||||
<ExternalAppProvider>
|
||||
<Routes />
|
||||
</ExternalAppProvider>
|
||||
</AppChannelProvider>
|
||||
</ShopProvider>
|
||||
</AuthProvider>
|
||||
</AppStateProvider>
|
||||
</BackgroundTasksProvider>
|
||||
</MessageManagerProvider>
|
||||
</LocaleProvider>
|
||||
</DateProvider>
|
||||
</ThemeProvider>
|
||||
</BrowserRouter>
|
||||
</ApolloProvider>
|
||||
<SaleorProvider client={saleorClient}>
|
||||
<ApolloProvider client={apolloClient}>
|
||||
<BrowserRouter basename={APP_MOUNT_URI}>
|
||||
<ThemeProvider overrides={themeOverrides}>
|
||||
<DateProvider>
|
||||
<LocaleProvider>
|
||||
<MessageManagerProvider>
|
||||
<ServiceWorker />
|
||||
<BackgroundTasksProvider>
|
||||
<AppStateProvider>
|
||||
<AuthProvider>
|
||||
<ShopProvider>
|
||||
<AppChannelProvider>
|
||||
<ExternalAppProvider>
|
||||
<Routes />
|
||||
</ExternalAppProvider>
|
||||
</AppChannelProvider>
|
||||
</ShopProvider>
|
||||
</AuthProvider>
|
||||
</AppStateProvider>
|
||||
</BackgroundTasksProvider>
|
||||
</MessageManagerProvider>
|
||||
</LocaleProvider>
|
||||
</DateProvider>
|
||||
</ThemeProvider>
|
||||
</BrowserRouter>
|
||||
</ApolloProvider>
|
||||
</SaleorProvider>
|
||||
);
|
||||
|
||||
const Routes: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const [, dispatchAppState] = useAppState();
|
||||
const {
|
||||
hasToken,
|
||||
isAuthenticated,
|
||||
tokenAuthLoading,
|
||||
tokenVerifyLoading
|
||||
} = useAuth();
|
||||
const { authenticated, authenticating } = useUser();
|
||||
|
||||
const { channel } = useAppChannel(false);
|
||||
|
||||
const channelLoaded = typeof channel !== "undefined";
|
||||
|
||||
const homePageLoaded =
|
||||
channelLoaded &&
|
||||
isAuthenticated &&
|
||||
!tokenAuthLoading &&
|
||||
!tokenVerifyLoading;
|
||||
const homePageLoaded = channelLoaded && authenticated;
|
||||
|
||||
const homePageLoading =
|
||||
(isAuthenticated && !channelLoaded) || (hasToken && tokenVerifyLoading);
|
||||
const homePageLoading = (authenticated && !channelLoaded) || authenticating;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -4,9 +4,9 @@ import React from "react";
|
|||
import { Mutation, MutationFunction, MutationResult } from "react-apollo";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { useUser } from "./auth";
|
||||
import { isJwtError } from "./auth/errors";
|
||||
import useNotifier from "./hooks/useNotifier";
|
||||
import useUser from "./hooks/useUser";
|
||||
import { commonMessages } from "./intl";
|
||||
import { getMutationStatus } from "./misc";
|
||||
import { MutationResultAdditionalProps } from "./types";
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { useUser } from "@saleor/auth";
|
||||
import { WindowTitle } from "@saleor/components/WindowTitle";
|
||||
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
|
||||
import { useCustomerAddressesQuery } from "@saleor/customers/queries";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import useUser from "@saleor/hooks/useUser";
|
||||
import { CustomerEditData } from "@saleor/orders/components/OrderCustomer";
|
||||
import OrderCustomerAddressesEditDialog, {
|
||||
OrderCustomerAddressesEditDialogOutput
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useUser } from "@saleor/auth";
|
||||
import { WindowTitle } from "@saleor/components/WindowTitle";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import useUser from "@saleor/hooks/useUser";
|
||||
import OrderCannotCancelOrderDialog from "@saleor/orders/components/OrderCannotCancelOrderDialog";
|
||||
import OrderFulfillmentApproveDialog from "@saleor/orders/components/OrderFulfillmentApproveDialog";
|
||||
import OrderInvoiceEmailSendDialog from "@saleor/orders/components/OrderInvoiceEmailSendDialog";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useUser } from "@saleor/auth";
|
||||
import { WindowTitle } from "@saleor/components/WindowTitle";
|
||||
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import useUser from "@saleor/hooks/useUser";
|
||||
import OrderCannotCancelOrderDialog from "@saleor/orders/components/OrderCannotCancelOrderDialog";
|
||||
import OrderFulfillmentApproveDialog from "@saleor/orders/components/OrderFulfillmentApproveDialog";
|
||||
import OrderInvoiceEmailSendDialog from "@saleor/orders/components/OrderInvoiceEmailSendDialog";
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { useUser } from "@saleor/auth";
|
||||
import { WindowTitle } from "@saleor/components/WindowTitle";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import useNotifier from "@saleor/hooks/useNotifier";
|
||||
import useShop from "@saleor/hooks/useShop";
|
||||
import useUser from "@saleor/hooks/useUser";
|
||||
import { PermissionData } from "@saleor/permissionGroups/components/PermissionGroupDetailsPage";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { Button } from "@material-ui/core";
|
||||
import { useUser } from "@saleor/auth";
|
||||
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
|
||||
import useBulkActions from "@saleor/hooks/useBulkActions";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import useNotifier from "@saleor/hooks/useNotifier";
|
||||
import useShop from "@saleor/hooks/useShop";
|
||||
import useStateFromProps from "@saleor/hooks/useStateFromProps";
|
||||
import useUser from "@saleor/hooks/useUser";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import MembersErrorDialog from "@saleor/permissionGroups/components/MembersErrorDialog";
|
||||
import {
|
||||
|
|
|
@ -4,10 +4,9 @@ import React from "react";
|
|||
import { Query, QueryResult } from "react-apollo";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { handleQueryAuthError } from "./auth";
|
||||
import { handleQueryAuthError, useUser } from "./auth";
|
||||
import useAppState from "./hooks/useAppState";
|
||||
import useNotifier from "./hooks/useNotifier";
|
||||
import useUser from "./hooks/useUser";
|
||||
import { RequireAtLeastOne } from "./misc";
|
||||
|
||||
export interface LoadMore<TData, TVariables> {
|
||||
|
@ -79,13 +78,7 @@ export function TypedQuery<TData, TVariables>(
|
|||
context={{ useBatching: true }}
|
||||
errorPolicy="all"
|
||||
onError={error =>
|
||||
handleQueryAuthError(
|
||||
error,
|
||||
notify,
|
||||
user.tokenRefresh,
|
||||
user.logout,
|
||||
intl
|
||||
)
|
||||
handleQueryAuthError(error, notify, user.logout, intl)
|
||||
}
|
||||
>
|
||||
{(queryData: QueryResult<TData, TVariables>) => {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { DialogContentText, IconButton } from "@material-ui/core";
|
||||
import DeleteIcon from "@material-ui/icons/Delete";
|
||||
import { useUser } from "@saleor/auth";
|
||||
import ActionDialog from "@saleor/components/ActionDialog";
|
||||
import { configurationMenuUrl } from "@saleor/configuration";
|
||||
import useBulkActions from "@saleor/hooks/useBulkActions";
|
||||
|
@ -11,7 +12,6 @@ import usePaginator, {
|
|||
createPaginationState
|
||||
} from "@saleor/hooks/usePaginator";
|
||||
import useShop from "@saleor/hooks/useShop";
|
||||
import useUser from "@saleor/hooks/useUser";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { getStringOrPlaceholder, maybe } from "@saleor/misc";
|
||||
import { getById } from "@saleor/orders/components/OrderReturnPage/utils";
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { DialogContentText } from "@material-ui/core";
|
||||
import { useUser } from "@saleor/auth";
|
||||
import ActionDialog from "@saleor/components/ActionDialog";
|
||||
import NotFoundPage from "@saleor/components/NotFoundPage";
|
||||
import { WindowTitle } from "@saleor/components/WindowTitle";
|
||||
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import useNotifier from "@saleor/hooks/useNotifier";
|
||||
import useUser from "@saleor/hooks/useUser";
|
||||
import { commonMessages, errorMessages } from "@saleor/intl";
|
||||
import { getStringOrPlaceholder, maybe } from "@saleor/misc";
|
||||
import usePermissionGroupSearch from "@saleor/searches/usePermissionGroupSearch";
|
||||
|
|
|
@ -6,14 +6,12 @@ export const UserDecorator = (user: User) => storyFn => (
|
|||
<UserContext.Provider
|
||||
value={{
|
||||
login: undefined,
|
||||
loginByExternalPlugin: undefined,
|
||||
loginByToken: undefined,
|
||||
logout: undefined,
|
||||
requestLoginByExternalPlugin: undefined,
|
||||
tokenAuthLoading: false,
|
||||
tokenRefresh: undefined,
|
||||
tokenVerifyLoading: false,
|
||||
user
|
||||
loginByExternalPlugin: undefined,
|
||||
logout: undefined,
|
||||
user,
|
||||
authenticated: false,
|
||||
authenticating: false
|
||||
}}
|
||||
>
|
||||
{storyFn()}
|
||||
|
|
|
@ -36087,7 +36087,7 @@ exports[`Storyshots Views / Authentication / Log in default 1`] = `
|
|||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<button
|
||||
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-outlined-id MuiButton-outlinedPrimary-id MuiButton-outlinedSizeLarge-id MuiButton-sizeLarge-id MuiButton-fullWidth-id"
|
||||
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-outlined-id MuiButton-outlinedPrimary-id MuiButton-fullWidth-id"
|
||||
data-test="external-authentication"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
|
@ -36244,7 +36244,7 @@ exports[`Storyshots Views / Authentication / Log in disabled 1`] = `
|
|||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<button
|
||||
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-outlined-id MuiButton-outlinedPrimary-id MuiButton-outlinedSizeLarge-id MuiButton-sizeLarge-id MuiButton-disabled-id MuiButton-fullWidth-id MuiButtonBase-disabled-id"
|
||||
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-outlined-id MuiButton-outlinedPrimary-id MuiButton-disabled-id MuiButton-fullWidth-id MuiButtonBase-disabled-id"
|
||||
data-test="external-authentication"
|
||||
disabled=""
|
||||
tabindex="-1"
|
||||
|
@ -36262,7 +36262,171 @@ exports[`Storyshots Views / Authentication / Log in disabled 1`] = `
|
|||
</div>
|
||||
`;
|
||||
|
||||
exports[`Storyshots Views / Authentication / Log in error 1`] = `
|
||||
exports[`Storyshots Views / Authentication / Log in error external login 1`] = `
|
||||
<div
|
||||
style="padding:24px"
|
||||
>
|
||||
<div
|
||||
class="MuiPaper-root-id MuiCard-root-id MuiPaper-elevation0-id MuiPaper-rounded-id"
|
||||
style="margin:auto;overflow:visible;position:relative;width:400px"
|
||||
>
|
||||
<div
|
||||
class="MuiCardContent-root-id"
|
||||
>
|
||||
<form>
|
||||
<div
|
||||
class="LoginCard-panel-id"
|
||||
data-test="loginErrorMessage"
|
||||
>
|
||||
<div
|
||||
class="MuiTypography-root-id MuiTypography-caption-id"
|
||||
>
|
||||
Sorry, login went wrong. Please try again.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="MuiFormControl-root-id MuiTextField-root-id MuiFormControl-fullWidth-id"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root-id MuiInputLabel-root-id MuiInputLabel-formControl-id MuiInputLabel-animated-id MuiInputLabel-outlined-id"
|
||||
data-shrink="false"
|
||||
>
|
||||
E-mail Address
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root-id MuiOutlinedInput-root-id MuiInputBase-fullWidth-id MuiInputBase-formControl-id"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
autocomplete="username"
|
||||
autofocus=""
|
||||
class="MuiInputBase-input-id MuiOutlinedInput-input-id"
|
||||
data-test="email"
|
||||
name="email"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-id MuiOutlinedInput-notchedOutline-id"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-id"
|
||||
>
|
||||
<span>
|
||||
E-mail Address
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<div
|
||||
class="MuiFormControl-root-id MuiTextField-root-id MuiFormControl-fullWidth-id"
|
||||
>
|
||||
<label
|
||||
class="MuiFormLabel-root-id MuiInputLabel-root-id MuiInputLabel-formControl-id MuiInputLabel-animated-id MuiInputLabel-outlined-id"
|
||||
data-shrink="false"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<div
|
||||
class="MuiInputBase-root-id MuiOutlinedInput-root-id MuiInputBase-fullWidth-id MuiInputBase-formControl-id"
|
||||
>
|
||||
<input
|
||||
aria-invalid="false"
|
||||
autocomplete="password"
|
||||
class="MuiInputBase-input-id MuiOutlinedInput-input-id"
|
||||
data-test="password"
|
||||
name="password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<fieldset
|
||||
aria-hidden="true"
|
||||
class="PrivateNotchedOutline-root-id MuiOutlinedInput-notchedOutline-id"
|
||||
>
|
||||
<legend
|
||||
class="PrivateNotchedOutline-legendLabelled-id"
|
||||
>
|
||||
<span>
|
||||
Password
|
||||
</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<div
|
||||
class="LoginCard-buttonContainer-id"
|
||||
>
|
||||
<button
|
||||
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id LoginCard-loginButton-id MuiButton-containedPrimary-id"
|
||||
data-test="submit"
|
||||
tabindex="0"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="MuiButton-label-id"
|
||||
>
|
||||
Login
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<div
|
||||
class="MuiTypography-root-id MuiTypography-body1-id"
|
||||
>
|
||||
Forgot password?
|
||||
<a
|
||||
class="LoginCard-link-id"
|
||||
data-test-id="reset-password-link"
|
||||
>
|
||||
Use this link to recover it
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<hr
|
||||
class="MuiDivider-root-id"
|
||||
/>
|
||||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<div
|
||||
class="MuiTypography-root-id MuiTypography-body1-id"
|
||||
>
|
||||
or login using
|
||||
</div>
|
||||
<div
|
||||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<button
|
||||
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-outlined-id MuiButton-outlinedPrimary-id MuiButton-fullWidth-id"
|
||||
data-test="external-authentication"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="MuiButton-label-id"
|
||||
>
|
||||
Example auth plugin
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Storyshots Views / Authentication / Log in error login 1`] = `
|
||||
<div
|
||||
style="padding:24px"
|
||||
>
|
||||
|
@ -36409,7 +36573,7 @@ exports[`Storyshots Views / Authentication / Log in error 1`] = `
|
|||
class="FormSpacer-spacer-id"
|
||||
/>
|
||||
<button
|
||||
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-outlined-id MuiButton-outlinedPrimary-id MuiButton-outlinedSizeLarge-id MuiButton-sizeLarge-id MuiButton-fullWidth-id"
|
||||
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-outlined-id MuiButton-outlinedPrimary-id MuiButton-fullWidth-id"
|
||||
data-test="external-authentication"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { User } from "@saleor/fragments/types/User";
|
||||
import { LoginData } from "@saleor/sdk";
|
||||
|
||||
export const isSupported = !!(
|
||||
navigator?.credentials?.preventSilentAccess && window.PasswordCredential
|
||||
|
@ -22,14 +22,13 @@ export async function login<T>(
|
|||
}
|
||||
|
||||
export function saveCredentials(
|
||||
user: User,
|
||||
user: LoginData["user"],
|
||||
password: string
|
||||
): Promise<CredentialType | null> {
|
||||
let result: Promise<CredentialType | null>;
|
||||
|
||||
if (isSupported) {
|
||||
const cred = new PasswordCredential({
|
||||
iconURL: user.avatar ? user.avatar.url : undefined,
|
||||
id: user.email,
|
||||
name: user.firstName ? `${user.firstName} ${user.lastName}` : undefined,
|
||||
password
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { AccountErrorFragment } from "@saleor/fragments/types/AccountErrorFragment";
|
||||
import { SetPasswordData } from "@saleor/sdk";
|
||||
import { AccountErrorCode } from "@saleor/types/globalTypes";
|
||||
import { defineMessages, IntlShape } from "react-intl";
|
||||
|
||||
|
@ -31,10 +31,11 @@ const messages = defineMessages({
|
|||
}
|
||||
});
|
||||
|
||||
function getAccountErrorMessage(
|
||||
err: AccountErrorFragment,
|
||||
intl: IntlShape
|
||||
): string {
|
||||
interface ErrorFragment {
|
||||
code: AccountErrorCode | SetPasswordData["errors"][number]["code"];
|
||||
}
|
||||
|
||||
function getAccountErrorMessage(err: ErrorFragment, intl: IntlShape): string {
|
||||
if (err) {
|
||||
switch (err.code) {
|
||||
case AccountErrorCode.INVALID_PASSWORD:
|
||||
|
|
|
@ -17,7 +17,7 @@ type CommonErrorCode = "GRAPHQL_ERROR" | "INVALID" | "REQUIRED";
|
|||
|
||||
interface CommonError<ErrorCode> {
|
||||
code: ErrorCode | CommonErrorCode;
|
||||
field: string | null;
|
||||
field?: string | null;
|
||||
}
|
||||
|
||||
export function getCommonFormFieldErrorMessage<ErrorCode>(
|
||||
|
|
|
@ -7,14 +7,7 @@ function getStaffErrorMessage(
|
|||
err: StaffErrorFragment,
|
||||
intl: IntlShape
|
||||
): string {
|
||||
return getAccountErrorMessage(
|
||||
err && {
|
||||
...err,
|
||||
__typename: "AccountError",
|
||||
addressType: null
|
||||
},
|
||||
intl
|
||||
);
|
||||
return getAccountErrorMessage(err, intl);
|
||||
}
|
||||
|
||||
export default getStaffErrorMessage;
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import NodeHttpAdapter from "@pollyjs/adapter-node-http";
|
||||
import { Polly } from "@pollyjs/core";
|
||||
import FSPersister from "@pollyjs/persister-fs";
|
||||
import { createFetch } from "@saleor/sdk";
|
||||
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";
|
||||
|
||||
|
@ -36,8 +36,7 @@ function setupApi() {
|
|||
});
|
||||
const cache = new InMemoryCache();
|
||||
const link = new BatchHttpLink({
|
||||
// @ts-ignore
|
||||
fetch,
|
||||
fetch: createFetch(),
|
||||
uri: process.env.API_URI || "http://localhost:8000/graphql/"
|
||||
});
|
||||
const apolloClient = new ApolloClient({
|
||||
|
|
Loading…
Reference in a new issue