├── .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 |
--------------------------------------------------------------------------------