├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── qa.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── apollo.config.js ├── codegen-introspection.yml ├── codegen.yml ├── introspection.json ├── local-schema.graphql ├── package-lock.json ├── package.json ├── recordings └── user-api_3210680802 │ ├── creates-the-user-account-address_3774855855 │ └── recording.har │ ├── sends-a-request-to-delete-user-account_1456582714 │ └── recording.har │ ├── sends-request-to-change-user-email_1148528273 │ └── recording.har │ ├── sets-address-as-a-default-billing-address_3543212209 │ └── recording.har │ └── updates-the-user-first-name_3574048358 │ └── recording.har ├── schema.graphql ├── src ├── apollo │ ├── apollo-helpers.ts │ ├── client.ts │ ├── fragments.ts │ ├── index.ts │ ├── mutations.ts │ ├── queries.ts │ └── types.ts ├── config.ts ├── constants.ts ├── core │ ├── auth.ts │ ├── constants.ts │ ├── createSaleorClient.ts │ ├── helpers.ts │ ├── index.ts │ ├── state.ts │ ├── storage.ts │ ├── types.ts │ └── user.ts ├── helpers.ts ├── index.ts └── react │ ├── components │ ├── SaleorProvider.tsx │ └── index.ts │ ├── helpers │ ├── hookFactory.ts │ └── hookStateFactory.ts │ ├── hooks │ ├── auth.ts │ ├── index.ts │ ├── saleorConfig.ts │ └── user.ts │ ├── index.ts │ └── tests │ └── .gitkeep ├── test ├── auth.test.ts ├── authAutoTokenRefresh.test.ts ├── mocks │ ├── accountUpdate.ts │ ├── externalAuthenticationUrl.ts │ ├── externalLogout.ts │ ├── externalObtainAccessTokens.ts │ ├── externalRefresh.ts │ ├── externalVerify.ts │ ├── index.ts │ ├── login.ts │ ├── passwordChange.ts │ ├── refreshToken.ts │ ├── register.ts │ ├── requestPasswordReset.ts │ └── verifyToken.ts ├── setup.ts ├── user.test.ts └── utils.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | API_URI= 2 | TEST_AUTH_EMAIL= 3 | TEST_AUTH_PASSWORD= 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | .env 6 | .envrc 7 | src/apollo/types.ts 8 | src/apollo/apollo-helpers.ts 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | "react-app", 4 | "plugin:@typescript-eslint/recommended", 5 | "prettier/@typescript-eslint", 6 | "plugin:prettier/recommended", 7 | ], 8 | parser: "@typescript-eslint/parser", 9 | plugins: ["@typescript-eslint/eslint-plugin", "eslint-plugin-tsdoc"], 10 | rules: { 11 | "tsdoc/syntax": "warn", 12 | }, 13 | settings: { 14 | react: { 15 | version: "detect", 16 | }, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /.github/workflows/qa.yml: -------------------------------------------------------------------------------- 1 | name: QA 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | check-lock: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v3 11 | with: 12 | node-version-file: ".nvmrc" 13 | - name: Validate lock file 14 | run: | 15 | npx lockfile-lint --path package-lock.json --allowed-hosts npm yarn 16 | checks-and-linters: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: actions/setup-node@v3 21 | with: 22 | node-version-file: ".nvmrc" 23 | - name: Cache node modules 24 | uses: actions/cache@v2 25 | env: 26 | cache-name: cache-node-modules 27 | with: 28 | # npm cache files are stored in `~/.npm` on Linux/macOS 29 | path: ~/.npm 30 | key: ${{ runner.os }}-qa-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 31 | restore-keys: | 32 | ${{ runner.os }}-qa-${{ env.cache-name }}- 33 | ${{ runner.os }}-qa- 34 | ${{ runner.os }}- 35 | - name: Install dependencies 36 | run: | 37 | npm ci 38 | - name: Run lint 39 | run: | 40 | npm run lint 41 | - name: Check size limit 42 | uses: andresz1/size-limit-action@v1 43 | with: 44 | github_token: ${{ secrets.GITHUB_TOKEN }} 45 | 46 | jest-tests: 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v2 50 | - uses: actions/setup-node@v3 51 | with: 52 | node-version-file: ".nvmrc" 53 | - name: Cache node modules 54 | uses: actions/cache@v2 55 | env: 56 | cache-name: cache-node-modules 57 | with: 58 | # npm cache files are stored in `~/.npm` on Linux/macOS 59 | path: ~/.npm 60 | key: ${{ runner.os }}-qa-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 61 | restore-keys: | 62 | ${{ runner.os }}-qa-${{ env.cache-name }}- 63 | ${{ runner.os }}-qa- 64 | ${{ runner.os }}- 65 | - name: Install deps 66 | run: | 67 | npm ci 68 | - name: Run jest 69 | run: | 70 | npm run test 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | .env 6 | .envrc 7 | *.tgz 8 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm test 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 14 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | types/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Saleor Commerce 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Saleor SDK - DEPRECATED

3 |
4 | 5 | This package contains methods providing Saleor business logic for a storefront and apps. It handles Saleor GraphQL queries and mutations, manages Apollo cache, and provides an internal state to manage popular storefront use cases, like user authentication. 6 | 7 | > :warning: **Note: Saleor SDK is DEPRECATED. [See the announcement](https://github.com/saleor/saleor/discussions/12891).** 8 | > 9 | > **For authentication, follow the [Saleor docs](https://docs.saleor.io/docs/3.x/api-usage/authentication) and use [@saleor/auth-sdk](https://www.npmjs.com/package/@saleor/auth-sdk).** 10 | 11 | ## Table of Contents 12 | 13 | - [Setup](#setup) 14 | - [Features](#features) 15 | - [Local development](#local-development) 16 | 17 | ## Setup 18 | 19 | There are two ways to use SDK - making custom implementation or using React components and hooks, which already has that implementation ready. 20 | 21 | ### Using React 22 | 23 | First install following dependencies to your project 24 | 25 | ```bash 26 | npm install @saleor/sdk @apollo/client graphql 27 | ``` 28 | 29 | Use `SaleorProvider` with passed Saleor's client created by `createSaleorClient` in a prop. Then use React hooks in any component passed as child to `SaleorProvider`. 30 | 31 | ```tsx 32 | import { 33 | SaleorProvider, 34 | createSaleorClient, 35 | useAuth, 36 | useAuthState, 37 | } from "@saleor/sdk"; 38 | 39 | const client = createSaleorClient({ 40 | apiUrl: "", 41 | channel: "", 42 | }); 43 | 44 | const rootElement = document.getElementById("root"); 45 | ReactDOM.render( 46 | 47 | 48 | , 49 | rootElement 50 | ); 51 | 52 | const App = () => { 53 | const { login } = useAuth(); 54 | const { authenticated, user, authenticating } = useAuthState(); 55 | 56 | const handleSignIn = async () => { 57 | const { data } = await login({ 58 | email: "admin@example.com", 59 | password: "admin", 60 | }); 61 | 62 | if (data.tokenCreate.errors.length > 0) { 63 | /** 64 | * Unable to sign in. 65 | **/ 66 | } else if (data) { 67 | /** 68 | * User signed in successfully. 69 | **/ 70 | } 71 | }; 72 | 73 | // Important: wait till the authentication process finishes 74 | if (authenticating) { 75 | return
Loading...
76 | } 77 | 78 | if (authenticated && user) { 79 | return Signed in as {user.email}; 80 | } 81 | 82 | return ; 83 | }; 84 | ``` 85 | 86 | ### Using with NodeJS and other frameworks 87 | 88 | ```bash 89 | npm install @saleor/sdk @apollo/client graphql 90 | ``` 91 | 92 | Then use `createSaleorClient` to get Saleor api methods and internal config variables like channel and Apollo client. 93 | 94 | ```tsx 95 | import { createSaleorClient } from "@saleor/sdk"; 96 | 97 | const client = createSaleorClient({ 98 | apiUrl: "", 99 | channel: "", 100 | }); 101 | 102 | const { auth, config, _internal } = client; 103 | ``` 104 | 105 | Finally, API methods can be used: 106 | 107 | ```tsx 108 | const { data } = await auth.login({ 109 | email: "admin@example.com", 110 | password: "admin", 111 | }); 112 | 113 | if (data.tokenCreate.errors.length > 0) { 114 | /** 115 | * Unable to sign in. 116 | **/ 117 | } else if (data) { 118 | /** 119 | * User signed in successfully. 120 | **/ 121 | const userData = api.auth.tokenCreate.user; 122 | } 123 | ``` 124 | 125 | ### Custom HTTP communication with SDK authorization 126 | 127 | SDK provides fetch method with all the necessary authorization headers assigned to HTTP requests and handled authorization token creation, verification and refresh inside, which you may use instead of built-in browser fetch. Using SDK `auth` login or logout methods will appropriately alter fetch behavior automatically, like including Authorization Bearer header in HTTP request. 128 | 129 | ```tsx 130 | import { createFetch } from "@saleor/sdk"; 131 | 132 | const authorizedFetch = createFetch(); 133 | 134 | const result = await authorizedFetch("https://example.com"); 135 | ``` 136 | 137 | If you use GraphQL you may pass SDK fetch to the Apollo client: 138 | 139 | ```tsx 140 | const authorizationLink = new HttpLink({ 141 | fetch: authorizedFetch, 142 | }); 143 | const apolloClient = new ApolloClient({ 144 | link: authorizationLink, 145 | }); 146 | ``` 147 | 148 | SDK fetch method uses [cross-fetch](https://github.com/lquixada/cross-fetch) under the hood. 149 | 150 | ## Features 151 | 152 | We provide an API with methods and fields, performing one, scoped type of work. You may access them straight from `createSaleorClient()` or use React hooks: 153 | 154 | | API object | React hook | Description | 155 | | :----------- | :------------------ | :----------------------------------------------------------------------- | 156 | | `getState()` | `useAuthState()` | Returns current SDK state: `user`, `authenticated` and `authenticating`. | 157 | | `auth` | `useAuth()` | Handles user authentication methods. | 158 | | `user` | `useUser()` | Handles user account methods. | 159 | | `config` | `useSaleorConfig()` | Handles SDK configuration. | 160 | 161 | SDK supports OpenId Connect methods provided by Saleor API. They are under `auth` object and `useAuth` hook. For more usage details, please check https://docs.saleor.io/docs/3.0/developer/available-plugins/openid-connect. 162 | 163 | ## Local development 164 | 165 | Our aim it to build SDK, highly configurable, as a separate package, which you will not require modifications. Although if you want to alter the project, especially if you want to contribute, it is possible to develop storefront and SDK simultaneously. To do this, you need 166 | to link it to the storefront's project. 167 | 168 | ```bash 169 | $ cd lib 170 | $ npm link 171 | $ cd 172 | $ npm link @saleor/sdk 173 | ``` 174 | 175 | Notice that in [our example storefront](https://github.com/mirumee/saleor-storefront) 176 | webpack is configured to always resolve `react` to `./node_modules/react`. It may 177 | seem redundant for the most use cases, but helps in sdk's local development, because 178 | it overcomes `npm`'s limitations regarding peer dependencies hoisting, explicitly 179 | telling webpack to always have one and only copy of `react`. 180 | 181 | ### Configuration 182 | 183 | Set environment variables: 184 | 185 | ```bash 186 | export API_URI=https://your.saleor.instance.com/graphql/ 187 | export TEST_AUTH_EMAIL=admin@example.com 188 | export TEST_AUTH_PASSWORD=admin 189 | ``` 190 | 191 | ### Development 192 | 193 | 1. Download repository 194 | 2. Install dependencies: `npm i` 195 | 3. Now you can start files watcher with: `npm run start` 196 | 197 | ### Production build 198 | 199 | ```bash 200 | npm run build 201 | ``` 202 | 203 | ### Tests 204 | 205 | Tests are located at `/test` directory. To start the test suite: 206 | 207 | ```bash 208 | npm run test 209 | ``` 210 | 211 | All communication with API is prerecorded using [Polly.JS](https://netflix.github.io/pollyjs/#/README). Unless requests changed or code executes new ones, no requests to API will be made. 212 | 213 | Changes in `/recordings` directory should be reviewed before committing to make sure that changes in communication are intentional. 214 | 215 | ### Code quality 216 | 217 | The project has configured Prettier and ESLint. To check your code: 218 | 219 | ```bash 220 | npm run lint 221 | ``` 222 | 223 | ### Fetch current GraphQL schema 224 | 225 | ```bash 226 | npm run download-schema 227 | ``` 228 | 229 | Command will overwrite `introspection.json` with server schema, based on `API_URL` variable. 230 | 231 | ### Updating TS types 232 | 233 | GraphQL Code Generator is an automatic tool that converts schema to TS types. After changing schema file run: 234 | 235 | ```bash 236 | npm run build-types 237 | ``` 238 | 239 | ### Updating recordings 240 | 241 | Changes in the core user methods and tests may result in failing tests. Typically you may encounter the following error: 242 | 243 | ```bash 244 | request to http://localhost:8000/graphql/ failed, reason: connect ECONNREFUSED 127.0.0.1:8000 245 | ``` 246 | 247 | To fix this run `npm run test` with the following variables: 248 | 249 | - `API_URI` 250 | - `TEST_AUTH_EMAIL` 251 | - `TEST_AUTH_PASSWORD` 252 | 253 | After the tests run the recordings should be updated. Next time you run tests without variables, tests will use updated recordings. 254 | -------------------------------------------------------------------------------- /apollo.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | client: { 3 | includes: ["src/**/*.ts"], 4 | name: "sdk", 5 | service: { 6 | localSchemaFile: "schema.graphql", 7 | name: "saleor", 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /codegen-introspection.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: ${API_URI} 3 | generates: 4 | introspection.json: 5 | plugins: 6 | - introspection 7 | config: 8 | minify: false 9 | -------------------------------------------------------------------------------- /codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: 3 | - introspection.json 4 | - local-schema.graphql 5 | documents: src/apollo/*.ts 6 | generates: 7 | src/apollo/types.ts: 8 | schema: introspection.json 9 | plugins: 10 | - typescript 11 | - typescript-operations 12 | config: 13 | dedupeOperationSuffix: true 14 | enumsAsTypes: true 15 | skipTypename: true 16 | avoidOptionals: 17 | field: true 18 | inputValue: false 19 | object: false 20 | defaultValue: false 21 | src/apollo/apollo-helpers.ts: 22 | plugins: 23 | - typescript-apollo-client-helpers 24 | -------------------------------------------------------------------------------- /local-schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | authenticated: Boolean! 3 | authenticating: Boolean! 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.7.0", 3 | "license": "MIT", 4 | "homepage": "https://saleor.io/", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/mirumee/saleor-sdk.git" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/mirumee/saleor-sdk/issues" 11 | }, 12 | "keywords": [ 13 | "saleor", 14 | "sdk", 15 | "@saleor/sdk", 16 | "react", 17 | "typescript" 18 | ], 19 | "main": "dist/index.js", 20 | "typings": "dist/index.d.ts", 21 | "files": [ 22 | "dist", 23 | "src" 24 | ], 25 | "engines": { 26 | "node": ">=10" 27 | }, 28 | "scripts": { 29 | "start": "npm run build-types && tsdx watch", 30 | "build": "npm run build-types && tsdx build", 31 | "test": "tsdx test", 32 | "test:watch": "tsdx test --watch", 33 | "lint": "tsdx lint src test", 34 | "prepare": "npm run build && husky install", 35 | "size": "size-limit", 36 | "analyze": "size-limit --why", 37 | "build-types": "graphql-codegen --config codegen.yml", 38 | "download-schema": "graphql-codegen --config codegen-introspection.yml", 39 | "release": "np" 40 | }, 41 | "peerDependencies": { 42 | "@apollo/client": "^3.3.19", 43 | "graphql": "^15.5.0", 44 | "react": "^17.0.0 || ^18.0.0" 45 | }, 46 | "husky": { 47 | "hooks": { 48 | "pre-commit": "tsdx lint" 49 | } 50 | }, 51 | "prettier": { 52 | "printWidth": 80, 53 | "semi": true, 54 | "singleQuote": false, 55 | "trailingComma": "es5" 56 | }, 57 | "np": { 58 | "yarn": false 59 | }, 60 | "name": "@saleor/sdk", 61 | "author": "Saleor Commerce", 62 | "description": "Saleor TS SDK", 63 | "module": "dist/sdk.esm.js", 64 | "private": false, 65 | "publishConfig": { 66 | "access": "public" 67 | }, 68 | "size-limit": [ 69 | { 70 | "path": "dist/sdk.cjs.production.min.js", 71 | "limit": "11 KB" 72 | }, 73 | { 74 | "path": "dist/sdk.esm.js", 75 | "limit": "11 KB" 76 | } 77 | ], 78 | "devDependencies": { 79 | "@apollo/client": "^3.3.19", 80 | "@graphql-codegen/cli": "1.21.5", 81 | "@graphql-codegen/introspection": "^1.18.2", 82 | "@graphql-codegen/typescript": "^1.22.3", 83 | "@graphql-codegen/typescript-apollo-client-helpers": "^1.1.9", 84 | "@graphql-codegen/typescript-operations": "^1.18.2", 85 | "@pollyjs/adapter-node-http": "^5.1.1", 86 | "@pollyjs/core": "^5.1.1", 87 | "@pollyjs/persister-fs": "^5.1.1", 88 | "@size-limit/preset-small-lib": "^4.10.2", 89 | "@types/omit-deep-lodash": "^1.1.1", 90 | "@types/pollyjs__adapter-node-http": "^2.0.1", 91 | "@types/pollyjs__core": "^4.3.2", 92 | "@types/pollyjs__persister-fs": "^2.0.1", 93 | "@types/react": "^18.2.7", 94 | "@types/setup-polly-jest": "^0.5.1", 95 | "eslint-plugin-tsdoc": "^0.2.14", 96 | "graphql": "^15.5.1", 97 | "husky": "^6.0.0", 98 | "jest": "^27.0.5", 99 | "jsonwebtoken": "^8.5.1", 100 | "msw": "^0.35.0", 101 | "np": "^7.5.0", 102 | "omit-deep-lodash": "^1.1.5", 103 | "react": "^18.2.0", 104 | "react-dom": "^18.2.0", 105 | "setup-polly-jest": "^0.9.1", 106 | "size-limit": "^4.10.2", 107 | "ts-jest": "^27.0.3", 108 | "tsdx": "^0.14.1", 109 | "tslib": "^2.3.1", 110 | "typescript": "^4.2.4" 111 | }, 112 | "dependencies": { 113 | "cross-fetch": "^3.1.4", 114 | "jwt-decode": "^3.1.2" 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /recordings/user-api_3210680802/creates-the-user-account-address_3774855855/recording.har: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "_recordingName": "user api/creates the user account address", 4 | "creator": { 5 | "comment": "persister:fs", 6 | "name": "Polly.JS", 7 | "version": "5.1.1" 8 | }, 9 | "entries": [ 10 | { 11 | "_id": "f7df852e2c12565c12accbd3188ba3ef", 12 | "_order": 0, 13 | "cache": {}, 14 | "request": { 15 | "bodySize": 1326, 16 | "cookies": [], 17 | "headers": [ 18 | { 19 | "_fromType": "array", 20 | "name": "accept", 21 | "value": "*/*" 22 | }, 23 | { 24 | "_fromType": "array", 25 | "name": "content-type", 26 | "value": "application/json" 27 | }, 28 | { 29 | "_fromType": "array", 30 | "name": "content-length", 31 | "value": "1326" 32 | }, 33 | { 34 | "_fromType": "array", 35 | "name": "user-agent", 36 | "value": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)" 37 | }, 38 | { 39 | "_fromType": "array", 40 | "name": "accept-encoding", 41 | "value": "gzip,deflate" 42 | }, 43 | { 44 | "_fromType": "array", 45 | "name": "connection", 46 | "value": "close" 47 | }, 48 | { 49 | "name": "host", 50 | "value": "master.staging.saleor.cloud" 51 | } 52 | ], 53 | "headersSize": 999, 54 | "httpVersion": "HTTP/1.1", 55 | "method": "POST", 56 | "postData": { 57 | "mimeType": "application/json", 58 | "params": [], 59 | "text": "{\"operationName\":\"login\",\"variables\":{},\"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 UserBaseFragment on User {\\n id\\n email\\n firstName\\n lastName\\n isStaff\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n __typename\\n}\\n\\nfragment UserDetailsFragment on User {\\n ...UserBaseFragment\\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 token\\n refreshToken\\n errors {\\n ...AccountErrorFragment\\n __typename\\n }\\n user {\\n ...UserDetailsFragment\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}" 60 | }, 61 | "queryString": [], 62 | "url": "https://master.staging.saleor.cloud/graphql/" 63 | }, 64 | "response": { 65 | "bodySize": 1611, 66 | "content": { 67 | "_isBinary": true, 68 | "mimeType": "application/json", 69 | "size": 1611, 70 | "text": "[\"1f8b0800438a466402ffd597598fabc815c7bf8ae5d74cb70c5eda9ea760b3db2c66876884ca509832aba1b00d57fddd03dd7d3373758934e3240fb164446dbfffa9c339b57c9b860083e9af936f535c2430df551060f87bb97f9bc2568c4f5c801424ea66271032126a21c3a5b71356c28541879d38830e35b4ef2de288a2e36b3fa4749d23522e0ca9d04227d3474232827ed8667ecaac56b8142820d9dab337ad80ee08387237d4015e9b05bcb43ab41bec3af2cc738426986bb147a68dd72e3bd77ea4a7b9d0b8e4e316dac79b3717e3808b1fa7dd7a3023853cf547cdc58766aedd00693503df75920764ce33632e10a623ca83b6671331b0ef83f9bddea0297612c249af5fb8d9260e328dfa6eeb214b6fc398904fef9ede4f3f632f2e697541dbbb24b7ba7e6c04ece3c0b26d6659bb6479830c7105f360b0af0c884d17726ce6216115f222e139b357dab745853ab4abc395704a917cd11f5e7a401b42c1197b179a68c3524b17a17b73edea069db30b8d8f30770fda528c9c8cbe2e96415df2b7d9da9cdbb4292a1bf91077d7e0adc864113a3e0e9a0d936eb7a86eee5b3f784bd9b673d3985c11fb78b1bb05b26430fe6a76aab1be219b18ce926641e7f9b2c813555e6f90759652ade6370707091c3fb38344c998e6905c5abd65d8f801d47ccbdfa95393650afb60faf030222ef76759b2b48abcf5d5b39b1a731bc79bc7f1edb8b4aed1ad3b86dbbce15ab54ea32c8a1fdc125df292bdf34615472e94fcb3a05f82cdd25898519b5e289aa26c7a7513e1baf38c38505649a5789785c3d3067830fa9b9abfc99e1cf25b3b8ecdc763b5de9fa7bf4ca6158c2a58c7c6ff75201f9752e712fff340cec5d4ebff01791ef48980b45a874c9341cb4a63e5c4c9d7d33c964186e75ff38c3ee693b948c9b536b4cdde277217649e7922717a42834b6555b7350493e461e5726cdab3b99bb192370f6d3d0f771a1f679213cfa025de4ec43683a6d58034dcba99665bbc968054c3411ef380f576dec5128fb69585b37af82ce7487323b3f197d28a5e351ce479755356cafa82ce7e9c2d3adfedc475656f48c7d9161c4b35a2b528819b9dfdb746f3489d13e66b5abe396e27afed9038eb7b71f772509819129d4827ae6eb4040532fdbbdb28068fed95aaa2d53ccdc1a2c5ea3d774d2361e60fb9bdde4d31ebc0fc5623d65d6b5282624f9f3194e69c36812996d13ea25d5b114d25b77979161e68cf278ab3ff22cf95addd35ac485bbb644fc253ec324abbe2c83dc7946c2ad817bc5cea69b6ba54ec9abb022ab788f36351eccb3e71c085b35f02caabe692e174c79d73cb495e7bbb76a4e1267b2831ab3832dacb92dde2bb24c22d41d27721c5b16017b75a7a4936c15556c2d9f2b8306e3619aa720bec9addde879c81555554759f2dfff8ad2f3535ac3e3603140e096439721a5c8a8774493e3b6700a54303863586554dfe2d04757c2a4015febd06292caa57540c1d2354d5b80f9e6167990e1529f8b18c6a1d8328ea8bb86ae097b20aab0cd5352af20f83be4d8322fc18c153327d60fc1dcfecf68a69e80321ffa2f1200f5338096218244583eba1cdf7715bc2ef3dcc1fd0d3f75f263f9155ca951879145c823683f9935c8372983128060ff8178982a4329aaec854bf7a9b3aa3fd912a6465ff358abcdfca2783235fff1a5aa2648a637c4a557fb055023938c30928cbfa29deae7782cc1cc698410cf21ca6f5eb93e09138f817f9a938f822d382be534c79943c04783de9bfdfe45634bd4cf5a4f59cc01afe8ed2e8118d338af024e8d3e949761fc4e698ed39b8a133c0fdb0e7b8cab68f388bda0a07c17047f8c5a967dcc009a508b7cf2968741fde63e82a7cdad56aff186396fde33f40fa86ab32badfa7b84f1986266c4de3dfca4c06fe67d8008c2b746af0d3d2079313e451a1b439a3fcb9905735853677a3115f5645d804f859733fc17fda599f62ff457fe98c610832379acb1063949f9f05f382aaf6e431708ccab2273f093628961da30e7be573c89f76a02fe4133bd077a246c9fa8132046534167105f23afd586b9e74efb0bb8d2eed4d8d8beccf2d06bf8df618c6651083af3bf0c7912784116852ac7f7d382a0cfb3bc470fec89b34fdbd7d8bd274ac197c56c0cf23d4fb4fb29f17eccf2bc9fbd00c1f18e6dfcf38c3dc6bfcf152c16b339caac26303ab76f7594d0c068307ca9a8cbaf5672f704a07ea72d6ffdedfdfff0930f1db54ce0f0000\"]" 71 | }, 72 | "cookies": [], 73 | "headers": [ 74 | { 75 | "name": "content-type", 76 | "value": "application/json" 77 | }, 78 | { 79 | "name": "content-length", 80 | "value": "1611" 81 | }, 82 | { 83 | "name": "connection", 84 | "value": "close" 85 | }, 86 | { 87 | "name": "server", 88 | "value": "CloudFront" 89 | }, 90 | { 91 | "name": "date", 92 | "value": "Mon, 24 Apr 2023 13:55:17 GMT" 93 | }, 94 | { 95 | "name": "referrer-policy", 96 | "value": "same-origin" 97 | }, 98 | { 99 | "name": "x-content-type-options", 100 | "value": "nosniff" 101 | }, 102 | { 103 | "name": "content-encoding", 104 | "value": "gzip" 105 | }, 106 | { 107 | "name": "vary", 108 | "value": "Origin, Access-Control-Request-Headers, Access-Control-Request-Method" 109 | }, 110 | { 111 | "name": "x-xss-protection", 112 | "value": "1" 113 | }, 114 | { 115 | "name": "x-frame-options", 116 | "value": "DENY" 117 | }, 118 | { 119 | "name": "strict-transport-security", 120 | "value": "max-age=31536000; includeSubDomains" 121 | }, 122 | { 123 | "name": "x-cache", 124 | "value": "Miss from cloudfront" 125 | }, 126 | { 127 | "name": "via", 128 | "value": "1.1 84f381696dd33e92960b92250106e464.cloudfront.net (CloudFront)" 129 | }, 130 | { 131 | "name": "x-amz-cf-pop", 132 | "value": "FRA56-C2" 133 | }, 134 | { 135 | "name": "x-amz-cf-id", 136 | "value": "t8rcn2wnwzCALWZwYOOc8L5r6e8nXCv-_KDluibnbn3s5dy25NufXg==" 137 | } 138 | ], 139 | "headersSize": 1525, 140 | "httpVersion": "HTTP/1.1", 141 | "redirectURL": "", 142 | "status": 200, 143 | "statusText": "OK" 144 | }, 145 | "startedDateTime": "2023-04-24T13:55:14.961Z", 146 | "time": 2709, 147 | "timings": { 148 | "blocked": -1, 149 | "connect": -1, 150 | "dns": -1, 151 | "receive": 0, 152 | "send": 0, 153 | "ssl": -1, 154 | "wait": 2709 155 | } 156 | }, 157 | { 158 | "_id": "0bf5b13dc683e489c640beb36c582351", 159 | "_order": 0, 160 | "cache": {}, 161 | "request": { 162 | "bodySize": 1462, 163 | "cookies": [], 164 | "headers": [ 165 | { 166 | "_fromType": "array", 167 | "name": "accept", 168 | "value": "*/*" 169 | }, 170 | { 171 | "_fromType": "array", 172 | "name": "content-type", 173 | "value": "application/json" 174 | }, 175 | { 176 | "_fromType": "array", 177 | "name": "content-length", 178 | "value": "1462" 179 | }, 180 | { 181 | "_fromType": "array", 182 | "name": "user-agent", 183 | "value": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)" 184 | }, 185 | { 186 | "_fromType": "array", 187 | "name": "accept-encoding", 188 | "value": "gzip,deflate" 189 | }, 190 | { 191 | "_fromType": "array", 192 | "name": "connection", 193 | "value": "close" 194 | }, 195 | { 196 | "name": "host", 197 | "value": "master.staging.saleor.cloud" 198 | } 199 | ], 200 | "headersSize": 999, 201 | "httpVersion": "HTTP/1.1", 202 | "method": "POST", 203 | "postData": { 204 | "mimeType": "application/json", 205 | "params": [], 206 | "text": "{\"operationName\":\"createAccountAddress\",\"variables\":{\"input\":{\"firstName\":\"Test name\",\"lastName\":\"Test lastname\",\"streetAddress1\":\"Test street address\",\"city\":\"Test city\",\"postalCode\":\"12-345\",\"country\":\"PL\"}},\"query\":\"fragment 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 UserBaseFragment on User {\\n id\\n email\\n firstName\\n lastName\\n isStaff\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n __typename\\n}\\n\\nfragment UserDetailsFragment on User {\\n ...UserBaseFragment\\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\\nfragment AccountErrorFragment on AccountError {\\n code\\n field\\n message\\n __typename\\n}\\n\\nmutation createAccountAddress($input: AddressInput!) {\\n accountAddressCreate(input: $input) {\\n address {\\n ...AddressFragment\\n __typename\\n }\\n errors {\\n ...AccountErrorFragment\\n __typename\\n }\\n user {\\n ...UserDetailsFragment\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}" 207 | }, 208 | "queryString": [], 209 | "url": "https://master.staging.saleor.cloud/graphql/" 210 | }, 211 | "response": { 212 | "bodySize": 826, 213 | "content": { 214 | "_isBinary": true, 215 | "mimeType": "application/json", 216 | "size": 826, 217 | "text": "[\"1f8b0800468a466402ffed975f4fdb3010c0bf4a94d76d683078998434370d10ad4d439332d0842237715b836367b6c328a8df7d76fe144a0d036fdad3fad026bebbdf9dcf67fb7aefe65042f7b373efc22c63159520cf3912c2e3084ad4089a91fa19e7eac73dfd36bece8ab3bbec8edd0e93f97e787a78e8be77dc19e64286b0d0766e828474a87e5112029f08f44027cc585142baece47a48488e5017cbeedaaa1976ba889e2aee75e61996cbdac88f13c70b928b6e10a869754a251312128fe5b5d7ddbd0f9ff60f9a70541af8b29e6fd64aa3c1a6c48d188134d783692a9725a26df05ea3d2c7a22470e9ae1eac365c2f185dcf158b3e9ac18ac81e2604d33958e79b56843c568817b82c4d1a9b317462ed1c71ceb8d6fc7ea9de2a81f8a3753c3b0f4976a5d6f0eafaa78e041510132d902ad9888bbd7739148b29833cff2220418cef60b6b5d04fd7b799532ce16ca65e25af50eb3942bcc0426046eb801eb27b02c2fec04fbd13dffb3a9a24b126749339516926c8c91628bb669514db299f6ca0f5acb7c811b818faa1115cc26581a8253701e7be092ae12d7a23311846fe381e8520f1d349ec8f1f5383a254abc1a8da918e4ee4cedbd0431082633f0551b411eb105238470e2c4b61c5f35412427f6062660b48292262c7126ca88335d9aa0e5a723f88bdd124349275810b47ad9f73c32ae5865b467f1c1c25a907c67d838f399e492753dbc992ad8a78628a9dc21b3c875299d971473d557167a0170cdab3f2099f4d15e3064e31d107ab9587715f95b709cd73eb5447eacbc42cd5d71f20d3e422f2e3546df11424c938e84d9267dd389adf940d9492e36925ad5d0f26c741687444aa39a676251f8d47fd8967acf892b3bccaa46db80df8d5c96a9cfdc57cc57e9204e1b1712f2329d53d690b3e09a248914de0f606b60427e0e8c844d577a51d72eb066a91163750471c83301e802418196b51724805a9cf1acbf4eadbcd78b45742b2e27587c1a55143db1548c2b6a7ad5b9efc37ad53fe72efd5b69aa8ed58fef7bfffb2ff9d4122d0cb0d70a7f24c077cb9da9699fee7acea5ef95622dab5a77aee42d60f1cfda874439c9f56882fbd667857d71abcc54555801bd536c329d1f8838feab35aad7e01a6d11b7e590d0000\"]" 218 | }, 219 | "cookies": [], 220 | "headers": [ 221 | { 222 | "name": "content-type", 223 | "value": "application/json" 224 | }, 225 | { 226 | "name": "content-length", 227 | "value": "826" 228 | }, 229 | { 230 | "name": "connection", 231 | "value": "close" 232 | }, 233 | { 234 | "name": "server", 235 | "value": "CloudFront" 236 | }, 237 | { 238 | "name": "date", 239 | "value": "Mon, 24 Apr 2023 13:55:20 GMT" 240 | }, 241 | { 242 | "name": "referrer-policy", 243 | "value": "same-origin" 244 | }, 245 | { 246 | "name": "x-content-type-options", 247 | "value": "nosniff" 248 | }, 249 | { 250 | "name": "content-encoding", 251 | "value": "gzip" 252 | }, 253 | { 254 | "name": "vary", 255 | "value": "Origin, Access-Control-Request-Headers, Access-Control-Request-Method" 256 | }, 257 | { 258 | "name": "x-xss-protection", 259 | "value": "1" 260 | }, 261 | { 262 | "name": "x-frame-options", 263 | "value": "DENY" 264 | }, 265 | { 266 | "name": "strict-transport-security", 267 | "value": "max-age=31536000; includeSubDomains" 268 | }, 269 | { 270 | "name": "x-cache", 271 | "value": "Miss from cloudfront" 272 | }, 273 | { 274 | "name": "via", 275 | "value": "1.1 07fbd2276304c86925071791c7032950.cloudfront.net (CloudFront)" 276 | }, 277 | { 278 | "name": "x-amz-cf-pop", 279 | "value": "FRA56-C2" 280 | }, 281 | { 282 | "name": "x-amz-cf-id", 283 | "value": "JQJ6p5JGMxtQ_CuwZzfOG7zfFj5pIGqyL0Lh4LheaRitqgtKEwFPKQ==" 284 | } 285 | ], 286 | "headersSize": 600, 287 | "httpVersion": "HTTP/1.1", 288 | "redirectURL": "", 289 | "status": 200, 290 | "statusText": "OK" 291 | }, 292 | "startedDateTime": "2023-04-24T13:55:17.681Z", 293 | "time": 2461, 294 | "timings": { 295 | "blocked": -1, 296 | "connect": -1, 297 | "dns": -1, 298 | "receive": 0, 299 | "send": 0, 300 | "ssl": -1, 301 | "wait": 2461 302 | } 303 | } 304 | ], 305 | "pages": [], 306 | "version": "1.2" 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /recordings/user-api_3210680802/sends-a-request-to-delete-user-account_1456582714/recording.har: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "_recordingName": "user api/sends a request to delete user account", 4 | "creator": { 5 | "comment": "persister:fs", 6 | "name": "Polly.JS", 7 | "version": "5.1.1" 8 | }, 9 | "entries": [ 10 | { 11 | "_id": "212d5d019736d7b4c35e6125c30c912c", 12 | "_order": 0, 13 | "cache": {}, 14 | "request": { 15 | "bodySize": 497, 16 | "cookies": [], 17 | "headers": [ 18 | { 19 | "_fromType": "array", 20 | "name": "accept", 21 | "value": "*/*" 22 | }, 23 | { 24 | "_fromType": "array", 25 | "name": "content-type", 26 | "value": "application/json" 27 | }, 28 | { 29 | "_fromType": "array", 30 | "name": "content-length", 31 | "value": "497" 32 | }, 33 | { 34 | "_fromType": "array", 35 | "name": "user-agent", 36 | "value": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)" 37 | }, 38 | { 39 | "_fromType": "array", 40 | "name": "accept-encoding", 41 | "value": "gzip,deflate" 42 | }, 43 | { 44 | "_fromType": "array", 45 | "name": "connection", 46 | "value": "close" 47 | }, 48 | { 49 | "name": "host", 50 | "value": "master.staging.saleor.cloud" 51 | } 52 | ], 53 | "headersSize": 661, 54 | "httpVersion": "HTTP/1.1", 55 | "method": "POST", 56 | "postData": { 57 | "mimeType": "application/json", 58 | "params": [], 59 | "text": "{\"operationName\":\"accountRequestDeletion\",\"variables\":{\"channel\":\"default-channel\"},\"query\":\"fragment AccountErrorFragment on AccountError {\\n code\\n field\\n message\\n __typename\\n}\\n\\nmutation accountRequestDeletion($channel: String!, $redirectUrl: String!) {\\n accountRequestDeletion(channel: $channel, redirectUrl: $redirectUrl) {\\n errors {\\n ...AccountErrorFragment\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}" 60 | }, 61 | "queryString": [], 62 | "url": "https://master.staging.saleor.cloud/graphql/" 63 | }, 64 | "response": { 65 | "bodySize": 92, 66 | "content": { 67 | "mimeType": "application/json", 68 | "size": 92, 69 | "text": "{\"data\":{\"accountRequestDeletion\":{\"errors\":[],\"__typename\":\"AccountRequestDeletion\"}}}" 70 | }, 71 | "cookies": [], 72 | "headers": [ 73 | { 74 | "name": "content-type", 75 | "value": "application/json" 76 | }, 77 | { 78 | "name": "content-length", 79 | "value": "92" 80 | }, 81 | { 82 | "name": "connection", 83 | "value": "close" 84 | }, 85 | { 86 | "name": "server", 87 | "value": "CloudFront" 88 | }, 89 | { 90 | "name": "date", 91 | "value": "Thu, 07 Oct 2021 23:09:48 GMT" 92 | }, 93 | { 94 | "name": "x-content-type-options", 95 | "value": "nosniff" 96 | }, 97 | { 98 | "name": "referrer-policy", 99 | "value": "same-origin" 100 | }, 101 | { 102 | "name": "vary", 103 | "value": "Origin, Access-Control-Request-Headers, Access-Control-Request-Method" 104 | }, 105 | { 106 | "name": "x-xss-protection", 107 | "value": "1" 108 | }, 109 | { 110 | "name": "x-frame-options", 111 | "value": "DENY" 112 | }, 113 | { 114 | "name": "strict-transport-security", 115 | "value": "max-age=31536000; includeSubDomains" 116 | }, 117 | { 118 | "name": "x-edge-origin-shield-skipped", 119 | "value": "0" 120 | }, 121 | { 122 | "name": "x-cache", 123 | "value": "Miss from cloudfront" 124 | }, 125 | { 126 | "name": "via", 127 | "value": "1.1 1c140222cf7df6d0df745770e90c311a.cloudfront.net (CloudFront)" 128 | }, 129 | { 130 | "name": "x-amz-cf-pop", 131 | "value": "WAW50-C1" 132 | }, 133 | { 134 | "name": "x-amz-cf-id", 135 | "value": "B7tjcLdYkzPEbwtpoOAen6TkjLWOetqz9GlmWl7qPj7LzCbiZkF1xQ==" 136 | } 137 | ], 138 | "headersSize": 608, 139 | "httpVersion": "HTTP/1.1", 140 | "redirectURL": "", 141 | "status": 200, 142 | "statusText": "OK" 143 | }, 144 | "startedDateTime": "2021-10-07T23:09:47.768Z", 145 | "time": 434, 146 | "timings": { 147 | "blocked": -1, 148 | "connect": -1, 149 | "dns": -1, 150 | "receive": 0, 151 | "send": 0, 152 | "ssl": -1, 153 | "wait": 434 154 | } 155 | }, 156 | { 157 | "_id": "f7df852e2c12565c12accbd3188ba3ef", 158 | "_order": 0, 159 | "cache": {}, 160 | "request": { 161 | "bodySize": 1326, 162 | "cookies": [], 163 | "headers": [ 164 | { 165 | "_fromType": "array", 166 | "name": "accept", 167 | "value": "*/*" 168 | }, 169 | { 170 | "_fromType": "array", 171 | "name": "content-type", 172 | "value": "application/json" 173 | }, 174 | { 175 | "_fromType": "array", 176 | "name": "content-length", 177 | "value": "1326" 178 | }, 179 | { 180 | "_fromType": "array", 181 | "name": "user-agent", 182 | "value": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)" 183 | }, 184 | { 185 | "_fromType": "array", 186 | "name": "accept-encoding", 187 | "value": "gzip,deflate" 188 | }, 189 | { 190 | "_fromType": "array", 191 | "name": "connection", 192 | "value": "close" 193 | }, 194 | { 195 | "name": "host", 196 | "value": "master.staging.saleor.cloud" 197 | } 198 | ], 199 | "headersSize": 282, 200 | "httpVersion": "HTTP/1.1", 201 | "method": "POST", 202 | "postData": { 203 | "mimeType": "application/json", 204 | "params": [], 205 | "text": "{\"operationName\":\"login\",\"variables\":{},\"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 UserBaseFragment on User {\\n id\\n email\\n firstName\\n lastName\\n isStaff\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n __typename\\n}\\n\\nfragment UserDetailsFragment on User {\\n ...UserBaseFragment\\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 token\\n refreshToken\\n errors {\\n ...AccountErrorFragment\\n __typename\\n }\\n user {\\n ...UserDetailsFragment\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}" 206 | }, 207 | "queryString": [], 208 | "url": "https://master.staging.saleor.cloud/graphql/" 209 | }, 210 | "response": { 211 | "bodySize": 1618, 212 | "content": { 213 | "_isBinary": true, 214 | "mimeType": "application/json", 215 | "size": 1618, 216 | "text": "[\"1f8b08003b8a466402ffd5975b8faa4a16c7bf8ae9d7397600c5d6f3348880a8dc2f0a931352402177140a0576fabb4fd1dd7bced9d94e728e33f3302612eaf6fbaf5aacaa55f5ed250408bcfc3af9f682aa0c966c0d0182bf97f1db0bec77b12f048992ec0c6b104939111bb1401797151762ca25077647c01333b6ef6d524b22ed150fb938272d51528e5236e2206f3452da6478d86ae617762fa65512507ce31e57bd98dc13709287b10e6c7522d84a8b43bf42ce4926dc93d806333d76a9bc757b7a708e5deecfc4d6a1ba5b78d46eee6c170742dcf9ec723423875be68f9af30fcd52bf01ca6e47be73ca3ac89d09732692d669278fdaee918cc1f13e9a8ff546cddd202528c3fa9553ace2a0d099efb61e8afc368e09b7f9dd35f0f40b3e75287b087aec92d21ef0d8081cb591753c7274e350971be4c82b9805a37d97805c0da1c0176e222ec2ed8e744fc4ab9bde1a5ad837551cebb9780efd9beb446f25d8f3115d20a1a4c3a2d910f30d528d9e4f8fd3b330276715e795d2b0103ae26e110ac5ae254d35633b5bad8fc85dddef87b4dd63072cd6ea25b207a18a8c43961d18ff2d536c52d1bad5f5ca11c7aa1a8ed192629ac3251367c99d5ee81d58c18563a766cc09565cb2767717ee6026432ba8cfb915d1c7f5a124ce09f716a6911a47b49c09fb55ed1b4bc76e6e8a9e363cc19b8459b87cd70af369cb192b9aa8bd86d9b7824d2ec3fa0d556633b582539e5a291d541e975f15d1ae4195d112770e983ef58fee66c6b3f4815632d810ac86769e9b71a849a0b6dbd3512a79252d6a7b5553a68ebaf634a292b4265c1b571053738768efe7975f262f358c6ad8c4e6ff75206bb43438e4ff3c90cb5deee27f409d477d32a0ecfe44e5d9a865e7b1e20bf2d59fc53228d0ec6b9ed1c77c0a27514abd0f8f16f6893c04856bf914ca7d1ce0626ecf24cb9d2bbc6d39947c0c0997d78adddee5f49d34ab7a89a8ee70a36b1219f7721e4bfaa08363e96646ca3bdaec129bc2ae0aa98606f96e610f72ac9cf43bc4f3c39f25bb667b347383ad4f991e717797ab4397b9b363ba8246df74e782f772af410d2b5c93ea4dab4e7d4569cc7677caa6d691743af72a0dc6d29a1781355895070eea7526abf289a9966c492eb671bf63f424e510a971d6fad4b95baa89e839c509b76e27de96bd7fbbda79bd494d62bebe1d145e54d8b6d6ae71ec588bbe7637ec76ce535e7d7a5b2d8958704f7cc8a97756999fa671e85cfda58a6de7ae8ae590abf96210d7d254dfa815f9d6716f9756bb2fd34d24aa4e177aec366d3359842033f39c6965b572870c99673fd4daadbff19d7973d92fb7c274665069d316746d12bb78baf1d826decd86735d14bc2e240cad9c39b3dc14975496a6bb5b47f26feb08c6b5538ae91a6ff50bafd3c63503ebbaaa1bbc5afef11b2eb50dac3f9241128e0bc83ec97990569d9466f78fce0548f2b101c106c1baa1fe168226f62b50877f6f400eabfa35a9c68e5152370807cf98595ec68a1cfc584e1a038128c24554b7f04b59857591344d52951f067d7b09aaf063c4969137070efb8663f78a651a23a1fca26d4119e67012c430c8aa1635639be7a1fe02bff7b07e40bfbcff32f989ac328ec4c90fc117d017b07c926b3227ee1114810efe45a228a99c6e283263729e6570fa1fa96271c15fa32a712a9f8c8e7cfd6b6889911981f31855fdc1560994e00c27e072699ee2b1d809327778c40c625096306f5e9f043f88837f919f8a832ff2463458c5921f92c7006f26f8fb4d6e558b65ea27ad1744def45846df3cd03827119a0478393dc9c6416c3db2bd04b7e40c101ef61c5759e388b399b578104de701bff231e306fc244f50ff9c82bec1e1fd085d874fbb5ac58f47cc0b7efc0748cf7454cef0f012f718d3d4c5b565fe5b99c9c8ff0c1b80509df82d7a5afa6009a2fc50286fcf49f95cc8abbab2b1d887117fa9abb00dd0b3e67e82ffb4b33ec5fe8bfe3238d31465e1e15a860825e5f959f05654554c7e048e93cb05939f049b0ccf3fa28eb9f239e44f19e80bf94406fa4ed419d93830a6a83c8c455483b2c93ff69a27dd3b66b7875b7bdba0aaf8739bc16f0f7b8ce30a88c0d71df8e3c813c208b43932be3e1c1386f80e319e3fca36cf7f6f5f2779fea8197c56c0cf23d4fb4fb29f17eccf2bc9fbd80c3b04cbef679c71ee0dfa78a9e1b51d4f55f89c07eb9efdac2647834197146dc1dcf0d90bf8f948a509fc7b7f7fff270ec8d990ce0f0000\"]" 217 | }, 218 | "cookies": [], 219 | "headers": [ 220 | { 221 | "name": "content-type", 222 | "value": "application/json" 223 | }, 224 | { 225 | "name": "content-length", 226 | "value": "1618" 227 | }, 228 | { 229 | "name": "connection", 230 | "value": "close" 231 | }, 232 | { 233 | "name": "server", 234 | "value": "CloudFront" 235 | }, 236 | { 237 | "name": "date", 238 | "value": "Mon, 24 Apr 2023 13:55:09 GMT" 239 | }, 240 | { 241 | "name": "referrer-policy", 242 | "value": "same-origin" 243 | }, 244 | { 245 | "name": "x-content-type-options", 246 | "value": "nosniff" 247 | }, 248 | { 249 | "name": "content-encoding", 250 | "value": "gzip" 251 | }, 252 | { 253 | "name": "vary", 254 | "value": "Origin, Access-Control-Request-Headers, Access-Control-Request-Method" 255 | }, 256 | { 257 | "name": "x-xss-protection", 258 | "value": "1" 259 | }, 260 | { 261 | "name": "x-frame-options", 262 | "value": "DENY" 263 | }, 264 | { 265 | "name": "strict-transport-security", 266 | "value": "max-age=31536000; includeSubDomains" 267 | }, 268 | { 269 | "name": "x-cache", 270 | "value": "Miss from cloudfront" 271 | }, 272 | { 273 | "name": "via", 274 | "value": "1.1 34435958fa6d40b77fd22fa1c1f56176.cloudfront.net (CloudFront)" 275 | }, 276 | { 277 | "name": "x-amz-cf-pop", 278 | "value": "FRA56-C2" 279 | }, 280 | { 281 | "name": "x-amz-cf-id", 282 | "value": "sa2nZ39SkB39svRKTntVpV0PbLQr0_ZJOfpJZDw-GTqHLHv4LFWKUQ==" 283 | } 284 | ], 285 | "headersSize": 1525, 286 | "httpVersion": "HTTP/1.1", 287 | "redirectURL": "", 288 | "status": 200, 289 | "statusText": "OK" 290 | }, 291 | "startedDateTime": "2023-04-24T13:55:06.729Z", 292 | "time": 2833, 293 | "timings": { 294 | "blocked": -1, 295 | "connect": -1, 296 | "dns": -1, 297 | "receive": 0, 298 | "send": 0, 299 | "ssl": -1, 300 | "wait": 2833 301 | } 302 | } 303 | ], 304 | "pages": [], 305 | "version": "1.2" 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /recordings/user-api_3210680802/sends-request-to-change-user-email_1148528273/recording.har: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "_recordingName": "user api/sends request to change user email", 4 | "creator": { 5 | "comment": "persister:fs", 6 | "name": "Polly.JS", 7 | "version": "5.1.1" 8 | }, 9 | "entries": [ 10 | { 11 | "_id": "f7df852e2c12565c12accbd3188ba3ef", 12 | "_order": 0, 13 | "cache": {}, 14 | "request": { 15 | "bodySize": 1326, 16 | "cookies": [], 17 | "headers": [ 18 | { 19 | "_fromType": "array", 20 | "name": "accept", 21 | "value": "*/*" 22 | }, 23 | { 24 | "_fromType": "array", 25 | "name": "content-type", 26 | "value": "application/json" 27 | }, 28 | { 29 | "_fromType": "array", 30 | "name": "content-length", 31 | "value": "1326" 32 | }, 33 | { 34 | "_fromType": "array", 35 | "name": "user-agent", 36 | "value": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)" 37 | }, 38 | { 39 | "_fromType": "array", 40 | "name": "accept-encoding", 41 | "value": "gzip,deflate" 42 | }, 43 | { 44 | "_fromType": "array", 45 | "name": "connection", 46 | "value": "close" 47 | }, 48 | { 49 | "name": "host", 50 | "value": "master.staging.saleor.cloud" 51 | } 52 | ], 53 | "headersSize": 999, 54 | "httpVersion": "HTTP/1.1", 55 | "method": "POST", 56 | "postData": { 57 | "mimeType": "application/json", 58 | "params": [], 59 | "text": "{\"operationName\":\"login\",\"variables\":{},\"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 UserBaseFragment on User {\\n id\\n email\\n firstName\\n lastName\\n isStaff\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n __typename\\n}\\n\\nfragment UserDetailsFragment on User {\\n ...UserBaseFragment\\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 token\\n refreshToken\\n errors {\\n ...AccountErrorFragment\\n __typename\\n }\\n user {\\n ...UserDetailsFragment\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}" 60 | }, 61 | "queryString": [], 62 | "url": "https://master.staging.saleor.cloud/graphql/" 63 | }, 64 | "response": { 65 | "bodySize": 1614, 66 | "content": { 67 | "_isBinary": true, 68 | "mimeType": "application/json", 69 | "size": 1614, 70 | "text": "[\"1f8b08003d8a466402ffbd976993aa481686ff8a515fa7ab42412d994f830892289bec4c7410c99eca26246e37eabf4f62d59dee1bd789e8767a4643427279dec3c937c9cc6f2f31c4f0e5efa36f2fb83e2415d7261027bfdd937f2fc955cac37584542419d60d4c14043a50e2c6e7c01cec79b4e5a471e2b243fdc69ee828d5df4897c67375a4ee794a5d819bb2d227b2c9926e0c1d96f615ec6b145142e73bcc15a03382ae721bcaa0b81b47a23cdf5e19ecb9cad877411fd1bbdca78adebfce6e9e7329421af41e7539c58e7ef269298fd6f925e41643184522b2bfd79cde35abdd0952763ff03df77049f86c6cd26062b9923268fbce2487ce79089fe80d9ad24d46f840f46baf64f2a8dcb1df63dd96c569e8138bc5d937c8e397c2dea3ec5b742529a9ec1be99b42471f588ec3cf3a8f6a4e093f39423a1ae26ba209738bd742e923308f4569e2bbe3b7ecdc47974685697e3e67f306205aceea9e0b3c4628db859b9b13914221272bbada7a1304d793250f5d7ac6d81acd33e760c1c176d9787a1735852fd01a432f0df29dbac6d1b3785272deaa51c066afe0a0cd2c05158b75c5a7f3d58dc640dd728b4bde8ce79c55cc42f172a69b85d053f2adf3305572d7d9abc29eca82d3407604ba88c0bbdd95ef45f73e95ecc81476ef4d1c34e7d562a76c444d80699db9d57b15adc6dc8101bd1498146d1ef6818e021f07cec29d8587d73acfdcd96b6e2d6456dc04fb4d3893c2bd724b02eb7adca370dc2dd2c3b63714ce4fb989224dcbbd106ffa09cf344b693beeec0acbb30b9e9ed635a6522f608e530634a15eed3ba4e1e5abe26fe77698bdfc327a6993b44dbadcfccb8dccff1f8daccfe49bf7a9f9bf347225153ef9455436e84f22cabeba547118b4ec2257c3b5720ce95c8125a6bf9e33bd3f4fe921b5da5d63c72239516e51e95b21858b100d293dd0e65e92433756e4b14e79454c6ca6d486c35c642b9febf66e92f0874bbccae790980cee8907d760acf28d62d9d9d8b373c71fd79790920c43506eb02836f2e1300ccb56ed5b9ec7d7a2df3b9cc3c4a63581d81edbf395f19e06e7d607015835a8ed600e84d0e1ea83df4bb4dfcd5612f0fa8276aff0684af43a0b6ec1262a22c0af14c9f6a0ce98f92ed08ee835ca5c152d6dfa8ab2a3007df1749c26592a6b6d688529c7a5c8d50f1d0b445d37aa25e493d5e626c593e228e887b333771b8e36b575399f2647a10e75555cd389d0cd91065e1b79799efab515dc94c6f16c63b96d96714a4733682fdf0dd59de2f662076c3ac3eeca593995dd5256decea7c85f76c85b1c8048c2c08bbc69679b1806e3997469e69733b0dc925244359f0845c7ecb61dc3cacb57d8bd7294ba139daa8c438e52c245d5795e7360dcb836dfb7c2dc45b77ddbe7d782d1ee732669dbbaedc86cf9e7afe4aeef92f6be18a0789840b6ab14d1bebec8fbc3f9deb884a8182a70d2e1a4eda8bfc5b0cbc31ab6f13f3a582475fb86eaa1614a860213f30c2bcbcb5050c01fef51676098a6e416b77df2a5ac256d89ba0ed5d53da06f2f511ddf7b88acb2daf20127f2dc46b54c6320545f34115671918ca23c890e758fbba12e08f0b549beb7b07e40bf7cfc32fa89acb19ecc2b0fc10dbc9649f524d7645dfe1114c34bf2278940d6f89da12aacc90796c1ef7e4f05654346a3aec8523e1a12f9f6e7d032abb06b3e6035ed87586558c12c19c1a6e99ee27124090abf7dc48c72585549d1bd3d097ee0837f939ff2c11779050c4eb59487e4c1e0dd888cdfe854f744a67d32fa3510cc806377ab071a194af12822d3e9493631b1f528f60a9e500631e9f61c575d12c7d9ec126c81e93de0d721619c60880a84afcf29ec56c4de8fd06dfc74aa357279c46cc8e5bf4006a6a7f14640a678c09ae60e2c2df33fca8c06fea76d20c62d0a7bfcb4f4d65a03e5a150d167a87acef2da4e5d59dc43c7376d1df7117e36dc4ff01f4ed6a7d85f982f83374da0ac1fcee504635465cf8245a06984fc089ca3a621e427c1262b088fa8c35af91cf2a715e80bf9c40af49db8631563cb9a407de845dcc2aa2beeef9a27d33bac6e0f5fed7d87ebf28fbd0c7e7dd862e85726187e9d81ef5b9e3849615f60e36be0d83826678861ff51f545f15bfd1215c5a36af859907c6ea13e7e92fd3c607f1e493e86eae48293eafb1e6778f60edfffb4c9b11f7655b1de27ed95fb2c9e0c01c30b2afb923d91bd170c8b813a1b93cfc7c7c7bf00e1ea72e1ce0f0000\"]" 71 | }, 72 | "cookies": [], 73 | "headers": [ 74 | { 75 | "name": "content-type", 76 | "value": "application/json" 77 | }, 78 | { 79 | "name": "content-length", 80 | "value": "1614" 81 | }, 82 | { 83 | "name": "connection", 84 | "value": "close" 85 | }, 86 | { 87 | "name": "server", 88 | "value": "CloudFront" 89 | }, 90 | { 91 | "name": "date", 92 | "value": "Mon, 24 Apr 2023 13:55:11 GMT" 93 | }, 94 | { 95 | "name": "referrer-policy", 96 | "value": "same-origin" 97 | }, 98 | { 99 | "name": "x-content-type-options", 100 | "value": "nosniff" 101 | }, 102 | { 103 | "name": "content-encoding", 104 | "value": "gzip" 105 | }, 106 | { 107 | "name": "vary", 108 | "value": "Origin, Access-Control-Request-Headers, Access-Control-Request-Method" 109 | }, 110 | { 111 | "name": "x-xss-protection", 112 | "value": "1" 113 | }, 114 | { 115 | "name": "x-frame-options", 116 | "value": "DENY" 117 | }, 118 | { 119 | "name": "strict-transport-security", 120 | "value": "max-age=31536000; includeSubDomains" 121 | }, 122 | { 123 | "name": "x-cache", 124 | "value": "Miss from cloudfront" 125 | }, 126 | { 127 | "name": "via", 128 | "value": "1.1 b25bc331cb2e5e7e25d9488f5ecdc940.cloudfront.net (CloudFront)" 129 | }, 130 | { 131 | "name": "x-amz-cf-pop", 132 | "value": "FRA56-C2" 133 | }, 134 | { 135 | "name": "x-amz-cf-id", 136 | "value": "3Huh54IMnfXOLgvpJ8ZjOnKJFR8WRlRe0TpC0ThOU_AXAktQOknkeQ==" 137 | } 138 | ], 139 | "headersSize": 1525, 140 | "httpVersion": "HTTP/1.1", 141 | "redirectURL": "", 142 | "status": 200, 143 | "statusText": "OK" 144 | }, 145 | "startedDateTime": "2023-04-24T13:55:09.603Z", 146 | "time": 1748, 147 | "timings": { 148 | "blocked": -1, 149 | "connect": -1, 150 | "dns": -1, 151 | "receive": 0, 152 | "send": 0, 153 | "ssl": -1, 154 | "wait": 1748 155 | } 156 | }, 157 | { 158 | "_id": "a4c690350326ebb084337b34d4f0d21a", 159 | "_order": 0, 160 | "cache": {}, 161 | "request": { 162 | "bodySize": 1547, 163 | "cookies": [], 164 | "headers": [ 165 | { 166 | "_fromType": "array", 167 | "name": "accept", 168 | "value": "*/*" 169 | }, 170 | { 171 | "_fromType": "array", 172 | "name": "content-type", 173 | "value": "application/json" 174 | }, 175 | { 176 | "_fromType": "array", 177 | "name": "content-length", 178 | "value": "1547" 179 | }, 180 | { 181 | "_fromType": "array", 182 | "name": "user-agent", 183 | "value": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)" 184 | }, 185 | { 186 | "_fromType": "array", 187 | "name": "accept-encoding", 188 | "value": "gzip,deflate" 189 | }, 190 | { 191 | "_fromType": "array", 192 | "name": "connection", 193 | "value": "close" 194 | }, 195 | { 196 | "name": "host", 197 | "value": "master.staging.saleor.cloud" 198 | } 199 | ], 200 | "headersSize": 999, 201 | "httpVersion": "HTTP/1.1", 202 | "method": "POST", 203 | "postData": { 204 | "mimeType": "application/json", 205 | "params": [], 206 | "text": "{\"operationName\":\"requestEmailChange\",\"variables\":{\"channel\":\"default-channel\"},\"query\":\"fragment 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 UserBaseFragment on User {\\n id\\n email\\n firstName\\n lastName\\n isStaff\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n __typename\\n}\\n\\nfragment UserDetailsFragment on User {\\n ...UserBaseFragment\\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\\nfragment AccountErrorFragment on AccountError {\\n code\\n field\\n message\\n __typename\\n}\\n\\nmutation requestEmailChange($channel: String!, $newEmail: String!, $password: String!, $redirectUrl: String!) {\\n requestEmailChange(\\n channel: $channel\\n newEmail: $newEmail\\n password: $password\\n redirectUrl: $redirectUrl\\n ) {\\n errors {\\n ...AccountErrorFragment\\n __typename\\n }\\n user {\\n ...UserDetailsFragment\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}" 207 | }, 208 | "queryString": [], 209 | "url": "https://master.staging.saleor.cloud/graphql/" 210 | }, 211 | "response": { 212 | "bodySize": 651, 213 | "content": { 214 | "_isBinary": true, 215 | "mimeType": "application/json", 216 | "size": 651, 217 | "text": "[\"1f8b08003f8a466402ffad96df6fda3010c7ff9528afabd036692f7b9a0929448390e547d56aaaa22339c0ade364b6c34088ff7d36d0ad159ed47ae3212477f6e74ee7afeddbfb3528f03f7b7b5fe08f1ea50a1ba02c58035fe1d18c42b442ead7eff7579edf4b144733adf59f7f731bb3eaa1ddce1e1e7ffada8d66b271284d42213fbeab41ae172d88fa8b0486ad18d0d60c5c5221550c8d89e11b038397df54660a964bfda9448fe7c8098a864a495b7e4c68ef576d7d9c3121f1681a96c1240cbece8b3c33047ea64d80d70cbd6a8dd563db2b697c65a9761d3e8d285ea0fdc39577414ec8dd2c8cade00e760d72476e4e6e431b54c116df488c66499866f398e461596461fa9c1a359d5e8d968342cf1472f036f48cc4641c9624495ee43a030e2bf4a0eba4132fd04588c3a98d596901726472e008b6e8e037d9490767f228ca8279115bc946e0d2d3ebe76dda5e87118ed98fa3ebbc0c483ab2c458d1a5f22abd9d1cd95ac4852d770e1bba02a5a7b971e743adb81b328ca6517e67e1b70bcdd8c08232aa766e11d29196b70d2d6ae75227fa616376faf10fc832bf4bc2acd45bbc24799e46c322ff6b18cff04fb201a5045df4ca39f4b41847b13510eb5794bb493e49e7a322b02abe136ddd57ca35dd13f8d5c53a05fb8ff5cac23c8fe2b1752fa35294af5cc193284934d9065ed3aed36447704eaeaf6d547357ba212f6ea033d2e1067a22a624cea6248fe6562d2a015cb2e359e3585e73bb598ff65eaab679dd61706f1d61e635a8e0dc141d5b9e1a97d033959d178ed4b54069fa0fde33f6c73fa48cd9dc7032e0a9853a5c844d2fdbae8319855b85fca9d5312590ea799b86f5b71ec52e38993f98bc614b9bbe211b0d820533f04feff5ef7038fc023eba762ee6090000\"]" 218 | }, 219 | "cookies": [], 220 | "headers": [ 221 | { 222 | "name": "content-type", 223 | "value": "application/json" 224 | }, 225 | { 226 | "name": "content-length", 227 | "value": "651" 228 | }, 229 | { 230 | "name": "connection", 231 | "value": "close" 232 | }, 233 | { 234 | "name": "server", 235 | "value": "CloudFront" 236 | }, 237 | { 238 | "name": "date", 239 | "value": "Mon, 24 Apr 2023 13:55:12 GMT" 240 | }, 241 | { 242 | "name": "referrer-policy", 243 | "value": "same-origin" 244 | }, 245 | { 246 | "name": "x-content-type-options", 247 | "value": "nosniff" 248 | }, 249 | { 250 | "name": "content-encoding", 251 | "value": "gzip" 252 | }, 253 | { 254 | "name": "vary", 255 | "value": "Origin, Access-Control-Request-Headers, Access-Control-Request-Method" 256 | }, 257 | { 258 | "name": "x-xss-protection", 259 | "value": "1" 260 | }, 261 | { 262 | "name": "x-frame-options", 263 | "value": "DENY" 264 | }, 265 | { 266 | "name": "strict-transport-security", 267 | "value": "max-age=31536000; includeSubDomains" 268 | }, 269 | { 270 | "name": "x-cache", 271 | "value": "Miss from cloudfront" 272 | }, 273 | { 274 | "name": "via", 275 | "value": "1.1 0d4b487d54766de7560aa02de852bbf8.cloudfront.net (CloudFront)" 276 | }, 277 | { 278 | "name": "x-amz-cf-pop", 279 | "value": "FRA56-C2" 280 | }, 281 | { 282 | "name": "x-amz-cf-id", 283 | "value": "ulAZjLwumtKHQdOjxxec6D__w6zkBchIlT48ke8Fd-y02htUyVtjkw==" 284 | } 285 | ], 286 | "headersSize": 600, 287 | "httpVersion": "HTTP/1.1", 288 | "redirectURL": "", 289 | "status": 200, 290 | "statusText": "OK" 291 | }, 292 | "startedDateTime": "2023-04-24T13:55:11.368Z", 293 | "time": 1140, 294 | "timings": { 295 | "blocked": -1, 296 | "connect": -1, 297 | "dns": -1, 298 | "receive": 0, 299 | "send": 0, 300 | "ssl": -1, 301 | "wait": 1140 302 | } 303 | } 304 | ], 305 | "pages": [], 306 | "version": "1.2" 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /recordings/user-api_3210680802/updates-the-user-first-name_3574048358/recording.har: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "_recordingName": "user api/updates the user first name", 4 | "creator": { 5 | "comment": "persister:fs", 6 | "name": "Polly.JS", 7 | "version": "5.1.1" 8 | }, 9 | "entries": [ 10 | { 11 | "_id": "f7df852e2c12565c12accbd3188ba3ef", 12 | "_order": 0, 13 | "cache": {}, 14 | "request": { 15 | "bodySize": 1326, 16 | "cookies": [], 17 | "headers": [ 18 | { 19 | "_fromType": "array", 20 | "name": "accept", 21 | "value": "*/*" 22 | }, 23 | { 24 | "_fromType": "array", 25 | "name": "content-type", 26 | "value": "application/json" 27 | }, 28 | { 29 | "_fromType": "array", 30 | "name": "content-length", 31 | "value": "1326" 32 | }, 33 | { 34 | "_fromType": "array", 35 | "name": "user-agent", 36 | "value": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)" 37 | }, 38 | { 39 | "_fromType": "array", 40 | "name": "accept-encoding", 41 | "value": "gzip,deflate" 42 | }, 43 | { 44 | "_fromType": "array", 45 | "name": "connection", 46 | "value": "close" 47 | }, 48 | { 49 | "name": "host", 50 | "value": "master.staging.saleor.cloud" 51 | } 52 | ], 53 | "headersSize": 999, 54 | "httpVersion": "HTTP/1.1", 55 | "method": "POST", 56 | "postData": { 57 | "mimeType": "application/json", 58 | "params": [], 59 | "text": "{\"operationName\":\"login\",\"variables\":{},\"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 UserBaseFragment on User {\\n id\\n email\\n firstName\\n lastName\\n isStaff\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n __typename\\n}\\n\\nfragment UserDetailsFragment on User {\\n ...UserBaseFragment\\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 token\\n refreshToken\\n errors {\\n ...AccountErrorFragment\\n __typename\\n }\\n user {\\n ...UserDetailsFragment\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}" 60 | }, 61 | "queryString": [], 62 | "url": "https://master.staging.saleor.cloud/graphql/" 63 | }, 64 | "response": { 65 | "bodySize": 1613, 66 | "content": { 67 | "_isBinary": true, 68 | "mimeType": "application/json", 69 | "size": 1613, 70 | "text": "[\"1f8b0800408a466402ffd597598fab3816c7bf4aa95ea7ab144865a19f86843d40d8b7512b32e004b3074c12b8aaef3e505577baaf6e46eacecc3c4ca420bc9cdffff8f818dbdf9e6380c1f3af4fdf9e7195c172db4080e1efe5f1ed19f65212f211da23c9b407915091d88a05ae83adb8145316c95b69063d7a6adf39848e8efaeb6852fb9e8ef6294bee197150199d502c6534a3e661e1f4625aa188e4dac0a57a115d11f0d461aa0382318b046529f714f63d7516786217cd8d2420f32ee81783efdef2702e763e79bbc4ae7e09e65212f1c92ddcae27377228d07fd47cfbd02c8d0b209d6ee2fb5e7683ec6966cd45c2f62475d20e5c2201ee75727fd49b34a54141381bf52bbfa092a830e8efbeca457e996c6221bf06e638fc824b7dd219a27e0c49e90ca3ed11b8fac4725d76d1fa647d812c7106f368f2af8e086a8879ae0890b88c058908bcd9abd4eb91a759e92d3eabc2a9ad33352b95b9790ea31e99a9c1152f52442e9978c5c9f0f0724ce6fbc86f88c50996477ea7ed97b6c2e4e49986123ea6faf5b6356ff232b8a6f5d6716fb14cad3075be4af4da07f2d9d8ce7515e83ebd1c6eda9a5d1f63d62f05273d260b121e2049f1845cbfcc5fe8c68b1c74958fd2aec2cd7e7e71659cc5e09018cbb5b56b82d98be4579b370126a701b89efdc2e9edaea4048ea9982d768943a2b14cbd10457a4be6e76b0e18ff5ada6ce4d4ee3ccfd8655b154b33ae66d9cab8ba585d8b7a5df92f65936e981d8df58c482cc0a6e622bf38caec58eb0ba122703854164c2561e652129504f1a54a2522511549f6d4533258321711ea595ae7e5fc90d3cfbf3c3d37f0d8c036b1feaf13595f28834ffccf13b994f260fc47e469d22722d2e93d32cf262d274ff621af9ec379a28202cfbfc679fc184fe1a37d69f4b16b8f315187a808ec90c47988a690e699c3e9bd6a0565e0265cec4a4bc3769031b76f0a2171eaec3ac419e643d6788b1c470f3dd5824c603b4250bbd9fa169386eb66b9683b6aefb0b566d8b875ddb7695ae46ab571cd5646a5b74ce7a62323f76a090da516ba9476219515678d367cbcdcf90d30e60b77554baeb3326bdeb8360c74779bd198b0dcab3b67fa95ab94ec3030596ce8e8421da132565538171017943ed9f55a7ca6de945d95f5b4b10e7d65ddee17413a388bd4178ecb71a1d931c34b3b11215fe70e6f5cef96d596ea173519f2a542241cdfc22c76647d575c3d44e51d2b0e823faba81d1f91d7733f4b7cf2a5a0ccc5b162ce8b6b7de3a2384bcebc3888d965dd9bb2b747d7b75d3e282caf8874bfaae55bb751bad999dcd00abb9eed2ec85ed51c2fb246bb3a64feea4d43282a9444589620e03786e985b74a26d385ed6c971ad5ed026db393a384b6836e79c2e685c8d78a3ead19d83455d38eabe51fbf8da5ae85cdc76680e26901399e9a47697553d2ecfad1b900289f1a306c316c5af26f316893b0024dfcf716e4b06a5e5135753ca2a6c563f24c3bcbf35491831fcba83531381ec7226e3af8a5acc1a6406d8baaf2c3a16fcf51157f5808b4cac8ec612bb0dbdddeb6cc89507ed10450c6397c8a1218655587dba9ed70c07d0dbff7b07f403fbffff2f41359a37d8555ef826bd017b07c906bd11e7b0f8ac10dfe45a2a868ac61ee55da620fb6c91a7fa48a453dce46558e5bf9d314c8d7bf86566895e6d903ad693ff8aa80129ce013a8ebf621de760c82cacaf7985102ca12e6edeb83e03b79f02ff24379f045664473bbb7d5bbe429c1dba771fe9e2e5537ca340f7acf8b9c75d8d2067347e3848ef8291a97d383ec3189ed7bbe97e0824e008f668f71f79b31e31c7a23caa2e5dfe157e1c8b88010e508f78f2918cc98def7d04dfc70a8b5f1718f598f8fff0079b07c8d350fe3123fd09665881bdbfab7324f13ff336d00c60d0a3bfcb0b46cf3a27a5728ef4ea87c2ce53563cfd8dbbb195f3755dc45f851773fc17f3a589f62ffc57899ac65892a7f772d438c51797a142c889a3692ef811354d723f941b04573dc3deab4573e86fc6907fa423eb0037d271ab46acab425eeefe6226e40d9e61fdf9a07c33bed6e773fed5d8babe2cf7d0c7ebbdb63b22b20065f77e08f234f0c8fa0cbb1f93571741c8f7788e9fc517679fe7bfb06e5f9bd66f059013f8f50ef3fc97e5eb03faf24ef5333bc61587e3fe34c636ff1c74b03cfdd74aa8af50e36fdf6b39a981c06375474057d19cf5e20cc27ea6236fededfdfff091862e8bace0f0000\"]" 71 | }, 72 | "cookies": [], 73 | "headers": [ 74 | { 75 | "name": "content-type", 76 | "value": "application/json" 77 | }, 78 | { 79 | "name": "content-length", 80 | "value": "1613" 81 | }, 82 | { 83 | "name": "connection", 84 | "value": "close" 85 | }, 86 | { 87 | "name": "server", 88 | "value": "CloudFront" 89 | }, 90 | { 91 | "name": "date", 92 | "value": "Mon, 24 Apr 2023 13:55:13 GMT" 93 | }, 94 | { 95 | "name": "referrer-policy", 96 | "value": "same-origin" 97 | }, 98 | { 99 | "name": "x-content-type-options", 100 | "value": "nosniff" 101 | }, 102 | { 103 | "name": "content-encoding", 104 | "value": "gzip" 105 | }, 106 | { 107 | "name": "vary", 108 | "value": "Origin, Access-Control-Request-Headers, Access-Control-Request-Method" 109 | }, 110 | { 111 | "name": "x-xss-protection", 112 | "value": "1" 113 | }, 114 | { 115 | "name": "x-frame-options", 116 | "value": "DENY" 117 | }, 118 | { 119 | "name": "strict-transport-security", 120 | "value": "max-age=31536000; includeSubDomains" 121 | }, 122 | { 123 | "name": "x-cache", 124 | "value": "Miss from cloudfront" 125 | }, 126 | { 127 | "name": "via", 128 | "value": "1.1 27f780feafa4114cfc67d86fca85d124.cloudfront.net (CloudFront)" 129 | }, 130 | { 131 | "name": "x-amz-cf-pop", 132 | "value": "FRA56-C2" 133 | }, 134 | { 135 | "name": "x-amz-cf-id", 136 | "value": "VSmPcB2CW9IjH-z4G71xlAJDyFOJ2DRGWJF2d2zpScJF1uGI89HUGg==" 137 | } 138 | ], 139 | "headersSize": 1525, 140 | "httpVersion": "HTTP/1.1", 141 | "redirectURL": "", 142 | "status": 200, 143 | "statusText": "OK" 144 | }, 145 | "startedDateTime": "2023-04-24T13:55:12.525Z", 146 | "time": 1532, 147 | "timings": { 148 | "blocked": -1, 149 | "connect": -1, 150 | "dns": -1, 151 | "receive": 0, 152 | "send": 0, 153 | "ssl": -1, 154 | "wait": 1532 155 | } 156 | }, 157 | { 158 | "_id": "87458252820a4831305b37b4cfb1cee0", 159 | "_order": 0, 160 | "cache": {}, 161 | "request": { 162 | "bodySize": 1244, 163 | "cookies": [], 164 | "headers": [ 165 | { 166 | "_fromType": "array", 167 | "name": "accept", 168 | "value": "*/*" 169 | }, 170 | { 171 | "_fromType": "array", 172 | "name": "content-type", 173 | "value": "application/json" 174 | }, 175 | { 176 | "_fromType": "array", 177 | "name": "content-length", 178 | "value": "1244" 179 | }, 180 | { 181 | "_fromType": "array", 182 | "name": "user-agent", 183 | "value": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)" 184 | }, 185 | { 186 | "_fromType": "array", 187 | "name": "accept-encoding", 188 | "value": "gzip,deflate" 189 | }, 190 | { 191 | "_fromType": "array", 192 | "name": "connection", 193 | "value": "close" 194 | }, 195 | { 196 | "name": "host", 197 | "value": "master.staging.saleor.cloud" 198 | } 199 | ], 200 | "headersSize": 999, 201 | "httpVersion": "HTTP/1.1", 202 | "method": "POST", 203 | "postData": { 204 | "mimeType": "application/json", 205 | "params": [], 206 | "text": "{\"operationName\":\"accountUpdate\",\"variables\":{\"input\":{\"firstName\":\"\"}},\"query\":\"fragment 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 UserBaseFragment on User {\\n id\\n email\\n firstName\\n lastName\\n isStaff\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n __typename\\n}\\n\\nfragment UserDetailsFragment on User {\\n ...UserBaseFragment\\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\\nfragment AccountErrorFragment on AccountError {\\n code\\n field\\n message\\n __typename\\n}\\n\\nmutation accountUpdate($input: AccountInput!) {\\n accountUpdate(input: $input) {\\n errors {\\n ...AccountErrorFragment\\n __typename\\n }\\n user {\\n ...UserDetailsFragment\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}" 207 | }, 208 | "queryString": [], 209 | "url": "https://master.staging.saleor.cloud/graphql/" 210 | }, 211 | "response": { 212 | "bodySize": 653, 213 | "content": { 214 | "_isBinary": true, 215 | "mimeType": "application/json", 216 | "size": 653, 217 | "text": "[\"1f8b0800428a466402ffad965b6fd33014c7bf4a9457a6099078e10937cdda88360db94c9bd0149d266eebcdb1832fa5d5d4ef3e3bed806a46da0c79c8e5d8fe9d93e3bfedf318b6a020fc1c3c86d0345c3355f5c682070b16820b695ebfdf5d04a196580c66d29a47787d93d2e69eefe6f70f3f43d38c3b20d436282c1516f2e3bb16e466c941b45f2450ccc525e1b6e38a08a952e8ac8fd01a289c7f13592858adcca7121a9f3c675874444ac2d910d063d8f076183145e97816d7d1348ebe2eaab2b00476a24d81b51407cd06370f5c2b69dbea5aed7bfcdca33a4387878be0053943b7f33875827bd8779879724b7413bba00a76f88dc4649ec579b1485119d75511e77f5293ae37b3c19999d4c026f2f26de8394ad124ae51969dc53a07066b1c40df4b2f5e649290c63317b3d9006398ca4b4fb04307bfc85e3a3891c749112daad449b602978199bf60cbb571233ca39f2457651da17cecf0b1262b1534663979b28d882b57ec0cb6640dca0cf3e32e464671d76894cc92f2d6c1e74bc3d8c29250a2f67e1ef2b191b70b2d5aef5467e6e662f6e6f60fc8babccde2a2364bbc46659927a3aafcab9bc0f28fb201a504596ae5ed7a564d92d4e988ea35617e92cff2c5b88a9c8aef056f75a37cc33d825f9daca3b3ff98af222ecb249d38d732568ab0b52f789a649921bbc01bd2f786ec092ed1d5958b6acf4a3fe48b13e884f438819e89394a8b192a9385538b4a009374d86b3cd36b4f37e7d6aea5e2ddeb36833b670f3baec30a4ef5d050f2b478059aaae23471a86d0596b6fe609ad2dfed2342a9ab198e067c2ca10e2fdca2b38aeb603be09dc2ecb9cab17f2fd5f022f00f6debaaf69bc6621f1dcd1f6cc8b0239deed0d6545fb0a496fbe9bdb90e87c31330443349dc090000\"]" 218 | }, 219 | "cookies": [], 220 | "headers": [ 221 | { 222 | "name": "content-type", 223 | "value": "application/json" 224 | }, 225 | { 226 | "name": "content-length", 227 | "value": "653" 228 | }, 229 | { 230 | "name": "connection", 231 | "value": "close" 232 | }, 233 | { 234 | "name": "server", 235 | "value": "CloudFront" 236 | }, 237 | { 238 | "name": "date", 239 | "value": "Mon, 24 Apr 2023 13:55:14 GMT" 240 | }, 241 | { 242 | "name": "referrer-policy", 243 | "value": "same-origin" 244 | }, 245 | { 246 | "name": "x-content-type-options", 247 | "value": "nosniff" 248 | }, 249 | { 250 | "name": "content-encoding", 251 | "value": "gzip" 252 | }, 253 | { 254 | "name": "vary", 255 | "value": "Origin, Access-Control-Request-Headers, Access-Control-Request-Method" 256 | }, 257 | { 258 | "name": "x-xss-protection", 259 | "value": "1" 260 | }, 261 | { 262 | "name": "x-frame-options", 263 | "value": "DENY" 264 | }, 265 | { 266 | "name": "strict-transport-security", 267 | "value": "max-age=31536000; includeSubDomains" 268 | }, 269 | { 270 | "name": "x-cache", 271 | "value": "Miss from cloudfront" 272 | }, 273 | { 274 | "name": "via", 275 | "value": "1.1 d8670b0c6b76371fb58f730881dfe504.cloudfront.net (CloudFront)" 276 | }, 277 | { 278 | "name": "x-amz-cf-pop", 279 | "value": "FRA56-C2" 280 | }, 281 | { 282 | "name": "x-amz-cf-id", 283 | "value": "z5vwTMMNG4Wf8RDJg8IutOFLefMNp9F0o0qmbFEf4ohzkqIiKjnnEA==" 284 | } 285 | ], 286 | "headersSize": 600, 287 | "httpVersion": "HTTP/1.1", 288 | "redirectURL": "", 289 | "status": 200, 290 | "statusText": "OK" 291 | }, 292 | "startedDateTime": "2023-04-24T13:55:14.066Z", 293 | "time": 870, 294 | "timings": { 295 | "blocked": -1, 296 | "connect": -1, 297 | "dns": -1, 298 | "receive": 0, 299 | "send": 0, 300 | "ssl": -1, 301 | "wait": 870 302 | } 303 | } 304 | ], 305 | "pages": [], 306 | "version": "1.2" 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/apollo/client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApolloClient, 3 | FetchResult, 4 | InMemoryCache, 5 | NormalizedCacheObject, 6 | Reference, 7 | createHttpLink, 8 | } from "@apollo/client"; 9 | import fetch from "cross-fetch"; 10 | import jwtDecode from "jwt-decode"; 11 | 12 | import { JWTToken } from "../core"; 13 | import { AuthSDK, auth } from "../core/auth"; 14 | import { storage } from "../core/storage"; 15 | import { isInternalToken } from "../helpers"; 16 | import { TypedTypePolicies } from "./apollo-helpers"; 17 | import { ExternalRefreshMutation, RefreshTokenMutation } from "./types"; 18 | 19 | let client: ApolloClient; 20 | let authClient: AuthSDK; 21 | let refreshPromise: 22 | | ReturnType 23 | | ReturnType 24 | | null = null; 25 | const isTokenRefreshExternal = ( 26 | result: RefreshTokenMutation | ExternalRefreshMutation 27 | ): result is ExternalRefreshMutation => "externalRefresh" in result; 28 | 29 | export type FetchConfig = Partial<{ 30 | /** 31 | * Enable auto token refreshing. Default to `true`. 32 | */ 33 | autoTokenRefresh: boolean; 34 | /** 35 | * Set a value for skew between local time and token expiration date in 36 | * seconds (only together with `autoTokenRefresh`). Defaults to `120`. 37 | */ 38 | tokenRefreshTimeSkew: number; 39 | /** 40 | * Refresh token and retry the request when Saleor responds with `Unauthorized` error. 41 | * Defaults to `true`. 42 | */ 43 | refreshOnUnauthorized: boolean; 44 | }>; 45 | 46 | export const createFetch = ({ 47 | autoTokenRefresh = true, 48 | tokenRefreshTimeSkew = 120, 49 | refreshOnUnauthorized = true, 50 | }: FetchConfig = {}) => async ( 51 | input: RequestInfo, 52 | init: RequestInit = {} 53 | ): Promise => { 54 | if (!client) { 55 | throw new Error( 56 | "Could not find Saleor's client instance. Did you forget to call createSaleorClient()?" 57 | ); 58 | } 59 | 60 | let token = storage.getAccessToken(); 61 | 62 | try { 63 | if ( 64 | ["refreshToken", "externalRefresh"].includes( 65 | // INFO: Non-null assertion is enabled because the block is wrapped inside try/catch 66 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 67 | JSON.parse(init.body!.toString()).operationName 68 | ) 69 | ) { 70 | return fetch(input, init); 71 | } 72 | } catch (e) {} 73 | 74 | if (autoTokenRefresh && token) { 75 | // auto refresh token before provided time skew (in seconds) until it expires 76 | const decodedToken = jwtDecode(token); 77 | const expirationTime = (decodedToken.exp - tokenRefreshTimeSkew) * 1000; 78 | const owner = decodedToken.owner; 79 | 80 | try { 81 | if (refreshPromise) { 82 | await refreshPromise; 83 | } else if (Date.now() >= expirationTime) { 84 | if (isInternalToken(owner)) { 85 | await authClient.refreshToken(); 86 | } else { 87 | await authClient.refreshExternalToken(); 88 | } 89 | } 90 | } catch (e) { 91 | } finally { 92 | refreshPromise = null; 93 | } 94 | token = storage.getAccessToken(); 95 | } 96 | 97 | if (token) { 98 | init.headers = { 99 | ...init.headers, 100 | "authorization-bearer": token, 101 | }; 102 | } 103 | 104 | if (refreshOnUnauthorized && token) { 105 | const response = await fetch(input, init); 106 | const data: FetchResult = await response.clone().json(); 107 | const isUnauthenticated = data?.errors?.some( 108 | error => error.extensions?.exception.code === "ExpiredSignatureError" 109 | ); 110 | let refreshTokenResponse: FetchResult< 111 | RefreshTokenMutation | ExternalRefreshMutation, 112 | Record, 113 | Record 114 | > | null = null; 115 | const owner = jwtDecode(token).owner; 116 | 117 | if (isUnauthenticated) { 118 | try { 119 | if (refreshPromise) { 120 | refreshTokenResponse = await refreshPromise; 121 | } else { 122 | refreshPromise = isInternalToken(owner) 123 | ? authClient.refreshToken() 124 | : authClient.refreshExternalToken(); 125 | refreshTokenResponse = await refreshPromise; 126 | } 127 | 128 | if ( 129 | refreshTokenResponse.data && 130 | isTokenRefreshExternal(refreshTokenResponse.data) 131 | ? refreshTokenResponse.data.externalRefresh?.token 132 | : refreshTokenResponse.data?.tokenRefresh?.token 133 | ) { 134 | // check if mutation returns a valid token after refresh and retry the request 135 | return createFetch({ 136 | autoTokenRefresh: false, 137 | refreshOnUnauthorized: false, 138 | })(input, init); 139 | } else { 140 | // after Saleor returns ExpiredSignatureError status and token refresh fails 141 | // we log out the user and return the failed response 142 | authClient.logout(); 143 | } 144 | } catch (e) { 145 | } finally { 146 | refreshPromise = null; 147 | } 148 | } 149 | 150 | return response; 151 | } 152 | 153 | return fetch(input, init); 154 | }; 155 | 156 | const getTypePolicies = (autologin: boolean): TypedTypePolicies => ({ 157 | Query: { 158 | fields: { 159 | authenticated: { 160 | read(_, { readField, toReference }): boolean { 161 | return !!readField( 162 | "id", 163 | toReference({ 164 | __typename: "User", 165 | }) 166 | ); 167 | }, 168 | }, 169 | me: { 170 | read(_, { toReference, canRead }): Reference | undefined | null { 171 | const ref = toReference({ 172 | __typename: "User", 173 | }); 174 | 175 | return canRead(ref) ? ref : null; 176 | }, 177 | }, 178 | authenticating: { 179 | read( 180 | read = autologin && !!storage.getRefreshToken(), 181 | { readField } 182 | ): boolean { 183 | if (readField("authenticated")) { 184 | return false; 185 | } 186 | 187 | return read; 188 | }, 189 | }, 190 | }, 191 | }, 192 | User: { 193 | /** 194 | * IMPORTANT 195 | * This works as long as we have 1 User cache object which is the current logged in User. 196 | * If the client should ever fetch additional Users, this should be removed 197 | * and the login methods (token create or verify) should be responsible for writing USER query cache manually. 198 | */ 199 | keyFields: [], 200 | fields: { 201 | addresses: { 202 | merge: false, 203 | }, 204 | }, 205 | }, 206 | }); 207 | 208 | export const createApolloClient = ( 209 | apiUrl: string, 210 | autologin: boolean, 211 | fetchOptions?: FetchConfig 212 | ): ApolloClient => { 213 | const httpLink = createHttpLink({ 214 | fetch: createFetch(fetchOptions), 215 | uri: apiUrl, 216 | credentials: "include", 217 | }); 218 | 219 | const cache = new InMemoryCache({ 220 | typePolicies: getTypePolicies(autologin), 221 | }); 222 | 223 | client = new ApolloClient({ 224 | cache, 225 | link: httpLink, 226 | }); 227 | 228 | /** 229 | * Refreshing token code should stay under core/auth.ts To get this method available, 230 | * we need to call "auth()" here. refreshToken mutation doesn't require channel, so it 231 | * doesn't have to be populated with value. 232 | */ 233 | authClient = auth({ apolloClient: client, channel: "" }); 234 | 235 | return client; 236 | }; 237 | -------------------------------------------------------------------------------- /src/apollo/fragments.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | export const accountErrorFragment = gql` 4 | fragment AccountErrorFragment on AccountError { 5 | code 6 | field 7 | message 8 | } 9 | `; 10 | 11 | export const addressFragment = gql` 12 | fragment AddressFragment on Address { 13 | id 14 | firstName 15 | lastName 16 | companyName 17 | streetAddress1 18 | streetAddress2 19 | city 20 | cityArea 21 | postalCode 22 | country { 23 | code 24 | country 25 | } 26 | countryArea 27 | phone 28 | isDefaultBillingAddress 29 | isDefaultShippingAddress 30 | } 31 | `; 32 | 33 | export const userBaseFragment = gql` 34 | fragment UserBaseFragment on User { 35 | id 36 | email 37 | firstName 38 | lastName 39 | isStaff 40 | userPermissions { 41 | code 42 | name 43 | } 44 | } 45 | `; 46 | 47 | export const userDetailsFragment = gql` 48 | ${addressFragment} 49 | ${userBaseFragment} 50 | fragment UserDetailsFragment on User { 51 | ...UserBaseFragment 52 | metadata { 53 | key 54 | value 55 | } 56 | defaultShippingAddress { 57 | ...AddressFragment 58 | } 59 | defaultBillingAddress { 60 | ...AddressFragment 61 | } 62 | addresses { 63 | ...AddressFragment 64 | } 65 | } 66 | `; 67 | -------------------------------------------------------------------------------- /src/apollo/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./client"; 2 | -------------------------------------------------------------------------------- /src/apollo/mutations.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | import { 4 | accountErrorFragment, 5 | addressFragment, 6 | userBaseFragment, 7 | userDetailsFragment, 8 | } from "./fragments"; 9 | 10 | export const LOGIN_WITHOUT_DETAILS = gql` 11 | ${accountErrorFragment} 12 | ${userBaseFragment} 13 | mutation loginWithoutDetails($email: String!, $password: String!) { 14 | tokenCreate(email: $email, password: $password) { 15 | refreshToken 16 | token 17 | errors { 18 | ...AccountErrorFragment 19 | } 20 | user { 21 | ...UserBaseFragment 22 | } 23 | } 24 | } 25 | `; 26 | 27 | export const LOGIN = gql` 28 | ${accountErrorFragment} 29 | ${userDetailsFragment} 30 | mutation login($email: String!, $password: String!) { 31 | tokenCreate(email: $email, password: $password) { 32 | token 33 | refreshToken 34 | errors { 35 | ...AccountErrorFragment 36 | } 37 | user { 38 | ...UserDetailsFragment 39 | } 40 | } 41 | } 42 | `; 43 | 44 | export const REGISTER = gql` 45 | ${accountErrorFragment} 46 | mutation register($input: AccountRegisterInput!) { 47 | accountRegister(input: $input) { 48 | errors { 49 | ...AccountErrorFragment 50 | } 51 | requiresConfirmation 52 | } 53 | } 54 | `; 55 | 56 | export const REFRESH_TOKEN = gql` 57 | ${accountErrorFragment} 58 | mutation refreshToken($refreshToken: String!) { 59 | tokenRefresh(refreshToken: $refreshToken) { 60 | token 61 | errors { 62 | ...AccountErrorFragment 63 | } 64 | } 65 | } 66 | `; 67 | 68 | // separate mutation so the request payload is minimal when user is not needed 69 | // used for initial authentication 70 | export const REFRESH_TOKEN_WITH_USER = gql` 71 | ${accountErrorFragment} 72 | ${userDetailsFragment} 73 | mutation refreshTokenWithUser($refreshToken: String!) { 74 | tokenRefresh(refreshToken: $refreshToken) { 75 | token 76 | user { 77 | ...UserDetailsFragment 78 | } 79 | errors { 80 | ...AccountErrorFragment 81 | } 82 | } 83 | } 84 | `; 85 | 86 | export const VERIFY_TOKEN = gql` 87 | ${accountErrorFragment} 88 | ${userDetailsFragment} 89 | mutation verifyToken($token: String!) { 90 | tokenVerify(token: $token) { 91 | isValid 92 | payload 93 | user { 94 | ...UserDetailsFragment 95 | } 96 | errors { 97 | ...AccountErrorFragment 98 | } 99 | } 100 | } 101 | `; 102 | 103 | export const EXTERNAL_AUTHENTICATION_URL = gql` 104 | ${accountErrorFragment} 105 | mutation externalAuthenticationUrl( 106 | $pluginId: String = "mirumee.authentication.openidconnect" 107 | $input: JSONString! 108 | ) { 109 | externalAuthenticationUrl(pluginId: $pluginId, input: $input) { 110 | authenticationData 111 | errors { 112 | ...AccountErrorFragment 113 | } 114 | } 115 | } 116 | `; 117 | 118 | export const OBTAIN_EXTERNAL_ACCESS_TOKEN = gql` 119 | ${accountErrorFragment} 120 | ${userDetailsFragment} 121 | mutation externalObtainAccessTokens( 122 | $pluginId: String = "mirumee.authentication.openidconnect" 123 | $input: JSONString! 124 | ) { 125 | externalObtainAccessTokens(pluginId: $pluginId, input: $input) { 126 | token 127 | refreshToken 128 | user { 129 | ...UserDetailsFragment 130 | } 131 | errors { 132 | ...AccountErrorFragment 133 | } 134 | } 135 | } 136 | `; 137 | 138 | export const EXTERNAL_REFRESH = gql` 139 | ${accountErrorFragment} 140 | mutation externalRefresh( 141 | $pluginId: String = "mirumee.authentication.openidconnect" 142 | $input: JSONString! 143 | ) { 144 | externalRefresh(pluginId: $pluginId, input: $input) { 145 | token 146 | refreshToken 147 | errors { 148 | ...AccountErrorFragment 149 | } 150 | } 151 | } 152 | `; 153 | 154 | export const EXTERNAL_REFRESH_WITH_USER = gql` 155 | ${accountErrorFragment} 156 | ${userDetailsFragment} 157 | mutation externalRefreshWithUser( 158 | $pluginId: String = "mirumee.authentication.openidconnect" 159 | $input: JSONString! 160 | ) { 161 | externalRefresh(pluginId: $pluginId, input: $input) { 162 | token 163 | refreshToken 164 | user { 165 | ...UserDetailsFragment 166 | } 167 | errors { 168 | ...AccountErrorFragment 169 | } 170 | } 171 | } 172 | `; 173 | 174 | export const EXTERNAL_VERIFY_TOKEN = gql` 175 | ${accountErrorFragment} 176 | ${userDetailsFragment} 177 | mutation externalVerify( 178 | $pluginId: String = "mirumee.authentication.openidconnect" 179 | $input: JSONString! 180 | ) { 181 | externalVerify(pluginId: $pluginId, input: $input) { 182 | isValid 183 | verifyData 184 | user { 185 | ...UserDetailsFragment 186 | userPermissions { 187 | code 188 | name 189 | } 190 | } 191 | errors { 192 | ...AccountErrorFragment 193 | } 194 | } 195 | } 196 | `; 197 | 198 | export const EXTERNAL_LOGOUT = gql` 199 | ${accountErrorFragment} 200 | mutation externalLogout( 201 | $pluginId: String = "mirumee.authentication.openidconnect" 202 | $input: JSONString! 203 | ) { 204 | externalLogout(pluginId: $pluginId, input: $input) { 205 | logoutData 206 | errors { 207 | ...AccountErrorFragment 208 | } 209 | } 210 | } 211 | `; 212 | 213 | export const CHANGE_USER_PASSWORD = gql` 214 | ${accountErrorFragment} 215 | mutation passwordChange($newPassword: String!, $oldPassword: String!) { 216 | passwordChange(newPassword: $newPassword, oldPassword: $oldPassword) { 217 | errors { 218 | ...AccountErrorFragment 219 | } 220 | } 221 | } 222 | `; 223 | 224 | export const REQUEST_PASSWORD_RESET = gql` 225 | ${accountErrorFragment} 226 | mutation requestPasswordReset( 227 | $email: String! 228 | $redirectUrl: String! 229 | $channel: String! 230 | ) { 231 | requestPasswordReset( 232 | email: $email 233 | redirectUrl: $redirectUrl 234 | channel: $channel 235 | ) { 236 | errors { 237 | ...AccountErrorFragment 238 | } 239 | } 240 | } 241 | `; 242 | 243 | export const SET_PASSWORD = gql` 244 | ${userDetailsFragment} 245 | ${accountErrorFragment} 246 | mutation setPassword($token: String!, $email: String!, $password: String!) { 247 | setPassword(token: $token, email: $email, password: $password) { 248 | errors { 249 | ...AccountErrorFragment 250 | } 251 | token 252 | refreshToken 253 | user { 254 | ...UserDetailsFragment 255 | } 256 | } 257 | } 258 | `; 259 | 260 | export const REQUEST_EMAIL_CHANGE = gql` 261 | ${userDetailsFragment} 262 | ${accountErrorFragment} 263 | mutation requestEmailChange( 264 | $channel: String! 265 | $newEmail: String! 266 | $password: String! 267 | $redirectUrl: String! 268 | ) { 269 | requestEmailChange( 270 | channel: $channel 271 | newEmail: $newEmail 272 | password: $password 273 | redirectUrl: $redirectUrl 274 | ) { 275 | errors { 276 | ...AccountErrorFragment 277 | } 278 | user { 279 | ...UserDetailsFragment 280 | } 281 | } 282 | } 283 | `; 284 | 285 | export const CONFIRM_EMAIL_CHANGE = gql` 286 | ${userDetailsFragment} 287 | ${accountErrorFragment} 288 | mutation confirmEmailChange($channel: String!, $token: String!) { 289 | confirmEmailChange(channel: $channel, token: $token) { 290 | errors { 291 | ...AccountErrorFragment 292 | } 293 | user { 294 | ...UserDetailsFragment 295 | } 296 | } 297 | } 298 | `; 299 | 300 | export const REQUEST_DELETE_ACCOUNT = gql` 301 | ${accountErrorFragment} 302 | mutation accountRequestDeletion($channel: String!, $redirectUrl: String!) { 303 | accountRequestDeletion(channel: $channel, redirectUrl: $redirectUrl) { 304 | errors { 305 | ...AccountErrorFragment 306 | } 307 | } 308 | } 309 | `; 310 | 311 | export const DELETE_ACCOUNT = gql` 312 | ${userDetailsFragment} 313 | ${accountErrorFragment} 314 | mutation accountDelete($token: String!) { 315 | accountDelete(token: $token) { 316 | errors { 317 | ...AccountErrorFragment 318 | } 319 | user { 320 | ...UserDetailsFragment 321 | } 322 | } 323 | } 324 | `; 325 | 326 | export const UPDATE_ACCOUNT = gql` 327 | ${userDetailsFragment} 328 | ${accountErrorFragment} 329 | mutation accountUpdate($input: AccountInput!) { 330 | accountUpdate(input: $input) { 331 | errors { 332 | ...AccountErrorFragment 333 | } 334 | user { 335 | ...UserDetailsFragment 336 | } 337 | } 338 | } 339 | `; 340 | 341 | export const SET_ACCOUNT_DEFAULT_ADDRESS = gql` 342 | ${userDetailsFragment} 343 | ${accountErrorFragment} 344 | mutation setAccountDefaultAddress($id: ID!, $type: AddressTypeEnum!) { 345 | accountSetDefaultAddress(id: $id, type: $type) { 346 | errors { 347 | ...AccountErrorFragment 348 | } 349 | user { 350 | ...UserDetailsFragment 351 | } 352 | } 353 | } 354 | `; 355 | 356 | export const DELETE_ACCOUNT_ADDRESS = gql` 357 | ${userDetailsFragment} 358 | ${accountErrorFragment} 359 | mutation deleteAccountAddress($addressId: ID!) { 360 | accountAddressDelete(id: $addressId) { 361 | errors { 362 | ...AccountErrorFragment 363 | } 364 | user { 365 | ...UserDetailsFragment 366 | } 367 | } 368 | } 369 | `; 370 | 371 | export const CREATE_ACCOUNT_ADDRESS = gql` 372 | ${addressFragment} 373 | ${userDetailsFragment} 374 | ${accountErrorFragment} 375 | mutation createAccountAddress($input: AddressInput!) { 376 | accountAddressCreate(input: $input) { 377 | address { 378 | ...AddressFragment 379 | } 380 | errors { 381 | ...AccountErrorFragment 382 | } 383 | user { 384 | ...UserDetailsFragment 385 | } 386 | } 387 | } 388 | `; 389 | 390 | export const UPDATE_ACCOUNT_ADDRESS = gql` 391 | ${addressFragment} 392 | ${userDetailsFragment} 393 | ${accountErrorFragment} 394 | mutation updateAccountAddress($input: AddressInput!, $id: ID!) { 395 | accountAddressUpdate(input: $input, id: $id) { 396 | address { 397 | ...AddressFragment 398 | } 399 | errors { 400 | ...AccountErrorFragment 401 | } 402 | user { 403 | ...UserDetailsFragment 404 | } 405 | } 406 | } 407 | `; 408 | 409 | export const CONFIRM_ACCOUNT = gql` 410 | ${userDetailsFragment} 411 | ${accountErrorFragment} 412 | mutation accountConfirm($email: String!, $token: String!) { 413 | confirmAccount(email: $email, token: $token) { 414 | user { 415 | ...UserDetailsFragment 416 | } 417 | errors { 418 | ...AccountErrorFragment 419 | } 420 | } 421 | } 422 | `; 423 | -------------------------------------------------------------------------------- /src/apollo/queries.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | import { userBaseFragment, userDetailsFragment } from "./fragments"; 3 | 4 | export const USER_WITHOUT_DETAILS = gql` 5 | ${userBaseFragment} 6 | query UserWithoutDetails { 7 | user: me { 8 | ...UserBaseFragment 9 | } 10 | authenticated @client 11 | authenticating @client 12 | } 13 | `; 14 | 15 | export const USER = gql` 16 | ${userDetailsFragment} 17 | query User { 18 | user: me { 19 | ...UserDetailsFragment 20 | } 21 | authenticated @client 22 | authenticating @client 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const API_URI = process.env.API_URI || "http://localhost:8000/graphql/"; 2 | export const TEST_AUTH_EMAIL = process.env.TEST_AUTH_EMAIL || ""; 3 | export const TEST_AUTH_PASSWORD = process.env.TEST_AUTH_PASSWORD || ""; 4 | export const TEST_AUTH_SECOND_EMAIL = "second+testers+dashboard@saleor.io"; 5 | export const TEST_AUTH_SECOND_PASSWORD = "secondtest1234"; 6 | export const TEST_AUTH_EXTERNAL_LOGIN_CALLBACK = 7 | "https://localhost:9000/login/callback/"; 8 | export const TEST_AUTH_EXTERNAL_LOGOUT_CALLBACK = 9 | "https://localhost:9000/logout/callback/"; 10 | export const TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_ID = 11 | "mirumee.authentication.openidconnect"; 12 | export const TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_CODE = 13 | "Mx1h3u3YFfDVNv5iJL8lzzeHpU_lyCti"; 14 | export const TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_STATE = 15 | "4/0AX4XfWgK2COkwLueNWzvgkGFfOp_o5FJB4KsRYG3or4lPE0o_3SIvGykNRV7CjU3-7B9sg"; 16 | export const TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_SECOND_CODE = 17 | "Second3YFfDVNv5iJL8lzzeHpU_lyCti"; 18 | export const TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_SECOND_STATE = 19 | "SecondXfWgK2COkwLueNWzvgkGFfOp_o5FJB4KsRYG3or4lPE0o_3SIvGykNRV7CjU3-7B9sg"; 20 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const WINDOW_EXISTS = typeof window !== "undefined"; 2 | export const LOCAL_STORAGE_EXISTS = WINDOW_EXISTS && !!window.localStorage; 3 | export const DEVELOPMENT_MODE = process.env.NODE_ENV === "development"; 4 | -------------------------------------------------------------------------------- /src/core/auth.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CHANGE_USER_PASSWORD, 3 | EXTERNAL_AUTHENTICATION_URL, 4 | EXTERNAL_LOGOUT, 5 | EXTERNAL_REFRESH, 6 | EXTERNAL_REFRESH_WITH_USER, 7 | EXTERNAL_VERIFY_TOKEN, 8 | LOGIN, 9 | LOGIN_WITHOUT_DETAILS, 10 | OBTAIN_EXTERNAL_ACCESS_TOKEN, 11 | REFRESH_TOKEN, 12 | REFRESH_TOKEN_WITH_USER, 13 | REGISTER, 14 | REQUEST_PASSWORD_RESET, 15 | SET_PASSWORD, 16 | VERIFY_TOKEN, 17 | } from "../apollo/mutations"; 18 | import { USER, USER_WITHOUT_DETAILS } from "../apollo/queries"; 19 | import { 20 | ExternalAuthenticationUrlMutation, 21 | ExternalAuthenticationUrlMutationVariables, 22 | ExternalLogoutMutation, 23 | ExternalLogoutMutationVariables, 24 | ExternalObtainAccessTokensMutation, 25 | ExternalObtainAccessTokensMutationVariables, 26 | ExternalRefreshMutation, 27 | ExternalRefreshMutationVariables, 28 | ExternalRefreshWithUserMutation, 29 | ExternalRefreshWithUserMutationVariables, 30 | ExternalVerifyMutation, 31 | ExternalVerifyMutationVariables, 32 | LoginMutation, 33 | LoginMutationVariables, 34 | PasswordChangeMutation, 35 | PasswordChangeMutationVariables, 36 | RefreshTokenMutation, 37 | RefreshTokenMutationVariables, 38 | RefreshTokenWithUserMutation, 39 | RefreshTokenWithUserMutationVariables, 40 | RegisterMutation, 41 | RegisterMutationVariables, 42 | RequestPasswordResetMutation, 43 | RequestPasswordResetMutationVariables, 44 | SetPasswordMutation, 45 | SetPasswordMutationVariables, 46 | VerifyTokenMutation, 47 | VerifyTokenMutationVariables, 48 | } from "../apollo/types"; 49 | import { hasNonEmptyPermissions } from "./helpers"; 50 | import { storage } from "./storage"; 51 | import { 52 | ChangePasswordOpts, 53 | ChangePasswordResult, 54 | GetExternalAccessTokenOpts, 55 | GetExternalAccessTokenResult, 56 | GetExternalAuthUrlOpts, 57 | GetExternalAuthUrlResult, 58 | LoginOpts, 59 | LoginResult, 60 | LogoutOpts, 61 | LogoutResult, 62 | RefreshExternalTokenResult, 63 | RefreshTokenResult, 64 | RegisterOpts, 65 | RegisterResult, 66 | RequestPasswordResetOpts, 67 | RequestPasswordResetResult, 68 | SaleorClientMethodsProps, 69 | SetPasswordOpts, 70 | SetPasswordResult, 71 | VerifyExternalTokenResult, 72 | VerifyTokenResult, 73 | } from "./types"; 74 | 75 | export interface AuthSDK { 76 | /** 77 | * Change the password of the logged in user. 78 | * 79 | * @param opts - Object with password and new password. 80 | * @returns Errors if the passoword change has failed. 81 | */ 82 | changePassword: (opts: ChangePasswordOpts) => Promise; 83 | /** 84 | * Authenticates user with email and password. 85 | * 86 | * @param opts - Object with user's email, password and a boolean includeDetails - whether to fetch user details. 87 | * Default for includeDetails is true. 88 | * @returns Promise resolved with CreateToken type data. 89 | */ 90 | login: (opts: LoginOpts) => Promise; 91 | /** 92 | * Clears stored token and Apollo store. If external plugin was used to log in, the mutation will prepare 93 | * the logout URL. All values passed in field input will be added as GET parameters to the logout request. 94 | * 95 | * @param opts - Object with input as JSON with returnTo - the URL where a user should be redirected 96 | * when external plugin was used to log in 97 | * @returns Logout data and errors if external plugin was used to log in. Otherwise null. 98 | */ 99 | logout: (opts?: LogoutOpts) => Promise; 100 | /** 101 | * Refresh JWT token. Mutation will try to take refreshToken from the function's arguments. 102 | * If it fails, it will try to use refreshToken from the http-only cookie called refreshToken. 103 | * 104 | * @param includeUser - Whether to fetch user. Default false. 105 | * @returns Authorization token. 106 | */ 107 | refreshToken: (includeUser?: boolean) => Promise; 108 | /** 109 | * Registers user with email and password. 110 | * 111 | * @param opts - Object with user's data. Email and password are required fields. 112 | * "channel" can be changed by using first "setChannel" method from api. 113 | * @returns Promise resolved with AccountRegister type data. 114 | */ 115 | register: (opts: RegisterOpts) => Promise; 116 | /** 117 | * Sends an email with the account password modification link. 118 | * 119 | * @param opts - Object with slug of a channel which will be used for notify user, 120 | * email of the user that will be used for password recovery and URL of a view 121 | * where users should be redirected to reset the password. URL in RFC 1808 format. 122 | * 123 | * @returns Errors if there were some. 124 | */ 125 | requestPasswordReset: ( 126 | opts: RequestPasswordResetOpts 127 | ) => Promise; 128 | /** 129 | * Sets the user's password from the token sent by email. 130 | * 131 | * @param opts - Object with user's email, password and one-time token required to set the password. 132 | * @returns User instance, JWT token, JWT refresh token and CSRF token. 133 | */ 134 | setPassword: (opts: SetPasswordOpts) => Promise; 135 | /** 136 | * Verify JWT token. 137 | * 138 | * @param token - Token value. 139 | * @returns User assigned to token and the information if the token is valid or not. 140 | */ 141 | verifyToken: () => Promise; 142 | /** 143 | * Executing externalAuthenticationUrl mutation will prepare special URL which will redirect user to requested 144 | * page after successfull authentication. After redirection state and code fields will be added to the URL. 145 | * 146 | * @param opts - Object withpluginId default value set as "mirumee.authentication.openidconnect" and input as 147 | * JSON with redirectUrl - the URL where the user should be redirected after successful authentication. 148 | * @returns Authentication data and errors 149 | */ 150 | getExternalAuthUrl: ( 151 | opts: GetExternalAuthUrlOpts 152 | ) => Promise; 153 | /** 154 | * The externalObtainAccessTokens mutation will generate requested access tokens. 155 | * 156 | * @param opts - Object withpluginId default value set as "mirumee.authentication.openidconnect" and input as 157 | * JSON with code - the authorization code received from the OAuth provider and state - the state value received 158 | * from the OAuth provider 159 | * @returns Login authentication data and errors 160 | */ 161 | getExternalAccessToken: ( 162 | opts: GetExternalAccessTokenOpts 163 | ) => Promise; 164 | /** 165 | * The externalRefresh mutation will generate new access tokens when provided with a valid refresh token. 166 | * 167 | * @param includeUser - Whether to fetch user. Default false. 168 | * @returns Token refresh data and errors 169 | */ 170 | refreshExternalToken: ( 171 | includeUser?: boolean 172 | ) => Promise; 173 | /** 174 | * The mutation will verify the authentication token. 175 | * 176 | * @returns Token verification data and errors 177 | */ 178 | verifyExternalToken: () => Promise; 179 | } 180 | 181 | export const auth = ({ 182 | apolloClient: client, 183 | channel, 184 | }: SaleorClientMethodsProps): AuthSDK => { 185 | const login: AuthSDK["login"] = ({ includeDetails = true, ...opts }) => { 186 | const query = includeDetails ? USER : USER_WITHOUT_DETAILS; 187 | const loginMutation = includeDetails ? LOGIN : LOGIN_WITHOUT_DETAILS; 188 | 189 | client.writeQuery({ 190 | query, 191 | data: { 192 | authenticating: true, 193 | }, 194 | }); 195 | 196 | return client.mutate({ 197 | mutation: loginMutation, 198 | variables: { 199 | ...opts, 200 | }, 201 | update: (_, { data }) => { 202 | if (data?.tokenCreate?.token) { 203 | storage.setTokens({ 204 | accessToken: data.tokenCreate.token, 205 | refreshToken: data.tokenCreate.refreshToken, 206 | }); 207 | } else { 208 | client.writeQuery({ 209 | query, 210 | data: { 211 | authenticating: false, 212 | }, 213 | }); 214 | } 215 | }, 216 | }); 217 | }; 218 | 219 | const logout: AuthSDK["logout"] = async opts => { 220 | const authPluginId = storage.getAuthPluginId(); 221 | 222 | storage.clear(); 223 | 224 | client.writeQuery({ 225 | query: USER, 226 | data: { 227 | authenticating: false, 228 | }, 229 | }); 230 | 231 | client.resetStore(); 232 | 233 | if (authPluginId && opts?.input) { 234 | const result = await client.mutate< 235 | ExternalLogoutMutation, 236 | ExternalLogoutMutationVariables 237 | >({ 238 | mutation: EXTERNAL_LOGOUT, 239 | variables: { 240 | ...opts, 241 | pluginId: authPluginId, 242 | }, 243 | }); 244 | return result; 245 | } 246 | return null; 247 | }; 248 | 249 | const register: AuthSDK["register"] = async opts => 250 | await client.mutate({ 251 | mutation: REGISTER, 252 | variables: { 253 | input: { 254 | ...opts, 255 | channel, 256 | }, 257 | }, 258 | }); 259 | 260 | const refreshToken: AuthSDK["refreshToken"] = (includeUser = false) => { 261 | const refreshToken = storage.getRefreshToken(); 262 | 263 | if (!refreshToken) { 264 | throw Error("refreshToken not present"); 265 | } 266 | 267 | if (includeUser) { 268 | return client.mutate< 269 | RefreshTokenWithUserMutation, 270 | RefreshTokenWithUserMutationVariables 271 | >({ 272 | mutation: REFRESH_TOKEN_WITH_USER, 273 | variables: { 274 | refreshToken, 275 | }, 276 | update: (_, { data }) => { 277 | if (data?.tokenRefresh?.token) { 278 | storage.setAccessToken(data.tokenRefresh.token); 279 | } else { 280 | logout(); 281 | } 282 | }, 283 | }); 284 | } 285 | 286 | return client.mutate({ 287 | mutation: REFRESH_TOKEN, 288 | variables: { 289 | refreshToken, 290 | }, 291 | update: (_, { data }) => { 292 | if (data?.tokenRefresh?.token) { 293 | storage.setAccessToken(data.tokenRefresh.token); 294 | } else { 295 | logout(); 296 | } 297 | }, 298 | }); 299 | }; 300 | 301 | const verifyToken: AuthSDK["verifyToken"] = async () => { 302 | const token = storage.getAccessToken(); 303 | 304 | if (!token) { 305 | throw Error("Token not present"); 306 | } 307 | 308 | const result = await client.mutate< 309 | VerifyTokenMutation, 310 | VerifyTokenMutationVariables 311 | >({ 312 | mutation: VERIFY_TOKEN, 313 | variables: { token }, 314 | }); 315 | 316 | if (!result.data?.tokenVerify?.isValid) { 317 | logout(); 318 | } 319 | 320 | return result; 321 | }; 322 | 323 | const changePassword: AuthSDK["changePassword"] = async opts => { 324 | const result = await client.mutate< 325 | PasswordChangeMutation, 326 | PasswordChangeMutationVariables 327 | >({ 328 | mutation: CHANGE_USER_PASSWORD, 329 | variables: { ...opts }, 330 | }); 331 | 332 | return result; 333 | }; 334 | 335 | const requestPasswordReset: AuthSDK["requestPasswordReset"] = async opts => { 336 | const result = await client.mutate< 337 | RequestPasswordResetMutation, 338 | RequestPasswordResetMutationVariables 339 | >({ 340 | mutation: REQUEST_PASSWORD_RESET, 341 | variables: { ...opts, channel }, 342 | }); 343 | 344 | return result; 345 | }; 346 | 347 | const setPassword: AuthSDK["setPassword"] = opts => { 348 | return client.mutate({ 349 | mutation: SET_PASSWORD, 350 | variables: { ...opts }, 351 | update: (_, { data }) => { 352 | if (data?.setPassword?.token) { 353 | storage.setTokens({ 354 | accessToken: data.setPassword.token, 355 | refreshToken: data.setPassword.refreshToken, 356 | }); 357 | } 358 | }, 359 | }); 360 | }; 361 | 362 | const getExternalAuthUrl: AuthSDK["getExternalAuthUrl"] = async opts => { 363 | const result = await client.mutate< 364 | ExternalAuthenticationUrlMutation, 365 | ExternalAuthenticationUrlMutationVariables 366 | >({ 367 | mutation: EXTERNAL_AUTHENTICATION_URL, 368 | variables: { ...opts }, 369 | }); 370 | 371 | return result; 372 | }; 373 | 374 | const getExternalAccessToken: AuthSDK["getExternalAccessToken"] = opts => { 375 | client.writeQuery({ 376 | query: USER, 377 | data: { 378 | authenticating: true, 379 | }, 380 | }); 381 | 382 | return client.mutate< 383 | ExternalObtainAccessTokensMutation, 384 | ExternalObtainAccessTokensMutationVariables 385 | >({ 386 | mutation: OBTAIN_EXTERNAL_ACCESS_TOKEN, 387 | variables: { 388 | ...opts, 389 | }, 390 | update: (_, { data }) => { 391 | storage.setAuthPluginId(opts.pluginId); 392 | if ( 393 | data?.externalObtainAccessTokens?.token && 394 | hasNonEmptyPermissions( 395 | data?.externalObtainAccessTokens?.user?.userPermissions 396 | ) 397 | ) { 398 | storage.setTokens({ 399 | accessToken: data.externalObtainAccessTokens.token, 400 | refreshToken: data.externalObtainAccessTokens.refreshToken, 401 | }); 402 | } else { 403 | client.writeQuery({ 404 | query: USER, 405 | data: { 406 | authenticating: false, 407 | }, 408 | }); 409 | } 410 | }, 411 | }); 412 | }; 413 | 414 | const refreshExternalToken: AuthSDK["refreshExternalToken"] = ( 415 | includeUser = false 416 | ) => { 417 | const refreshToken = storage.getRefreshToken(); 418 | const authPluginId = storage.getAuthPluginId(); 419 | 420 | if (!refreshToken) { 421 | throw Error("refreshToken not present"); 422 | } 423 | 424 | if (includeUser) { 425 | return client.mutate< 426 | ExternalRefreshWithUserMutation, 427 | ExternalRefreshWithUserMutationVariables 428 | >({ 429 | mutation: EXTERNAL_REFRESH_WITH_USER, 430 | variables: { 431 | pluginId: authPluginId, 432 | input: JSON.stringify({ 433 | refreshToken, 434 | }), 435 | }, 436 | update: (_, { data }) => { 437 | if (data?.externalRefresh?.token) { 438 | storage.setTokens({ 439 | accessToken: data.externalRefresh.token, 440 | refreshToken: data.externalRefresh.refreshToken, 441 | }); 442 | } else { 443 | logout(); 444 | } 445 | }, 446 | }); 447 | } 448 | 449 | return client.mutate< 450 | ExternalRefreshMutation, 451 | ExternalRefreshMutationVariables 452 | >({ 453 | mutation: EXTERNAL_REFRESH, 454 | variables: { 455 | pluginId: authPluginId, 456 | input: JSON.stringify({ 457 | refreshToken, 458 | }), 459 | }, 460 | update: (_, { data }) => { 461 | if (data?.externalRefresh?.token) { 462 | storage.setTokens({ 463 | accessToken: data.externalRefresh.token, 464 | refreshToken: data.externalRefresh.refreshToken, 465 | }); 466 | } else { 467 | logout(); 468 | } 469 | }, 470 | }); 471 | }; 472 | 473 | const verifyExternalToken: AuthSDK["verifyExternalToken"] = async () => { 474 | const refreshToken = storage.getRefreshToken(); 475 | const authPluginId = storage.getAuthPluginId(); 476 | 477 | if (!refreshToken) { 478 | throw Error("refreshToken not present"); 479 | } 480 | 481 | const result = await client.mutate< 482 | ExternalVerifyMutation, 483 | ExternalVerifyMutationVariables 484 | >({ 485 | mutation: EXTERNAL_VERIFY_TOKEN, 486 | variables: { 487 | pluginId: authPluginId, 488 | input: JSON.stringify({ 489 | refreshToken, 490 | }), 491 | }, 492 | }); 493 | 494 | if (!result.data?.externalVerify?.isValid) { 495 | storage.clear(); 496 | } 497 | 498 | return result; 499 | }; 500 | 501 | return { 502 | changePassword, 503 | getExternalAccessToken, 504 | getExternalAuthUrl, 505 | login, 506 | logout, 507 | refreshExternalToken, 508 | refreshToken, 509 | register, 510 | requestPasswordReset, 511 | setPassword, 512 | verifyExternalToken, 513 | verifyToken, 514 | }; 515 | }; 516 | -------------------------------------------------------------------------------- /src/core/constants.ts: -------------------------------------------------------------------------------- 1 | export const SALEOR_AUTH_PLUGIN_ID = "_saleorAuthPluginId"; 2 | export const SALEOR_REFRESH_TOKEN = "_saleorRefreshToken"; 3 | -------------------------------------------------------------------------------- /src/core/createSaleorClient.ts: -------------------------------------------------------------------------------- 1 | import { createApolloClient } from "../apollo"; 2 | import { auth } from "./auth"; 3 | import { getState, State } from "./state"; 4 | import { JWTToken, SaleorClient, SaleorClientOpts } from "./types"; 5 | import { user } from "./user"; 6 | 7 | import jwtDecode from "jwt-decode"; 8 | import { DEVELOPMENT_MODE, WINDOW_EXISTS } from "../constants"; 9 | import { isInternalToken } from "../helpers"; 10 | import { createStorage, storage } from "./storage"; 11 | 12 | export const createSaleorClient = ({ 13 | apiUrl, 14 | channel, 15 | opts = {}, 16 | }: SaleorClientOpts): SaleorClient => { 17 | let _channel = channel; 18 | const { autologin = true, fetchOpts } = opts; 19 | 20 | const setChannel = (channel: string): string => { 21 | _channel = channel; 22 | return _channel; 23 | }; 24 | 25 | createStorage(autologin); 26 | const apolloClient = createApolloClient(apiUrl, autologin, fetchOpts); 27 | const coreInternals = { apolloClient, channel: _channel }; 28 | const authSDK = auth(coreInternals); 29 | const userSDK = user(coreInternals); 30 | 31 | const refreshToken = storage.getRefreshToken(); 32 | 33 | if (autologin && refreshToken) { 34 | const owner = jwtDecode(refreshToken).owner; 35 | 36 | if (isInternalToken(owner)) { 37 | authSDK.refreshToken(true); 38 | } else { 39 | authSDK.refreshExternalToken(true); 40 | } 41 | } 42 | 43 | const client = { 44 | auth: authSDK, 45 | user: userSDK, 46 | config: { channel: _channel, setChannel, autologin }, 47 | _internal: { apolloClient }, 48 | getState: (): State => getState(apolloClient), 49 | }; 50 | 51 | if (DEVELOPMENT_MODE && WINDOW_EXISTS) { 52 | (window as any).__SALEOR_CLIENT__ = client; 53 | } 54 | 55 | return client; 56 | }; 57 | -------------------------------------------------------------------------------- /src/core/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Maybe, UserPermission } from "../apollo/types"; 2 | 3 | export const hasNonEmptyPermissions = ( 4 | permissions: 5 | | Maybe>>> 6 | | undefined 7 | ): boolean => (permissions ? permissions.length > 0 : false); 8 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./createSaleorClient"; 2 | export * from "./types"; 3 | export { createFetch } from "../apollo/client"; 4 | -------------------------------------------------------------------------------- /src/core/state.ts: -------------------------------------------------------------------------------- 1 | import { USER } from "../apollo/queries"; 2 | import { UserQuery } from "../apollo/types"; 3 | import { SaleorClientInternals } from "./types"; 4 | 5 | export type State = UserQuery | null; 6 | 7 | export const getState = ( 8 | client: SaleorClientInternals["apolloClient"] 9 | ): State => 10 | client.readQuery({ 11 | query: USER, 12 | }); 13 | -------------------------------------------------------------------------------- /src/core/storage.ts: -------------------------------------------------------------------------------- 1 | import { LOCAL_STORAGE_EXISTS } from "../constants"; 2 | import { SALEOR_AUTH_PLUGIN_ID, SALEOR_REFRESH_TOKEN } from "./constants"; 3 | 4 | export let storage: { 5 | setAuthPluginId: (method: string | null) => void; 6 | getAuthPluginId: () => string | null; 7 | setAccessToken: (token: string | null) => void; 8 | setRefreshToken: (token: string | null) => void; 9 | getAccessToken: () => string | null; 10 | getRefreshToken: () => string | null; 11 | setTokens: (tokens: { 12 | accessToken: string | null; 13 | refreshToken: string | null; 14 | }) => void; 15 | clear: () => void; 16 | }; 17 | 18 | export const createStorage = (autologinEnabled: boolean): void => { 19 | let authPluginId: string | null = LOCAL_STORAGE_EXISTS 20 | ? localStorage.getItem(SALEOR_AUTH_PLUGIN_ID) 21 | : null; 22 | let accessToken: string | null = null; 23 | let refreshToken: string | null = 24 | autologinEnabled && LOCAL_STORAGE_EXISTS 25 | ? localStorage.getItem(SALEOR_REFRESH_TOKEN) 26 | : null; 27 | 28 | const setAuthPluginId = (pluginId: string | null): void => { 29 | if (LOCAL_STORAGE_EXISTS) { 30 | if (pluginId) { 31 | localStorage.setItem(SALEOR_AUTH_PLUGIN_ID, pluginId); 32 | } else { 33 | localStorage.removeItem(SALEOR_AUTH_PLUGIN_ID); 34 | } 35 | } 36 | 37 | authPluginId = pluginId; 38 | }; 39 | 40 | const setRefreshToken = (token: string | null): void => { 41 | if (token) { 42 | localStorage.setItem(SALEOR_REFRESH_TOKEN, token); 43 | } else { 44 | localStorage.removeItem(SALEOR_REFRESH_TOKEN); 45 | } 46 | 47 | refreshToken = token; 48 | }; 49 | 50 | const setAccessToken = (token: string | null): void => { 51 | accessToken = token; 52 | }; 53 | 54 | const getAuthPluginId = (): string | null => authPluginId; 55 | const getAccessToken = (): string | null => accessToken; 56 | const getRefreshToken = (): string | null => refreshToken; 57 | 58 | const setTokens = ({ 59 | accessToken, 60 | refreshToken, 61 | }: { 62 | accessToken: string | null; 63 | refreshToken: string | null; 64 | }): void => { 65 | setAccessToken(accessToken); 66 | setRefreshToken(refreshToken); 67 | }; 68 | 69 | const clear = (): void => { 70 | setAuthPluginId(null); 71 | setAccessToken(null); 72 | setRefreshToken(null); 73 | }; 74 | 75 | storage = { 76 | setAuthPluginId, 77 | setAccessToken, 78 | setRefreshToken, 79 | getAuthPluginId, 80 | getAccessToken, 81 | getRefreshToken, 82 | setTokens, 83 | clear, 84 | }; 85 | }; 86 | -------------------------------------------------------------------------------- /src/core/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApolloClient, 3 | FetchResult, 4 | NormalizedCacheObject, 5 | } from "@apollo/client"; 6 | import { FetchConfig } from "../apollo"; 7 | import { 8 | AccountConfirmMutation, 9 | AccountConfirmMutationVariables, 10 | AccountDeleteMutation, 11 | AccountRegisterInput, 12 | AccountRequestDeletionMutation, 13 | AccountUpdateMutation, 14 | ConfirmEmailChangeMutation, 15 | CreateAccountAddressMutation, 16 | DeleteAccountAddressMutation, 17 | ExternalAuthenticationUrlMutation, 18 | ExternalLogoutMutation, 19 | ExternalObtainAccessTokensMutation, 20 | ExternalRefreshMutation, 21 | ExternalVerifyMutation, 22 | LoginMutation, 23 | MutationAccountAddressCreateArgs, 24 | MutationAccountAddressUpdateArgs, 25 | MutationAccountSetDefaultAddressArgs, 26 | MutationAccountUpdateArgs, 27 | MutationExternalAuthenticationUrlArgs, 28 | MutationExternalLogoutArgs, 29 | MutationExternalObtainAccessTokensArgs, 30 | MutationPasswordChangeArgs, 31 | MutationRequestEmailChangeArgs, 32 | MutationRequestPasswordResetArgs, 33 | MutationSetPasswordArgs, 34 | MutationTokenCreateArgs, 35 | MutationTokenRefreshArgs, 36 | PasswordChangeMutation, 37 | RefreshTokenMutation, 38 | RegisterMutation, 39 | RequestEmailChangeMutation, 40 | RequestPasswordResetMutation, 41 | SetAccountDefaultAddressMutation, 42 | SetPasswordMutation, 43 | UpdateAccountAddressMutation, 44 | VerifyTokenMutation, 45 | } from "../apollo/types"; 46 | import { AuthSDK } from "./auth"; 47 | import { State } from "./state"; 48 | import { UserSDK } from "./user"; 49 | 50 | export interface SaleorClientInternals { 51 | apolloClient: ApolloClient; 52 | } 53 | export interface SaleorClientConfig { 54 | channel: string; 55 | autologin: boolean; 56 | setChannel(channel: string): string; 57 | } 58 | export interface SaleorClient { 59 | auth: AuthSDK; 60 | user: UserSDK; 61 | config: SaleorClientConfig; 62 | _internal: SaleorClientInternals; 63 | getState(): State; 64 | } 65 | 66 | interface SaleorClientFetchOpts { 67 | autologin?: boolean; 68 | fetchOpts?: FetchConfig; 69 | } 70 | 71 | export interface SaleorClientOpts { 72 | apiUrl: string; 73 | channel: string; 74 | opts?: SaleorClientFetchOpts; 75 | } 76 | 77 | export type SaleorClientMethodsProps = SaleorClientInternals & 78 | Pick; 79 | 80 | export type JWTToken = { 81 | iat: number; 82 | iss: string; 83 | owner: string; 84 | exp: number; 85 | token: string; 86 | email: string; 87 | type: string; 88 | user_id: string; 89 | is_staff: boolean; 90 | }; 91 | 92 | // Meethods opts 93 | // Auth 94 | export type ChangePasswordOpts = MutationPasswordChangeArgs; 95 | export type LoginOpts = MutationTokenCreateArgs & { includeDetails?: boolean }; 96 | export type RefreshTokenOpts = Pick; 97 | export type RegisterOpts = AccountRegisterInput; 98 | export type RequestPasswordResetOpts = MutationRequestPasswordResetArgs; 99 | export type SetPasswordOpts = MutationSetPasswordArgs; 100 | export type GetExternalAuthUrlOpts = MutationExternalAuthenticationUrlArgs; 101 | export type GetExternalAccessTokenOpts = MutationExternalObtainAccessTokensArgs; 102 | export type LogoutOpts = Pick; 103 | // User 104 | export type CreateAccountAddressOpts = MutationAccountAddressCreateArgs; 105 | export type RequestEmailChangeOpts = MutationRequestEmailChangeArgs; 106 | export type SetAccountDefaultAddressOpts = MutationAccountSetDefaultAddressArgs; 107 | export type UpdateAccountOpts = MutationAccountUpdateArgs; 108 | export type UpdateAccountAddressOpts = MutationAccountAddressUpdateArgs; 109 | export type ConfirmAccountOpts = AccountConfirmMutationVariables; 110 | 111 | // Meethods results 112 | // Auth 113 | export type ChangePasswordResult = FetchResult; 114 | export type ChangePasswordData = PasswordChangeMutation["passwordChange"]; 115 | export type LoginResult = FetchResult; 116 | export type LoginData = LoginMutation["tokenCreate"]; 117 | export type LogoutResult = FetchResult | null; 118 | export type LogoutData = ExternalLogoutMutation["externalLogout"] | null; 119 | export type RefreshTokenResult = FetchResult; 120 | export type RefreshTokenData = RefreshTokenMutation["tokenRefresh"]; 121 | export type RegisterResult = FetchResult; 122 | export type RegisterData = RegisterMutation["accountRegister"]; 123 | export type RequestPasswordResetResult = FetchResult< 124 | RequestPasswordResetMutation 125 | >; 126 | export type RequestPasswordResetData = RequestPasswordResetMutation["requestPasswordReset"]; 127 | export type SetPasswordResult = FetchResult; 128 | export type SetPasswordData = SetPasswordMutation["setPassword"]; 129 | export type VerifyTokenResult = FetchResult; 130 | export type VerifyTokenData = VerifyTokenMutation["tokenVerify"]; 131 | export type GetExternalAuthUrlResult = FetchResult< 132 | ExternalAuthenticationUrlMutation 133 | >; 134 | export type GetExternalAuthUrlData = ExternalAuthenticationUrlMutation["externalAuthenticationUrl"]; 135 | export type GetExternalAccessTokenResult = FetchResult< 136 | ExternalObtainAccessTokensMutation 137 | >; 138 | export type GetExternalAccessTokenData = ExternalObtainAccessTokensMutation["externalObtainAccessTokens"]; 139 | export type RefreshExternalTokenResult = FetchResult; 140 | export type RefreshExternalTokenData = ExternalRefreshMutation["externalRefresh"]; 141 | export type VerifyExternalTokenResult = FetchResult; 142 | export type VerifyExternalTokenData = ExternalVerifyMutation["externalVerify"]; 143 | // User 144 | export type AccountDeleteResult = FetchResult; 145 | export type AccountDeleteData = AccountDeleteMutation["accountDelete"]; 146 | export type AccountRequestDeletionResult = FetchResult< 147 | AccountRequestDeletionMutation 148 | >; 149 | export type AccountRequestDeletionData = AccountRequestDeletionMutation["accountRequestDeletion"]; 150 | export type ConfirmEmailChangeResult = FetchResult; 151 | export type ConfirmEmailChangeData = ConfirmEmailChangeMutation["confirmEmailChange"]; 152 | export type CreateAccountAddressResult = FetchResult< 153 | CreateAccountAddressMutation 154 | >; 155 | export type CreateAccountAddressData = CreateAccountAddressMutation["accountAddressCreate"]; 156 | export type DeleteAccountAddressResult = FetchResult< 157 | DeleteAccountAddressMutation 158 | >; 159 | export type DeleteAccountAddressData = DeleteAccountAddressMutation["accountAddressDelete"]; 160 | export type RequestEmailChangeResult = FetchResult; 161 | export type RequestEmailChangeData = RequestEmailChangeMutation["requestEmailChange"]; 162 | export type SetAccountDefaultAddressResult = FetchResult< 163 | SetAccountDefaultAddressMutation 164 | >; 165 | export type SetAccountDefaultAddressData = SetAccountDefaultAddressMutation["accountSetDefaultAddress"]; 166 | export type UpdateAccountResult = FetchResult; 167 | export type UpdateAccountData = AccountUpdateMutation["accountUpdate"]; 168 | export type UpdateAccountAddressResult = FetchResult< 169 | UpdateAccountAddressMutation 170 | >; 171 | export type UpdateAccountAddressData = UpdateAccountAddressMutation["accountAddressUpdate"]; 172 | export type ConfirmAccountResult = FetchResult; 173 | export type ConfirmAccountData = AccountConfirmMutation["confirmAccount"]; 174 | -------------------------------------------------------------------------------- /src/core/user.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CONFIRM_ACCOUNT, 3 | CONFIRM_EMAIL_CHANGE, 4 | CREATE_ACCOUNT_ADDRESS, 5 | DELETE_ACCOUNT, 6 | DELETE_ACCOUNT_ADDRESS, 7 | REQUEST_DELETE_ACCOUNT, 8 | REQUEST_EMAIL_CHANGE, 9 | SET_ACCOUNT_DEFAULT_ADDRESS, 10 | UPDATE_ACCOUNT, 11 | UPDATE_ACCOUNT_ADDRESS, 12 | } from "../apollo/mutations"; 13 | import { 14 | AccountConfirmMutation, 15 | AccountConfirmMutationVariables, 16 | AccountDeleteMutation, 17 | AccountDeleteMutationVariables, 18 | AccountRequestDeletionMutation, 19 | AccountRequestDeletionMutationVariables, 20 | AccountUpdateMutation, 21 | AccountUpdateMutationVariables, 22 | ConfirmEmailChangeMutation, 23 | ConfirmEmailChangeMutationVariables, 24 | CreateAccountAddressMutation, 25 | CreateAccountAddressMutationVariables, 26 | DeleteAccountAddressMutation, 27 | DeleteAccountAddressMutationVariables, 28 | RequestEmailChangeMutation, 29 | RequestEmailChangeMutationVariables, 30 | SetAccountDefaultAddressMutation, 31 | SetAccountDefaultAddressMutationVariables, 32 | UpdateAccountAddressMutation, 33 | UpdateAccountAddressMutationVariables, 34 | } from "../apollo/types"; 35 | import { auth } from "./auth"; 36 | import { 37 | AccountDeleteResult, 38 | AccountRequestDeletionResult, 39 | ConfirmAccountOpts, 40 | ConfirmAccountResult, 41 | ConfirmEmailChangeResult, 42 | CreateAccountAddressResult, 43 | DeleteAccountAddressResult, 44 | RequestEmailChangeResult, 45 | SaleorClientMethodsProps, 46 | SetAccountDefaultAddressResult, 47 | UpdateAccountAddressResult, 48 | UpdateAccountResult, 49 | } from "./types"; 50 | import { 51 | CreateAccountAddressOpts, 52 | RequestEmailChangeOpts, 53 | SetAccountDefaultAddressOpts, 54 | UpdateAccountOpts, 55 | UpdateAccountAddressOpts, 56 | } from "./types"; 57 | 58 | export interface UserSDK { 59 | /** 60 | * Remove user account. 61 | * 62 | * @param token - A one-time token required to remove account. Sent by email using AccountRequestDeletion mutation. 63 | * @returns Deleted user's account data and errors. 64 | */ 65 | accountDelete: (token: string) => Promise; 66 | /** 67 | * Sends an email with the account removal link for the logged-in user. 68 | * 69 | * @param redirectUrl - URL of a view where users should be redirected to delete their account. URL in RFC 1808 format. 70 | * @returns Errors if there are some. 71 | */ 72 | accountRequestDeletion: ( 73 | redirectUrl: string 74 | ) => Promise; 75 | /** 76 | * Confirm the email change of the logged-in user. 77 | * 78 | * @param token - A one-time token required to change the email. 79 | * @returns A user instance with a new email and errors. 80 | */ 81 | confirmEmailChange: (token: string) => Promise; 82 | /** 83 | * Create a new address for the account. 84 | * 85 | * @param opts - Object with fields required to create address and a type of address. 86 | * If provided, the new address will be automatically assigned as the customer's default address of that type. 87 | * @returns Updated user account. 88 | */ 89 | createAccountAddress: ( 90 | opts: CreateAccountAddressOpts 91 | ) => Promise; 92 | /** 93 | * Delete an address of the logged-in account. 94 | * 95 | * @param addressId - ID of the address to delete. 96 | * @returns Updated user account. 97 | */ 98 | deleteAccountAddress: ( 99 | addressId: string 100 | ) => Promise; 101 | /** 102 | * Request email change of the logged in user. 103 | * 104 | * @param opts - Object with new user email, user's password and URL of a view where users should be redirected to update the email address. 105 | * @returns A user instance and errors. 106 | */ 107 | requestEmailChange: ( 108 | opts: RequestEmailChangeOpts 109 | ) => Promise; 110 | /** 111 | * Sets a default address for the authenticated account. 112 | * 113 | * @param opts - Object with ID of the address to set as default and the type of address. 114 | * @returns Updated user account. 115 | */ 116 | setAccountDefaultAddress: ( 117 | opts: SetAccountDefaultAddressOpts 118 | ) => Promise; 119 | /** 120 | * Updates the account of the logged-in account. 121 | * 122 | * @param opts - Fields required to update the account of the logged-in account. 123 | * @returns Updated user account. 124 | */ 125 | updateAccount: (opts: UpdateAccountOpts) => Promise; 126 | /** 127 | * Updates an address of the logged-in account. 128 | * 129 | * @param opts - Object with ID of the address to update and fields required to update the address. 130 | * @returns Updated user account. 131 | */ 132 | updateAccountAddress: ( 133 | opts: UpdateAccountAddressOpts 134 | ) => Promise; 135 | /** 136 | * Confirms user account with token sent by email during registration. 137 | * 138 | * @param opts - Object with email of the user and one-time token required to confirm the account. 139 | * @returns Confirmed user account. 140 | */ 141 | confirmAccount: (opts: ConfirmAccountOpts) => Promise; 142 | } 143 | 144 | export const user = ({ 145 | apolloClient: client, 146 | channel, 147 | }: SaleorClientMethodsProps): UserSDK => { 148 | const _auth = auth({ apolloClient: client, channel }); 149 | 150 | const accountDelete: UserSDK["accountDelete"] = async token => { 151 | const result = await client.mutate< 152 | AccountDeleteMutation, 153 | AccountDeleteMutationVariables 154 | >({ 155 | mutation: DELETE_ACCOUNT, 156 | variables: { token }, 157 | }); 158 | 159 | _auth.logout(); 160 | 161 | return result; 162 | }; 163 | 164 | const accountRequestDeletion: UserSDK["accountRequestDeletion"] = async redirectUrl => { 165 | const result = await client.mutate< 166 | AccountRequestDeletionMutation, 167 | AccountRequestDeletionMutationVariables 168 | >({ 169 | mutation: REQUEST_DELETE_ACCOUNT, 170 | variables: { channel, redirectUrl }, 171 | }); 172 | 173 | return result; 174 | }; 175 | 176 | const confirmEmailChange: UserSDK["confirmEmailChange"] = async token => { 177 | const result = await client.mutate< 178 | ConfirmEmailChangeMutation, 179 | ConfirmEmailChangeMutationVariables 180 | >({ 181 | mutation: CONFIRM_EMAIL_CHANGE, 182 | variables: { channel, token }, 183 | }); 184 | 185 | return result; 186 | }; 187 | 188 | const requestEmailChange: UserSDK["requestEmailChange"] = async opts => { 189 | const result = await client.mutate< 190 | RequestEmailChangeMutation, 191 | RequestEmailChangeMutationVariables 192 | >({ 193 | mutation: REQUEST_EMAIL_CHANGE, 194 | variables: { ...opts, channel: opts.channel || channel }, 195 | }); 196 | 197 | return result; 198 | }; 199 | 200 | const updateAccount: UserSDK["updateAccount"] = async opts => { 201 | const result = await client.mutate< 202 | AccountUpdateMutation, 203 | AccountUpdateMutationVariables 204 | >({ 205 | mutation: UPDATE_ACCOUNT, 206 | variables: { ...opts }, 207 | }); 208 | 209 | return result; 210 | }; 211 | 212 | const setAccountDefaultAddress: UserSDK["setAccountDefaultAddress"] = async opts => { 213 | const result = await client.mutate< 214 | SetAccountDefaultAddressMutation, 215 | SetAccountDefaultAddressMutationVariables 216 | >({ 217 | mutation: SET_ACCOUNT_DEFAULT_ADDRESS, 218 | variables: { ...opts }, 219 | }); 220 | 221 | return result; 222 | }; 223 | 224 | const createAccountAddress: UserSDK["createAccountAddress"] = async opts => { 225 | const result = await client.mutate< 226 | CreateAccountAddressMutation, 227 | CreateAccountAddressMutationVariables 228 | >({ 229 | mutation: CREATE_ACCOUNT_ADDRESS, 230 | variables: { ...opts }, 231 | }); 232 | 233 | return result; 234 | }; 235 | 236 | const deleteAccountAddress: UserSDK["deleteAccountAddress"] = async addressId => { 237 | const result = await client.mutate< 238 | DeleteAccountAddressMutation, 239 | DeleteAccountAddressMutationVariables 240 | >({ 241 | mutation: DELETE_ACCOUNT_ADDRESS, 242 | variables: { addressId }, 243 | }); 244 | 245 | return result; 246 | }; 247 | 248 | const updateAccountAddress: UserSDK["updateAccountAddress"] = async opts => { 249 | const result = await client.mutate< 250 | UpdateAccountAddressMutation, 251 | UpdateAccountAddressMutationVariables 252 | >({ 253 | mutation: UPDATE_ACCOUNT_ADDRESS, 254 | variables: { ...opts }, 255 | }); 256 | 257 | return result; 258 | }; 259 | 260 | const confirmAccount: UserSDK["confirmAccount"] = async opts => { 261 | const result = await client.mutate< 262 | AccountConfirmMutation, 263 | AccountConfirmMutationVariables 264 | >({ 265 | mutation: CONFIRM_ACCOUNT, 266 | variables: { ...opts }, 267 | }); 268 | 269 | return result; 270 | }; 271 | 272 | return { 273 | accountDelete, 274 | accountRequestDeletion, 275 | confirmEmailChange, 276 | createAccountAddress, 277 | deleteAccountAddress, 278 | requestEmailChange, 279 | updateAccount, 280 | updateAccountAddress, 281 | setAccountDefaultAddress, 282 | confirmAccount, 283 | }; 284 | }; 285 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | export const isInternalToken = (owner: string): boolean => owner === "saleor"; 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./core"; 2 | export * from "./apollo"; 3 | export * from "./react"; 4 | -------------------------------------------------------------------------------- /src/react/components/SaleorProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | import { SaleorClient } from "../../core"; 3 | 4 | export interface SaleorProviderProps { 5 | children: ReactNode; 6 | client: SaleorClient; 7 | } 8 | export type SaleorContextType = { 9 | client: SaleorClient; 10 | }; 11 | 12 | export const SaleorContext = React.createContext(null); 13 | 14 | export function SaleorProvider({ client, children }: SaleorProviderProps) { 15 | const [context, setContext] = React.useState(client); 16 | 17 | React.useEffect(() => { 18 | setContext(client); 19 | }, [client]); 20 | 21 | if (context) { 22 | return ( 23 | 24 | {children} 25 | 26 | ); 27 | } 28 | 29 | return null; 30 | } 31 | -------------------------------------------------------------------------------- /src/react/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./SaleorProvider"; 2 | -------------------------------------------------------------------------------- /src/react/helpers/hookFactory.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { SaleorClient } from "../../core/types"; 3 | import { SaleorContext } from "../components/SaleorProvider"; 4 | 5 | const CreateSaleorHook = ( 6 | key: T 7 | ): SaleorClient[T] => { 8 | const saleorClient = useContext(SaleorContext); 9 | 10 | if (!saleorClient) { 11 | throw new Error( 12 | "Could not find saleor's apollo client in the context. Did you forget to wrap the root component in a ?" 13 | ); 14 | } 15 | 16 | const getHookData = (): SaleorClient[T] => { 17 | return saleorClient[key]; 18 | }; 19 | 20 | return getHookData(); 21 | }; 22 | 23 | export const hookFactory = ( 24 | query: T 25 | ) => (): SaleorClient[T] => CreateSaleorHook(query); 26 | -------------------------------------------------------------------------------- /src/react/helpers/hookStateFactory.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { DocumentNode, QueryResult, useQuery } from "@apollo/client"; 3 | import { SaleorContext } from "../components/SaleorProvider"; 4 | 5 | const CreateSaleorStateHook = ( 6 | query: DocumentNode 7 | ): QueryResult => { 8 | const saleorClient = useContext(SaleorContext); 9 | 10 | if (!saleorClient) { 11 | throw new Error( 12 | "Could not find saleor's apollo client in the context. Did you forget to wrap the root component in a ?" 13 | ); 14 | } 15 | 16 | return useQuery(query, { 17 | client: saleorClient._internal.apolloClient, 18 | fetchPolicy: "cache-only", 19 | }); 20 | }; 21 | 22 | export const hookStateFactory = ( 23 | query: DocumentNode 24 | ): QueryResult => 25 | CreateSaleorStateHook(query); 26 | -------------------------------------------------------------------------------- /src/react/hooks/auth.ts: -------------------------------------------------------------------------------- 1 | import { USER } from "../../apollo/queries"; 2 | import { UserQuery, UserQueryVariables } from "../../apollo/types"; 3 | import { hookFactory } from "../helpers/hookFactory"; 4 | import { hookStateFactory } from "../helpers/hookStateFactory"; 5 | 6 | /** 7 | * React hook to get authorization methods 8 | * 9 | * @returns Saleor's authorization methods 10 | */ 11 | export const useAuth = hookFactory("auth"); 12 | 13 | /** 14 | * React hook to get user's authentication data. 15 | * 16 | * @returns Object with user's data 17 | */ 18 | export const useAuthState = (): UserQuery => { 19 | const { data } = hookStateFactory(USER); 20 | 21 | if (!data) { 22 | throw new Error( 23 | "Cache query result is undefined. Invalid cache configuration." 24 | ); 25 | } 26 | 27 | return data; 28 | }; 29 | -------------------------------------------------------------------------------- /src/react/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth"; 2 | export * from "./user"; 3 | export * from "./saleorConfig"; 4 | -------------------------------------------------------------------------------- /src/react/hooks/saleorConfig.ts: -------------------------------------------------------------------------------- 1 | import { hookFactory } from "../helpers/hookFactory"; 2 | 3 | /** 4 | * React hook to get client's config methods 5 | * 6 | * @returns Saleor's client's config methods 7 | */ 8 | export const useSaleorConfig = hookFactory("config"); 9 | -------------------------------------------------------------------------------- /src/react/hooks/user.ts: -------------------------------------------------------------------------------- 1 | import { hookFactory } from "../helpers/hookFactory"; 2 | 3 | /** 4 | * React hook to get user's account methods 5 | * 6 | * @returns Saleor's user's account methods 7 | */ 8 | export const useUser = hookFactory("user"); 9 | -------------------------------------------------------------------------------- /src/react/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components"; 2 | export * from "./hooks"; 3 | -------------------------------------------------------------------------------- /src/react/tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saleor/saleor-sdk/ac1c910d5e574eb7b557ac4c7ea6130dff4a5fc4/src/react/tests/.gitkeep -------------------------------------------------------------------------------- /test/auth.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | API_URI, 3 | TEST_AUTH_EMAIL, 4 | TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_CODE, 5 | TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_SECOND_CODE, 6 | TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_SECOND_STATE, 7 | TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_STATE, 8 | TEST_AUTH_EXTERNAL_LOGOUT_CALLBACK, 9 | TEST_AUTH_PASSWORD, 10 | TEST_AUTH_SECOND_EMAIL, 11 | TEST_AUTH_SECOND_PASSWORD, 12 | } from "../src/config"; 13 | import { storage } from "../src/core/storage"; 14 | import { setupMockServer, setupSaleorClient } from "./setup"; 15 | import { loginWithExternalPlugin } from "./utils"; 16 | 17 | describe("auth api", () => { 18 | const saleor = setupSaleorClient(); 19 | const mockServer = setupMockServer(); 20 | 21 | beforeAll(() => mockServer.listen()); 22 | 23 | afterEach(() => { 24 | mockServer.resetHandlers(); 25 | storage.clear(); 26 | /* 27 | Clear cache to avoid legacy state persistance between tests: 28 | https://github.com/apollographql/apollo-client/issues/3766#issuecomment-578075556 29 | */ 30 | saleor._internal.apolloClient.stop(); 31 | saleor._internal.apolloClient.clearStore(); 32 | }); 33 | 34 | afterAll(() => mockServer.close()); 35 | 36 | it("can login", async () => { 37 | const { data } = await saleor.auth.login({ 38 | email: TEST_AUTH_EMAIL, 39 | password: TEST_AUTH_PASSWORD, 40 | }); 41 | expect(data?.tokenCreate?.errors).toHaveLength(0); 42 | expect(data?.tokenCreate?.user?.id).toBeDefined(); 43 | expect(data?.tokenCreate?.user?.email).toBe(TEST_AUTH_EMAIL); 44 | expect(data?.tokenCreate?.token).toBeDefined(); 45 | expect(storage.getAccessToken()).not.toBeNull(); 46 | expect(storage.getRefreshToken()).not.toBeNull(); 47 | }); 48 | 49 | it("can login without details", async () => { 50 | const { data } = await saleor.auth.login({ 51 | email: TEST_AUTH_EMAIL, 52 | password: TEST_AUTH_PASSWORD, 53 | includeDetails: false, 54 | }); 55 | expect(data?.tokenCreate?.errors).toHaveLength(0); 56 | expect(data?.tokenCreate?.user?.id).toBeDefined(); 57 | expect(data?.tokenCreate?.user?.email).toBe(TEST_AUTH_EMAIL); 58 | expect(data?.tokenCreate?.token).toBeDefined(); 59 | expect(data?.tokenCreate?.user?.addresses).toBeUndefined(); 60 | expect(data?.tokenCreate?.user?.defaultBillingAddress).toBeUndefined(); 61 | expect(data?.tokenCreate?.user?.defaultShippingAddress).toBeUndefined(); 62 | expect(data?.tokenCreate?.user?.metadata).toBeUndefined(); 63 | expect(storage.getAccessToken()).not.toBeNull(); 64 | expect(storage.getRefreshToken()).not.toBeNull(); 65 | }); 66 | 67 | it("login caches user data", async () => { 68 | await saleor.auth.login({ 69 | email: TEST_AUTH_EMAIL, 70 | password: TEST_AUTH_PASSWORD, 71 | }); 72 | const state = saleor.getState(); 73 | expect(state?.user?.id).toBeDefined(); 74 | expect(state?.user?.email).toBe(TEST_AUTH_EMAIL); 75 | expect(state?.authenticated).toBe(true); 76 | }); 77 | 78 | it("will throw an error if login credentials are invalid", async () => { 79 | const { data } = await saleor.auth.login({ 80 | email: "wrong@example.com", 81 | password: "wrong", 82 | }); 83 | expect(data?.tokenCreate?.user).toBeFalsy(); 84 | expect(data?.tokenCreate?.token).toBeFalsy(); 85 | expect(data?.tokenCreate?.errors).not.toHaveLength(0); 86 | }); 87 | 88 | it("manually refreshes auth token", async () => { 89 | await saleor.auth.login({ 90 | email: TEST_AUTH_EMAIL, 91 | password: TEST_AUTH_PASSWORD, 92 | }); 93 | const state = saleor.getState(); 94 | const previousToken = storage.getAccessToken(); 95 | expect(state?.authenticated).toBe(true); 96 | 97 | const { data } = await saleor.auth.refreshToken(); 98 | const newToken = storage.getAccessToken(); 99 | expect(state?.user?.id).toBeDefined(); 100 | expect(state?.user?.email).toBe(TEST_AUTH_EMAIL); 101 | expect(state?.authenticated).toBe(true); 102 | expect(newToken).toBeTruthy(); 103 | expect(data?.tokenRefresh?.token).toEqual(newToken); 104 | expect(previousToken).not.toEqual(newToken); 105 | }); 106 | 107 | it("can register", async () => { 108 | const { data } = await saleor.auth.register({ 109 | email: `register+${Date.now().toString()}@example.com`, 110 | password: "register", 111 | redirectUrl: API_URI, 112 | }); 113 | expect(data?.accountRegister?.errors).toHaveLength(0); 114 | }); 115 | 116 | it("logout clears user cache", async () => { 117 | await saleor.auth.login({ 118 | email: TEST_AUTH_EMAIL, 119 | password: TEST_AUTH_PASSWORD, 120 | }); 121 | await saleor.auth.logout(); 122 | const state = saleor.getState(); 123 | expect(state?.user).toBeFalsy(); 124 | expect(state?.authenticated).toBe(false); 125 | expect(storage.getAccessToken()).toBeNull(); 126 | expect(storage.getRefreshToken()).toBeNull(); 127 | }); 128 | 129 | it("verifies if token is valid", async () => { 130 | const { data } = await saleor.auth.login({ 131 | email: TEST_AUTH_EMAIL, 132 | password: TEST_AUTH_PASSWORD, 133 | }); 134 | 135 | if (data?.tokenCreate?.token) { 136 | const { data: result } = await saleor.auth.verifyToken(); 137 | expect(result?.tokenVerify?.isValid).toBe(true); 138 | } 139 | }); 140 | 141 | it("sends request to reset password", async () => { 142 | const { data } = await saleor.auth.requestPasswordReset({ 143 | email: TEST_AUTH_EMAIL, 144 | redirectUrl: API_URI, 145 | }); 146 | expect(data?.requestPasswordReset?.errors).toHaveLength(0); 147 | }); 148 | 149 | it("changes user's password", async () => { 150 | await saleor.auth.login({ 151 | email: TEST_AUTH_EMAIL, 152 | password: TEST_AUTH_PASSWORD, 153 | }); 154 | const { data } = await saleor.auth.changePassword({ 155 | oldPassword: TEST_AUTH_PASSWORD, 156 | newPassword: TEST_AUTH_PASSWORD, 157 | }); 158 | expect(data?.passwordChange?.errors).toHaveLength(0); 159 | }); 160 | 161 | it("can login with external plugin", async () => { 162 | const accessToken = await loginWithExternalPlugin(saleor, { 163 | code: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_CODE, 164 | state: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_STATE, 165 | }); 166 | expect(accessToken?.externalObtainAccessTokens?.errors).toHaveLength(0); 167 | expect(accessToken?.externalObtainAccessTokens?.user?.id).toBeDefined(); 168 | expect(accessToken?.externalObtainAccessTokens?.user?.email).toBe( 169 | TEST_AUTH_EMAIL 170 | ); 171 | expect(accessToken?.externalObtainAccessTokens?.token).toBeDefined(); 172 | expect(storage.getAccessToken()).not.toBeNull(); 173 | expect(storage.getRefreshToken()).not.toBeNull(); 174 | expect(storage.getAuthPluginId()).not.toBeNull(); 175 | }); 176 | 177 | it("login with external plugin caches user data", async () => { 178 | await loginWithExternalPlugin(saleor, { 179 | code: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_CODE, 180 | state: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_STATE, 181 | }); 182 | const state = saleor.getState(); 183 | expect(state?.user?.id).toBeDefined(); 184 | expect(state?.user?.email).toBe(TEST_AUTH_EMAIL); 185 | expect(state?.authenticated).toBe(true); 186 | }); 187 | 188 | it("fail to login with external plugin", async () => { 189 | const accessToken = await loginWithExternalPlugin(saleor, { 190 | code: "wrong", 191 | state: "wrong", 192 | }); 193 | expect(accessToken?.externalObtainAccessTokens?.user).toBeFalsy(); 194 | expect(accessToken?.externalObtainAccessTokens?.token).toBeFalsy(); 195 | expect(accessToken?.externalObtainAccessTokens?.errors).not.toHaveLength(0); 196 | }); 197 | 198 | it("logout with external plugin clears user cache", async () => { 199 | await loginWithExternalPlugin(saleor, { 200 | code: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_CODE, 201 | state: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_STATE, 202 | }); 203 | await saleor.auth.logout({ 204 | input: JSON.stringify({ 205 | returnTo: TEST_AUTH_EXTERNAL_LOGOUT_CALLBACK, 206 | }), 207 | }); 208 | const state = saleor.getState(); 209 | expect(state?.user).toBeFalsy(); 210 | expect(state?.authenticated).toBe(false); 211 | expect(storage.getAccessToken()).toBeNull(); 212 | expect(storage.getRefreshToken()).toBeNull(); 213 | }); 214 | 215 | it("logout with external plugin returns external logout URL", async () => { 216 | await loginWithExternalPlugin(saleor, { 217 | code: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_CODE, 218 | state: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_STATE, 219 | }); 220 | const result = await saleor.auth.logout({ 221 | input: JSON.stringify({ 222 | returnTo: TEST_AUTH_EXTERNAL_LOGOUT_CALLBACK, 223 | }), 224 | }); 225 | expect(result?.data?.externalLogout?.errors).toHaveLength(0); 226 | const logoutUrl = JSON.parse( 227 | result?.data?.externalLogout?.logoutData || "{}" 228 | ).logoutUrl; 229 | expect(logoutUrl).toBeDefined(); 230 | const logoutUrlReturnToQueryParam = decodeURIComponent(logoutUrl as string) 231 | .split("?")[1] 232 | .split("="); 233 | expect(logoutUrlReturnToQueryParam[0]).toBe("returnTo"); 234 | expect(logoutUrlReturnToQueryParam[1]).toBe( 235 | TEST_AUTH_EXTERNAL_LOGOUT_CALLBACK 236 | ); 237 | }); 238 | 239 | it("manually refresh external access token", async () => { 240 | await loginWithExternalPlugin(saleor, { 241 | code: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_CODE, 242 | state: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_STATE, 243 | }); 244 | const state = saleor.getState(); 245 | const previousToken = storage.getAccessToken(); 246 | expect(state?.authenticated).toBe(true); 247 | 248 | const { data } = await saleor.auth.refreshExternalToken(); 249 | 250 | const newToken = storage.getAccessToken(); 251 | expect(state?.user?.id).toBeDefined(); 252 | expect(state?.authenticated).toBe(true); 253 | expect(newToken).toBeTruthy(); 254 | expect(data?.externalRefresh?.token).toEqual(newToken); 255 | expect(previousToken).not.toEqual(newToken); 256 | }); 257 | 258 | it("verifies if external token is valid", async () => { 259 | const data = await loginWithExternalPlugin(saleor, { 260 | code: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_CODE, 261 | state: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_STATE, 262 | }); 263 | 264 | if (data?.externalObtainAccessTokens?.token) { 265 | const { data: result } = await saleor.auth.verifyExternalToken(); 266 | expect(result?.externalVerify?.isValid).toBe(true); 267 | } 268 | }); 269 | 270 | it("login, logout and login with different credentials", async () => { 271 | let state; 272 | 273 | await saleor.auth.login({ 274 | email: TEST_AUTH_EMAIL, 275 | password: TEST_AUTH_PASSWORD, 276 | }); 277 | state = saleor.getState(); 278 | expect(state?.user?.id).toBeDefined(); 279 | expect(state?.user?.email).toBe(TEST_AUTH_EMAIL); 280 | expect(state?.authenticated).toBe(true); 281 | expect(storage.getAccessToken()).toBeTruthy(); 282 | expect(storage.getRefreshToken()).toBeTruthy(); 283 | expect(storage.getAuthPluginId()).toBeNull(); 284 | 285 | await saleor.auth.logout(); 286 | state = saleor.getState(); 287 | expect(state?.user?.id).toBeFalsy(); 288 | expect(state?.user?.email).toBeFalsy(); 289 | expect(state?.authenticated).toBe(false); 290 | expect(storage.getAccessToken()).toBeNull(); 291 | expect(storage.getRefreshToken()).toBeNull(); 292 | expect(storage.getAuthPluginId()).toBeNull(); 293 | 294 | await saleor.auth.login({ 295 | email: TEST_AUTH_SECOND_EMAIL, 296 | password: TEST_AUTH_SECOND_PASSWORD, 297 | }); 298 | state = saleor.getState(); 299 | expect(state?.user?.id).toBeDefined(); 300 | expect(state?.user?.email).toBe(TEST_AUTH_SECOND_EMAIL); 301 | expect(state?.authenticated).toBe(true); 302 | expect(storage.getAccessToken()).toBeTruthy(); 303 | expect(storage.getRefreshToken()).toBeTruthy(); 304 | expect(storage.getAuthPluginId()).toBeNull(); 305 | }); 306 | 307 | it("login, logout and login with different credentials with external plugin", async () => { 308 | let state; 309 | 310 | await loginWithExternalPlugin(saleor, { 311 | code: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_CODE, 312 | state: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_STATE, 313 | }); 314 | state = saleor.getState(); 315 | expect(state?.user?.id).toBeDefined(); 316 | expect(state?.user?.email).toBe(TEST_AUTH_EMAIL); 317 | expect(state?.authenticated).toBe(true); 318 | expect(storage.getAccessToken()).toBeTruthy(); 319 | expect(storage.getRefreshToken()).toBeTruthy(); 320 | expect(storage.getAuthPluginId()).toBeTruthy(); 321 | 322 | await saleor.auth.logout({ 323 | input: JSON.stringify({ 324 | returnTo: TEST_AUTH_EXTERNAL_LOGOUT_CALLBACK, 325 | }), 326 | }); 327 | state = saleor.getState(); 328 | expect(state?.user?.id).toBeFalsy(); 329 | expect(state?.user?.email).toBeFalsy(); 330 | expect(state?.authenticated).toBe(false); 331 | expect(storage.getAccessToken()).toBeNull(); 332 | expect(storage.getRefreshToken()).toBeNull(); 333 | expect(storage.getAuthPluginId()).toBeNull(); 334 | 335 | await loginWithExternalPlugin(saleor, { 336 | code: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_SECOND_CODE, 337 | state: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_SECOND_STATE, 338 | }); 339 | state = saleor.getState(); 340 | expect(state?.user?.id).toBeDefined(); 341 | expect(state?.user?.email).toBe(TEST_AUTH_SECOND_EMAIL); 342 | expect(state?.authenticated).toBe(true); 343 | expect(storage.getAccessToken()).toBeTruthy(); 344 | expect(storage.getRefreshToken()).toBeTruthy(); 345 | expect(storage.getAuthPluginId()).toBeTruthy(); 346 | }); 347 | 348 | it("caches user data correctly in steps sequence: login, logout, login with external plugin", async () => { 349 | let state; 350 | 351 | await saleor.auth.login({ 352 | email: TEST_AUTH_EMAIL, 353 | password: TEST_AUTH_PASSWORD, 354 | }); 355 | state = saleor.getState(); 356 | expect(state?.user?.id).toBeDefined(); 357 | expect(state?.user?.email).toBe(TEST_AUTH_EMAIL); 358 | expect(state?.authenticated).toBe(true); 359 | expect(storage.getAccessToken()).toBeTruthy(); 360 | expect(storage.getRefreshToken()).toBeTruthy(); 361 | expect(storage.getAuthPluginId()).toBeNull(); 362 | 363 | await saleor.auth.logout(); 364 | state = saleor.getState(); 365 | expect(state?.user?.id).toBeFalsy(); 366 | expect(state?.user?.email).toBeFalsy(); 367 | expect(state?.authenticated).toBe(false); 368 | expect(storage.getAccessToken()).toBeNull(); 369 | expect(storage.getRefreshToken()).toBeNull(); 370 | expect(storage.getAuthPluginId()).toBeNull(); 371 | 372 | await loginWithExternalPlugin(saleor, { 373 | code: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_SECOND_CODE, 374 | state: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_SECOND_STATE, 375 | }); 376 | state = saleor.getState(); 377 | expect(state?.user?.id).toBeDefined(); 378 | expect(state?.user?.email).toBe(TEST_AUTH_SECOND_EMAIL); 379 | expect(state?.authenticated).toBe(true); 380 | expect(storage.getAccessToken()).toBeTruthy(); 381 | expect(storage.getRefreshToken()).toBeTruthy(); 382 | expect(storage.getAuthPluginId()).toBeTruthy(); 383 | }); 384 | 385 | it("caches user data correctly in steps sequence: login with external plugin, logout, login", async () => { 386 | let state; 387 | 388 | await loginWithExternalPlugin(saleor, { 389 | code: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_CODE, 390 | state: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_STATE, 391 | }); 392 | state = saleor.getState(); 393 | expect(state?.user?.id).toBeDefined(); 394 | expect(state?.user?.email).toBe(TEST_AUTH_EMAIL); 395 | expect(state?.authenticated).toBe(true); 396 | expect(storage.getAccessToken()).toBeTruthy(); 397 | expect(storage.getRefreshToken()).toBeTruthy(); 398 | expect(storage.getAuthPluginId()).toBeTruthy(); 399 | 400 | await saleor.auth.logout({ 401 | input: JSON.stringify({ 402 | returnTo: TEST_AUTH_EXTERNAL_LOGOUT_CALLBACK, 403 | }), 404 | }); 405 | state = saleor.getState(); 406 | expect(state?.user?.id).toBeFalsy(); 407 | expect(state?.user?.email).toBeFalsy(); 408 | expect(state?.authenticated).toBe(false); 409 | expect(storage.getAccessToken()).toBeNull(); 410 | expect(storage.getRefreshToken()).toBeNull(); 411 | expect(storage.getAuthPluginId()).toBeNull(); 412 | 413 | await saleor.auth.login({ 414 | email: TEST_AUTH_SECOND_EMAIL, 415 | password: TEST_AUTH_SECOND_PASSWORD, 416 | }); 417 | state = saleor.getState(); 418 | expect(state?.user?.id).toBeDefined(); 419 | expect(state?.user?.email).toBe(TEST_AUTH_SECOND_EMAIL); 420 | expect(state?.authenticated).toBe(true); 421 | expect(storage.getAccessToken()).toBeTruthy(); 422 | expect(storage.getRefreshToken()).toBeTruthy(); 423 | expect(storage.getAuthPluginId()).toBeNull(); 424 | }); 425 | }); 426 | -------------------------------------------------------------------------------- /test/authAutoTokenRefresh.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TEST_AUTH_EMAIL, 3 | TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_CODE, 4 | TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_STATE, 5 | TEST_AUTH_PASSWORD, 6 | } from "../src/config"; 7 | import { SaleorClient } from "../src/core"; 8 | import { storage } from "../src/core/storage"; 9 | import { setupMockServer, setupSaleorClient } from "./setup"; 10 | import { loginWithExternalPlugin } from "./utils"; 11 | 12 | interface RefreshTokenOnDelayedExample { 13 | previousToken: string | null; 14 | newToken: string | null; 15 | } 16 | 17 | const testRefreshTokenOnDelayedExampleRequests = async ( 18 | saleor: SaleorClient, 19 | delayInSeconds: number 20 | ): Promise => { 21 | // Check if initially logged in 22 | const state = saleor.getState(); 23 | const previousToken = storage.getAccessToken(); 24 | expect(state?.user?.id).toBeDefined(); 25 | expect(state?.user?.email).toBe(TEST_AUTH_EMAIL); 26 | expect(state?.authenticated).toBe(true); 27 | 28 | // Wait until token can be refreshed 29 | await new Promise(r => setTimeout(r, delayInSeconds * 1000)); 30 | 31 | // Check that token was not refreshed before making another request 32 | const unchangedPreviousToken = storage.getAccessToken(); 33 | expect(previousToken).toEqual(unchangedPreviousToken); 34 | 35 | // Make another requests 36 | const firstUpdateAccountPromise = saleor.user.updateAccount({ 37 | input: { 38 | firstName: state?.user?.firstName, 39 | lastName: state?.user?.lastName, 40 | }, 41 | }); 42 | const secondUpdateAccountPromise = saleor.user.updateAccount({ 43 | input: { 44 | firstName: state?.user?.firstName, 45 | lastName: state?.user?.lastName, 46 | }, 47 | }); 48 | 49 | // Check that token was refreshed with first another request which did not return errors 50 | const firstUpdateAccount = await firstUpdateAccountPromise; 51 | expect(firstUpdateAccount.data?.accountUpdate?.errors).toHaveLength(0); 52 | const newFirstToken = storage.getAccessToken(); 53 | expect(state?.user?.id).toBeDefined(); 54 | expect(state?.user?.email).toBe(TEST_AUTH_EMAIL); 55 | expect(state?.authenticated).toBe(true); 56 | expect(newFirstToken).toBeTruthy(); 57 | const secondUpdateAccount = await secondUpdateAccountPromise; 58 | expect(secondUpdateAccount.data?.accountUpdate?.errors).toHaveLength(0); 59 | const newSecondToken = storage.getAccessToken(); 60 | expect(state?.user?.id).toBeDefined(); 61 | expect(state?.user?.email).toBe(TEST_AUTH_EMAIL); 62 | expect(state?.authenticated).toBe(true); 63 | expect(newFirstToken).toBeTruthy(); 64 | 65 | // Check if tokenRefresh mutation only executes once on first request, 66 | // and the rest awaits the initial promise instead of creating a new one 67 | expect(newFirstToken).toEqual(newSecondToken); 68 | 69 | return { 70 | previousToken, 71 | newToken: newFirstToken, 72 | }; 73 | }; 74 | 75 | jest.setTimeout(15000); 76 | 77 | describe("auth api auto token refresh", () => { 78 | const tokenExpirationPeriod = 6; 79 | const tokenExpirationPeriodCheckWait = tokenExpirationPeriod + 0.1; 80 | const tokenRefreshTimeSkew = 3; 81 | const tokenRefreshTimeSkewCheckWait = tokenRefreshTimeSkew + 0.1; 82 | const noCheckWait = 0; 83 | 84 | const saleor = setupSaleorClient({ 85 | tokenRefreshTimeSkew, 86 | }); 87 | const mockServer = setupMockServer({ 88 | tokenExpirationPeriod, 89 | }); 90 | 91 | beforeAll(() => mockServer.listen()); 92 | 93 | afterEach(() => { 94 | mockServer.resetHandlers(); 95 | storage.clear(); 96 | /* 97 | Clear cache to avoid legacy state persistance between tests: 98 | https://github.com/apollographql/apollo-client/issues/3766#issuecomment-578075556 99 | */ 100 | saleor._internal.apolloClient.stop(); 101 | saleor._internal.apolloClient.clearStore(); 102 | }); 103 | 104 | afterAll(() => mockServer.close()); 105 | 106 | it("does not refresh access token before another request when refresh time skew not reached", async () => { 107 | await saleor.auth.login({ 108 | email: TEST_AUTH_EMAIL, 109 | password: TEST_AUTH_PASSWORD, 110 | }); 111 | const { 112 | previousToken, 113 | newToken, 114 | } = await testRefreshTokenOnDelayedExampleRequests(saleor, noCheckWait); 115 | expect(previousToken).toEqual(newToken); 116 | }); 117 | 118 | it("does not refresh external access token before another request when refresh time skew not reached", async () => { 119 | await loginWithExternalPlugin(saleor, { 120 | code: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_CODE, 121 | state: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_STATE, 122 | }); 123 | const { 124 | previousToken, 125 | newToken, 126 | } = await testRefreshTokenOnDelayedExampleRequests(saleor, noCheckWait); 127 | expect(previousToken).toEqual(newToken); 128 | }); 129 | 130 | it("automatically refresh access token before another request when refresh time skew reached", async () => { 131 | await saleor.auth.login({ 132 | email: TEST_AUTH_EMAIL, 133 | password: TEST_AUTH_PASSWORD, 134 | }); 135 | const { 136 | previousToken, 137 | newToken, 138 | } = await testRefreshTokenOnDelayedExampleRequests( 139 | saleor, 140 | tokenRefreshTimeSkewCheckWait 141 | ); 142 | expect(previousToken).not.toEqual(newToken); 143 | }); 144 | 145 | it("automatically refresh external access token before another request when refresh time skew reached", async () => { 146 | await loginWithExternalPlugin(saleor, { 147 | code: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_CODE, 148 | state: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_STATE, 149 | }); 150 | const { 151 | previousToken, 152 | newToken, 153 | } = await testRefreshTokenOnDelayedExampleRequests( 154 | saleor, 155 | tokenRefreshTimeSkewCheckWait 156 | ); 157 | expect(previousToken).not.toEqual(newToken); 158 | }); 159 | 160 | it("automatically refresh access token before another request when expiration period reached", async () => { 161 | await saleor.auth.login({ 162 | email: TEST_AUTH_EMAIL, 163 | password: TEST_AUTH_PASSWORD, 164 | }); 165 | const { 166 | previousToken, 167 | newToken, 168 | } = await testRefreshTokenOnDelayedExampleRequests( 169 | saleor, 170 | tokenExpirationPeriodCheckWait 171 | ); 172 | expect(previousToken).not.toEqual(newToken); 173 | }); 174 | 175 | it("automatically refresh external access token before another request when expiration period reached", async () => { 176 | await loginWithExternalPlugin(saleor, { 177 | code: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_CODE, 178 | state: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_STATE, 179 | }); 180 | const { 181 | previousToken, 182 | newToken, 183 | } = await testRefreshTokenOnDelayedExampleRequests( 184 | saleor, 185 | tokenExpirationPeriodCheckWait 186 | ); 187 | expect(previousToken).not.toEqual(newToken); 188 | }); 189 | 190 | it("check if another request has been called, no matter if automatically refresh access token fails", async () => { 191 | await saleor.auth.login({ 192 | email: TEST_AUTH_EMAIL, 193 | password: TEST_AUTH_PASSWORD, 194 | }); 195 | 196 | // Check if initially logged in 197 | const state = saleor.getState(); 198 | const previousToken = storage.getAccessToken(); 199 | expect(state?.user?.id).toBeDefined(); 200 | expect(state?.user?.email).toBe(TEST_AUTH_EMAIL); 201 | expect(state?.authenticated).toBe(true); 202 | 203 | // Wait until token can be refreshed 204 | await new Promise(r => setTimeout(r, tokenRefreshTimeSkewCheckWait * 1000)); 205 | 206 | // Check that token was not refreshed before making another request 207 | const unchangedPreviousToken = storage.getAccessToken(); 208 | expect(previousToken).toEqual(unchangedPreviousToken); 209 | 210 | // Remove csrf token to fail next automatically refresh access token 211 | storage.setRefreshToken(null); 212 | 213 | // Make another requests 214 | const updateAccount = await saleor.user.updateAccount({ 215 | input: { 216 | firstName: state?.user?.firstName, 217 | lastName: state?.user?.lastName, 218 | }, 219 | }); 220 | 221 | // Check that token is still in use and another request did not return errors 222 | expect(updateAccount.data?.accountUpdate?.errors).toHaveLength(0); 223 | const newToken = storage.getAccessToken(); 224 | expect(state?.user?.id).toBeDefined(); 225 | expect(state?.user?.email).toBeDefined(); 226 | expect(state?.authenticated).toBe(true); 227 | expect(newToken).toBeTruthy(); 228 | 229 | // Check if token failed to refresh 230 | expect(previousToken).toEqual(newToken); 231 | }); 232 | 233 | it("check if another request has been called, no matter if automatically refresh external access token fails", async () => { 234 | await loginWithExternalPlugin(saleor, { 235 | code: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_CODE, 236 | state: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_STATE, 237 | }); 238 | 239 | // Check if initially logged in 240 | const state = saleor.getState(); 241 | const previousToken = storage.getAccessToken(); 242 | expect(state?.user?.id).toBeDefined(); 243 | expect(state?.user?.email).toBe(TEST_AUTH_EMAIL); 244 | expect(state?.authenticated).toBe(true); 245 | 246 | // Wait until token can be refreshed 247 | await new Promise(r => setTimeout(r, tokenRefreshTimeSkewCheckWait * 1000)); 248 | 249 | // Check that token was not refreshed before making another request 250 | const unchangedPreviousToken = storage.getAccessToken(); 251 | expect(previousToken).toEqual(unchangedPreviousToken); 252 | 253 | // Remove csrf token to fail next automatically refresh access token 254 | storage.setRefreshToken(null); 255 | 256 | // Make another requests 257 | const updateAccount = await saleor.user.updateAccount({ 258 | input: { 259 | firstName: state?.user?.firstName, 260 | lastName: state?.user?.lastName, 261 | }, 262 | }); 263 | 264 | // Check that token is still in use and another request did not return errors 265 | expect(updateAccount.data?.accountUpdate?.errors).toHaveLength(0); 266 | const newToken = storage.getAccessToken(); 267 | expect(state?.user?.id).toBeDefined(); 268 | expect(state?.user?.email).toBeDefined(); 269 | expect(state?.authenticated).toBe(true); 270 | expect(newToken).toBeTruthy(); 271 | 272 | // Check if token failed to refresh 273 | expect(previousToken).toEqual(newToken); 274 | }); 275 | }); 276 | -------------------------------------------------------------------------------- /test/mocks/accountUpdate.ts: -------------------------------------------------------------------------------- 1 | import { graphql } from "msw"; 2 | import { 3 | AccountUpdateMutation, 4 | AccountUpdateMutationVariables, 5 | } from "../../src/apollo/types"; 6 | import { TEST_AUTH_EMAIL } from "../../src/config"; 7 | import { verifyAuthorization } from "../utils"; 8 | 9 | const accountUpdate = () => 10 | ({ 11 | accountUpdate: { 12 | __typename: "AccountUpdate", 13 | user: { 14 | id: "VXNlcjoxMDMz", 15 | email: TEST_AUTH_EMAIL, 16 | firstName: "", 17 | lastName: "", 18 | isStaff: true, 19 | metadata: [], 20 | addresses: [], 21 | defaultBillingAddress: null, 22 | defaultShippingAddress: null, 23 | userPermissions: [], 24 | __typename: "User", 25 | }, 26 | errors: [], 27 | }, 28 | } as AccountUpdateMutation); 29 | 30 | const accountUpdateError = () => 31 | ({ 32 | accountUpdate: { 33 | __typename: "AccountUpdate", 34 | user: null, 35 | errors: [ 36 | { 37 | message: "Account cannot be updated", 38 | code: "GRAPHQL_ERROR", 39 | field: null, 40 | }, 41 | ], 42 | }, 43 | } as AccountUpdateMutation); 44 | 45 | export const accountUpdateHandler = graphql.mutation< 46 | AccountUpdateMutation, 47 | AccountUpdateMutationVariables 48 | >("accountUpdate", (req, res, ctx) => { 49 | const { input } = req.variables; 50 | 51 | const isAuthorised = verifyAuthorization(req); 52 | 53 | if (!!input && isAuthorised) { 54 | return res(ctx.data(accountUpdate())); 55 | } 56 | 57 | return res(ctx.data(accountUpdateError())); 58 | }); 59 | -------------------------------------------------------------------------------- /test/mocks/externalAuthenticationUrl.ts: -------------------------------------------------------------------------------- 1 | import { graphql } from "msw"; 2 | import { 3 | ExternalAuthenticationUrlMutation, 4 | ExternalAuthenticationUrlMutationVariables, 5 | } from "../../src/apollo/types"; 6 | import { 7 | TEST_AUTH_EXTERNAL_LOGIN_CALLBACK, 8 | TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_ID, 9 | } from "../../src/config"; 10 | 11 | const externalAuthenticationUrl = () => 12 | ({ 13 | externalAuthenticationUrl: { 14 | __typename: "ExternalAuthenticationUrl", 15 | authenticationData: JSON.stringify({ 16 | redirect: "https://example.com/auth", 17 | }), 18 | errors: [], 19 | }, 20 | } as ExternalAuthenticationUrlMutation); 21 | 22 | const externalAuthenticationUrlError = () => 23 | ({ 24 | externalAuthenticationUrl: { 25 | __typename: "ExternalAuthenticationUrl", 26 | authenticationData: null, 27 | errors: [ 28 | { 29 | message: "Invalid code or state", 30 | code: "INVALID", 31 | field: null, 32 | }, 33 | ], 34 | }, 35 | } as ExternalAuthenticationUrlMutation); 36 | 37 | export const externalAuthenticationUrlHandler = graphql.mutation< 38 | ExternalAuthenticationUrlMutation, 39 | ExternalAuthenticationUrlMutationVariables 40 | >("externalAuthenticationUrl", (req, res, ctx) => { 41 | const { pluginId, input } = req.variables; 42 | const parsedInput = JSON.parse(input); 43 | 44 | if ( 45 | pluginId === TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_ID && 46 | parsedInput.redirectUri === TEST_AUTH_EXTERNAL_LOGIN_CALLBACK 47 | ) { 48 | return res(ctx.data(externalAuthenticationUrl())); 49 | } 50 | 51 | return res(ctx.data(externalAuthenticationUrlError())); 52 | }); 53 | -------------------------------------------------------------------------------- /test/mocks/externalLogout.ts: -------------------------------------------------------------------------------- 1 | import { graphql } from "msw"; 2 | import { 3 | ExternalLogoutMutation, 4 | ExternalLogoutMutationVariables, 5 | } from "../../src/apollo/types"; 6 | import { 7 | TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_ID, 8 | TEST_AUTH_EXTERNAL_LOGOUT_CALLBACK, 9 | } from "../../src/config"; 10 | 11 | const externalLogout = (returnTo: string) => 12 | ({ 13 | externalLogout: { 14 | __typename: "ExternalLogout", 15 | logoutData: JSON.stringify({ 16 | logoutUrl: `https://example.com?returnTo=${encodeURIComponent( 17 | returnTo 18 | )}`, 19 | }), 20 | errors: [], 21 | }, 22 | } as ExternalLogoutMutation); 23 | 24 | const externalLogoutError = () => 25 | ({ 26 | externalLogout: { 27 | __typename: "ExternalLogout", 28 | logoutData: null, 29 | errors: [ 30 | { 31 | message: "Unable to external logout", 32 | code: "GRAPHQL_ERROR", 33 | field: null, 34 | }, 35 | ], 36 | }, 37 | } as ExternalLogoutMutation); 38 | 39 | export const externalLogoutHandler = graphql.mutation< 40 | ExternalLogoutMutation, 41 | ExternalLogoutMutationVariables 42 | >("externalLogout", (req, res, ctx) => { 43 | const { pluginId, input } = req.variables; 44 | const parsedInput = JSON.parse(input); 45 | 46 | if ( 47 | pluginId === TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_ID && 48 | parsedInput.returnTo === TEST_AUTH_EXTERNAL_LOGOUT_CALLBACK 49 | ) { 50 | return res(ctx.data(externalLogout(TEST_AUTH_EXTERNAL_LOGOUT_CALLBACK))); 51 | } 52 | 53 | return res(ctx.data(externalLogoutError())); 54 | }); 55 | -------------------------------------------------------------------------------- /test/mocks/externalObtainAccessTokens.ts: -------------------------------------------------------------------------------- 1 | import { graphql } from "msw"; 2 | import { 3 | ExternalObtainAccessTokensMutation, 4 | ExternalObtainAccessTokensMutationVariables, 5 | PermissionEnum, 6 | } from "../../src/apollo/types"; 7 | import { 8 | TEST_AUTH_EMAIL, 9 | TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_ID, 10 | TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_CODE, 11 | TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_SECOND_CODE, 12 | TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_SECOND_STATE, 13 | TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_STATE, 14 | TEST_AUTH_SECOND_EMAIL, 15 | } from "../../src/config"; 16 | import { createTestToken, testRefreshToken } from "../utils"; 17 | 18 | const externalObtainAccessTokens = ( 19 | tokenExpirationPeriodInSeconds?: number, 20 | email?: string 21 | ) => 22 | ({ 23 | externalObtainAccessTokens: { 24 | __typename: "ExternalObtainAccessTokens", 25 | token: createTestToken(tokenExpirationPeriodInSeconds), 26 | refreshToken: testRefreshToken, 27 | user: { 28 | id: "VXNlcjoxMDMz", 29 | email: email, 30 | firstName: "", 31 | lastName: "", 32 | isStaff: true, 33 | metadata: [], 34 | addresses: [], 35 | defaultBillingAddress: null, 36 | defaultShippingAddress: null, 37 | userPermissions: [ 38 | { 39 | code: "HANDLE_CHECKOUTS" as PermissionEnum, 40 | name: "Handle checkouts", 41 | }, 42 | ], 43 | __typename: "User", 44 | }, 45 | errors: [], 46 | }, 47 | } as ExternalObtainAccessTokensMutation); 48 | 49 | const externalObtainAccessTokensError = () => 50 | ({ 51 | externalObtainAccessTokens: { 52 | __typename: "ExternalObtainAccessTokens", 53 | user: null, 54 | token: null, 55 | refreshToken: null, 56 | errors: [ 57 | { 58 | message: "Invalid code or state", 59 | code: "INVALID", 60 | field: null, 61 | }, 62 | ], 63 | }, 64 | } as ExternalObtainAccessTokensMutation); 65 | 66 | export const externalObtainAccessTokensHandler = ( 67 | tokenExpirationPeriodInSeconds?: number 68 | ) => 69 | graphql.mutation< 70 | ExternalObtainAccessTokensMutation, 71 | ExternalObtainAccessTokensMutationVariables 72 | >("externalObtainAccessTokens", (req, res, ctx) => { 73 | const { pluginId, input } = req.variables; 74 | const parsedInput = JSON.parse(input); 75 | 76 | if ( 77 | pluginId === TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_ID && 78 | parsedInput.code === TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_CODE && 79 | parsedInput.state === TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_STATE 80 | ) { 81 | return res( 82 | ctx.data( 83 | externalObtainAccessTokens( 84 | tokenExpirationPeriodInSeconds, 85 | TEST_AUTH_EMAIL 86 | ) 87 | ) 88 | ); 89 | } 90 | if ( 91 | pluginId === TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_ID && 92 | parsedInput.code === 93 | TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_SECOND_CODE && 94 | parsedInput.state === 95 | TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_RESPONSE_SECOND_STATE 96 | ) { 97 | return res( 98 | ctx.data( 99 | externalObtainAccessTokens( 100 | tokenExpirationPeriodInSeconds, 101 | TEST_AUTH_SECOND_EMAIL 102 | ) 103 | ) 104 | ); 105 | } 106 | 107 | return res(ctx.data(externalObtainAccessTokensError())); 108 | }); 109 | -------------------------------------------------------------------------------- /test/mocks/externalRefresh.ts: -------------------------------------------------------------------------------- 1 | import { graphql } from "msw"; 2 | import { 3 | ExternalRefreshMutation, 4 | ExternalRefreshMutationVariables, 5 | } from "../../src/apollo/types"; 6 | import { TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_ID } from "../../src/config"; 7 | import { createTestToken, testRefreshToken } from "../utils"; 8 | 9 | const externalRefresh = (tokenExpirationPeriodInSeconds?: number) => 10 | ({ 11 | externalRefresh: { 12 | __typename: "ExternalRefresh", 13 | token: createTestToken(tokenExpirationPeriodInSeconds), 14 | refreshToken: testRefreshToken, 15 | errors: [], 16 | }, 17 | } as ExternalRefreshMutation); 18 | 19 | const externalRefreshError = () => 20 | ({ 21 | externalRefresh: { 22 | __typename: "ExternalRefresh", 23 | refreshToken: null, 24 | token: null, 25 | errors: [ 26 | { 27 | message: "Token cannot be refreshed", 28 | code: "GRAPHQL_ERROR", 29 | field: null, 30 | }, 31 | ], 32 | }, 33 | } as ExternalRefreshMutation); 34 | 35 | export const externalRefreshHandler = ( 36 | tokenExpirationPeriodInSeconds?: number 37 | ) => 38 | graphql.mutation( 39 | "externalRefresh", 40 | (req, res, ctx) => { 41 | const { pluginId, input } = req.variables; 42 | const parsedInput = JSON.parse(input); 43 | 44 | if ( 45 | pluginId === TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_ID && 46 | !!parsedInput.refreshToken 47 | ) { 48 | return res(ctx.data(externalRefresh(tokenExpirationPeriodInSeconds))); 49 | } 50 | 51 | return res(ctx.data(externalRefreshError())); 52 | } 53 | ); 54 | -------------------------------------------------------------------------------- /test/mocks/externalVerify.ts: -------------------------------------------------------------------------------- 1 | import { graphql } from "msw"; 2 | import { 3 | ExternalVerifyMutation, 4 | ExternalVerifyMutationVariables, 5 | } from "../../src/apollo/types"; 6 | import { 7 | TEST_AUTH_EMAIL, 8 | TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_ID, 9 | } from "../../src/config"; 10 | import { verifyTestToken } from "../utils"; 11 | 12 | const externalVerify = () => 13 | ({ 14 | externalVerify: { 15 | __typename: "ExternalVerify", 16 | isValid: true, 17 | verifyData: null, 18 | user: { 19 | id: "VXNlcjoxMDMz", 20 | email: TEST_AUTH_EMAIL, 21 | firstName: "", 22 | lastName: "", 23 | isStaff: true, 24 | metadata: [], 25 | addresses: [], 26 | defaultBillingAddress: null, 27 | defaultShippingAddress: null, 28 | userPermissions: [], 29 | __typename: "User", 30 | }, 31 | errors: [], 32 | }, 33 | } as ExternalVerifyMutation); 34 | 35 | const externalVerifyError = () => 36 | ({ 37 | externalVerify: { 38 | __typename: "ExternalVerify", 39 | isValid: false, 40 | user: null, 41 | verifyData: null, 42 | errors: [ 43 | { 44 | message: "Token invalid", 45 | code: "INVALID", 46 | field: null, 47 | }, 48 | ], 49 | }, 50 | } as ExternalVerifyMutation); 51 | 52 | export const externalVerifyHandler = graphql.mutation< 53 | ExternalVerifyMutation, 54 | ExternalVerifyMutationVariables 55 | >("externalVerify", (req, res, ctx) => { 56 | const { pluginId, input } = req.variables; 57 | const token = req.headers.get("authorization-bearer") || ""; 58 | const parsedInput = JSON.parse(input); 59 | 60 | const isTokenValid = verifyTestToken(token); 61 | 62 | if ( 63 | pluginId === TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_ID && 64 | !!parsedInput.refreshToken && 65 | isTokenValid 66 | ) { 67 | return res(ctx.data(externalVerify())); 68 | } 69 | 70 | return res(ctx.data(externalVerifyError())); 71 | }); 72 | -------------------------------------------------------------------------------- /test/mocks/index.ts: -------------------------------------------------------------------------------- 1 | import { accountUpdateHandler } from "./accountUpdate"; 2 | import { externalAuthenticationUrlHandler } from "./externalAuthenticationUrl"; 3 | import { externalLogoutHandler } from "./externalLogout"; 4 | import { externalObtainAccessTokensHandler } from "./externalObtainAccessTokens"; 5 | import { externalRefreshHandler } from "./externalRefresh"; 6 | import { externalVerifyHandler } from "./externalVerify"; 7 | import { loginHandler, loginHandlerWithoutDetails } from "./login"; 8 | import { passwordChangeHandler } from "./passwordChange"; 9 | import { refreshTokenHandler } from "./refreshToken"; 10 | import { registerHandler } from "./register"; 11 | import { requestPasswordResetHandler } from "./requestPasswordReset"; 12 | import { verifyTokenHandler } from "./verifyToken"; 13 | 14 | export interface MockHandlersOpts { 15 | tokenExpirationPeriod?: number; 16 | } 17 | 18 | export const mockHandlers = ({ 19 | tokenExpirationPeriod, 20 | }: MockHandlersOpts = {}) => [ 21 | // Auth - Internal login 22 | loginHandler(tokenExpirationPeriod), 23 | loginHandlerWithoutDetails(tokenExpirationPeriod), 24 | refreshTokenHandler(tokenExpirationPeriod), 25 | verifyTokenHandler, 26 | requestPasswordResetHandler, 27 | passwordChangeHandler, 28 | registerHandler, 29 | 30 | // Auth - External login 31 | externalAuthenticationUrlHandler, 32 | externalObtainAccessTokensHandler(tokenExpirationPeriod), 33 | externalRefreshHandler(tokenExpirationPeriod), 34 | externalVerifyHandler, 35 | externalLogoutHandler, 36 | 37 | // User 38 | accountUpdateHandler, 39 | ]; 40 | -------------------------------------------------------------------------------- /test/mocks/login.ts: -------------------------------------------------------------------------------- 1 | import { graphql } from "msw"; 2 | import { 3 | LoginMutation, 4 | LoginMutationVariables, 5 | LoginWithoutDetailsMutation, 6 | LoginWithoutDetailsMutationVariables, 7 | } from "../../src/apollo/types"; 8 | import { 9 | TEST_AUTH_EMAIL, 10 | TEST_AUTH_PASSWORD, 11 | TEST_AUTH_SECOND_EMAIL, 12 | TEST_AUTH_SECOND_PASSWORD, 13 | } from "../../src/config"; 14 | import { createTestToken, testRefreshToken } from "../utils"; 15 | 16 | const login = (tokenExpirationPeriodInSeconds?: number, email?: string) => 17 | ({ 18 | tokenCreate: { 19 | __typename: "CreateToken", 20 | token: createTestToken(tokenExpirationPeriodInSeconds), 21 | refreshToken: testRefreshToken, 22 | user: { 23 | id: "VXNlcjoxMDMz", 24 | email: email, 25 | firstName: "", 26 | lastName: "", 27 | isStaff: true, 28 | metadata: [], 29 | addresses: [], 30 | defaultBillingAddress: null, 31 | defaultShippingAddress: null, 32 | __typename: "User", 33 | userPermissions: [], 34 | }, 35 | errors: [], 36 | }, 37 | } as LoginMutation); 38 | 39 | const loginWithoutDetails = ( 40 | tokenExpirationPeriodInSeconds?: number, 41 | email?: string 42 | ) => 43 | ({ 44 | tokenCreate: { 45 | __typename: "CreateToken", 46 | token: createTestToken(tokenExpirationPeriodInSeconds), 47 | refreshToken: testRefreshToken, 48 | user: { 49 | id: "VXNlcjoxMDMz", 50 | email: email, 51 | firstName: "", 52 | lastName: "", 53 | isStaff: true, 54 | __typename: "User", 55 | userPermissions: [], 56 | }, 57 | errors: [], 58 | }, 59 | } as LoginWithoutDetailsMutation); 60 | 61 | const loginError = () => 62 | ({ 63 | tokenCreate: { 64 | __typename: "RefreshToken", 65 | refreshToken: null, 66 | token: null, 67 | user: null, 68 | errors: [ 69 | { 70 | message: "Unable to login", 71 | code: "GRAPHQL_ERROR", 72 | field: null, 73 | }, 74 | ], 75 | }, 76 | } as LoginMutation); 77 | 78 | export const loginHandler = (tokenExpirationPeriodInSeconds?: number) => 79 | graphql.mutation( 80 | "login", 81 | (req, res, ctx) => { 82 | const { email, password } = req.variables; 83 | 84 | if (email === TEST_AUTH_EMAIL && password === TEST_AUTH_PASSWORD) { 85 | return res( 86 | ctx.data(login(tokenExpirationPeriodInSeconds, TEST_AUTH_EMAIL)) 87 | ); 88 | } 89 | if ( 90 | email === TEST_AUTH_SECOND_EMAIL && 91 | password === TEST_AUTH_SECOND_PASSWORD 92 | ) { 93 | return res( 94 | ctx.data( 95 | login(tokenExpirationPeriodInSeconds, TEST_AUTH_SECOND_EMAIL) 96 | ) 97 | ); 98 | } 99 | 100 | return res(ctx.data(loginError())); 101 | } 102 | ); 103 | 104 | export const loginHandlerWithoutDetails = ( 105 | tokenExpirationPeriodInSeconds?: number 106 | ) => 107 | graphql.mutation< 108 | LoginWithoutDetailsMutation, 109 | LoginWithoutDetailsMutationVariables 110 | >("loginWithoutDetails", (req, res, ctx) => { 111 | const { email, password } = req.variables; 112 | 113 | if (email === TEST_AUTH_EMAIL && password === TEST_AUTH_PASSWORD) { 114 | return res( 115 | ctx.data( 116 | loginWithoutDetails(tokenExpirationPeriodInSeconds, TEST_AUTH_EMAIL) 117 | ) 118 | ); 119 | } 120 | if ( 121 | email === TEST_AUTH_SECOND_EMAIL && 122 | password === TEST_AUTH_SECOND_PASSWORD 123 | ) { 124 | return res( 125 | ctx.data( 126 | loginWithoutDetails( 127 | tokenExpirationPeriodInSeconds, 128 | TEST_AUTH_SECOND_EMAIL 129 | ) 130 | ) 131 | ); 132 | } 133 | 134 | return res(ctx.data(loginError())); 135 | }); 136 | -------------------------------------------------------------------------------- /test/mocks/passwordChange.ts: -------------------------------------------------------------------------------- 1 | import { graphql } from "msw"; 2 | import { 3 | PasswordChangeMutation, 4 | PasswordChangeMutationVariables, 5 | } from "../../src/apollo/types"; 6 | import { 7 | TEST_AUTH_PASSWORD, 8 | TEST_AUTH_SECOND_PASSWORD, 9 | } from "../../src/config"; 10 | import { verifyAuthorization } from "../utils"; 11 | 12 | const passwordChange = () => 13 | ({ 14 | passwordChange: { 15 | __typename: "PasswordChange", 16 | errors: [], 17 | }, 18 | } as PasswordChangeMutation); 19 | 20 | const passwordChangeError = () => 21 | ({ 22 | passwordChange: { 23 | __typename: "PasswordChange", 24 | errors: [ 25 | { 26 | message: "Unable to change password", 27 | code: "GRAPHQL_ERROR", 28 | field: null, 29 | }, 30 | ], 31 | }, 32 | } as PasswordChangeMutation); 33 | 34 | export const passwordChangeHandler = graphql.mutation< 35 | PasswordChangeMutation, 36 | PasswordChangeMutationVariables 37 | >("passwordChange", (req, res, ctx) => { 38 | const { oldPassword } = req.variables; 39 | 40 | const isAuthorised = verifyAuthorization(req); 41 | 42 | if ( 43 | (oldPassword === TEST_AUTH_PASSWORD || 44 | oldPassword === TEST_AUTH_SECOND_PASSWORD) && 45 | isAuthorised 46 | ) { 47 | return res(ctx.data(passwordChange())); 48 | } 49 | 50 | return res(ctx.data(passwordChangeError())); 51 | }); 52 | -------------------------------------------------------------------------------- /test/mocks/refreshToken.ts: -------------------------------------------------------------------------------- 1 | import { graphql } from "msw"; 2 | import { 3 | RefreshTokenMutation, 4 | RefreshTokenMutationVariables, 5 | } from "../../src/apollo/types"; 6 | import { createTestToken } from "../utils"; 7 | 8 | const createRefreshToken = (tokenExpirationPeriodInSeconds?: number) => 9 | ({ 10 | tokenRefresh: { 11 | __typename: "RefreshToken", 12 | token: createTestToken(tokenExpirationPeriodInSeconds), 13 | errors: [], 14 | }, 15 | } as RefreshTokenMutation); 16 | 17 | const refreshTokenError = () => 18 | ({ 19 | tokenRefresh: { 20 | __typename: "RefreshToken", 21 | token: null, 22 | errors: [ 23 | { 24 | message: "Token cannot be refreshed", 25 | code: "GRAPHQL_ERROR", 26 | field: null, 27 | }, 28 | ], 29 | }, 30 | } as RefreshTokenMutation); 31 | 32 | export const refreshTokenHandler = (tokenExpirationPeriodInSeconds?: number) => 33 | graphql.mutation( 34 | "refreshToken", 35 | (req, res, ctx) => { 36 | const { refreshToken } = req.variables; 37 | 38 | if (!!refreshToken) { 39 | return res( 40 | ctx.data(createRefreshToken(tokenExpirationPeriodInSeconds)) 41 | ); 42 | } 43 | 44 | return res(ctx.data(refreshTokenError())); 45 | } 46 | ); 47 | -------------------------------------------------------------------------------- /test/mocks/register.ts: -------------------------------------------------------------------------------- 1 | import { graphql } from "msw"; 2 | import { 3 | RegisterMutation, 4 | RegisterMutationVariables, 5 | } from "../../src/apollo/types"; 6 | 7 | const register = () => 8 | ({ 9 | accountRegister: { 10 | __typename: "AccountRegister", 11 | requiresConfirmation: true, 12 | errors: [], 13 | }, 14 | } as RegisterMutation); 15 | 16 | const registerError = () => 17 | ({ 18 | accountRegister: { 19 | __typename: "CreateToken", 20 | requiresConfirmation: null, 21 | errors: [ 22 | { 23 | message: "Unable to register", 24 | code: "GRAPHQL_ERROR", 25 | field: null, 26 | }, 27 | ], 28 | }, 29 | } as RegisterMutation); 30 | 31 | export const registerHandler = graphql.mutation< 32 | RegisterMutation, 33 | RegisterMutationVariables 34 | >("register", (req, res, ctx) => { 35 | const { input } = req.variables; 36 | 37 | if (!!input.email && !!input.password) { 38 | return res(ctx.data(register())); 39 | } 40 | 41 | return res(ctx.data(registerError())); 42 | }); 43 | -------------------------------------------------------------------------------- /test/mocks/requestPasswordReset.ts: -------------------------------------------------------------------------------- 1 | import { graphql } from "msw"; 2 | import { 3 | RequestPasswordResetMutation, 4 | RequestPasswordResetMutationVariables, 5 | } from "../../src/apollo/types"; 6 | import { TEST_AUTH_EMAIL, TEST_AUTH_SECOND_EMAIL } from "../../src/config"; 7 | 8 | const requestPasswordReset = () => 9 | ({ 10 | requestPasswordReset: { 11 | __typename: "RequestPasswordReset", 12 | errors: [], 13 | }, 14 | } as RequestPasswordResetMutation); 15 | 16 | const requestPasswordResetError = () => 17 | ({ 18 | requestPasswordReset: { 19 | __typename: "RequestPasswordReset", 20 | errors: [ 21 | { 22 | message: "Unable to reset password", 23 | code: "GRAPHQL_ERROR", 24 | field: null, 25 | }, 26 | ], 27 | }, 28 | } as RequestPasswordResetMutation); 29 | 30 | export const requestPasswordResetHandler = graphql.mutation< 31 | RequestPasswordResetMutation, 32 | RequestPasswordResetMutationVariables 33 | >("requestPasswordReset", (req, res, ctx) => { 34 | const { email } = req.variables; 35 | 36 | if (email === TEST_AUTH_EMAIL || email === TEST_AUTH_SECOND_EMAIL) { 37 | return res(ctx.data(requestPasswordReset())); 38 | } 39 | 40 | return res(ctx.data(requestPasswordResetError())); 41 | }); 42 | -------------------------------------------------------------------------------- /test/mocks/verifyToken.ts: -------------------------------------------------------------------------------- 1 | import { graphql } from "msw"; 2 | import { 3 | VerifyTokenMutation, 4 | VerifyTokenMutationVariables, 5 | } from "../../src/apollo/types"; 6 | import { TEST_AUTH_EMAIL } from "../../src/config"; 7 | import { verifyTestToken } from "../utils"; 8 | 9 | const verifyToken = () => 10 | ({ 11 | tokenVerify: { 12 | __typename: "VerifyToken", 13 | isValid: true, 14 | payload: null, 15 | user: { 16 | id: "VXNlcjoxMDMz", 17 | email: TEST_AUTH_EMAIL, 18 | firstName: "", 19 | lastName: "", 20 | isStaff: true, 21 | metadata: [], 22 | addresses: [], 23 | defaultBillingAddress: null, 24 | defaultShippingAddress: null, 25 | userPermissions: [], 26 | __typename: "User", 27 | }, 28 | errors: [], 29 | }, 30 | } as VerifyTokenMutation); 31 | 32 | const verifyTokenError = () => 33 | ({ 34 | tokenVerify: { 35 | __typename: "VerifyToken", 36 | isValid: false, 37 | user: null, 38 | payload: null, 39 | errors: [ 40 | { 41 | message: "Token invalid", 42 | code: "INVALID", 43 | field: null, 44 | }, 45 | ], 46 | }, 47 | } as VerifyTokenMutation); 48 | 49 | export const verifyTokenHandler = graphql.mutation< 50 | VerifyTokenMutation, 51 | VerifyTokenMutationVariables 52 | >("verifyToken", (req, res, ctx) => { 53 | const { token } = req.variables; 54 | 55 | const isTokenValid = verifyTestToken(token || ""); 56 | 57 | if (isTokenValid) { 58 | return res(ctx.data(verifyToken())); 59 | } 60 | 61 | return res(ctx.data(verifyTokenError())); 62 | }); 63 | -------------------------------------------------------------------------------- /test/setup.ts: -------------------------------------------------------------------------------- 1 | import NodeHttpAdapter from "@pollyjs/adapter-node-http"; 2 | import { Polly, PollyServer } from "@pollyjs/core"; 3 | import FSPersister from "@pollyjs/persister-fs"; 4 | import path from "path"; 5 | import { Context, setupPolly } from "setup-polly-jest"; 6 | 7 | import { setupServer } from "msw/node"; 8 | import { FetchConfig } from "../src/apollo"; 9 | import { API_URI } from "../src/config"; 10 | import { SaleorClient, createSaleorClient } from "../src/core"; 11 | import { MockHandlersOpts, mockHandlers } from "./mocks"; 12 | import { removeBlacklistedVariables } from "./utils"; 13 | 14 | Polly.register(NodeHttpAdapter); 15 | Polly.register(FSPersister); 16 | 17 | export const setupPollyMiddleware = (server: PollyServer): void => { 18 | server.any().on("beforePersist", (_, recording) => { 19 | const requestJson = JSON.parse(recording.request.postData.text); 20 | const responseHeaders = recording.response.headers.filter( 21 | (el: Record) => 22 | !["authorization-bearer", "set-cookie"].includes(el.name) 23 | ); 24 | const requestHeaders = recording.request.headers.filter( 25 | (el: Record) => 26 | !["authorization-bearer", "set-cookie"].includes(el.name) 27 | ); 28 | 29 | const filteredRequestJson = removeBlacklistedVariables(requestJson); 30 | 31 | const responseJson = JSON.parse(recording.response.content.text); 32 | const filteredResponseJson = removeBlacklistedVariables(responseJson); 33 | 34 | recording.request.postData.text = JSON.stringify(filteredRequestJson); 35 | recording.request.headers = requestHeaders; 36 | recording.response.cookies = []; 37 | recording.response.content.text = JSON.stringify(filteredResponseJson); 38 | recording.response.headers = responseHeaders; 39 | }); 40 | }; 41 | export const setupRecording = (): Context => 42 | setupPolly({ 43 | adapterOptions: { 44 | fetch: { 45 | context: global, 46 | }, 47 | }, 48 | adapters: ["node-http"], 49 | matchRequestsBy: { 50 | headers: { 51 | exclude: ["authorization-bearer", "host", "content-length"], 52 | }, 53 | url: false, 54 | body(body): string { 55 | const json = JSON.parse(body); 56 | const filteredJson = removeBlacklistedVariables(json); 57 | 58 | return JSON.stringify(filteredJson); 59 | }, 60 | }, 61 | persister: "fs", 62 | persisterOptions: { 63 | fs: { 64 | recordingsDir: path.resolve(__dirname, "../recordings"), 65 | }, 66 | }, 67 | recordIfMissing: true, 68 | }); 69 | 70 | export const setupMockServer = (opts?: MockHandlersOpts) => 71 | setupServer(...mockHandlers(opts)); 72 | 73 | export const setupSaleorClient = (fetchOpts?: FetchConfig): SaleorClient => { 74 | const saleor = createSaleorClient({ 75 | apiUrl: API_URI, 76 | channel: "default-channel", 77 | opts: { 78 | fetchOpts, 79 | }, 80 | }); 81 | 82 | return saleor; 83 | }; 84 | -------------------------------------------------------------------------------- /test/user.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | setupRecording, 3 | setupSaleorClient, 4 | setupPollyMiddleware, 5 | } from "./setup"; 6 | import { API_URI, TEST_AUTH_EMAIL, TEST_AUTH_PASSWORD } from "../src/config"; 7 | import { CountryCode } from "../src/apollo/types"; 8 | 9 | describe("user api", () => { 10 | const context = setupRecording(); 11 | const saleor = setupSaleorClient(); 12 | 13 | const testAddress = { 14 | firstName: "Test name", 15 | lastName: "Test lastname", 16 | streetAddress1: "Test street address", 17 | city: "Test city", 18 | postalCode: "12-345", 19 | country: "PL" as CountryCode, 20 | }; 21 | 22 | // beforeAll(async () => { 23 | // const email = `register+${Date.now().toString()}@example.com`; 24 | // const password = "register"; 25 | 26 | // await saleor.auth.register({ 27 | // email, 28 | // password, 29 | // redirectUrl: API_URI, 30 | // }); 31 | // }); 32 | 33 | beforeEach(async () => { 34 | const { server } = context.polly; 35 | setupPollyMiddleware(server); 36 | 37 | await saleor.auth.login({ 38 | email: TEST_AUTH_EMAIL, 39 | password: TEST_AUTH_PASSWORD, 40 | }); 41 | }); 42 | 43 | it("sends a request to delete user account", async () => { 44 | const { data } = await saleor.user.accountRequestDeletion(API_URI); 45 | expect(data?.accountRequestDeletion?.errors).toHaveLength(0); 46 | }); 47 | 48 | it("sends request to change user email", async () => { 49 | const { data } = await saleor.user.requestEmailChange({ 50 | newEmail: `register+${Date.now().toString()}@example.com`, 51 | password: TEST_AUTH_PASSWORD, 52 | redirectUrl: API_URI, 53 | }); 54 | expect(data?.requestEmailChange?.errors).toHaveLength(0); 55 | }); 56 | 57 | it("updates the user first name", async () => { 58 | const { data } = await saleor.user.updateAccount({ 59 | input: { 60 | firstName: "", 61 | }, 62 | }); 63 | expect(data?.accountUpdate?.user?.firstName).toBe(""); 64 | expect(data?.accountUpdate?.errors).toHaveLength(0); 65 | }); 66 | 67 | it("creates the user account address", async () => { 68 | const { data } = await saleor.user.createAccountAddress({ 69 | input: testAddress, 70 | }); 71 | 72 | if (data?.accountAddressCreate?.user?.addresses?.length) { 73 | expect( 74 | data?.accountAddressCreate?.user?.addresses[ 75 | data?.accountAddressCreate?.user?.addresses?.length - 1 76 | ]?.firstName === testAddress.firstName 77 | ); 78 | } 79 | expect(data?.accountAddressCreate?.errors).toHaveLength(0); 80 | }); 81 | 82 | it.skip("updates the user account address", async () => { 83 | const { data: newAddress } = await saleor.user.createAccountAddress({ 84 | input: testAddress, 85 | }); 86 | const newAddresses = newAddress?.accountAddressCreate?.user?.addresses; 87 | const index = newAddresses?.length ? newAddresses.length - 1 : 0; 88 | const addressId = newAddresses?.length ? newAddresses[index]?.id : null; 89 | if (addressId) { 90 | const newTestName = "New test name"; 91 | const { data } = await saleor.user.updateAccountAddress({ 92 | id: addressId, 93 | input: { 94 | ...testAddress, 95 | firstName: newTestName, 96 | }, 97 | }); 98 | const state = saleor.getState(); 99 | if (data?.accountAddressUpdate?.user?.addresses?.length) { 100 | expect( 101 | data?.accountAddressUpdate?.user?.addresses[index]?.firstName 102 | ).toBe(newTestName); 103 | expect(state?.user?.addresses?.[index]?.firstName).toBe(newTestName); 104 | } 105 | expect(data?.accountAddressUpdate?.errors).toHaveLength(0); 106 | } 107 | }); 108 | 109 | it("sets address as a default billing address", async () => { 110 | const { data: newAddress } = await saleor.user.createAccountAddress({ 111 | input: testAddress, 112 | }); 113 | const newAddresses = newAddress?.accountAddressCreate?.user?.addresses; 114 | const index = newAddresses?.length ? newAddresses.length - 1 : 0; 115 | const addressId = newAddresses?.length ? newAddresses[index]?.id : null; 116 | expect(addressId).toBeTruthy(); 117 | if (addressId) { 118 | const oldAddress = 119 | newAddress?.accountAddressCreate?.user?.defaultBillingAddress; 120 | const { data } = await saleor.user.setAccountDefaultAddress({ 121 | id: addressId, 122 | type: "BILLING", 123 | }); 124 | expect( 125 | data?.accountSetDefaultAddress?.user?.defaultBillingAddress?.id 126 | ).toBe(addressId); 127 | expect(data?.accountSetDefaultAddress?.errors).toHaveLength(0); 128 | 129 | // Set back default testing address 130 | if (oldAddress) { 131 | const { 132 | data: revertedData, 133 | } = await saleor.user.setAccountDefaultAddress({ 134 | id: oldAddress.id, 135 | type: "BILLING", 136 | }); 137 | expect( 138 | revertedData?.accountSetDefaultAddress?.user?.defaultBillingAddress 139 | ?.id 140 | ).toBe(oldAddress?.id); 141 | expect(revertedData?.accountSetDefaultAddress?.errors).toHaveLength(0); 142 | } 143 | } 144 | }); 145 | 146 | it.skip("deletes user address", async () => { 147 | const { data: newAddress } = await saleor.user.createAccountAddress({ 148 | input: testAddress, 149 | }); 150 | const newAddresses = newAddress?.accountAddressCreate?.user?.addresses; 151 | const index = newAddresses?.length ? newAddresses.length - 1 : 0; 152 | const addressId = newAddresses?.length ? newAddresses[index]?.id : null; 153 | expect(addressId).toBeTruthy(); 154 | if (addressId) { 155 | const { data } = await saleor.user.deleteAccountAddress(addressId); 156 | const state = saleor.getState(); 157 | expect(data?.accountAddressDelete?.user?.addresses).toHaveLength(index); 158 | expect(state?.user?.addresses).toHaveLength(index); 159 | expect(data?.accountAddressDelete?.errors).toHaveLength(0); 160 | } 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import { GraphQLRequest } from "msw"; 3 | import omitDeep from "omit-deep-lodash"; 4 | import { ExternalObtainAccessTokensMutation } from "../src/apollo/types"; 5 | import { 6 | TEST_AUTH_EXTERNAL_LOGIN_CALLBACK, 7 | TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_ID, 8 | } from "../src/config"; 9 | import { SaleorClient } from "../src/core"; 10 | 11 | export const removeBlacklistedVariables = (obj: {}): {} => { 12 | const variablesBlacklist = [ 13 | "email", 14 | "password", 15 | "redirectUrl", 16 | "newPassword", 17 | "oldPassword", 18 | "newEmail", 19 | "token", 20 | "refreshToken", 21 | "csrfToken", 22 | ]; 23 | 24 | return omitDeep(obj, ...variablesBlacklist); 25 | }; 26 | 27 | const testTokenExpirationPeriodInSeconds = 3600; // 1 hour by default 28 | export const testTokenSecret = "secret"; 29 | export const testRefreshToken = 30 | "sSrkI91Yyho52LTNWLuh6WkPwC5NAP49n1TdB4Oh4Hrw7NuQ1oj7ga3j5aE82b2O"; 31 | export const createTestToken = ( 32 | expirationPeriodInSeconds: number = testTokenExpirationPeriodInSeconds 33 | ): string => 34 | jwt.sign( 35 | { 36 | data: Math.random(), // to prevent generating the same tokens within the same second - some tests may try to create tokens quickly 37 | exp: Math.floor(Date.now() / 1000) + expirationPeriodInSeconds, 38 | owner: "saleor", 39 | }, 40 | testTokenSecret 41 | ); 42 | export const verifyTestToken = (token: string): boolean => { 43 | try { 44 | jwt.verify(token, testTokenSecret); 45 | } catch (err) { 46 | return false; 47 | } 48 | return true; 49 | }; 50 | export const verifyAuthorization = (request: GraphQLRequest) => { 51 | const token = request.headers.get("authorization-bearer") || ""; 52 | const isTokenValid = verifyTestToken(token); 53 | 54 | return isTokenValid; 55 | }; 56 | 57 | interface CallbackQueryParams { 58 | code: string; 59 | state: string; 60 | } 61 | 62 | export const loginWithExternalPlugin = async ( 63 | saleor: SaleorClient, 64 | callbackQueryParams: CallbackQueryParams 65 | ): Promise => { 66 | const { data: authUrl } = await saleor.auth.getExternalAuthUrl({ 67 | pluginId: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_ID, 68 | input: JSON.stringify({ 69 | redirectUri: TEST_AUTH_EXTERNAL_LOGIN_CALLBACK, 70 | }), 71 | }); 72 | expect(authUrl?.externalAuthenticationUrl?.errors).toHaveLength(0); 73 | // Assume client redirects to external plugin and redirects back to callback address with given callback query params 74 | const { data: accessToken } = await saleor.auth.getExternalAccessToken({ 75 | pluginId: TEST_AUTH_EXTERNAL_LOGIN_PLUGIN_ID, 76 | input: JSON.stringify(callbackQueryParams), 77 | }); 78 | 79 | return accessToken; 80 | }; 81 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "test", "types"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // output .js.map sourcemap files for consumers 11 | "sourceMap": true, 12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 13 | "rootDir": ".", 14 | // stricter type-checking for stronger correctness. Recommended by TS 15 | "strict": true, 16 | // linter checks for common issues 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | // use Node's module resolution algorithm, instead of the legacy TS one 23 | "moduleResolution": "node", 24 | // transpile JSX to React.createElement 25 | "jsx": "react", 26 | // interop between ESM and CJS modules. Recommended by TS 27 | "esModuleInterop": true, 28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 29 | "skipLibCheck": true, 30 | // error out if import and file system have a casing mismatch. Recommended by TS 31 | "forceConsistentCasingInFileNames": true, 32 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 33 | "noEmit": true 34 | } 35 | } 36 | --------------------------------------------------------------------------------