├── .github ├── CODEOWNERS └── workflows │ └── deploy.yaml ├── .gitignore ├── LICENSE ├── README.md ├── build.savant ├── examples ├── browser-example │ ├── README.md │ ├── browser-example.iml │ ├── example.js │ ├── fusionauth-typescript-client.js │ ├── fusionauth-typescript-client.min.js │ ├── fusionauth-typescript-client.min.js.map │ └── index.html ├── hybrid-example │ ├── .gitignore │ ├── README.md │ ├── example.ts │ ├── hybrid-example.iml │ ├── index.html │ ├── package.json │ └── tsconfig.json └── node-example │ ├── .gitignore │ ├── README.md │ ├── example.ts │ ├── node-example.iml │ ├── package.json │ └── tsconfig.json ├── fusionauth-typescript-client.iml ├── index.ts ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── ClientResponse.ts ├── DefaultRESTClient.ts ├── DefaultRESTClientBuilder.ts ├── FusionAuthClient.ts ├── IRESTClient.ts └── IRESTClientBuilder.ts ├── test ├── DefaultRESTClientTest.ts ├── FusionAuthClientTest.ts └── tsconfig.json └── tsconfig.json /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This is a managed file. Manual changes will be overwritten. 2 | # https://github.com/FusionAuth/fusionauth-public-repos/ 3 | 4 | .github/ @fusionauth/owners @fusionauth/platform 5 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Deploy 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | workflow_dispatch: 12 | inputs: 13 | command: 14 | type: choice 15 | options: 16 | - build # build only 17 | - publish # build & publish to npmjs 18 | - release # build & release to svn 19 | default: build 20 | 21 | permissions: 22 | contents: read 23 | id-token: write 24 | 25 | jobs: 26 | deploy: 27 | runs-on: ubuntu-latest 28 | defaults: 29 | run: 30 | shell: /usr/bin/bash -l -e -o pipefail {0} 31 | steps: 32 | - name: checkout 33 | uses: actions/checkout@v4 34 | 35 | - name: setup java 36 | uses: actions/setup-java@v4 37 | with: 38 | distribution: temurin 39 | java-version: 21 40 | java-package: jre 41 | 42 | - name: install savant 43 | run: | 44 | curl -O https://repository.savantbuild.org/org/savantbuild/savant-core/2.0.0/savant-2.0.0.tar.gz 45 | tar xzvf savant-2.0.0.tar.gz 46 | savant-2.0.0/bin/sb --version 47 | SAVANT_PATH=$(realpath -s "./savant-2.0.0/bin") 48 | echo "${SAVANT_PATH}" >> $GITHUB_PATH 49 | mkdir -p ~/.savant/plugins 50 | cat << EOF > ~/.savant/plugins/org.savantbuild.plugin.java.properties 51 | 21=${JAVA_HOME} 52 | EOF 53 | 54 | - name: compile 55 | run: sb compile 56 | 57 | ### Everything below this line will only run on a workflow_dispatch 58 | 59 | - name: set aws credentials 60 | if: inputs.command == 'release' || inputs.command == 'publish' 61 | uses: aws-actions/configure-aws-credentials@v4 62 | with: 63 | role-to-assume: arn:aws:iam::752443094709:role/gha-fusionauth-typescript-client 64 | role-session-name: aws-auth-action 65 | aws-region: us-west-2 66 | 67 | - name: get secret 68 | if: inputs.command == 'release' || inputs.command == 'publish' 69 | run: | 70 | while IFS=$'\t' read -r key value; do 71 | echo "::add-mask::${value}" 72 | echo "${key}=${value}" >> $GITHUB_ENV 73 | done < <(aws secretsmanager get-secret-value \ 74 | --region us-west-2 \ 75 | --secret-id platform/npmjs \ 76 | --query SecretString \ 77 | --output text | \ 78 | jq -r 'to_entries[] | [.key, .value] | @tsv') 79 | 80 | - name: create npmrc 81 | if: inputs.command == 'release' || inputs.command == 'publish' 82 | run: | 83 | echo "color=false" > ~/.npmrc 84 | echo "//registry.npmjs.org/:_authToken=${{ env.API_KEY }}" >> ~/.npmrc 85 | chmod 600 ~/.npmrc 86 | 87 | - name: release to svn 88 | if: inputs.command == 'release' 89 | run: sb release 90 | 91 | - name: publish to npmjs 92 | if: inputs.command == 'publish' 93 | run: sb publish 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | npm-debug.log 4 | *.zip 5 | dist 6 | **/.DS_Store 7 | .savant/cache 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## FusionAuth TypeScript Client 2 | ![semver 2.0.0 compliant](http://img.shields.io/badge/semver-2.0.0-brightgreen.svg?style=flat-square) [![npm](https://img.shields.io/npm/v/@fusionauth/typescript-client?style=flat-square)](https://www.npmjs.com/package/@fusionauth/typescript-client) 3 | 4 | If you're integrating FusionAuth with a Typescript application, this library will speed up your development time. It also works with node and browser applications as well. 5 | 6 | For additional information and documentation on FusionAuth refer to [https://fusionauth.io](https://fusionauth.io). 7 | 8 | ## Credits 9 | * Thanks to [@tjpeden](https://github.com/tjpeden) for your excellent contributions! 10 | 11 | ## Installation 12 | To install fusionauth-typescript-client, use npm 13 | 14 | ```bash 15 | npm install @fusionauth/typescript-client 16 | ``` 17 | 18 | Refer to the FusionAuth API documentation to for request and response formats. 19 | * https://fusionauth.io/docs/v1/tech/apis/ 20 | * https://fusionauth.io/docs/v1/tech/client-libraries/typescript 21 | 22 | ## Development 23 | 24 | * Set up a fusionauth instance. (Not sure exactly how to configure, TBD.) 25 | * `sb test` 26 | 27 | ## Questions and support 28 | 29 | If you have a question or support issue regarding this client library, we'd love to hear from you. 30 | 31 | If you have a paid edition with support included, please [open a ticket in your account portal](https://account.fusionauth.io/account/support/). Learn more about [paid editions here](https://fusionauth.io/pricing). 32 | 33 | Otherwise, please [post your question in the community forum](https://fusionauth.io/community/forum/). 34 | 35 | ## Contributing 36 | 37 | Bug reports and pull requests are welcome on GitHub. 38 | 39 | ## License 40 | 41 | This code is available as open source under the terms of the [Apache v2.0 License](https://opensource.org/licenses/Apache-2.0). 42 | 43 | 44 | ## Upgrade Policy 45 | 46 | This library is built automatically to keep track of the FusionAuth API, and may also receive updates with bug fixes, security patches, tests, code samples, or documentation changes. 47 | 48 | These releases may also update dependencies, language engines, and operating systems, as we\'ll follow the deprecation and sunsetting policies of the underlying technologies that it uses. 49 | 50 | This means that after a dependency (e.g. language, framework, or operating system) is deprecated by its maintainer, this library will also be deprecated by us, and will eventually be updated to use a newer version. 51 | -------------------------------------------------------------------------------- /build.savant: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2024, FusionAuth, All Rights Reserved 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an 12 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific 14 | * language governing permissions and limitations under the License. 15 | */ 16 | 17 | project(group: "io.fusionauth", name: "fusionauth-typescript-client", version: "1.58.0", licenses: ["ApacheV2_0"]) { 18 | workflow { 19 | fetch { 20 | cache() 21 | url(url: "https://repository.savantbuild.org") 22 | } 23 | publish { 24 | cache() 25 | } 26 | } 27 | 28 | publishWorkflow { 29 | subversion(repository: "https://svn.savantbuild.org") 30 | } 31 | 32 | publications { 33 | main { 34 | publication(name: "fusionauth-typescript-client", type: "zip", file: "build/fusionauth-typescript-client.zip") 35 | } 36 | } 37 | } 38 | 39 | // Plugins 40 | file = loadPlugin(id: "org.savantbuild.plugin:file:2.0.0") 41 | idea = loadPlugin(id: "org.savantbuild.plugin:idea:2.0.0") 42 | release = loadPlugin(id: "org.savantbuild.plugin:release-git:2.0.0") 43 | 44 | target(name: "init", description: "Initializes the project") { 45 | def proc = 'npm ci'.execute() 46 | proc.consumeProcessOutput(System.out, System.err) 47 | proc.waitFor() 48 | if (proc.exitValue() != 0) { 49 | fail("Failed to run npm install") 50 | } 51 | } 52 | 53 | target(name: "clean", description: "Cleans build directory", dependsOn: ["init"]) { 54 | file.prune(dir: "build") 55 | file.delete { 56 | fileSet(dir: ".", includePatterns: [~/.+\.zip/]) 57 | } 58 | } 59 | 60 | target(name: "compile", description: "Builds the js files", dependsOn: ["init"]) { 61 | def proc = 'npm ci'.execute() 62 | proc.consumeProcessOutput(System.out, System.err) 63 | proc.waitFor() 64 | if (proc.exitValue() != 0) { 65 | fail("Failed to get deps or compile typescript") 66 | } 67 | 68 | proc = 'npm run build-browser'.execute() 69 | proc.consumeProcessOutput(System.out, System.err) 70 | proc.waitFor() 71 | if (proc.exitValue() != 0) { 72 | fail("Failed to build browser version of client") 73 | } 74 | 75 | proc = 'npm run build-browser-min'.execute() 76 | proc.consumeProcessOutput(System.out, System.err) 77 | proc.waitFor() 78 | if (proc.exitValue() != 0) { 79 | fail("Failed to build minified browser version of client") 80 | } 81 | } 82 | 83 | target(name: "int", description: "Releases a local integration build of the project", dependsOn: ["compile"]) { 84 | file.delete { 85 | fileSet(dir: "build", includePatterns: [~/fusionauth-typescript-client.+\.zip/]) 86 | } 87 | 88 | file.zip(file: "build/fusionauth-typescript-client.zip") { 89 | fileSet(dir: "build", includePatterns: [ 90 | ~/index.*(js|ts)/, 91 | ~/src\/+/ 92 | ]) 93 | fileSet(dir: "dist", includePatterns: [~/fusionauth-typescript-client\..*js/]) 94 | } 95 | } 96 | 97 | target(name: "idea", description: "Updates the IntelliJ IDEA module file") { 98 | idea.iml() 99 | } 100 | 101 | target(name: "test", description: "Runs the tests", dependsOn: ["compile"]) { 102 | if (new ProcessBuilder('npm', 'test').inheritIO().start().waitFor() != 0) { 103 | fail("Tests failed") 104 | } 105 | } 106 | 107 | target(name: "publish", description: "Publish in NPM", dependsOn: ["clean", "int"]) { 108 | def npmPublish = 'npm publish --access=public'.execute() 109 | npmPublish.consumeProcessOutput(System.out, System.err) 110 | } 111 | 112 | target(name: "release", description: "Releases a full version of the project", dependsOn: ["int"]) { 113 | release.release() 114 | } 115 | -------------------------------------------------------------------------------- /examples/browser-example/README.md: -------------------------------------------------------------------------------- 1 | FusionAuth Client Browser Example 2 | ==== 3 | 4 | This directly contains the minimum layout for a browser project using FusionAuth. 5 | 6 | You will need the client downloaded from the [releases page](https://github.com/FusionAuth/fusionauth-typescript-client/releases) as well as a script tag on your website that imports the client. 7 | 8 | The client is namespaced under `FusionAuth` to prevent polluting the global namespace. 9 | -------------------------------------------------------------------------------- /examples/browser-example/browser-example.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/browser-example/example.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, FusionAuth, All Rights Reserved 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an 12 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific 14 | * language governing permissions and limitations under the License. 15 | */ 16 | 17 | const client = new FusionAuth.FusionAuthClient('bf69486b-4733-4470-a592-f1bfce7af580', 'https://local.fusionauth.io'); 18 | 19 | client.retrieveUserByEmail('example@example.com') 20 | .then(clientResponse => { 21 | console.log("User:", JSON.stringify(clientResponse.response.user, null, 2)); 22 | }).catch(console.error); 23 | -------------------------------------------------------------------------------- /examples/browser-example/fusionauth-typescript-client.js: -------------------------------------------------------------------------------- 1 | ../../dist/fusionauth-typescript-client.js -------------------------------------------------------------------------------- /examples/browser-example/fusionauth-typescript-client.min.js: -------------------------------------------------------------------------------- 1 | ../../dist/fusionauth-typescript-client.min.js -------------------------------------------------------------------------------- /examples/browser-example/fusionauth-typescript-client.min.js.map: -------------------------------------------------------------------------------- 1 | ../../dist/fusionauth-typescript-client.min.js.map -------------------------------------------------------------------------------- /examples/browser-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /examples/hybrid-example/.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | -------------------------------------------------------------------------------- /examples/hybrid-example/README.md: -------------------------------------------------------------------------------- 1 | FusionAuth Client Hybrid Example 2 | ==== 3 | 4 | This example exists as a minimal setup to get the FusionAuth Client working in both the browser and nodejs environment. For web publishing we chose to go with `browserify` for simplicity. This could also be done using webpack if preferred. 5 | 6 | ## Requirements 7 | 8 | * `browserify` - Compiles stuff for the browser 9 | * `tsify` - Compiles typescript for browserify 10 | * `browserify-shim` - Allows code to work in both the browser and node by replacing require(x) with require(y) 11 | * `typescript` - tsify doesn't depend on typescript so that you can choose exactly which version you want 12 | * `tsconfig.json` - Provides the rules for `tsc` and `tsify` to compile typescript, we provided a minimal example. 13 | * `example.ts` - A script that actually uses FusionAuth client 14 | * `index.html` - A webpage that uses the script 15 | 16 | ## Building 17 | 18 | To build nodejs you will need to use `tsc`. This compiles the project to `build/example.js` and produces a sourcemap. You can then run this in nodejs. 19 | 20 | To build for the browser we use `npx browserify example.ts --debug -p tsify -t browserify-shim -o dist/example-browser.js` but we also add this line of code to package.json so that you can instead just call `npm run build-browser`. Both of these commands outputs to `dist/example-browser.js`. This example currently uses a browserify-shim for the client itself to replace the node version with the browser version we ship. This also means that the client must be added to the index.html as a ` 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/hybrid-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hybrid-example", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build-browser": "npx browserify example.ts --debug -p tsify -t browserify-shim -o dist/example-browser.js" 10 | }, 11 | "keywords": [], 12 | "author": "Tyler Scott", 13 | "license": "Apache2.0", 14 | "browserify-shim": { 15 | "@fusionauth/typescript-client": "global:FusionAuth" 16 | }, 17 | "dependencies": { 18 | "@fusionauth/typescript-client": "^1.16.0-RC.1" 19 | }, 20 | "devDependencies": { 21 | "browserify": "^16.5.1", 22 | "browserify-shim": "^3.8.14", 23 | "tsify": "^4.0.1", 24 | "typescript": "^3.8.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/hybrid-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "lib": [ 7 | "esnext" 8 | ], 9 | "outDir": "build", 10 | "types": [ 11 | "node" 12 | ] 13 | }, 14 | "exclude": [ 15 | "node_modules", 16 | "build" 17 | ] 18 | } -------------------------------------------------------------------------------- /examples/node-example/.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | -------------------------------------------------------------------------------- /examples/node-example/README.md: -------------------------------------------------------------------------------- 1 | FusionAuth Client NodeJS Example 2 | ==== 3 | 4 | This example exists as a minimal setup to get the FusionAuth Client working in the nodejs environment. 5 | 6 | ## Requirements 7 | 8 | * `typescript` - We use typescript in this example for type completion while we code 9 | * `tsconfig.json` - Provides the rules for `tsc` to compile typescript, we provided a minimal example 10 | * `example.ts` - A script that actually uses FusionAuth client 11 | 12 | ## Building 13 | 14 | To build nodejs you will need to use `tsc`. This compiles the project to `build/example.js` and produces a sourcemap. You can then run this in nodejs. 15 | 16 | Assuming you have node but not typescript installed, you can do the following (tested with node v12): 17 | 18 | * `sudo npm install -g typescript # will install the `tsc` executable.` 19 | * `npm install @types/node --save-dev # solves the 'Build:Cannot find type definition file for 'node'' issue` 20 | * update the api key and FA location in `example.ts` 21 | * `tsc # compiles the typescript` 22 | * `node build/example.js # actually runs the code` 23 | 24 | -------------------------------------------------------------------------------- /examples/node-example/example.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, FusionAuth, All Rights Reserved 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an 12 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific 14 | * language governing permissions and limitations under the License. 15 | */ 16 | 17 | import FusionAuthClient from "@fusionauth/typescript-client"; 18 | 19 | const client = new FusionAuthClient('bf69486b-4733-4470-a592-f1bfce7af580', 'https://local.fusionauth.io'); 20 | 21 | client.retrieveUserByEmail('example@example.com') 22 | .then(clientResponse => { 23 | console.log("User:", JSON.stringify(clientResponse.response.user, null, 2)); 24 | }).catch(console.error); 25 | -------------------------------------------------------------------------------- /examples/node-example/node-example.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/node-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-example", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "Tyler Scott", 12 | "license": "Apache-2.0", 13 | "dependencies": { 14 | "@fusionauth/typescript-client": "^1.16.0-RC.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/node-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "lib": [ 7 | "esnext" 8 | ], 9 | "outDir": "build", 10 | "types": [ 11 | "node" 12 | ] 13 | }, 14 | "exclude": [ 15 | "node_modules", 16 | "build" 17 | ] 18 | } -------------------------------------------------------------------------------- /fusionauth-typescript-client.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, FusionAuth, All Rights Reserved 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an 12 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific 14 | * language governing permissions and limitations under the License. 15 | */ 16 | 17 | import FusionAuthClient from './src/FusionAuthClient' 18 | export default FusionAuthClient; 19 | 20 | export * from './src/FusionAuthClient'; 21 | export * from './src/IRESTClient'; 22 | export * from './src/DefaultRESTClientBuilder'; 23 | export * from './src/ClientResponse'; 24 | export * from './src/IRESTClient'; 25 | export * from './src/IRESTClientBuilder'; 26 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, FusionAuth, All Rights Reserved 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an 12 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific 14 | * language governing permissions and limitations under the License. 15 | */ 16 | 17 | const mocha = require("karma-mocha"); 18 | const mochaReporter = require("karma-mocha-reporter"); 19 | const chai = require("karma-chai"); 20 | const chrome = require("karma-chrome-launcher"); 21 | 22 | // Karma configuration 23 | module.exports = function(config) { 24 | config.set({ 25 | // base path that will be used to resolve all patterns (eg. files, exclude) 26 | basePath: '', 27 | 28 | // frameworks to use 29 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 30 | frameworks: ['mocha', 'chai'], 31 | 32 | // list of files / patterns to load in the browser (hosted files) 33 | files: [ 34 | 'dist/fusionauth-typescript-client-test.min.js' 35 | ], 36 | 37 | // list of files to exclude 38 | exclude: [], 39 | 40 | // preprocess matching files before serving them to the browser 41 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 42 | preprocessors: {}, 43 | 44 | plugins: [ 45 | mocha, 46 | chai, 47 | mochaReporter, 48 | chrome, 49 | ], 50 | 51 | // test results reporter to use 52 | // possible values: 'dots', 'progress' 53 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 54 | reporters: ['mocha'], 55 | 56 | // web server port 57 | port: 9876, 58 | 59 | // enable / disable colors in the output (reporters and logs) 60 | colors: true, 61 | 62 | // level of logging 63 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 64 | logLevel: config.LOG_INFO, 65 | 66 | // enable / disable watching file and executing tests whenever any file changes 67 | autoWatch: false, 68 | 69 | // start these browsers 70 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 71 | browsers: ['ChromiumHeadless'], 72 | 73 | // Continuous Integration mode 74 | // if true, Karma captures browsers, runs the tests and exits 75 | singleRun: true, 76 | 77 | // Concurrency level 78 | // how many browser should be started simultaneous 79 | concurrency: Infinity 80 | }) 81 | }; 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fusionauth/typescript-client", 3 | "version": "1.58.0", 4 | "description": "A typescript implementation of the FusionAuth client.", 5 | "main": "build/index.js", 6 | "types": "build/index.d.ts", 7 | "directories": { 8 | "lib": "build", 9 | "test": "test" 10 | }, 11 | "files": [ 12 | "build/src/*.js", 13 | "build/src/*.js.map", 14 | "build/src/*.d.ts", 15 | "build/*.js", 16 | "build/*.js.map", 17 | "build/*.d.ts", 18 | "dist/fusionauth-typescript-client.*" 19 | ], 20 | "scripts": { 21 | "test": "npx mocha build/test/*.js && npx karma start", 22 | "build-browser": "mkdir -p dist && npx browserify index.ts --standalone FusionAuth --debug -p [ tsify --target=es5 ] -t browserify-shim -o dist/fusionauth-typescript-client.js", 23 | "build-browser-min": "mkdir -p dist && npx browserify index.ts --standalone FusionAuth --debug -p [ tsify --target=es5 ] -t browserify-shim -t uglifyify | npx uglifyjs --source-map base -o dist/fusionauth-typescript-client.min.js", 24 | "pretest": "npx tsc && npx tsc -p test && mkdir -p dist && npx browserify test/*.ts --debug -t envify -p [ tsify --target=es5 -p test ] -t browserify-shim -t uglifyify | npx uglifyjs --source-map base -o dist/fusionauth-typescript-client-test.min.js", 25 | "prepare": "npx tsc && npm run build-browser && npm run build-browser-min" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/FusionAuth/fusionauth-typescript-client.git" 30 | }, 31 | "keywords": [ 32 | "FusionAuth", 33 | "client", 34 | "typescript" 35 | ], 36 | "author": "Tyler Scott", 37 | "license": "Apache-2.0", 38 | "bugs": { 39 | "url": "https://github.com/FusionAuth/fusionauth-typescript-client/issues" 40 | }, 41 | "homepage": "https://github.com/FusionAuth/fusionauth-typescript-client#readme", 42 | "devDependencies": { 43 | "@types/chai": "^4.2.11", 44 | "@types/mocha": "^7.0.2", 45 | "@types/node": "^13.13.4", 46 | "@types/node-fetch": "^2.5.7", 47 | "browserify": "^16.5.1", 48 | "browserify-shim": "^3.8.14", 49 | "chai": "^4.2.0", 50 | "cross-fetch": "^3.0.6", 51 | "envify": "^4.1.0", 52 | "karma": "^6.3.4", 53 | "karma-chai": "^0.1.0", 54 | "karma-chrome-launcher": "^3.1.0", 55 | "karma-mocha": "^2.0.0", 56 | "karma-mocha-reporter": "^2.2.3", 57 | "mocha": "^11.1.0", 58 | "ts-node": "^8.10.1", 59 | "tsify": "^4.0.1", 60 | "typescript": "^3.8.3", 61 | "uglify-js": "^3.9.1", 62 | "uglifyify": "^5.0.0" 63 | }, 64 | "browserify-shim": { 65 | "node-fetch": "cross-fetch", 66 | "url": "global:window" 67 | }, 68 | "dependencies": { 69 | "node-fetch": "^2.6.1" 70 | }, 71 | "browser": "dist/fusionauth-typescript-client.min.js" 72 | } 73 | -------------------------------------------------------------------------------- /src/ClientResponse.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, FusionAuth, All Rights Reserved 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an 12 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific 14 | * language governing permissions and limitations under the License. 15 | */ 16 | 17 | export default class ClientResponse { 18 | public statusCode: number; 19 | public response: T; 20 | public exception: Error; 21 | 22 | wasSuccessful() { 23 | return this.statusCode >= 200 && this.statusCode < 300; 24 | } 25 | } -------------------------------------------------------------------------------- /src/DefaultRESTClient.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2024, FusionAuth, All Rights Reserved 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an 12 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific 14 | * language governing permissions and limitations under the License. 15 | */ 16 | 17 | import IRESTClient, { ErrorResponseHandler, ResponseHandler } from "./IRESTClient"; 18 | import ClientResponse from "./ClientResponse"; 19 | import fetch, { BodyInit, RequestCredentials, Response } from 'node-fetch'; 20 | import { URLSearchParams } from "url"; 21 | 22 | /** 23 | * @author Brett P 24 | * @author Tyler Scott 25 | * @author TJ Peden 26 | */ 27 | export default class DefaultRESTClient implements IRESTClient { 28 | public body: BodyInit; 29 | public headers: Record = {}; 30 | public method: string; 31 | public parameters: Record = {}; 32 | public uri: string; 33 | public credentials: RequestCredentials; 34 | public responseHandler: ResponseHandler = DefaultRESTClient.JSONResponseHandler; 35 | public errorResponseHandler: ErrorResponseHandler = DefaultRESTClient.ErrorJSONResponseHandler; 36 | 37 | constructor(public host: string) { 38 | } 39 | 40 | /** 41 | * A function that returns the JSON form of the response text. 42 | * 43 | * @param response 44 | * @constructor 45 | */ 46 | static async JSONResponseHandler(response: Response): Promise> { 47 | let clientResponse = new ClientResponse(); 48 | 49 | clientResponse.statusCode = response.status; 50 | let type = response.headers.get("content-type"); 51 | if (type && type.startsWith("application/json")) { 52 | clientResponse.response = await response.json(); 53 | } 54 | 55 | return clientResponse; 56 | } 57 | 58 | /** 59 | * A function that returns the JSON form of the response text. 60 | * 61 | * @param response 62 | * @constructor 63 | */ 64 | static async ErrorJSONResponseHandler(response: Response): Promise> { 65 | let clientResponse = new ClientResponse(); 66 | 67 | clientResponse.statusCode = response.status; 68 | let type = response.headers.get("content-type"); 69 | if (type && type.startsWith("application/json")) { 70 | clientResponse.exception = await response.json(); 71 | } 72 | 73 | return clientResponse; 74 | } 75 | 76 | /** 77 | * Sets the authorization header using a key 78 | * 79 | * @param {string} key The value of the authorization header. 80 | * @returns {DefaultRESTClient} 81 | */ 82 | withAuthorization(key: string): DefaultRESTClient { 83 | if (key === null || typeof key === 'undefined') { 84 | return this; 85 | } 86 | 87 | this.withHeader('Authorization', key); 88 | return this; 89 | } 90 | 91 | /** 92 | * Adds a segment to the request uri 93 | */ 94 | withUriSegment(segment: string | number): DefaultRESTClient { 95 | if (segment === null || segment === undefined) { 96 | return this; 97 | } 98 | if (this.uri === null) { 99 | this.uri = ''; 100 | } 101 | if (this.uri.charAt(this.uri.length - 1) !== '/') { 102 | this.uri += '/'; 103 | } 104 | this.uri = this.uri + segment; 105 | return this; 106 | } 107 | 108 | /** 109 | * Get the full url + parameter list 110 | */ 111 | getFullUrl() { 112 | return this.host + this.uri + this.getQueryString(); 113 | } 114 | 115 | /** 116 | * Sets the body of the client request. 117 | * 118 | * @param body The object to be written to the request body as form data. 119 | */ 120 | withFormData(body: URLSearchParams): DefaultRESTClient { 121 | const body2 = new URLSearchParams(); 122 | if (body) { 123 | body.forEach((value, name, searchParams) => { 124 | if (value && value.length > 0 && value != "null" && value != "undefined") { 125 | body2.set(name, value); 126 | } 127 | }); 128 | body = body2; 129 | } 130 | this.body = body; 131 | this.withHeader('Content-Type', 'application/x-www-form-urlencoded'); 132 | return this; 133 | } 134 | 135 | /** 136 | * Adds a header to the request. 137 | * 138 | * @param key The name of the header. 139 | * @param value The value of the header. 140 | */ 141 | withHeader(key: string, value: string): DefaultRESTClient { 142 | this.headers[key] = value; 143 | return this; 144 | } 145 | 146 | /** 147 | * Sets the body of the client request. 148 | * 149 | * @param body The object to be written to the request body as JSON. 150 | */ 151 | withJSONBody(body: object): DefaultRESTClient { 152 | this.body = JSON.stringify(body); 153 | this.withHeader('Content-Type', 'application/json'); 154 | // Omit the Content-Length, this is set auto-magically by the request library 155 | return this; 156 | } 157 | 158 | /** 159 | * Sets the http method for the request 160 | */ 161 | withMethod(method: string): DefaultRESTClient { 162 | this.method = method; 163 | return this; 164 | } 165 | 166 | /** 167 | * Sets the uri of the request 168 | */ 169 | withUri(uri: string): DefaultRESTClient { 170 | this.uri = uri; 171 | return this; 172 | } 173 | 174 | /** 175 | * Adds parameters to the request. 176 | * 177 | * @param name The name of the parameter. 178 | * @param value The value of the parameter, may be a string, object or number. 179 | */ 180 | withParameter(name: string, value: any): DefaultRESTClient { 181 | this.parameters[name] = value; 182 | return this; 183 | } 184 | 185 | /** 186 | * Sets request's credentials. 187 | * 188 | * @param value A string indicating whether credentials will be sent with the request always, never, or only when sent to a same-origin URL. 189 | */ 190 | withCredentials(value: RequestCredentials): DefaultRESTClient { 191 | this.credentials = value; 192 | return this; 193 | } 194 | 195 | withResponseHandler(handler: ResponseHandler): DefaultRESTClient { 196 | this.responseHandler = handler; 197 | return this; 198 | } 199 | 200 | withErrorResponseHandler(handler: ErrorResponseHandler): DefaultRESTClient { 201 | this.errorResponseHandler = handler; 202 | return this; 203 | } 204 | 205 | /** 206 | * Run the request and return a promise. This promise will resolve if the request is successful 207 | * and reject otherwise. 208 | */ 209 | async go(): Promise> { 210 | const clientResponse = new ClientResponse(); 211 | 212 | let response: Response; 213 | try { 214 | response = await fetch( 215 | this.getFullUrl(), 216 | { 217 | method: this.method, 218 | headers: this.headers, 219 | body: this.body as BodyInit, 220 | // @ts-ignore (Credentials are not supported on NodeJS) 221 | credentials: this.credentials, 222 | }, 223 | ); 224 | 225 | if (response.ok) { 226 | return await this.responseHandler(response); 227 | } else { 228 | throw await this.errorResponseHandler(response); 229 | } 230 | } catch (error) { 231 | if (error instanceof ClientResponse) { 232 | throw error; // Don't catch a ClientResponse (we want this to trigger the catch of the promise 233 | } 234 | 235 | if (response) { // Try to recover the response status 236 | clientResponse.statusCode = response.status; 237 | } 238 | clientResponse.exception = error; 239 | 240 | throw clientResponse; 241 | } 242 | } 243 | 244 | private getQueryString() { 245 | let queryString = ''; 246 | const appendParam = (key: string, param: string) => { 247 | queryString += (queryString.length === 0) ? '?' : '&'; 248 | queryString += encodeURIComponent(key) + '=' + encodeURIComponent(param); 249 | } 250 | for (let key in this.parameters) { 251 | const value = this.parameters[key]; 252 | if (Array.isArray(value)) { 253 | value.forEach(val => appendParam(key, val)) 254 | } else { 255 | appendParam(key, value); 256 | } 257 | } 258 | return queryString; 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/DefaultRESTClientBuilder.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, FusionAuth, All Rights Reserved 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an 12 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific 14 | * language governing permissions and limitations under the License. 15 | */ 16 | 17 | import IRESTClient from "./IRESTClient"; 18 | import DefaultRESTClient from "./DefaultRESTClient"; 19 | import IRESTClientBuilder from "./IRESTClientBuilder"; 20 | 21 | export default class DefaultRESTClientBuilder implements IRESTClientBuilder { 22 | build(host: string): IRESTClient { 23 | return new DefaultRESTClient(host); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/IRESTClient.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, FusionAuth, All Rights Reserved 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an 12 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific 14 | * language governing permissions and limitations under the License. 15 | */ 16 | 17 | import ClientResponse from "./ClientResponse"; 18 | import {RequestCredentials, Response} from "node-fetch"; 19 | import {URLSearchParams} from "url"; 20 | 21 | export type ResponseHandler = (response: Response) => Promise>; 22 | export type ErrorResponseHandler = (response: Response) => Promise>; 23 | 24 | export interface IRESTClient { 25 | /** 26 | * Sets the authorization header using a key 27 | * 28 | * @param {string} key The value of the authorization header. 29 | * @returns {IRESTClient} 30 | */ 31 | withAuthorization(key: string): IRESTClient; 32 | 33 | /** 34 | * Adds a segment to the request uri 35 | */ 36 | withUriSegment(segment: string | number): IRESTClient; 37 | 38 | /** 39 | * Sets the body of the client request. 40 | * 41 | * @param body The object to be written to the request body as form data. 42 | */ 43 | withFormData(body: URLSearchParams): IRESTClient; 44 | 45 | /** 46 | * Adds a header to the request. 47 | * 48 | * @param key The name of the header. 49 | * @param value The value of the header. 50 | */ 51 | withHeader(key: string, value: string): IRESTClient; 52 | 53 | /** 54 | * Sets the body of the client request. 55 | * 56 | * @param body The object to be written to the request body as JSON. 57 | */ 58 | withJSONBody(body: object): IRESTClient; 59 | 60 | /** 61 | * Sets the http method for the request 62 | */ 63 | withMethod(method: string): IRESTClient; 64 | 65 | /** 66 | * Sets the uri of the request 67 | */ 68 | withUri(uri: string): IRESTClient; 69 | 70 | /** 71 | * Adds parameters to the request. 72 | * 73 | * @param name The name of the parameter. 74 | * @param value The value of the parameter, may be a string, object or number. 75 | */ 76 | withParameter(name: string, value: any): IRESTClient; 77 | 78 | /** 79 | * Sets request's credentials. 80 | * 81 | * @param value A string indicating whether credentials will be sent with the request always, never, or only when sent to a same-origin URL. 82 | */ 83 | withCredentials(value: RequestCredentials): IRESTClient; 84 | 85 | /** 86 | * Sets the response handler. This could do processing before the ClientResponse is returned depending on the APIs expected response. 87 | * 88 | * @param handler 89 | */ 90 | withResponseHandler(handler: ResponseHandler): IRESTClient; 91 | 92 | /** 93 | * Sets the error response handler. Error response handlers have a generic but due to typescript limitations, 94 | * this value will not be propagated to the Promise catch statement 95 | * 96 | * @param handler 97 | */ 98 | withErrorResponseHandler(handler: ErrorResponseHandler): IRESTClient; 99 | 100 | /** 101 | * Run the request and return a promise. This promise will resolve if the request is successful 102 | * and reject otherwise. 103 | */ 104 | go(): Promise>; 105 | } 106 | 107 | export default IRESTClient; 108 | -------------------------------------------------------------------------------- /src/IRESTClientBuilder.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, FusionAuth, All Rights Reserved 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an 12 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific 14 | * language governing permissions and limitations under the License. 15 | */ 16 | import IRESTClient from "./IRESTClient"; 17 | 18 | export default interface IRESTClientBuilder { 19 | build(host: string): IRESTClient; 20 | } -------------------------------------------------------------------------------- /test/DefaultRESTClientTest.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, FusionAuth, All Rights Reserved 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an 12 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific 14 | * language governing permissions and limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | import * as chai from 'chai'; 20 | import DefaultRESTClient from "../src/DefaultRESTClient" 21 | import {URLSearchParams} from "url"; 22 | 23 | 24 | describe('#DefaultRESTClient()', function () { 25 | 26 | it('Can Create DefaultRESTClient', async () => { 27 | const client = new DefaultRESTClient('http://localhost:9011'); 28 | chai.assert.isNotNull(client); 29 | }); 30 | 31 | describe('withFormData', function () { 32 | it('null', async () => { 33 | const client = new DefaultRESTClient('http://localhost:9011'); 34 | let body = client.withFormData(null).body; 35 | chai.assert.isNull(body); 36 | }); 37 | 38 | it('empty', async () => { 39 | const client = new DefaultRESTClient('http://localhost:9011'); 40 | let params = new URLSearchParams(); 41 | let body = client.withFormData(params).body; 42 | chai.assert.isNotNull(body); 43 | chai.assert.strictEqual(body.toString(), ""); 44 | }); 45 | 46 | it('with one value', async () => { 47 | const client = new DefaultRESTClient('http://localhost:9011'); 48 | let params = new URLSearchParams(); 49 | params.set('key','value'); 50 | let body = client.withFormData(params).body; 51 | chai.assert.isNotNull(body); 52 | chai.assert.strictEqual(body.toString(), "key=value"); 53 | }); 54 | 55 | it('with two values', async () => { 56 | const client = new DefaultRESTClient('http://localhost:9011'); 57 | let params = new URLSearchParams(); 58 | params.set('key','value'); 59 | params.set('key2','value2'); 60 | let body = client.withFormData(params).body; 61 | chai.assert.isNotNull(body); 62 | chai.assert.strictEqual(body.toString(), "key=value&key2=value2"); 63 | }); 64 | 65 | it('skips undefined value', async () => { 66 | const client = new DefaultRESTClient('http://localhost:9011'); 67 | let params = new URLSearchParams(); 68 | params.set('key','value'); 69 | params.set('key2',undefined); 70 | let body = client.withFormData(params).body; 71 | chai.assert.isNotNull(body); 72 | chai.assert.strictEqual(body.toString(), "key=value"); 73 | }); 74 | 75 | it('skips null value', async () => { 76 | const client = new DefaultRESTClient('http://localhost:9011'); 77 | let params = new URLSearchParams(); 78 | params.set('key','value'); 79 | params.set('key2',null); 80 | let body = client.withFormData(params).body; 81 | chai.assert.isNotNull(body); 82 | chai.assert.strictEqual(body.toString(), "key=value"); 83 | }); 84 | 85 | it('sets content type', async () => { 86 | const client = new DefaultRESTClient('http://localhost:9011'); 87 | let params = new URLSearchParams(); 88 | let headers = client.withFormData(params).headers 89 | chai.assert.isNotNull(headers); 90 | chai.assert.strictEqual(headers['Content-Type'], "application/x-www-form-urlencoded"); 91 | }); 92 | 93 | }); 94 | 95 | }); 96 | -------------------------------------------------------------------------------- /test/FusionAuthClientTest.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2024, FusionAuth, All Rights Reserved 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, 11 | * software distributed under the License is distributed on an 12 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific 14 | * language governing permissions and limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | import { ApplicationRequest, FusionAuthClient, GrantType, SearchResponse } from '../index'; 20 | import * as chai from 'chai'; 21 | import ClientResponse from "../src/ClientResponse"; 22 | 23 | let client; 24 | const fusionauthUrl = process.env.FUSIONAUTH_URL || "https://local.fusionauth.io"; 25 | const fusionauthApiKey = process.env.FUSIONAUTH_API_KEY || "bf69486b-4733-4470-a592-f1bfce7af580"; 26 | const applicationId = "e5e2b0b3-c329-4b08-896c-d4f9f612b5c0"; 27 | const tenantId = '65323339-6137-6531-3135-316238623265'; 28 | const userId = 'b164fdfc-db57-4da9-b241-8543671c6bb8'; 29 | 30 | describe('#FusionAuthClient()', function () { 31 | 32 | beforeEach(async () => { 33 | client = new FusionAuthClient(fusionauthApiKey, fusionauthUrl); 34 | 35 | let response = await client.retrieveTenants(); 36 | let desiredTenant = response.response.tenants.find((tenant) => { 37 | return tenant.id === tenantId 38 | }); 39 | 40 | if (!desiredTenant) { 41 | let defaultTenant = response.response.tenants.find((tenant) => { 42 | return tenant.name === "Default" 43 | }); 44 | defaultTenant.id = null; 45 | defaultTenant.name = "Typescript Tenant"; 46 | response = await client.createTenant(tenantId, {tenant: defaultTenant}); 47 | chai.assert.isTrue(response.wasSuccessful(), "Failed to create the tenant"); 48 | } 49 | 50 | // All future requests will use this now 51 | client.setTenantId(tenantId); 52 | 53 | try { 54 | await client.deleteApplication(applicationId); 55 | } catch (ignore) { 56 | } 57 | 58 | try { 59 | const applicationRequest: ApplicationRequest = {application: 60 | { 61 | name: 'TypeScript FusionAuth Client', 62 | oauthConfiguration: { 63 | enabledGrants: [ 64 | GrantType.password, 65 | GrantType.authorization_code 66 | ], 67 | authorizedRedirectURLs: ["http://localhost"] 68 | } 69 | }}; 70 | response = await client.createApplication(applicationId, applicationRequest); 71 | } catch (error) { 72 | console.error("Failed to setup FusionAuth Client for testing.", error) 73 | throw new Error(error); 74 | } 75 | 76 | chai.assert.isUndefined(response.exception); 77 | chai.assert.strictEqual(response.statusCode, 200); 78 | chai.assert.isNotNull(response.response); 79 | 80 | 81 | // Cleanup the user (just in case a test partially failed) 82 | try { 83 | response = await client.retrieveUserByEmail("typescript@fusionauth.io") 84 | if (response.wasSuccessful()) { 85 | await client.deleteUser(response.response.user.id) 86 | } 87 | } catch (ignore) { 88 | } 89 | 90 | // Create the user that is expected to exist 91 | try { 92 | response = await client.retrieveUser(userId); 93 | } catch (failed) { 94 | try { 95 | await client.createUser(userId, {user: {email: "exampleUser@fusionauth.io", password: "password"}}); 96 | } catch (e) { 97 | console.error("Failed to create the example user! Some tests may fail.", e); 98 | } 99 | } 100 | 101 | // Ensure that CORS allows patch 102 | try { 103 | response = await client.retrieveSystemConfiguration(); 104 | 105 | if (!response.response.systemConfiguration.corsConfiguration.allowedMethods.some(method => method === 'PATCH')) { 106 | response.response.systemConfiguration.corsConfiguration.allowedMethods.push('PATCH'); 107 | 108 | await client.updateSystemConfiguration({ 109 | systemConfiguration: response.response.systemConfiguration 110 | }); 111 | } 112 | } catch (e) { 113 | console.error("Failed to add patch to the CORS configuration. Your tests may fail!", e); 114 | } 115 | }); 116 | 117 | it('Create, Patch, Search, and Delete a User', async () => { 118 | let clientResponse = await client.createUser(null, { 119 | user: { 120 | email: 'nodejs@fusionauth.io', 121 | firstName: 'Jäne', 122 | password: 'password' 123 | }, 124 | skipVerification: true, 125 | sendSetPasswordEmail: false 126 | }); 127 | chai.assert.isUndefined(clientResponse.exception); 128 | chai.assert.strictEqual(clientResponse.statusCode, 200); 129 | chai.assert.isNotNull(clientResponse.response); 130 | chai.expect(clientResponse.response).to.have.property('user'); 131 | chai.expect(clientResponse.response.user).to.have.property('id'); 132 | 133 | const userId = clientResponse.response.user.id; 134 | 135 | // Patch the user 136 | clientResponse = await client.patchUser(userId, { 137 | user: { 138 | firstName: "Jan" 139 | } 140 | }); 141 | chai.assert.isUndefined(clientResponse.exception); 142 | chai.assert.strictEqual(clientResponse.statusCode, 200); 143 | chai.assert.isNotNull(clientResponse.response); 144 | chai.expect(clientResponse.response).to.have.property('user'); 145 | chai.expect(clientResponse.response.user.firstName).to.equal("Jan"); 146 | 147 | // create a second user and search them both 148 | clientResponse = await client.createUser(null, { 149 | user: { 150 | email: 'node2@fusionauth.io', 151 | firstName: 'Joan', 152 | password: 'password' 153 | }, 154 | skipVerification: true, 155 | sendSetPasswordEmail: false 156 | }); 157 | 158 | const secondUserId = clientResponse.response.user.id; 159 | const bothUsers = [userId, secondUserId]; 160 | 161 | const searchResp: ClientResponse = await client.searchUsersByIds(bothUsers); 162 | chai.assert.strictEqual(searchResp.statusCode, 200); 163 | chai.assert.strictEqual(searchResp.response.total, 2); 164 | // make sure each user was returned 165 | bothUsers.forEach(id => chai.assert.isNotNull(searchResp.response.users.find(user => user.id = id))); 166 | 167 | // delete both users 168 | for (const id of bothUsers) { 169 | clientResponse = await client.deleteUser(id); 170 | chai.assert.strictEqual(clientResponse.statusCode, 200); 171 | // Browser will return empty, node will return null, account for both scenarios 172 | if (clientResponse.response === null) { 173 | chai.assert.isNull(clientResponse.response); 174 | } else { 175 | chai.assert.isUndefined(clientResponse.response); 176 | } 177 | } 178 | 179 | // check that they are gone 180 | for (const email of ['nodejs@fusionauth.io', 'node2@fusionauth.io']) { 181 | try { 182 | await client.retrieveUserByEmail(email); 183 | chai.expect.fail(`The user with ${email} should have been deleted!`); 184 | } catch (clientResponse) { 185 | chai.assert.strictEqual(clientResponse.statusCode, 404); 186 | } 187 | } 188 | }); 189 | 190 | // Ensure that FusionAuth CORS is configured to support PATCH 191 | it('Patch Application', async () => { 192 | const applicationRequest: ApplicationRequest = { 193 | application: { 194 | name: 'Node.js FusionAuth Client patch', 195 | loginConfiguration: {allowTokenRefresh: true} 196 | } 197 | }; 198 | 199 | let response = await client.patchApplication(applicationId, applicationRequest); 200 | chai.assert.isUndefined(response.exception); 201 | chai.assert.strictEqual(response.statusCode, 200); 202 | chai.expect(response.response.application.loginConfiguration.allowTokenRefresh).to.be.true; 203 | }); 204 | 205 | /** 206 | * Tests a connection failure path for fetch exceptions 207 | */ 208 | it('Failed response', async () => { 209 | client = new FusionAuthClient('doesntmatter', 'https://local.fusionauth.example.com'); // Doesn't exist 210 | 211 | return client.retrieveTenants() 212 | .then((_) => { 213 | chai.assert.fail("This should not have succeeded"); 214 | }) 215 | .catch((response) => { 216 | chai.assert.instanceOf(response, ClientResponse); 217 | chai.assert.isNotNull(response.exception); 218 | chai.assert.isUndefined(response.statusCode); 219 | }); 220 | }); 221 | 222 | it('Error response', async () => { 223 | return client.createApplication(null, {application: {name: 'Bad Application', verifyRegistration: true}}) 224 | .then((_) => { 225 | chai.assert.fail("This should not have succeeded"); 226 | }) 227 | .catch((response) => { 228 | chai.assert.instanceOf(response, ClientResponse); 229 | chai.assert.isDefined(response.statusCode); 230 | chai.expect(response.statusCode).to.be.above(399).and.below(500); 231 | }); 232 | }); 233 | 234 | it('Login non existant user', async () => { 235 | try { 236 | await client.login({ 237 | loginId: "doesntexist", 238 | password: "pass" 239 | }); 240 | chai.assert.fail('This should not be reached'); 241 | } catch (e) { 242 | chai.assert.equal(e.statusCode, 404); 243 | // chai.assert.deepStrictEqual(e, { 244 | // statusCode: 404 245 | // }, "Unexpected error"); 246 | } 247 | }); 248 | 249 | it('OAuth login', async () => { 250 | try { 251 | let application = await client.retrieveApplication(applicationId); 252 | const clientId = application.response.application.oauthConfiguration.clientId; 253 | const clientSecret = application.response.application.oauthConfiguration.clientSecret; 254 | 255 | const accessTokenResponse = await client.exchangeUserCredentialsForAccessToken("exampleUser@fusionauth.io", "password", clientId, clientSecret, "email openid", null); 256 | 257 | // TODO Test the rest of the workflow somehow 258 | 259 | // const authCodeResponse = await client.exchangeOAuthCodeForAccessToken(accessTokenResponse.response.access_token, clientId, clientSecret, "http://localhost"); 260 | 261 | // const userResponse = await client.retrieveUserUsingJWT(authCodeResponse.successResponse.access_token); 262 | 263 | // console.log("User:", userResponse.response.user); 264 | } catch (e) { 265 | console.error(e); 266 | chai.assert.fail("Failed to perform an OAuth login"); 267 | } 268 | }); 269 | }); 270 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "files": [ 4 | "FusionAuthClientTest.ts", 5 | "DefaultRESTClientTest.ts" 6 | ], 7 | "compilerOptions": { 8 | "types": [ 9 | "node", 10 | "node-fetch", 11 | "mocha", 12 | "chai" 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "sourceMap": true, 7 | "lib": [ 8 | "es2015" 9 | ], 10 | "outDir": "build", 11 | "types": [ 12 | "node", 13 | "node-fetch" 14 | ] 15 | }, 16 | "exclude": [ 17 | "node_modules", 18 | "build", 19 | "dist", 20 | "test", 21 | "examples" 22 | ] 23 | } 24 | --------------------------------------------------------------------------------