refactor: add connector packages
the initial commit to move all connector packages to the main repo.
9
packages/connectors/.gitignore
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
# generated files
|
||||
/*/package.json
|
||||
/*/types
|
||||
/*/tsconfig.*
|
||||
/*/jest.config.*
|
||||
/*/rollup.config.*
|
||||
|
||||
# keep templates
|
||||
!/templates/**
|
174
packages/connectors/connector-alipay-native/CHANGELOG.md
Normal file
|
@ -0,0 +1,174 @@
|
|||
# Change Log
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 4ed07d2: bump connector-kit to v1.0.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 269d701: The console connector configuration page has been changed from JSON format to form view.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9ff0638: update connector-kit version
|
||||
- 7b0bf69: Bump version to upgrade connector kit
|
||||
- 4ec0889: bump connector-kit version
|
||||
- d8b9dea: 1. Update `@logto/connector-kit` from `1.0.0-beta.32` to `1.0.0-beta.33`.
|
||||
- d183d6d: Upgrade connector-kit
|
||||
|
||||
## 1.0.0-beta.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4ec0889: bump connector-kit version
|
||||
|
||||
## 1.0.0-beta.20
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 269d701: The console connector configuration page has been changed from JSON format to form view.
|
||||
|
||||
## 1.0.0-beta.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7b0bf69: Bump version to upgrade connector kit
|
||||
|
||||
## 1.0.0-beta.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9ff0638: update connector-kit version
|
||||
|
||||
## 1.0.0-beta.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d8b9dea: 1. Update `@logto/connector-kit` from `1.0.0-beta.32` to `1.0.0-beta.33`.
|
||||
|
||||
## 1.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d183d6d: Upgrade connector-kit
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.0-beta.12](https://github.com/logto-io/connectors/compare/v1.0.0-beta.11...v1.0.0-beta.12) (2022-11-29)
|
||||
|
||||
### Features
|
||||
|
||||
- add mock standard email connector ([#35](https://github.com/logto-io/connectors/issues/35)) ([479114e](https://github.com/logto-io/connectors/commit/479114e847fb4b11c6fbd697a36b7f5eb56305ed))
|
||||
|
||||
## [1.0.0-beta.11](https://github.com/logto-io/connectors/compare/v1.0.0-beta.10...v1.0.0-beta.11) (2022-11-06)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
## [1.0.0-beta.10](https://github.com/logto-io/connectors/compare/v1.0.0-beta.9...v1.0.0-beta.10) (2022-09-27)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
## 1.0.0-beta.9 (2022-09-18)
|
||||
|
||||
### Features
|
||||
|
||||
- add connectors ([#2](https://github.com/logto-io/connectors/issues/2)) ([2fbb578](https://github.com/logto-io/connectors/commit/2fbb57815406bace113617a6304eafcfc5db2d61))
|
||||
|
||||
## [1.0.0-beta.8](https://github.com/logto-io/logto/compare/v1.0.0-beta.6...v1.0.0-beta.8) (2022-09-01)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
## [1.0.0-beta.6](https://github.com/logto-io/logto/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-08-30)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
## [1.0.0-beta.5](https://github.com/logto-io/logto/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2022-08-19)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
## [1.0.0-beta.4](https://github.com/logto-io/logto/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-08-11)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
## [1.0.0-beta.3](https://github.com/logto-io/logto/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-08-01)
|
||||
|
||||
### Features
|
||||
|
||||
- **phrases:** tr language ([#1707](https://github.com/logto-io/logto/issues/1707)) ([411a8c2](https://github.com/logto-io/logto/commit/411a8c2fa2bfb16c4fef5f0a55c3c1dc5ead1124))
|
||||
|
||||
## [1.0.0-beta.2](https://github.com/logto-io/logto/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-07-25)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
## [1.0.0-beta.1](https://github.com/logto-io/logto/compare/v1.0.0-beta.0...v1.0.0-beta.1) (2022-07-19)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
## [1.0.0-beta.0](https://github.com/logto-io/logto/compare/v1.0.0-alpha.4...v1.0.0-beta.0) (2022-07-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **connector:** fix connector getConfig and validateConfig type ([#1530](https://github.com/logto-io/logto/issues/1530)) ([88a54aa](https://github.com/logto-io/logto/commit/88a54aaa9ebce419c149a33150a4927296cb705b))
|
||||
- **connector:** refactor ConnectorInstance as class ([#1541](https://github.com/logto-io/logto/issues/1541)) ([6b9ad58](https://github.com/logto-io/logto/commit/6b9ad580ae86fbcc100a100aab1d834090e682a3))
|
||||
|
||||
## [1.0.0-alpha.4](https://github.com/logto-io/logto/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) (2022-07-08)
|
||||
|
||||
### Features
|
||||
|
||||
- **connector:** connector error handler, throw errmsg on general errors ([#1458](https://github.com/logto-io/logto/issues/1458)) ([7da1de3](https://github.com/logto-io/logto/commit/7da1de33e97de4aeeec9f9b6cea59d1bf90ba623))
|
||||
|
||||
## [1.0.0-alpha.3](https://github.com/logto-io/logto/compare/v1.0.0-alpha.2...v1.0.0-alpha.3) (2022-07-07)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
## [1.0.0-alpha.2](https://github.com/logto-io/logto/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2022-07-07)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
## [1.0.0-alpha.1](https://github.com/logto-io/logto/compare/v1.0.0-alpha.0...v1.0.0-alpha.1) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
## [1.0.0-alpha.0](https://github.com/logto-io/logto/compare/v0.1.2-alpha.5...v1.0.0-alpha.0) (2022-07-04)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
### [0.1.2-alpha.5](https://github.com/logto-io/logto/compare/v0.1.2-alpha.4...v0.1.2-alpha.5) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
### [0.1.2-alpha.4](https://github.com/logto-io/logto/compare/v0.1.2-alpha.3...v0.1.2-alpha.4) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
### [0.1.2-alpha.3](https://github.com/logto-io/logto/compare/v0.1.2-alpha.2...v0.1.2-alpha.3) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
### [0.1.2-alpha.2](https://github.com/logto-io/logto/compare/v0.1.2-alpha.1...v0.1.2-alpha.2) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
### [0.1.2-alpha.1](https://github.com/logto-io/logto/compare/v0.1.2-alpha.0...v0.1.2-alpha.1) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-native
|
||||
|
||||
### [0.1.1-alpha.0](https://github.com/logto-io/logto/compare/v0.1.0-internal...v0.1.1-alpha.0) (2022-07-01)
|
||||
|
||||
### Features
|
||||
|
||||
- **connector-alipay-native:** add Alipay Native connector ([#873](https://github.com/logto-io/logto/issues/873)) ([9589aea](https://github.com/logto-io/logto/commit/9589aeafec8592531aa1dfe598ca6cec7325eded))
|
||||
- **connectors:** add logo for connectors ([#914](https://github.com/logto-io/logto/issues/914)) ([a3a7c52](https://github.com/logto-io/logto/commit/a3a7c52a91dba3603617a68e5ce47e0017081a91))
|
||||
- **connectors:** handle authorization callback parameters in each connector respectively ([#1166](https://github.com/logto-io/logto/issues/1166)) ([097aade](https://github.com/logto-io/logto/commit/097aade2e2e1b1ea1531bcb4c1cca8d24961a9b9))
|
||||
- **core,connectors:** update Aliyun logo and add logo_dark to Apple, Github ([#1194](https://github.com/logto-io/logto/issues/1194)) ([98f8083](https://github.com/logto-io/logto/commit/98f808320b1c79c51f8bd6f49e35ca44363ea560))
|
||||
- **core:** serve connector logo ([#931](https://github.com/logto-io/logto/issues/931)) ([5b44b71](https://github.com/logto-io/logto/commit/5b44b7194ed4f98c6c2e77aae828a39b477b6010))
|
||||
- **native-connectors:** pass random state to native connector sdk ([#922](https://github.com/logto-io/logto/issues/922)) ([9679620](https://github.com/logto-io/logto/commit/96796203dd4247d7ecdee044f13f3d57f04ca461))
|
||||
- remove target, platform from connector schema and add id to metadata ([#930](https://github.com/logto-io/logto/issues/930)) ([054b0f7](https://github.com/logto-io/logto/commit/054b0f7b6a6dfed66540042ea69b0721126fe695))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- alipay native ([895a24b](https://github.com/logto-io/logto/commit/895a24b41eafddde82c94668d742613a333b6991))
|
||||
- **connector-alipay-native:** fix data guard ([#992](https://github.com/logto-io/logto/issues/992)) ([2dc50d6](https://github.com/logto-io/logto/commit/2dc50d65318dfc7d64034bd3c501cec8feb5dde1))
|
268
packages/connectors/connector-alipay-native/README.md
Normal file
|
@ -0,0 +1,268 @@
|
|||
# Alipay Native
|
||||
|
||||
The official Logto connector for Alipay social sign-in in mobile-device native apps.
|
||||
|
||||
支付宝原生应用社交登录官方 Logto 连接器 [中文文档](#支付宝原生连接器)
|
||||
|
||||
**Table of contents**
|
||||
|
||||
- [Alipay Native](#alipay-native)
|
||||
- [Get started](#get-started)
|
||||
- [Register Alipay developer account](#register-alipay-developer-account)
|
||||
- [Create and configure Alipay app](#create-and-configure-alipay-app)
|
||||
- [Set up the Logto Alipay Native connector settings](#set-up-the-logto-alipay-native-connector-settings)
|
||||
- [Config types](#config-types)
|
||||
- [Enable Alipay native sign-in in your app](#enable-alipay-native-sign-in-in-your-app)
|
||||
- [iOS](#ios)
|
||||
- [Android](#android)
|
||||
- [Test Alipay native connector](#test-alipay-native-connector)
|
||||
- [References](#references)
|
||||
- [支付宝原生连接器](#支付宝原生连接器)
|
||||
- [开始上手](#开始上手)
|
||||
- [注册支付宝开发者账号](#注册支付宝开发者账号)
|
||||
- [在支付宝开放平台上创建并且配置应用](#在支付宝开放平台上创建并且配置应用)
|
||||
- [设置支付宝原生连接器](#设置支付宝原生连接器)
|
||||
- [配置类型](#配置类型)
|
||||
- [在你的应用中启用支付宝原生登录](#在你的应用中启用支付宝原生登录)
|
||||
- [iOS](#ios-1)
|
||||
- [Android](#android-1)
|
||||
- [测试支付宝原生连接器](#测试支付宝原生连接器)
|
||||
- [参考](#参考)
|
||||
|
||||
## Get started
|
||||
|
||||
Alipay Native connector works closely with Logto SDK on mobile platforms. It takes advantage of Alipay's OAuth 2.0 authentication workflow and enables Alipay users to sign in to other Apps using public Alipay user profiles without going through a troublesome register process.
|
||||
|
||||
## Register Alipay developer account
|
||||
|
||||
[Register an Alipay developer account](https://certifyweb.alipay.com/certify/reg/guide#/) if you don't have one.
|
||||
|
||||
## Create and configure Alipay app
|
||||
|
||||
1. Sign in to the [Alipay console](https://open.alipay.com/) with the account you have just registered.
|
||||
2. Go to "Web & Mobile Apps" (网页&移动应用) tab in "My Application" (我的应用) panel.
|
||||
3. Click "Create an App" (立即创建) button to start configuring your application.
|
||||
4. Name your application in "Application Name" (应用名称) following the naming conventions and upload your "Application Icon" (应用图标), make sure you choose "mobile application" (移动应用) as "App type" (应用类型). For building iOS App, a unique "Bundle ID" is required. Also, "application signature" (应用签名) and "application package name" (应用包名) are required for Android apps.
|
||||
5. After finishing creating the application, we come to the Overview page, where we should click "add ability" (添加能力) to add "Third-party application authorization" (第三方应用授权), "Get member information" (获取会员信息) and "App Alipay login" (App 支付宝登录) before enabling Alipay sign-in.
|
||||
6. Go to [Alipay Customer Center](https://b.alipay.com/index2.htm), and sign in with the Alipay developer account. Click "Account Center" (账号中心) on the topbar and go to "APPID binding" (APPID 绑定), whose entrance can be found at the bottom of the sidebar. "Add binding" (添加绑定) by type in the APPID of the mobile application you just created in step 4.
|
||||
7. Click on "Sign" button of "App Alipay login", and finish signing process following the guide. After finishing this step, you are expected to find abilities you have just added in step 5 kicks in.
|
||||
8. Come back to Alipay open platform console page, and you can find "Interface signing method" (接口加签方式(密钥/证书)) in "development information" (开发信息) section. Click "set up" (设置) button, and you can find yourself on a page setting signing method. "Public Key" (公钥) is the preferred signing mode, and fill in contents from the public key file you have generated in the text input box.
|
||||
9. Set up "Authorization Redirect URI" (授权回调地址) by clicking "set up" (设置) button on the bottom of the Alipay console page. `${your_logto_origin}/callback/${connector_id}` is the default redirect URI used in Logto core. The `connector_id` can be found on the top bar of the Logto Admin Console connector details page.
|
||||
10. After finishing all these steps, go back to the top right corner of Alipay console page, and click "Submit for review" (提交审核). Once the review is approved, you are good to go with a smooth Alipay sign-in flow.
|
||||
|
||||
> ℹ️ **Note**
|
||||
>
|
||||
> You can use _openssl_ to generate key pairs on your local machine by executing following code snippet in terminal.
|
||||
>
|
||||
> ```bash
|
||||
> openssl genrsa -out private.pem 2048
|
||||
> openssl rsa -in private.pem -outform PEM -pubout -out public.pem
|
||||
> ```
|
||||
>
|
||||
> When filling in the public key on the Alipay app setup website, you need to remove the header and footer of `public.pem`, delete all newline characters, and paste the rest of the contents into the text input box for "public key".
|
||||
|
||||
## Set up the Logto Alipay Native connector settings
|
||||
|
||||
1. In [the Alipay console workspace](https://open.alipay.com/dev/workspace) go to "My application" (我的应用) panel and click "Web & Mobile Apps" (网页&移动应用) tab, you can find APPID of all applications.
|
||||
2. In step 7 of the previous part, you have already generated a key pair including a private key and a public key.
|
||||
3. Fill out the Logto connector settings:
|
||||
- Fill out the `appId` field with APPID you've got from step 1.
|
||||
- Fill out the `privateKey` field with contents from the private key file mentioned in step 2. Please MAKE SURE to use '\n' to replace all newline characters. You don't need to remove header and footer in private key file.
|
||||
- Fill out the `signType` filed with 'RSA2' due to the `Public key` signing mode we chose in step 7 of "Create And Configure Alipay Apps".
|
||||
|
||||
### Config types
|
||||
|
||||
| Name | Type | Enum values |
|
||||
|------------|-------------|-----------------|
|
||||
| appId | string | N/A |
|
||||
| privateKey | string | N/A |
|
||||
| signType | enum string | 'RSA' \| 'RSA2' |
|
||||
|
||||
## Enable Alipay native sign-in in your app
|
||||
|
||||
### iOS
|
||||
|
||||
We assume you have integrated [Logto iOS SDK](https://docs.logto.io/docs/recipes/integrate-logto/ios) in your app. In this case, things are pretty simple, and you don't even need to read the Alipay SDK doc:
|
||||
|
||||
**1. Add `LogtoSocialPluginAlipay` to your Xcode project**
|
||||
|
||||
Add the framework:
|
||||
|
||||

|
||||
|
||||
> ℹ️ **Note**
|
||||
>
|
||||
> The plugin includes Alipay "minimalist SDK" (极简版 SDK). You can directly use `import AFServiceSDK` once imported the plugin.
|
||||
|
||||
**2. Add the plugin to your `LogtoClient` init options**
|
||||
|
||||
```swift
|
||||
let logtoClient = LogtoClient(
|
||||
useConfig: config,
|
||||
socialPlugins: [LogtoSocialPluginAlipay(callbackScheme: "your-scheme")]
|
||||
)
|
||||
```
|
||||
|
||||
Where `callbackScheme` is one of the [custom URL Schemes](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app) that can navigate to your app.
|
||||
|
||||
### Android
|
||||
|
||||
We assume you have integrated [Logto Android SDK](https://docs.logto.io/docs/recipes/integrate-logto/android) in your app. In this case, things are pretty simple, and you don't even need to read the Alipay SDK doc:
|
||||
|
||||
**1. Download the Alipay "minimalist SDK" and add it to your project**
|
||||
|
||||
Download the Alipay "minimalist SDK" (极简版 SDK) from [Logto 3rd-party Social SDKs](https://github.com/logto-io/social-sdks/blob/master/alipay/android/alipaySdk-15.7.9-20200727142846.aar) to your project's `app/libs` folder:
|
||||
|
||||
```bash
|
||||
project-path/app/libs/alipaySdk-15.7.9-20200727142846.aar
|
||||
```
|
||||
|
||||
**2. Add the Alipay "minimalist SDK" as a dependency**
|
||||
|
||||
Open your `build.gradle` file:
|
||||
|
||||
```bash
|
||||
project-path/app/build.gradle
|
||||
```
|
||||
|
||||
Add the dependency:
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
// ...
|
||||
implementation(files("./libs/alipaySdk-15.7.9-20200727142846.aar")) // kotlin-script
|
||||
// or
|
||||
implementation files('./libs/alipaySdk-15.7.9-20200727142846.aar') // groovy-script
|
||||
}
|
||||
```
|
||||
|
||||
### Test Alipay native connector
|
||||
|
||||
That's it. Don't forget to [Enable connector in sign-in experience](https://docs.logto.io/docs/tutorials/get-started/enable-social-sign-in#enable-connector-in-sign-in-experience).
|
||||
|
||||
Once Alipay native connector is enabled, you can build and run your app to see if it works.
|
||||
|
||||
## References
|
||||
|
||||
- [Alipay Docs - Access Preparation - How to create an app](https://opendocs.alipay.com/support/01rau6)
|
||||
- [Alipay Docs - Web & Mobile Apps - Create an app](https://opendocs.alipay.com/open/200/105310)
|
||||
|
||||
# 支付宝原生连接器
|
||||
|
||||
## 开始上手
|
||||
|
||||
支付宝原生连接器与 Logto 所提供的原生平台上的 SDK 紧密搭配使用。它利用支付宝所提供的 OAuth 2.0 身份认证服务,使支付宝用户无需繁琐的注册流程,即可直接用其在支付宝上公开的身份信息登录其他应用。
|
||||
|
||||
## 注册支付宝开发者账号
|
||||
|
||||
如果你还没有支付宝开发者账号,参考链接:[注册一个支付宝开发者账号](https://certifyweb.alipay.com/certify/reg/guide#/)
|
||||
|
||||
## 在支付宝开放平台上创建并且配置应用
|
||||
|
||||
1. 使用你所创建的支付宝开发者账号登录[支付宝开放平台控制台](https://open.alipay.com/)。
|
||||
2. 在「我的应用」中选择「网页&移动应用」标签页。
|
||||
3. 点击「立即创建」开始创建并且配置你的应用
|
||||
4. 根据平台的命名规则通过「应用名称」字段给你的应用命名;在「应用图标」中上传应用图标;将「应用类型」设定为「移动应用」。当要创建一个 iOS 应用时, 需要提供「Bundle ID」。当创建的是 Android 应用时,则需要提供「应用签名」和「应用名」。
|
||||
5. 当应用创建成功后,我们进入到了「概览」页面,接下来我们在「能力列表」中点击「+ 添加能力」,将「App 支付宝登录」、「获取会员信息」、「第三方应用授权」添加到能力列表中。
|
||||
6. 使用开发者账号登录[支付宝商家中心](https://b.alipay.com/index2.htm)后,从顶栏菜单的进入「账号中心」,然后选择从左侧的菜单栏底部进入「APPID 绑定」页面。点击「+ 添加绑定」,之后输入你在步骤 4 中所创建的应用的 APPID。
|
||||
7. 点按「App 支付宝登录」旁边「签约」按钮,并按照提示完成签约。当此步骤完成后,步骤 5 中所添加的各种「能力」即可生效。
|
||||
8. 回到「支付宝开放平台控制台」中第 5 步所创建的应用的「概览」页面, 在该页面的「开发信息」中点击「接口加签方式(密钥/证书)」的「设置」链接,将「选择加签模式」设定为「公钥」,然后将你生成的公钥填入下方「填写公钥字符」的文本编辑框中。
|
||||
9. 点击「授权回调地址」的「设置链接」,选择你所需要的「回调地址类型」,将 Logto Core 默认使用的 `${your_logto_origin}/callback/${connector_id}` 设置为「回调地址」。`connector_id` 在管理控制台相应连接器的详情页的顶栏中可以找到。
|
||||
10. 当设置完以上的所有步骤,点击「概览」页面上方的「提交审核」,当审核通过后,你将可以顺利地使用支付宝登录自己的应用。
|
||||
|
||||
> ℹ️ **注意**
|
||||
>
|
||||
> 你可以用 _openssl_ 来在本地机器上,用下面这一段代码在终端里生成一个密钥对。
|
||||
>
|
||||
> ```bash
|
||||
> openssl genrsa -out private.pem 2048
|
||||
> openssl rsa -in private.pem -outform PEM -pubout -out public.pem
|
||||
> ```
|
||||
>
|
||||
> 在支付宝应用设置网页上填写公钥时,需要把生成的 `public.pem` 文件中内容的文件头和文件尾去掉,同时删除所有的换行符,再把剩下的内容粘贴到填写公钥的文本框中。
|
||||
|
||||
## 设置支付宝原生连接器
|
||||
|
||||
1. 在[支付宝开放平台控制台](https://open.alipay.com/dev/workspace)中,点击「我的应用」面板中的「网页&移动应用」,获取应用的 APPID。
|
||||
2. 获取你在上一部分的第 7 个步骤中生成的密钥对。
|
||||
3. 配置你的应用的支付宝原生连接器:
|
||||
- 将你在第 1 步中获取的 APPID 填入 `appId` 字段。
|
||||
- 将你在第 2 步中获得的密钥对的私钥填入 `privateKey` 字段。请保留私钥文件内容中的文件头和文件尾,并 **确保** 使用 '\n' 替换了所有换行符。
|
||||
- 将你在第 2 步中所获得的密钥的签名模式 'RSA2' 填入 `signType` 字段。
|
||||
|
||||
### 配置类型
|
||||
|
||||
| 名称 | 类型 | 枚举值 |
|
||||
|------------|-------------|-----------------|
|
||||
| appId | string | N/A |
|
||||
| privateKey | string | N/A |
|
||||
| signType | enum string | 'RSA' \| 'RSA2' |
|
||||
|
||||
## 在你的应用中启用支付宝原生登录
|
||||
|
||||
### iOS
|
||||
|
||||
我们假设你已经在你的应用中集成了 [Logto iOS SDK](https://docs.logto.io/zh-cn/docs/recipes/integrate-logto/ios/)。之后的流程很简单,你甚至不需要阅读支付宝 SDK 文档:
|
||||
|
||||
**1. 添加 `LogtoSocialPluginAlipay` 到你的 Xcode 工程**
|
||||
|
||||
添加 framework:
|
||||
|
||||

|
||||
|
||||
> ℹ️ **Note**
|
||||
>
|
||||
> 该插件已包含支付宝极简 SDK。在引入插件后你可以直接使用 `import AFServiceSDK`。
|
||||
|
||||
**2. 将插件添加至 `LogtoClient` 的初始化项**
|
||||
|
||||
```swift
|
||||
let logtoClient = LogtoClient(
|
||||
useConfig: config,
|
||||
socialPlugins: [LogtoSocialPluginAlipay(callbackScheme: "your-scheme")]
|
||||
)
|
||||
```
|
||||
|
||||
其中 `callbackScheme` 是 [custom URL Schemes](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app) 中之一,它将被用来在登录成功后跳转回应用。
|
||||
|
||||
### Android
|
||||
|
||||
我们假设你已经在你的应用中集成了 [Logto Android SDK](https://docs.logto.io/docs/recipes/integrate-logto/android)。之后的流程很简单,你甚至不需要阅读支付宝 SDK 文档:
|
||||
|
||||
**1. 下载支付宝极简版 SDK 到你的项目中**
|
||||
|
||||
从 [Logto 3rd-party Social SDKs](https://github.com/logto-io/social-sdks/blob/master/alipay/android/alipaySdk-15.7.9-20200727142846.aar) 下载支付宝极简版 SDK 到项目的 `app/libs` 目录下:
|
||||
|
||||
```bash
|
||||
project-path/app/libs/alipaySdk-15.7.9-20200727142846.aar
|
||||
```
|
||||
|
||||
**2. 添加支付宝极简版 SDK 为项目依赖项**
|
||||
|
||||
打开 `build.gradle` 文件:
|
||||
|
||||
```bash
|
||||
project-path/app/build.gradle
|
||||
```
|
||||
|
||||
添加依赖:
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
// ...
|
||||
implementation(files("./libs/alipaySdk-15.7.9-20200727142846.aar")) // kotlin-script
|
||||
// or
|
||||
implementation files('./libs/alipaySdk-15.7.9-20200727142846.aar') // groovy-script
|
||||
}
|
||||
```
|
||||
|
||||
## 测试支付宝原生连接器
|
||||
|
||||
大功告成。别忘了 [在登录体验中启用社交登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in/#%E5%9C%A8%E7%99%BB%E5%BD%95%E4%BD%93%E9%AA%8C%E4%B8%AD%E5%90%AF%E7%94%A8%E8%BF%9E%E6%8E%A5%E5%99%A8)。
|
||||
|
||||
在支付宝原生连接器启用后,你可以构建并运行你的应用看看是否生效。
|
||||
|
||||
## 参考
|
||||
|
||||
- [支付宝文档中心 - 接入准备 - 如何创建应用](https://opendocs.alipay.com/support/01rau6)
|
||||
- [支付宝文档中心 - 网页&移动应用 - 创建应用](https://opendocs.alipay.com/open/200/105310)
|
After Width: | Height: | Size: 32 KiB |
11
packages/connectors/connector-alipay-native/logo.svg
Normal file
|
@ -0,0 +1,11 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_865_12300)">
|
||||
<path d="M20.0867 0H3.91188C1.80584 0 0.0999756 1.72052 0.0999756 3.84505V20.1556C0.0999756 22.2782 1.80584 24 3.91188 24H20.0867C22.1927 24 23.8973 22.2782 23.8973 20.1556V3.84505C23.8973 1.72052 22.1927 0 20.0867 0Z" fill="#1677FF"/>
|
||||
<path d="M6.5311 18.4567C2.8288 18.4567 1.73405 15.5152 3.56417 13.9068C4.17463 13.3626 5.29042 13.0976 5.88495 13.0377C8.08403 12.8184 10.12 13.6647 12.5223 14.8474C10.8337 17.0688 8.68302 18.4567 6.5311 18.4567ZM19.6943 15.0704C18.7417 14.7486 17.464 14.2567 16.0411 13.7373C16.895 12.2386 17.5781 10.5321 18.0267 8.67773H13.3361V6.97377H19.0826V6.02175H13.3361V3.18225H10.9911C10.5794 3.18225 10.5794 3.59135 10.5794 3.59135V6.02239H4.76789V6.97377H10.5794V8.67773H5.78109V9.62847H15.0872C14.7589 10.7742 14.3096 11.8818 13.7471 12.9325C10.7266 11.9276 7.50479 11.1132 5.48031 11.6141C4.1861 11.9359 3.35197 12.51 2.86258 13.1116C0.613798 15.8708 2.22662 20.0606 6.97462 20.0606C9.78161 20.0606 12.4866 18.4822 14.5825 15.8803C17.7087 17.3969 23.8988 19.9988 23.8988 19.9988V16.2901C23.8988 16.2901 23.1214 16.2276 19.6943 15.0704Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_865_12300">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package",
|
||||
"name": "@logto/connector-alipay-native",
|
||||
"version": "1.0.0",
|
||||
"description": "Alipay Native implementation.",
|
||||
"author": "Silverhand Inc. <contact@silverhand.io>",
|
||||
"dependencies": {
|
||||
"dayjs": "^1.10.5",
|
||||
"iconv-lite": "^0.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@shopify/jest-koa-mocks": "^5.0.0"
|
||||
}
|
||||
}
|
68
packages/connectors/connector-alipay-native/src/constant.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
import type { ConnectorMetadata } from '@logto/connector-kit';
|
||||
import { ConnectorConfigFormItemType, ConnectorPlatform } from '@logto/connector-kit';
|
||||
|
||||
export const authorizationEndpoint = 'alipay://'; // This is used to arouse the native Alipay App
|
||||
export const alipayEndpoint = 'https://openapi.alipay.com/gateway.do';
|
||||
export const methodForAccessToken = 'alipay.system.oauth.token';
|
||||
export const methodForUserInfo = 'alipay.user.info.share';
|
||||
|
||||
export const alipaySigningAlgorithmMapping = {
|
||||
RSA: 'RSA-SHA1',
|
||||
RSA2: 'RSA-SHA256',
|
||||
} as const;
|
||||
export const alipaySigningAlgorithms = ['RSA', 'RSA2'] as const;
|
||||
|
||||
export const invalidAccessTokenCode = ['20001'];
|
||||
|
||||
export const invalidAccessTokenSubCode = ['isv.code-invalid'];
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'alipay-native',
|
||||
target: 'alipay',
|
||||
platform: ConnectorPlatform.Native,
|
||||
name: {
|
||||
en: 'Alipay',
|
||||
'zh-CN': '支付宝',
|
||||
'tr-TR': 'Alipay',
|
||||
ko: 'Alipay',
|
||||
},
|
||||
logo: './logo.svg',
|
||||
logoDark: null,
|
||||
description: {
|
||||
en: 'Alipay is a third-party mobile and online payment platform.',
|
||||
'zh-CN': '支付宝是一个第三方支付平台。',
|
||||
'tr-TR': 'Alipay, üçüncü şahıslara ait bir mobil ve çevrimiçi ödeme platformudur.',
|
||||
ko: 'Alipay는 서드파티 모바일 및 온라인 결제 플랫폼 입니다.',
|
||||
},
|
||||
readme: './README.md',
|
||||
formItems: [
|
||||
{
|
||||
key: 'appId',
|
||||
label: 'App ID',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: true,
|
||||
placeholder: '<app-id-with-maximum-length-16>',
|
||||
},
|
||||
{
|
||||
key: 'privateKey',
|
||||
label: 'Private Key',
|
||||
type: ConnectorConfigFormItemType.MultilineText,
|
||||
required: true,
|
||||
placeholder: '<private-key>',
|
||||
},
|
||||
{
|
||||
key: 'signType',
|
||||
label: 'Signing Algorithm',
|
||||
type: ConnectorConfigFormItemType.Select,
|
||||
selectItems: [
|
||||
{ title: 'RSA-SHA1', value: 'RSA' },
|
||||
{ title: 'RSA-SHA256', value: 'RSA2' },
|
||||
],
|
||||
defaultValue: 'RSA2',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const defaultTimeout = 5000;
|
||||
|
||||
export const timestampFormat = 'YYYY-MM-DD HH:mm:ss';
|
257
packages/connectors/connector-alipay-native/src/index.test.ts
Normal file
|
@ -0,0 +1,257 @@
|
|||
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit';
|
||||
import nock from 'nock';
|
||||
|
||||
import { alipayEndpoint } from './constant.js';
|
||||
import createConnector, { getAccessToken } from './index.js';
|
||||
import { mockedAlipayNativeConfigWithValidPrivateKey } from './mock.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const getConfig = jest.fn().mockResolvedValue(mockedAlipayNativeConfigWithValidPrivateKey);
|
||||
|
||||
describe('getAuthorizationUri', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should get a valid uri by state', async () => {
|
||||
const connector = await createConnector({ getConfig });
|
||||
const authorizationUri = await connector.getAuthorizationUri(
|
||||
{
|
||||
state: 'dummy-state',
|
||||
redirectUri: 'dummy-redirect-uri',
|
||||
connectorId: 'dummy-connector-id',
|
||||
connectorFactoryId: 'dummy-connector-factory-id',
|
||||
jti: 'dummy-jti',
|
||||
headers: {},
|
||||
},
|
||||
jest.fn()
|
||||
);
|
||||
expect(authorizationUri).toBe('alipay://?app_id=2021000000000000&state=dummy-state');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccessToken', () => {
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const alipayEndpointUrl = new URL(alipayEndpoint);
|
||||
|
||||
it('should get an accessToken by exchanging with code', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_system_oauth_token_response: {
|
||||
user_id: '2088000000000000',
|
||||
access_token: 'access_token',
|
||||
expires_in: 3600,
|
||||
refresh_token: 'refresh_token',
|
||||
re_expires_in: 7200, // Expiration timeout of refresh token, in seconds
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const response = await getAccessToken('code', mockedAlipayNativeConfigWithValidPrivateKey);
|
||||
const { accessToken } = response;
|
||||
expect(accessToken).toEqual('access_token');
|
||||
});
|
||||
|
||||
it('throw General error if auth_code not provided in input', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
error_response: {
|
||||
code: '20001',
|
||||
msg: 'Invalid code',
|
||||
sub_code: 'isv.code-invalid ',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(connector.getUserInfo({}, jest.fn())).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.General, '{}')
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when accessToken is empty', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_system_oauth_token_response: {
|
||||
user_id: '2088000000000000',
|
||||
access_token: '',
|
||||
expires_in: 3600,
|
||||
refresh_token: 'refresh_token',
|
||||
re_expires_in: 7200, // Expiration timeout of refresh token, in seconds
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
await expect(
|
||||
getAccessToken('code', mockedAlipayNativeConfigWithValidPrivateKey)
|
||||
).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid));
|
||||
});
|
||||
|
||||
it('should fail with wrong code', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
error_response: {
|
||||
code: '20001',
|
||||
msg: 'Invalid code',
|
||||
sub_code: 'isv.code-invalid ',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
await expect(
|
||||
getAccessToken('wrong_code', mockedAlipayNativeConfigWithValidPrivateKey)
|
||||
).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, 'Invalid code')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserInfo', () => {
|
||||
beforeEach(() => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.once()
|
||||
.reply(200, {
|
||||
alipay_system_oauth_token_response: {
|
||||
user_id: '2088000000000000',
|
||||
access_token: 'access_token',
|
||||
expires_in: 3600,
|
||||
refresh_token: 'refresh_token',
|
||||
re_expires_in: 7200, // Expiration timeout of refresh token, in seconds
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const alipayEndpointUrl = new URL(alipayEndpoint);
|
||||
|
||||
it('should get userInfo with accessToken', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_user_info_share_response: {
|
||||
code: '10000',
|
||||
msg: 'Success',
|
||||
user_id: '2088000000000000',
|
||||
nick_name: 'PlayboyEric',
|
||||
avatar: 'https://www.alipay.com/xxx.jpg',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
const { id, name, avatar } = await connector.getUserInfo({ auth_code: 'code' }, jest.fn());
|
||||
expect(id).toEqual('2088000000000000');
|
||||
expect(name).toEqual('PlayboyEric');
|
||||
expect(avatar).toEqual('https://www.alipay.com/xxx.jpg');
|
||||
});
|
||||
|
||||
it('should throw SocialAccessTokenInvalid with code 20001', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_user_info_share_response: {
|
||||
code: '20001',
|
||||
msg: 'Invalid auth token',
|
||||
sub_code: 'aop.invalid-auth-token',
|
||||
sub_msg: '无效的访问令牌',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(
|
||||
connector.getUserInfo({ auth_code: 'wrong_code' }, jest.fn())
|
||||
).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, 'Invalid auth token')
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw SocialAuthCodeInvalid with sub_code `isv.code-invalid`', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_user_info_share_response: {
|
||||
code: '40002',
|
||||
msg: 'Invalid auth code',
|
||||
sub_code: 'isv.code-invalid',
|
||||
sub_msg: '授权码 (auth_code) 错误、状态不对或过期',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(
|
||||
connector.getUserInfo({ auth_code: 'wrong_code' }, jest.fn())
|
||||
).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, 'Invalid auth code')
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw General error with other response error codes', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_user_info_share_response: {
|
||||
code: '40002',
|
||||
msg: 'Invalid parameter',
|
||||
sub_code: 'isv.invalid-parameter',
|
||||
sub_msg: '参数无效',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(
|
||||
connector.getUserInfo({ auth_code: 'wrong_code' }, jest.fn())
|
||||
).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.General, {
|
||||
errorDescription: 'Invalid parameter',
|
||||
code: '40002',
|
||||
sub_code: 'isv.invalid-parameter',
|
||||
sub_msg: '参数无效',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw with right accessToken but empty userInfo', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_user_info_share_response: {
|
||||
code: '10000',
|
||||
msg: 'Success',
|
||||
user_id: undefined,
|
||||
nick_name: 'PlayboyEric',
|
||||
avatar: 'https://www.alipay.com/xxx.jpg',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(
|
||||
connector.getUserInfo({ auth_code: 'wrong_code' }, jest.fn())
|
||||
).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.InvalidResponse));
|
||||
});
|
||||
|
||||
it('should throw with other request errors', async () => {
|
||||
nock(alipayEndpointUrl.origin).post(alipayEndpointUrl.pathname).query(true).reply(500);
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(connector.getUserInfo({ auth_code: 'wrong_code' }, jest.fn())).rejects.toThrow();
|
||||
});
|
||||
});
|
195
packages/connectors/connector-alipay-native/src/index.ts
Normal file
|
@ -0,0 +1,195 @@
|
|||
/**
|
||||
* The Implementation of OpenID Connect of Alipay Web Open Platform.
|
||||
* https://opendocs.alipay.com/open/218/105325
|
||||
* https://opendocs.alipay.com/open/218/105327
|
||||
*
|
||||
* https://opendocs.alipay.com/open/204/105295/
|
||||
* https://opendocs.alipay.com/open/204/105296/
|
||||
*/
|
||||
|
||||
import type {
|
||||
GetAuthorizationUri,
|
||||
GetUserInfo,
|
||||
GetConnectorConfig,
|
||||
CreateConnector,
|
||||
SocialConnector,
|
||||
} from '@logto/connector-kit';
|
||||
import {
|
||||
ConnectorError,
|
||||
ConnectorErrorCodes,
|
||||
validateConfig,
|
||||
ConnectorType,
|
||||
parseJson,
|
||||
} from '@logto/connector-kit';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import dayjs from 'dayjs';
|
||||
import { got } from 'got';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
alipayEndpoint,
|
||||
authorizationEndpoint,
|
||||
methodForAccessToken,
|
||||
methodForUserInfo,
|
||||
defaultMetadata,
|
||||
defaultTimeout,
|
||||
timestampFormat,
|
||||
invalidAccessTokenCode,
|
||||
invalidAccessTokenSubCode,
|
||||
} from './constant.js';
|
||||
import type { AlipayNativeConfig, ErrorHandler } from './types.js';
|
||||
import {
|
||||
alipayNativeConfigGuard,
|
||||
accessTokenResponseGuard,
|
||||
userInfoResponseGuard,
|
||||
} from './types.js';
|
||||
import { signingParameters } from './utils.js';
|
||||
|
||||
export type { AlipayNativeConfig } from './types.js';
|
||||
|
||||
const getAuthorizationUri =
|
||||
(getConfig: GetConnectorConfig): GetAuthorizationUri =>
|
||||
async ({ state }) => {
|
||||
const config = await getConfig(defaultMetadata.id);
|
||||
validateConfig<AlipayNativeConfig>(config, alipayNativeConfigGuard);
|
||||
|
||||
const { appId } = config;
|
||||
|
||||
const queryParameters = new URLSearchParams({ app_id: appId, state });
|
||||
|
||||
return `${authorizationEndpoint}?${queryParameters.toString()}`;
|
||||
};
|
||||
|
||||
export const getAccessToken = async (code: string, config: AlipayNativeConfig) => {
|
||||
const initSearchParameters = {
|
||||
method: methodForAccessToken,
|
||||
format: 'JSON',
|
||||
timestamp: dayjs().format(timestampFormat),
|
||||
version: '1.0',
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
charset: 'utf8',
|
||||
...config,
|
||||
};
|
||||
const signedSearchParameters = signingParameters(initSearchParameters);
|
||||
|
||||
const httpResponse = await got.post(alipayEndpoint, {
|
||||
searchParams: signedSearchParameters,
|
||||
timeout: { request: defaultTimeout },
|
||||
});
|
||||
|
||||
const result = accessTokenResponseGuard.safeParse(parseJson(httpResponse.body));
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error);
|
||||
}
|
||||
|
||||
const { error_response, alipay_system_oauth_token_response } = result.data;
|
||||
|
||||
const { msg, sub_msg } = error_response ?? {};
|
||||
|
||||
assert(
|
||||
alipay_system_oauth_token_response,
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, msg ?? sub_msg)
|
||||
);
|
||||
const { access_token: accessToken } = alipay_system_oauth_token_response;
|
||||
assert(accessToken, new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid));
|
||||
|
||||
return { accessToken };
|
||||
};
|
||||
|
||||
const getUserInfo =
|
||||
(getConfig: GetConnectorConfig): GetUserInfo =>
|
||||
async (data) => {
|
||||
const { auth_code } = await authorizationCallbackHandler(data);
|
||||
const config = await getConfig(defaultMetadata.id);
|
||||
|
||||
validateConfig<AlipayNativeConfig>(config, alipayNativeConfigGuard);
|
||||
|
||||
const { accessToken } = await getAccessToken(auth_code, config);
|
||||
|
||||
assert(
|
||||
accessToken && config,
|
||||
new ConnectorError(ConnectorErrorCodes.InsufficientRequestParameters)
|
||||
);
|
||||
|
||||
const initSearchParameters = {
|
||||
method: methodForUserInfo,
|
||||
format: 'JSON',
|
||||
timestamp: dayjs().format(timestampFormat),
|
||||
version: '1.0',
|
||||
grant_type: 'authorization_code',
|
||||
auth_token: accessToken,
|
||||
biz_content: JSON.stringify({}),
|
||||
charset: 'utf8',
|
||||
...config,
|
||||
};
|
||||
const signedSearchParameters = signingParameters(initSearchParameters);
|
||||
|
||||
const httpResponse = await got.post(alipayEndpoint, {
|
||||
searchParams: signedSearchParameters,
|
||||
timeout: { request: defaultTimeout },
|
||||
});
|
||||
|
||||
const result = userInfoResponseGuard.safeParse(parseJson(httpResponse.body));
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error);
|
||||
}
|
||||
|
||||
const { alipay_user_info_share_response } = result.data;
|
||||
|
||||
errorHandler(alipay_user_info_share_response);
|
||||
|
||||
const { user_id: id, avatar, nick_name: name } = alipay_user_info_share_response;
|
||||
|
||||
if (!id) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse);
|
||||
}
|
||||
|
||||
return { id, avatar, name };
|
||||
};
|
||||
|
||||
const errorHandler: ErrorHandler = ({ code, msg, sub_code, sub_msg }) => {
|
||||
if (invalidAccessTokenCode.includes(code)) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, msg);
|
||||
}
|
||||
|
||||
if (sub_code) {
|
||||
assert(
|
||||
!invalidAccessTokenSubCode.includes(sub_code),
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, msg)
|
||||
);
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, {
|
||||
errorDescription: msg,
|
||||
code,
|
||||
sub_code,
|
||||
sub_msg,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const authorizationCallbackHandler = async (parameterObject: unknown) => {
|
||||
const dataGuard = z.object({ auth_code: z.string() });
|
||||
|
||||
const result = dataGuard.safeParse(parameterObject);
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(parameterObject));
|
||||
}
|
||||
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const createAlipayNativeConnector: CreateConnector<SocialConnector> = async ({ getConfig }) => {
|
||||
return {
|
||||
metadata: defaultMetadata,
|
||||
type: ConnectorType.Social,
|
||||
configGuard: alipayNativeConfigGuard,
|
||||
getAuthorizationUri: getAuthorizationUri(getConfig),
|
||||
getUserInfo: getUserInfo(getConfig),
|
||||
};
|
||||
};
|
||||
|
||||
export default createAlipayNativeConnector;
|
25
packages/connectors/connector-alipay-native/src/mock.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import type { AlipayNativeConfig } from './types.js';
|
||||
|
||||
export const mockedTimestamp = '2022-02-22 22:22:22';
|
||||
|
||||
export const mockedAlipayNativeConfig: AlipayNativeConfig = {
|
||||
appId: '2021000000000000',
|
||||
signType: 'RSA2',
|
||||
privateKey: '<private-key>',
|
||||
};
|
||||
|
||||
export const mockedAlipayNativeConfigWithValidPrivateKey: AlipayNativeConfig = {
|
||||
appId: '2021000000000000',
|
||||
signType: 'RSA2',
|
||||
privateKey:
|
||||
'-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC52SvnlRfzJJDR\nA1h4MX2JWV7Yt1j+1gvtQuLh0RYbE0AgRyz8CXFcJegO8gNyUQ05vrc1RMVzvNh8\njfjLpIX8an88KE4FyoG5P8NWrwPw5ZXOnzdvNxAV8QWOU+rT4WAdCsx4++mLlb5v\nGL18R77f3WLgY23bFtcGr9q7/qOaLzNxEe4idX1eLf7Ba/gQRY0awA55/Epd1Mi7\nLqTfxTd11PoBZQPe0vnuChp3P2l1MNpIJ5G1eQ4RXgI4UMClEbGRlBN7GUlXy5p7\ng6RtvOcwmBNoE4i0/HbvaanY3u7oenST3iSzEXa2hXMjnZPvg0G4Y5mq/V6XJPTh\nJrFc9XzFAgMBAAECggEAXfmNtN10LdN4kugBLU3BL9mMF0Om8b1kbIXc2djzN5+l\nVm0HNy7DLphQXnZL/ds0N9XTKFFtEpgUU+8qNjcsNTXYvp+WzGDY9cZjTQrUkFRX\nSxLBYjBSpvWoHI8ceCVHh4f1Wtvu/VEr6Vt2PUi+IM7+d35vh1BmTJBRp6wcKBMH\nXdfjWIi5z37pTXD3OTfUjBCtzA2DX0vY6UTsmD9UI0Mb6IJdT6qugiGODFdlsduA\nWJoZlXV1VbHcvGt7DoeQgzA45sr5siUnm+ntTVBHOR/hoZQrr0DY/O/MLKYUj/+r\nZMKKpx/7VHnWfMia2EOHfjW8vUlnraUzI+5E2/FzIQKBgQDgi7S7pfRux8YONGP2\nRtHPkF8d0YllsfKedhqF3cQlJ1dhxzVqHOi1IFn6ttuuYy5UsP5apYa2kj2UUPCa\nZGGi19Vnc+RHThpR4K6/OGFrpbINAgiVJLj7F8GXzqeA7W2ZHMp1R+oB+oTxih6t\nU0dbeTP01kbBV1/7+ZUKPhLE6QKBgQDT4cMgq01F/WIGGd1GUHZQjH5bqtNiJpIf\n2Q2OTw/gn1DVnwDXpHuXPxtC3NRoaRW/dTqsF6AAkMja3voPM3sYJurGBdU8pZPC\nquc9mqqu6TR5gX3KL1lSESvMBEgfLUy/f0gI3JNw1mG17pIhnXmOB2be3HfZPcj3\nwKWlluY/fQKBgDLll97c3A3sPGll2K6vGMmqmNTCdRlW/36JmLN1NAuT4kuoguP9\nj4XWwm6A2kSp+It73vue/20MsuaWfiMQ08y8jYO4kirTekXK3vE7D2H+GeC28EkW\nHNPVa61ES1V++9Oz4fQ5i8JNDatOOmvhL5B9ZZh+pWUXsAsGZJEAxvJZAoGAMPHO\n5GYN1KQil6wz3EFMA3Fg4wYEDIFCcg7uvbfvwACtaJtxU18QmbCfOIPQoUndFzwa\nUJSohljrvPuTIh3PSpX618GTL45EIszd2/I1iXAfig3qo+DqLjX/OwKmMmWBfB8H\n4dwqRv+O1LsGkLNS2AdHsSWWnd1S5kBfQ3AnQfUCgYACM8ldXZv7uGt9uZBmxile\nB0Hg5w7F1v9VD/m9ko+INAISz8OVkD83pCEoyHwlr20JjiF+yzAakOuq6rBi+l/V\n1veSiTDUcZhciuq1G178dFYepJqisFBu7bAM+WBS4agTTtxdSLZkHeS4VX+H3DOc\ntri43NXw6QS7uQ5/+2TsEw==\n-----END PRIVATE KEY-----',
|
||||
};
|
||||
|
||||
export const mockedAlipayNativePublicParameters = {
|
||||
format: 'JSON',
|
||||
grantType: 'authorization_code',
|
||||
timestamp: mockedTimestamp,
|
||||
version: '1.0',
|
||||
charset: 'utf8',
|
||||
method: '<method-placeholder>',
|
||||
};
|
59
packages/connectors/connector-alipay-native/src/types.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { alipaySigningAlgorithms } from './constant.js';
|
||||
|
||||
export const alipayNativeConfigGuard = z.object({
|
||||
appId: z.string().max(16),
|
||||
privateKey: z.string(),
|
||||
signType: z.enum(alipaySigningAlgorithms),
|
||||
});
|
||||
|
||||
export type AlipayNativeConfig = z.infer<typeof alipayNativeConfigGuard>;
|
||||
|
||||
// `error_response` and `alipay_system_oauth_token_response` are mutually exclusive.
|
||||
export const errorResponseGuard = z.object({
|
||||
code: z.string(),
|
||||
msg: z.string(), // To know `code` and `msg` details, see: https://opendocs.alipay.com/common/02km9f
|
||||
sub_code: z.string().optional(),
|
||||
sub_msg: z.string().optional(),
|
||||
});
|
||||
|
||||
export const alipaySystemOauthTokenResponseGuard = z.object({
|
||||
user_id: z.string(), // Unique Alipay ID, 16 digits starts with '2088'
|
||||
access_token: z.string(),
|
||||
expires_in: z.number(), // In seconds (is string type in docs which is not true)
|
||||
refresh_token: z.string(),
|
||||
re_expires_in: z.number(), // Expiration timeout of refresh token, in seconds (is string type in docs which is not true)
|
||||
});
|
||||
|
||||
export const accessTokenResponseGuard = z.object({
|
||||
sign: z.string(), // To know `sign` details, see: https://opendocs.alipay.com/common/02kf5q
|
||||
error_response: z.optional(errorResponseGuard),
|
||||
alipay_system_oauth_token_response: z.optional(alipaySystemOauthTokenResponseGuard),
|
||||
});
|
||||
|
||||
export type AccessTokenResponse = z.infer<typeof accessTokenResponseGuard>;
|
||||
|
||||
export const alipayUserInfoShareResponseGuard = z.object({
|
||||
user_id: z.string().optional(), // String of digits with max length of 16
|
||||
avatar: z.string().optional(), // URL of avatar
|
||||
province: z.string().optional(),
|
||||
city: z.string().optional(),
|
||||
nick_name: z.string().optional(),
|
||||
gender: z.string().optional(), // Enum type: 'F' for female, 'M' for male
|
||||
code: z.string(),
|
||||
msg: z.string(), // To know `code` and `msg` details, see: https://opendocs.alipay.com/common/02km9f
|
||||
sub_code: z.string().optional(),
|
||||
sub_msg: z.string().optional(),
|
||||
});
|
||||
|
||||
type AlipayUserInfoShareResponseGuard = z.infer<typeof alipayUserInfoShareResponseGuard>;
|
||||
|
||||
export const userInfoResponseGuard = z.object({
|
||||
sign: z.string(), // To know `sign` details, see: https://opendocs.alipay.com/common/02kf5q
|
||||
alipay_user_info_share_response: alipayUserInfoShareResponseGuard,
|
||||
});
|
||||
|
||||
export type UserInfoResponse = z.infer<typeof userInfoResponseGuard>;
|
||||
|
||||
export type ErrorHandler = (response: AlipayUserInfoShareResponseGuard) => void;
|
|
@ -0,0 +1,62 @@
|
|||
import { methodForAccessToken } from './constant.js';
|
||||
import {
|
||||
mockedAlipayNativeConfigWithValidPrivateKey,
|
||||
mockedAlipayNativePublicParameters,
|
||||
} from './mock.js';
|
||||
import { signingParameters } from './utils.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const listenJSONParse = jest.spyOn(JSON, 'parse');
|
||||
const listenJSONStringify = jest.spyOn(JSON, 'stringify');
|
||||
|
||||
describe('signingParameters', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const testingParameters = {
|
||||
...mockedAlipayNativePublicParameters,
|
||||
...mockedAlipayNativeConfigWithValidPrivateKey,
|
||||
method: methodForAccessToken,
|
||||
code: '7ffeb112fbb6495c9e7dfb720380DD39',
|
||||
};
|
||||
|
||||
it('should return exact signature with the given parameters (functionality check)', () => {
|
||||
const decamelizedParameters = signingParameters(testingParameters);
|
||||
expect(decamelizedParameters.sign).toBe(
|
||||
'jqVzRnwdvBEIocvKGZlZ4X3CK0pEsm8HpRWL9FtGS+P8ZRehh+Wvb3lmXWf0fhTIHmcZahQMAnLFO3OmqcwlUrs4PuRgPVmLG6mK087tkw/GP18hlstnD1hN3DS98eZZQsn8psxdHQ1qtzuik1fM0hiZvR7d/Pr72yNhIzgzWa66wBXJGYc6cmSQzB7g5hFg7L/SC55Xk205tkXkenPO9ti2TY8+bWOEZ4hAteWGftwCROz+1ne3EVrt2e/LpQQvRmDPhMIRVEShmcGTNj0ovnjN2K4Uo/YB7+hPLJkrGpYBV4hDEV91KQ9RybmE927xgIzXl7xbiHvK+BayFGNzFA=='
|
||||
);
|
||||
});
|
||||
|
||||
it('should return exact signature with the given parameters (with empty property in testingParameters)', () => {
|
||||
const decamelizedParameters = signingParameters({
|
||||
...testingParameters,
|
||||
emptyProperty: '',
|
||||
});
|
||||
expect(decamelizedParameters.sign).toBe(
|
||||
'jqVzRnwdvBEIocvKGZlZ4X3CK0pEsm8HpRWL9FtGS+P8ZRehh+Wvb3lmXWf0fhTIHmcZahQMAnLFO3OmqcwlUrs4PuRgPVmLG6mK087tkw/GP18hlstnD1hN3DS98eZZQsn8psxdHQ1qtzuik1fM0hiZvR7d/Pr72yNhIzgzWa66wBXJGYc6cmSQzB7g5hFg7L/SC55Xk205tkXkenPO9ti2TY8+bWOEZ4hAteWGftwCROz+1ne3EVrt2e/LpQQvRmDPhMIRVEShmcGTNj0ovnjN2K4Uo/YB7+hPLJkrGpYBV4hDEV91KQ9RybmE927xgIzXl7xbiHvK+BayFGNzFA=='
|
||||
);
|
||||
});
|
||||
|
||||
it('should not call JSON.parse() when biz_content is empty', () => {
|
||||
signingParameters(testingParameters);
|
||||
expect(listenJSONParse).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call JSON.parse() when biz_content is not empty', () => {
|
||||
signingParameters({
|
||||
...testingParameters,
|
||||
biz_content: JSON.stringify({ AB: 'AB' }),
|
||||
});
|
||||
expect(listenJSONParse).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call JSON.stringify() when some value is object string', () => {
|
||||
signingParameters({
|
||||
...testingParameters,
|
||||
testObject: JSON.stringify({ AB: 'AB' }),
|
||||
});
|
||||
expect(listenJSONStringify).toHaveBeenCalled();
|
||||
});
|
||||
});
|
52
packages/connectors/connector-alipay-native/src/utils.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import crypto from 'node:crypto';
|
||||
|
||||
import { parseJson } from '@logto/connector-kit';
|
||||
import iconv from 'iconv-lite';
|
||||
import snakeCaseKeys from 'snakecase-keys';
|
||||
|
||||
import { alipaySigningAlgorithmMapping } from './constant.js';
|
||||
import type { AlipayNativeConfig } from './types.js';
|
||||
|
||||
export type SigningParameters = (
|
||||
parameters: AlipayNativeConfig & Record<string, string | undefined>
|
||||
) => Record<string, string>;
|
||||
|
||||
// Reference: https://github.com/alipay/alipay-sdk-nodejs-all/blob/10d78e0adc7f310d5b07567ce7e4c13a3f6c768f/lib/util.ts
|
||||
export const signingParameters: SigningParameters = (
|
||||
parameters: AlipayNativeConfig & Record<string, string | undefined>
|
||||
): Record<string, string> => {
|
||||
const { biz_content, privateKey, ...rest } = parameters;
|
||||
const signParameters = snakeCaseKeys(
|
||||
biz_content
|
||||
? {
|
||||
...rest,
|
||||
bizContent: JSON.stringify(snakeCaseKeys(parseJson(biz_content))),
|
||||
}
|
||||
: rest
|
||||
);
|
||||
|
||||
const decamelizeParameters = snakeCaseKeys(signParameters);
|
||||
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
|
||||
const sortedParametersAsString = Object.entries(decamelizeParameters)
|
||||
.map(([key, value]) => {
|
||||
// Supported Encodings can be found at https://github.com/ashtuchkin/iconv-lite/wiki/Supported-Encodings
|
||||
|
||||
if (value) {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
return `${key}=${iconv.encode(value, rest.charset ?? 'utf8')}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
.join('&');
|
||||
|
||||
const sign = crypto
|
||||
.createSign(alipaySigningAlgorithmMapping[rest.signType])
|
||||
.update(sortedParametersAsString, 'utf8')
|
||||
.sign(privateKey, 'base64');
|
||||
|
||||
return { ...decamelizeParameters, sign };
|
||||
};
|
182
packages/connectors/connector-alipay-web/CHANGELOG.md
Normal file
|
@ -0,0 +1,182 @@
|
|||
# Change Log
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 4ed07d2: bump connector-kit to v1.0.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 269d701: The console connector configuration page has been changed from JSON format to form view.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9ff0638: update connector-kit version
|
||||
- 7b0bf69: Bump version to upgrade connector kit
|
||||
- 4ec0889: bump connector-kit version
|
||||
- d8b9dea: 1. Update `@logto/connector-kit` from `1.0.0-beta.32` to `1.0.0-beta.33`.
|
||||
- d183d6d: Upgrade connector-kit
|
||||
|
||||
## 1.0.0-beta.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4ec0889: bump connector-kit version
|
||||
|
||||
## 1.0.0-beta.20
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 269d701: The console connector configuration page has been changed from JSON format to form view.
|
||||
|
||||
## 1.0.0-beta.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7b0bf69: Bump version to upgrade connector kit
|
||||
|
||||
## 1.0.0-beta.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9ff0638: update connector-kit version
|
||||
|
||||
## 1.0.0-beta.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d8b9dea: 1. Update `@logto/connector-kit` from `1.0.0-beta.32` to `1.0.0-beta.33`.
|
||||
|
||||
## 1.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d183d6d: Upgrade connector-kit
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.0-beta.12](https://github.com/logto-io/connectors/compare/v1.0.0-beta.11...v1.0.0-beta.12) (2022-11-29)
|
||||
|
||||
### Features
|
||||
|
||||
- add mock standard email connector ([#35](https://github.com/logto-io/connectors/issues/35)) ([479114e](https://github.com/logto-io/connectors/commit/479114e847fb4b11c6fbd697a36b7f5eb56305ed))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **deps:** update dependency @logto/core-kit to v1.0.0-beta.26 ([#33](https://github.com/logto-io/connectors/issues/33)) ([876b635](https://github.com/logto-io/connectors/commit/876b63528bcaf69de4293196e8ab9d1e20b5571d))
|
||||
|
||||
## [1.0.0-beta.11](https://github.com/logto-io/connectors/compare/v1.0.0-beta.10...v1.0.0-beta.11) (2022-11-06)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-web
|
||||
|
||||
## [1.0.0-beta.10](https://github.com/logto-io/connectors/compare/v1.0.0-beta.9...v1.0.0-beta.10) (2022-09-27)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-web
|
||||
|
||||
## 1.0.0-beta.9 (2022-09-18)
|
||||
|
||||
### Features
|
||||
|
||||
- add connectors ([#2](https://github.com/logto-io/connectors/issues/2)) ([2fbb578](https://github.com/logto-io/connectors/commit/2fbb57815406bace113617a6304eafcfc5db2d61))
|
||||
|
||||
## [1.0.0-beta.8](https://github.com/logto-io/logto/compare/v1.0.0-beta.6...v1.0.0-beta.8) (2022-09-01)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-web
|
||||
|
||||
## [1.0.0-beta.6](https://github.com/logto-io/logto/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-08-30)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-web
|
||||
|
||||
## [1.0.0-beta.5](https://github.com/logto-io/logto/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2022-08-19)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-web
|
||||
|
||||
## [1.0.0-beta.4](https://github.com/logto-io/logto/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-08-11)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-web
|
||||
|
||||
## [1.0.0-beta.3](https://github.com/logto-io/logto/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-08-01)
|
||||
|
||||
### Features
|
||||
|
||||
- **phrases:** tr language ([#1707](https://github.com/logto-io/logto/issues/1707)) ([411a8c2](https://github.com/logto-io/logto/commit/411a8c2fa2bfb16c4fef5f0a55c3c1dc5ead1124))
|
||||
|
||||
## [1.0.0-beta.2](https://github.com/logto-io/logto/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-07-25)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-web
|
||||
|
||||
## [1.0.0-beta.1](https://github.com/logto-io/logto/compare/v1.0.0-beta.0...v1.0.0-beta.1) (2022-07-19)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay-web
|
||||
|
||||
## [1.0.0-beta.0](https://github.com/logto-io/logto/compare/v1.0.0-alpha.4...v1.0.0-beta.0) (2022-07-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **connector:** fix connector getConfig and validateConfig type ([#1530](https://github.com/logto-io/logto/issues/1530)) ([88a54aa](https://github.com/logto-io/logto/commit/88a54aaa9ebce419c149a33150a4927296cb705b))
|
||||
- **connector:** refactor ConnectorInstance as class ([#1541](https://github.com/logto-io/logto/issues/1541)) ([6b9ad58](https://github.com/logto-io/logto/commit/6b9ad580ae86fbcc100a100aab1d834090e682a3))
|
||||
|
||||
## [1.0.0-alpha.4](https://github.com/logto-io/logto/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) (2022-07-08)
|
||||
|
||||
### Features
|
||||
|
||||
- **connector:** connector error handler, throw errmsg on general errors ([#1458](https://github.com/logto-io/logto/issues/1458)) ([7da1de3](https://github.com/logto-io/logto/commit/7da1de33e97de4aeeec9f9b6cea59d1bf90ba623))
|
||||
|
||||
## [1.0.0-alpha.3](https://github.com/logto-io/logto/compare/v1.0.0-alpha.2...v1.0.0-alpha.3) (2022-07-07)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay
|
||||
|
||||
## [1.0.0-alpha.2](https://github.com/logto-io/logto/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2022-07-07)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay
|
||||
|
||||
## [1.0.0-alpha.1](https://github.com/logto-io/logto/compare/v1.0.0-alpha.0...v1.0.0-alpha.1) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay
|
||||
|
||||
## [1.0.0-alpha.0](https://github.com/logto-io/logto/compare/v0.1.2-alpha.5...v1.0.0-alpha.0) (2022-07-04)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay
|
||||
|
||||
### [0.1.2-alpha.5](https://github.com/logto-io/logto/compare/v0.1.2-alpha.4...v0.1.2-alpha.5) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay
|
||||
|
||||
### [0.1.2-alpha.4](https://github.com/logto-io/logto/compare/v0.1.2-alpha.3...v0.1.2-alpha.4) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay
|
||||
|
||||
### [0.1.2-alpha.3](https://github.com/logto-io/logto/compare/v0.1.2-alpha.2...v0.1.2-alpha.3) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay
|
||||
|
||||
### [0.1.2-alpha.2](https://github.com/logto-io/logto/compare/v0.1.2-alpha.1...v0.1.2-alpha.2) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay
|
||||
|
||||
### [0.1.2-alpha.1](https://github.com/logto-io/logto/compare/v0.1.2-alpha.0...v0.1.2-alpha.1) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-alipay
|
||||
|
||||
### [0.1.1-alpha.0](https://github.com/logto-io/logto/compare/v0.1.0-internal...v0.1.1-alpha.0) (2022-07-01)
|
||||
|
||||
### Features
|
||||
|
||||
- **connector-alipay:** parse code from json ([c248759](https://github.com/logto-io/logto/commit/c248759b53faa39a22906139f3a4049633c8b2a9))
|
||||
- **connectors:** add logo for connectors ([#914](https://github.com/logto-io/logto/issues/914)) ([a3a7c52](https://github.com/logto-io/logto/commit/a3a7c52a91dba3603617a68e5ce47e0017081a91))
|
||||
- **connectors:** handle authorization callback parameters in each connector respectively ([#1166](https://github.com/logto-io/logto/issues/1166)) ([097aade](https://github.com/logto-io/logto/commit/097aade2e2e1b1ea1531bcb4c1cca8d24961a9b9))
|
||||
- **console:** connector logo and platform icon ([#892](https://github.com/logto-io/logto/issues/892)) ([97e6bdd](https://github.com/logto-io/logto/commit/97e6bdd8aacdf12dcf99a984d7b5bcd2f61f1530))
|
||||
- **core,connectors:** update Aliyun logo and add logo_dark to Apple, Github ([#1194](https://github.com/logto-io/logto/issues/1194)) ([98f8083](https://github.com/logto-io/logto/commit/98f808320b1c79c51f8bd6f49e35ca44363ea560))
|
||||
- **core:** serve connector logo ([#931](https://github.com/logto-io/logto/issues/931)) ([5b44b71](https://github.com/logto-io/logto/commit/5b44b7194ed4f98c6c2e77aae828a39b477b6010))
|
||||
- **core:** update connector db schema ([#732](https://github.com/logto-io/logto/issues/732)) ([8e1533a](https://github.com/logto-io/logto/commit/8e1533a70267d459feea4e5174296b17bef84d48))
|
||||
- **core:** wrap aliyun direct mail connector ([#660](https://github.com/logto-io/logto/issues/660)) ([54b6209](https://github.com/logto-io/logto/commit/54b62094c8d8af0611cf64e39306c4f1a216e8f6))
|
||||
- **core:** wrap aliyun short message service connector ([#670](https://github.com/logto-io/logto/issues/670)) ([a06d3ee](https://github.com/logto-io/logto/commit/a06d3ee73ccc59f6aaef1dab4f45d6c118aab40d))
|
||||
- **native-connectors:** pass random state to native connector sdk ([#922](https://github.com/logto-io/logto/issues/922)) ([9679620](https://github.com/logto-io/logto/commit/96796203dd4247d7ecdee044f13f3d57f04ca461))
|
||||
- remove target, platform from connector schema and add id to metadata ([#930](https://github.com/logto-io/logto/issues/930)) ([054b0f7](https://github.com/logto-io/logto/commit/054b0f7b6a6dfed66540042ea69b0721126fe695))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- `lint:report` script ([#730](https://github.com/logto-io/logto/issues/730)) ([3b17324](https://github.com/logto-io/logto/commit/3b17324d189b2fe47985d0bee8b37b4ef1dbdd2b))
|
||||
- **connector-alipay-web:** rename input param name code to auth_code ([#1015](https://github.com/logto-io/logto/issues/1015)) ([1e27ee7](https://github.com/logto-io/logto/commit/1e27ee7630e08fb96cb08dfe9b5654b92bac6924))
|
150
packages/connectors/connector-alipay-web/README.md
Normal file
|
@ -0,0 +1,150 @@
|
|||
# Alipay Web
|
||||
|
||||
The official Logto connector for Alipay social sign-in in web apps.
|
||||
|
||||
支付宝 web 应用社交登录官方 Logto 连接器 [中文文档](#支付宝网页连接器)
|
||||
|
||||
**Table of contents**
|
||||
|
||||
- [Alipay Web](#alipay-web)
|
||||
- [Get started](#get-started)
|
||||
- [Register Alipay developer account](#register-alipay-developer-account)
|
||||
- [Create and configure Alipay app](#create-and-configure-alipay-app)
|
||||
- [Set up the Logto Alipay Web connector settings](#set-up-the-logto-alipay-web-connector-settings)
|
||||
- [Config types](#config-types)
|
||||
- [Test Alipay web connector](#test-alipay-web-connector)
|
||||
- [References](#references)
|
||||
- [支付宝网页连接器](#支付宝网页连接器)
|
||||
- [开始上手](#开始上手)
|
||||
- [注册支付宝开发者账号](#注册支付宝开发者账号)
|
||||
- [在支付宝开放平台上创建并且配置应用](#在支付宝开放平台上创建并且配置应用)
|
||||
- [设置支付宝网页连接器](#设置支付宝网页连接器)
|
||||
- [配置类型](#配置类型)
|
||||
- [测试支付宝网页连接器](#测试支付宝网页连接器)
|
||||
- [参考](#参考)
|
||||
|
||||
## Get started
|
||||
|
||||
Alipay Web connector is designed for desktop Web applications. It takes advantage of Alipay's OAuth 2.0 authentication workflow and enables Alipay users to sign in to other Apps using public Alipay user profiles without going through a troublesome register process.
|
||||
|
||||
## Register Alipay developer account
|
||||
|
||||
[Register an Alipay developer account](https://certifyweb.alipay.com/certify/reg/guide#/) if you don't have one.
|
||||
|
||||
## Create and configure Alipay app
|
||||
|
||||
1. Sign in to the [Alipay console](https://open.alipay.com/) with the account you have just registered.
|
||||
2. Go to "Web & Mobile Apps" (网页&移动应用) tab in "My Application" (我的应用) panel.
|
||||
3. Click "Create an App" (立即创建) button to start configuring your application.
|
||||
4. Name your application in "Application Name" (应用名称) following the naming conventions and upload your "Application Icon" (应用图标), make sure you choose "web application" (网页应用) as "App type" (应用类型).
|
||||
5. After finishing creating the application, we come to the Overview page, where we should click "add ability" (添加能力) to add "Third-party application authorization" (第三方应用授权), "Get member information" (获取会员信息) and "App Alipay login" (App 支付宝登录) before enabling Alipay sign-in.
|
||||
6. Go to [Alipay Customer Center](https://b.alipay.com/index2.htm), and sign in with the Alipay developer account. Click "Account Center" (账号中心) on the topbar and go to "APPID binding" (APPID 绑定), whose entrance can be found at the bottom of the sidebar. "Add binding" (添加绑定) by type in the APPID of the web application you just created in step 4.
|
||||
7. Click on "Sign" button of "App Alipay login", and finish signing process following the guide. After finishing this step, you are expected to find abilities you have just added in step 5 kicks in.
|
||||
8. Come back to Alipay open platform console page, and you can find "Interface signing method" (接口加签方式(密钥/证书)) in "development information" (开发信息) section. Click "set up" (设置) button, and you can find yourself on a page setting signing method. "Public Key" (公钥) is the preferred signing mode, and fill in contents from the public key file you have generated in the text input box.
|
||||
9. Set up "Authorization Redirect URI" (授权回调地址) by clicking "set up" (设置) button on the bottom of the Alipay console page. `${your_logto_origin}/callback/${connector_id}` is the default redirect URI used in Logto core. The `connector_id` can be found on the top bar of the Logto Admin Console connector details page.
|
||||
10. After finishing all these steps, go back to the top right corner of Alipay console page, and click "Submit for review" (提交审核). Once the review is approved, you are good to go with a smooth Alipay sign-in flow.
|
||||
|
||||
> ℹ️ **Note**
|
||||
>
|
||||
> You can use _openssl_ to generate key pairs on your local machine by executing following code snippet in terminal.
|
||||
>
|
||||
> ```bash
|
||||
> openssl genrsa -out private.pem 2048
|
||||
> openssl rsa -in private.pem -outform PEM -pubout -out public.pem
|
||||
> ```
|
||||
>
|
||||
> When filling in the public key on the Alipay app setup website, you need to remove the header and footer of `public.pem`, delete all newline characters, and paste the rest of the contents into the text input box for "public key".
|
||||
|
||||
## Set up the Logto Alipay Web connector settings
|
||||
|
||||
1. In [the Alipay console workspace](https://open.alipay.com/dev/workspace) go to "My application" (我的应用) panel and click "Web & Mobile Apps" (网页&移动应用) tab, you can find APPID of all applications.
|
||||
2. In step 7 of the previous part, you have already generated a key pair including a private key and a public key.
|
||||
3. Fill out the Logto connector settings:
|
||||
- Fill out the `appId` field with APPID you've got from step 1.
|
||||
- Fill out the `privateKey` field with contents from the private key file mentioned in step 2. Please MAKE SURE to use '\n' to replace all newline characters and do not remove header and footer in private key file.
|
||||
- Fill out the `signType` field with 'RSA2' due to the `Public key` signing mode we chose in step 7 of "Create And Configure Alipay Apps".
|
||||
- Fill out the `charset` field with either 'gbk' or 'utf8'. You can leave this field blank as it is OPTIONAL. The default value is set to be 'utf8'.
|
||||
|
||||
### Config types
|
||||
|
||||
| Name | Type | Enum values |
|
||||
|------------|------------------------|------------------------------|
|
||||
| appId | string | N/A |
|
||||
| privateKey | string | N/A |
|
||||
| signType | enum string | 'RSA' \| 'RSA2' |
|
||||
| charset | enum string (OPTIONAL) | 'gbk' \| 'utf8' \| undefined |
|
||||
|
||||
## Test Alipay web connector
|
||||
|
||||
That's it. Don't forget to [Enable connector in sign-in experience](https://docs.logto.io/docs/tutorials/get-started/enable-social-sign-in#enable-connector-in-sign-in-experience).
|
||||
|
||||
Once Alipay web connector is enabled, you can build and run your web app to see if it works.
|
||||
|
||||
## References
|
||||
|
||||
- [Alipay Docs - Access Preparation - How to create an app](https://opendocs.alipay.com/support/01rau6)
|
||||
- [Alipay Docs - Web & Mobile Apps - Create an app](https://opendocs.alipay.com/open/200/105310)
|
||||
|
||||
# 支付宝网页连接器
|
||||
|
||||
## 开始上手
|
||||
|
||||
支付宝网页连接器是为桌面网页应用设计的。它采用了支付宝的 OAuth 2.0 认证流程,并使支付宝用户可以使用公开的支付宝用户信息登录其他应用,而不需要进行令人困惑的注册过程。
|
||||
|
||||
## 注册支付宝开发者账号
|
||||
|
||||
如果你还没有支付宝开发者账号,参考链接:[注册一个支付宝开发者账号](https://certifyweb.alipay.com/certify/reg/guide#/)
|
||||
|
||||
## 在支付宝开放平台上创建并且配置应用
|
||||
|
||||
1. 使用你所创建的支付宝开发者账号登录 [支付宝开放平台控制台](https://open.alipay.com/)。
|
||||
2. 在「我的应用」中选择「网页&移动应用」标签页。
|
||||
3. 点击「立即创建」开始创建并且配置你的应用
|
||||
4. 根据平台的命名规则通过「应用名称」字段给你的应用命名;在「应用图标」中上传应用图标;将「应用类型」设定为「网页应用」。
|
||||
5. 当应用创建成功后,我们进入到了「概览」页面,接下来我们在「能力列表」中点击「+ 添加能力」,将「App 支付宝登录」、「获取会员信息」、「第三方应用授权」添加到能力列表中。
|
||||
6. 使用开发者账号登录 [支付宝商家中心](https://b.alipay.com/index2.htm) 后,从顶栏菜单的进入「账号中心」,然后选择从左侧的菜单栏底部进入「APPID 绑定」页面。点击「+ 添加绑定」,之后输入你在步骤 4 中所创建的应用的 APPID。
|
||||
7. 点按「App 支付宝登录」旁边「签约」按钮,并按照提示完成签约。当此步骤完成后,步骤 5 中所添加的各种「能力」即可生效。
|
||||
8. 回到「支付宝开放平台控制台」中第 5 步所创建的应用的「概览」页面, 在该页面的「开发信息」中点击「接口加签方式(密钥/证书)」的「设置」链接,将「选择加签模式」设定为「公钥」,然后将你生成的公钥填入下方「填写公钥字符」的文本编辑框中。
|
||||
9. 点击「授权回调地址」的「设置链接」,选择你所需要的「回调地址类型」,将 Logto Core 默认使用的 `${your_logto_origin}/callback/${connector_id}` 设置为「回调地址」。`connector_id` 在管理控制台相应连接器的详情页的顶栏中可以找到。
|
||||
10. 当设置完以上的所有步骤,点击「概览」页面上方的「提交审核」,当审核通过后,你将可以顺利地使用支付宝登录自己的网页应用。
|
||||
|
||||
> ℹ️ **注意**
|
||||
>
|
||||
> 你可以用 _openssl_ 来在本地机器上,用下面这一段代码在终端里生成一个密钥对。
|
||||
>
|
||||
> ```bash
|
||||
> openssl genrsa -out private.pem 2048
|
||||
> openssl rsa -in private.pem -outform PEM -pubout -out public.pem
|
||||
> ```
|
||||
>
|
||||
> 在支付宝应用设置网页上填写公钥时,需要把生成的 `public.pem` 文件中内容的文件头和文件尾去掉,同时删除所有的换行符,再把剩下的内容粘贴到填写公钥的文本框中。
|
||||
|
||||
## 设置支付宝网页连接器
|
||||
|
||||
1. 在 [支付宝开放平台控制台](https://open.alipay.com/dev/workspace) 中,点击「我的应用」面板中的「网页&移动应用」,获取应用的 APPID。
|
||||
2. 获取你在上一部分的第 7 个步骤中生成的密钥对。
|
||||
3. 配置你的应用的支付宝网页连接器:
|
||||
- 将你在第 1 步中获取的 APPID 填入 `appId` 字段。
|
||||
- 将你在第 2 步中获得的密钥对的私钥填入 `privateKey` 字段。请保留私钥文件内容中的文件头和文件尾,并 **确保** 使用 '\n' 替换了所有换行符。
|
||||
- 将你在第 2 步中所获得的密钥的签名模式 'RSA2' 填入 `signType` 字段。
|
||||
- 在 `charset` 字段中填入 'gbk' 或 'utf8' 字符串。这个字段也可以选择不填,此时我们会使用 'utf8' 的默认值。
|
||||
|
||||
### 配置类型
|
||||
|
||||
| 名称 | 类型 | 枚举值 |
|
||||
|------------|------------------------|------------------------------|
|
||||
| appId | string | N/A |
|
||||
| privateKey | string | N/A |
|
||||
| signType | enum string | 'RSA' \| 'RSA2' |
|
||||
| charset | enum string (OPTIONAL) | 'gbk' \| 'utf8' \| undefined |
|
||||
|
||||
## 测试支付宝网页连接器
|
||||
|
||||
大功告成。别忘了 [在登录体验中启用社交登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in/#%E5%9C%A8%E7%99%BB%E5%BD%95%E4%BD%93%E9%AA%8C%E4%B8%AD%E5%90%AF%E7%94%A8%E8%BF%9E%E6%8E%A5%E5%99%A8)。
|
||||
|
||||
在支付宝网页连接器启用后,你可以构建并运行你的网页应用看看是否生效。
|
||||
|
||||
## 参考
|
||||
|
||||
- [支付宝文档中心 - 接入准备 - 如何创建应用](https://opendocs.alipay.com/support/01rau6)
|
||||
- [支付宝文档中心 - 网页&移动应用 - 创建应用](https://opendocs.alipay.com/open/200/105310)
|
11
packages/connectors/connector-alipay-web/logo.svg
Normal file
|
@ -0,0 +1,11 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_865_12300)">
|
||||
<path d="M20.0867 0H3.91188C1.80584 0 0.0999756 1.72052 0.0999756 3.84505V20.1556C0.0999756 22.2782 1.80584 24 3.91188 24H20.0867C22.1927 24 23.8973 22.2782 23.8973 20.1556V3.84505C23.8973 1.72052 22.1927 0 20.0867 0Z" fill="#1677FF"/>
|
||||
<path d="M6.5311 18.4567C2.8288 18.4567 1.73405 15.5152 3.56417 13.9068C4.17463 13.3626 5.29042 13.0976 5.88495 13.0377C8.08403 12.8184 10.12 13.6647 12.5223 14.8474C10.8337 17.0688 8.68302 18.4567 6.5311 18.4567ZM19.6943 15.0704C18.7417 14.7486 17.464 14.2567 16.0411 13.7373C16.895 12.2386 17.5781 10.5321 18.0267 8.67773H13.3361V6.97377H19.0826V6.02175H13.3361V3.18225H10.9911C10.5794 3.18225 10.5794 3.59135 10.5794 3.59135V6.02239H4.76789V6.97377H10.5794V8.67773H5.78109V9.62847H15.0872C14.7589 10.7742 14.3096 11.8818 13.7471 12.9325C10.7266 11.9276 7.50479 11.1132 5.48031 11.6141C4.1861 11.9359 3.35197 12.51 2.86258 13.1116C0.613798 15.8708 2.22662 20.0606 6.97462 20.0606C9.78161 20.0606 12.4866 18.4822 14.5825 15.8803C17.7087 17.3969 23.8988 19.9988 23.8988 19.9988V16.2901C23.8988 16.2901 23.1214 16.2276 19.6943 15.0704Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_865_12300">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
14
packages/connectors/connector-alipay-web/package.extend.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package",
|
||||
"name": "@logto/connector-alipay-web",
|
||||
"version": "1.0.0",
|
||||
"description": "Alipay implementation.",
|
||||
"dependencies": {
|
||||
"@logto/core-kit": "1.0.0-beta.29",
|
||||
"dayjs": "^1.10.5",
|
||||
"iconv-lite": "^0.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@shopify/jest-koa-mocks": "^5.0.0"
|
||||
}
|
||||
}
|
81
packages/connectors/connector-alipay-web/src/constant.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
import type { ConnectorMetadata } from '@logto/connector-kit';
|
||||
import { ConnectorPlatform, ConnectorConfigFormItemType } from '@logto/connector-kit';
|
||||
|
||||
export const authorizationEndpoint = 'https://openauth.alipay.com/oauth2/publicAppAuthorize.htm';
|
||||
export const alipayEndpoint = 'https://openapi.alipay.com/gateway.do';
|
||||
export const scope = 'auth_user';
|
||||
export const methodForAccessToken = 'alipay.system.oauth.token';
|
||||
export const methodForUserInfo = 'alipay.user.info.share';
|
||||
|
||||
export const alipaySigningAlgorithmMapping = {
|
||||
RSA: 'RSA-SHA1',
|
||||
RSA2: 'RSA-SHA256',
|
||||
} as const;
|
||||
export const alipaySigningAlgorithms = ['RSA', 'RSA2'] as const;
|
||||
export const charsetEnum = ['gbk', 'utf8'] as const;
|
||||
export const fallbackCharset = 'utf8';
|
||||
|
||||
export const invalidAccessTokenCode = ['20001'];
|
||||
|
||||
export const invalidAccessTokenSubCode = ['isv.code-invalid'];
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'alipay-web',
|
||||
target: 'alipay',
|
||||
platform: ConnectorPlatform.Web,
|
||||
name: {
|
||||
en: 'Alipay',
|
||||
'zh-CN': '支付宝',
|
||||
'tr-TR': 'Alipay',
|
||||
ko: 'Alipay',
|
||||
},
|
||||
logo: './logo.svg',
|
||||
logoDark: null,
|
||||
description: {
|
||||
en: 'Alipay is a third-party mobile and online payment platform.',
|
||||
'zh-CN': '支付宝是一个第三方支付平台。',
|
||||
'tr-TR': 'Alipay, üçüncü şahıslara ait bir mobil ve çevrimiçi ödeme platformudur.',
|
||||
ko: 'Alipay는 서드파티 모바일 및 온라인 결제 플랫폼 입니다.',
|
||||
},
|
||||
readme: './README.md',
|
||||
formItems: [
|
||||
{
|
||||
key: 'appId',
|
||||
label: 'App ID',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: true,
|
||||
placeholder: '<app-id-with-maximum-length-16>',
|
||||
},
|
||||
{
|
||||
key: 'privateKey',
|
||||
label: 'Private Key',
|
||||
type: ConnectorConfigFormItemType.MultilineText,
|
||||
required: true,
|
||||
placeholder: '<private-key>',
|
||||
},
|
||||
{
|
||||
key: 'signType',
|
||||
label: 'Signing Algorithm',
|
||||
type: ConnectorConfigFormItemType.Select,
|
||||
selectItems: [
|
||||
{ title: 'RSA-SHA1', value: 'RSA' },
|
||||
{ title: 'RSA-SHA256', value: 'RSA2' },
|
||||
],
|
||||
defaultValue: 'RSA2',
|
||||
},
|
||||
{
|
||||
key: 'charset',
|
||||
label: 'Char Set',
|
||||
type: ConnectorConfigFormItemType.Select,
|
||||
selectItems: [
|
||||
{ title: 'gbk', value: 'gbk' },
|
||||
{ title: 'utf8', value: 'utf8' },
|
||||
],
|
||||
defaultValue: 'utf8',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const defaultTimeout = 5000;
|
||||
|
||||
export const timestampFormat = 'YYYY-MM-DD HH:mm:ss';
|
250
packages/connectors/connector-alipay-web/src/index.test.ts
Normal file
|
@ -0,0 +1,250 @@
|
|||
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit';
|
||||
import nock from 'nock';
|
||||
|
||||
import { alipayEndpoint, authorizationEndpoint } from './constant.js';
|
||||
import createConnector, { getAccessToken } from './index.js';
|
||||
import { mockedAlipayConfigWithValidPrivateKey } from './mock.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const getConfig = jest.fn().mockResolvedValue(mockedAlipayConfigWithValidPrivateKey);
|
||||
|
||||
describe('getAuthorizationUri', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should get a valid uri by redirectUri and state', async () => {
|
||||
const connector = await createConnector({ getConfig });
|
||||
const authorizationUri = await connector.getAuthorizationUri(
|
||||
{
|
||||
state: 'some_state',
|
||||
redirectUri: 'http://localhost:3001/callback',
|
||||
connectorId: 'some_connector_id',
|
||||
connectorFactoryId: 'some_connector_factory_id',
|
||||
jti: 'some_jti',
|
||||
headers: {},
|
||||
},
|
||||
jest.fn()
|
||||
);
|
||||
expect(authorizationUri).toEqual(
|
||||
`${authorizationEndpoint}?app_id=2021000000000000&redirect_uri=http%3A%2F%2Flocalhost%3A3001%2Fcallback&scope=auth_user&state=some_state`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccessToken', () => {
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const alipayEndpointUrl = new URL(alipayEndpoint);
|
||||
|
||||
it('should get an accessToken by exchanging with code', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_system_oauth_token_response: {
|
||||
user_id: '2088000000000000',
|
||||
access_token: 'access_token',
|
||||
expires_in: 3600,
|
||||
refresh_token: 'refresh_token',
|
||||
re_expires_in: 7200, // Expiration timeout of refresh token, in seconds
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const response = await getAccessToken('code', mockedAlipayConfigWithValidPrivateKey);
|
||||
const { accessToken } = response;
|
||||
expect(accessToken).toEqual('access_token');
|
||||
});
|
||||
|
||||
it('should throw when accessToken is empty', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_system_oauth_token_response: {
|
||||
user_id: '2088000000000000',
|
||||
access_token: '',
|
||||
expires_in: 3600,
|
||||
refresh_token: 'refresh_token',
|
||||
re_expires_in: 7200, // Expiration timeout of refresh token, in seconds
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
|
||||
await expect(
|
||||
getAccessToken('code', mockedAlipayConfigWithValidPrivateKey)
|
||||
).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid));
|
||||
});
|
||||
|
||||
it('should fail with wrong code', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
error_response: {
|
||||
code: '20001',
|
||||
msg: 'Invalid code',
|
||||
sub_code: 'isv.code-invalid ',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
|
||||
await expect(
|
||||
getAccessToken('wrong_code', mockedAlipayConfigWithValidPrivateKey)
|
||||
).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, 'Invalid code')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserInfo', () => {
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.once()
|
||||
.reply(200, {
|
||||
alipay_system_oauth_token_response: {
|
||||
user_id: '2088000000000000',
|
||||
access_token: 'access_token',
|
||||
expires_in: 3600,
|
||||
refresh_token: 'refresh_token',
|
||||
re_expires_in: 7200, // Expiration timeout of refresh token, in seconds
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
});
|
||||
|
||||
const alipayEndpointUrl = new URL(alipayEndpoint);
|
||||
|
||||
it('should get userInfo with accessToken', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_user_info_share_response: {
|
||||
code: '10000',
|
||||
msg: 'Success',
|
||||
user_id: '2088000000000000',
|
||||
nick_name: 'PlayboyEric',
|
||||
avatar: 'https://www.alipay.com/xxx.jpg',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
const { id, name, avatar } = await connector.getUserInfo({ auth_code: 'code' }, jest.fn());
|
||||
expect(id).toEqual('2088000000000000');
|
||||
expect(name).toEqual('PlayboyEric');
|
||||
expect(avatar).toEqual('https://www.alipay.com/xxx.jpg');
|
||||
});
|
||||
|
||||
it('throw General error if auth_code not provided in input', async () => {
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(connector.getUserInfo({}, jest.fn())).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.InvalidResponse, '{}')
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw SocialAccessTokenInvalid with code 20001', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_user_info_share_response: {
|
||||
code: '20001',
|
||||
msg: 'Invalid auth token',
|
||||
sub_code: 'aop.invalid-auth-token',
|
||||
sub_msg: '无效的访问令牌',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(
|
||||
connector.getUserInfo({ auth_code: 'wrong_code' }, jest.fn())
|
||||
).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, 'Invalid auth token')
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw SocialAuthCodeInvalid with sub_code `isv.code-invalid`', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_user_info_share_response: {
|
||||
code: '40002',
|
||||
msg: 'Invalid auth code',
|
||||
sub_code: 'isv.code-invalid',
|
||||
sub_msg: '授权码 (auth_code) 错误、状态不对或过期',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(
|
||||
connector.getUserInfo({ auth_code: 'wrong_code' }, jest.fn())
|
||||
).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, 'Invalid auth code')
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw General error with other response error codes', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_user_info_share_response: {
|
||||
code: '40002',
|
||||
msg: 'Invalid parameter',
|
||||
sub_code: 'isv.invalid-parameter',
|
||||
sub_msg: '参数无效',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(
|
||||
connector.getUserInfo({ auth_code: 'wrong_code' }, jest.fn())
|
||||
).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.General, {
|
||||
errorDescription: 'Invalid parameter',
|
||||
code: '40002',
|
||||
sub_code: 'isv.invalid-parameter',
|
||||
sub_msg: '参数无效',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw with right accessToken but empty userInfo', async () => {
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_user_info_share_response: {
|
||||
code: '10000',
|
||||
msg: 'Success',
|
||||
user_id: undefined,
|
||||
nick_name: 'PlayboyEric',
|
||||
avatar: 'https://www.alipay.com/xxx.jpg',
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(connector.getUserInfo({ auth_code: 'code' }, jest.fn())).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.InvalidResponse)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw with other request errors', async () => {
|
||||
nock(alipayEndpointUrl.origin).post(alipayEndpointUrl.pathname).query(true).reply(500);
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(connector.getUserInfo({ auth_code: 'code' }, jest.fn())).rejects.toThrow();
|
||||
});
|
||||
});
|
196
packages/connectors/connector-alipay-web/src/index.ts
Normal file
|
@ -0,0 +1,196 @@
|
|||
/**
|
||||
* The Implementation of OpenID Connect of Alipay Web Open Platform.
|
||||
* https://opendocs.alipay.com/support/01rg6h
|
||||
* https://opendocs.alipay.com/open/263/105808
|
||||
* https://opendocs.alipay.com/open/01emu5
|
||||
*/
|
||||
|
||||
import type {
|
||||
GetConnectorConfig,
|
||||
GetAuthorizationUri,
|
||||
GetUserInfo,
|
||||
CreateConnector,
|
||||
SocialConnector,
|
||||
} from '@logto/connector-kit';
|
||||
import {
|
||||
ConnectorError,
|
||||
ConnectorErrorCodes,
|
||||
validateConfig,
|
||||
ConnectorType,
|
||||
parseJson,
|
||||
} from '@logto/connector-kit';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import dayjs from 'dayjs';
|
||||
import { got } from 'got';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
alipayEndpoint,
|
||||
authorizationEndpoint,
|
||||
methodForAccessToken,
|
||||
methodForUserInfo,
|
||||
scope,
|
||||
defaultMetadata,
|
||||
defaultTimeout,
|
||||
timestampFormat,
|
||||
invalidAccessTokenCode,
|
||||
invalidAccessTokenSubCode,
|
||||
} from './constant.js';
|
||||
import type { AlipayConfig, ErrorHandler } from './types.js';
|
||||
import { alipayConfigGuard, accessTokenResponseGuard, userInfoResponseGuard } from './types.js';
|
||||
import { signingParameters } from './utils.js';
|
||||
|
||||
export type { AlipayConfig } from './types.js';
|
||||
|
||||
const getAuthorizationUri =
|
||||
(getConfig: GetConnectorConfig): GetAuthorizationUri =>
|
||||
async ({ state, redirectUri }) => {
|
||||
const config = await getConfig(defaultMetadata.id);
|
||||
validateConfig<AlipayConfig>(config, alipayConfigGuard);
|
||||
|
||||
const { appId: app_id } = config;
|
||||
|
||||
const redirect_uri = encodeURI(redirectUri);
|
||||
|
||||
const queryParameters = new URLSearchParams({
|
||||
app_id,
|
||||
redirect_uri, // The variable `redirectUri` should match {appId, appSecret}
|
||||
scope,
|
||||
state,
|
||||
});
|
||||
|
||||
return `${authorizationEndpoint}?${queryParameters.toString()}`;
|
||||
};
|
||||
|
||||
export const getAccessToken = async (code: string, config: AlipayConfig) => {
|
||||
const initSearchParameters = {
|
||||
method: methodForAccessToken,
|
||||
format: 'JSON',
|
||||
timestamp: dayjs().format(timestampFormat),
|
||||
version: '1.0',
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
...config,
|
||||
};
|
||||
const signedSearchParameters = signingParameters(initSearchParameters);
|
||||
|
||||
const httpResponse = await got.post(alipayEndpoint, {
|
||||
searchParams: signedSearchParameters,
|
||||
timeout: { request: defaultTimeout },
|
||||
});
|
||||
|
||||
const result = accessTokenResponseGuard.safeParse(parseJson(httpResponse.body));
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error);
|
||||
}
|
||||
|
||||
const { error_response, alipay_system_oauth_token_response } = result.data;
|
||||
|
||||
const { msg, sub_msg } = error_response ?? {};
|
||||
|
||||
assert(
|
||||
alipay_system_oauth_token_response,
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, msg ?? sub_msg)
|
||||
);
|
||||
const { access_token: accessToken } = alipay_system_oauth_token_response;
|
||||
assert(accessToken, new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid));
|
||||
|
||||
return { accessToken };
|
||||
};
|
||||
|
||||
const getUserInfo =
|
||||
(getConfig: GetConnectorConfig): GetUserInfo =>
|
||||
async (data) => {
|
||||
const { auth_code } = await authorizationCallbackHandler(data);
|
||||
const config = await getConfig(defaultMetadata.id);
|
||||
validateConfig<AlipayConfig>(config, alipayConfigGuard);
|
||||
|
||||
const { accessToken } = await getAccessToken(auth_code, config);
|
||||
|
||||
assert(
|
||||
accessToken && config,
|
||||
new ConnectorError(ConnectorErrorCodes.InsufficientRequestParameters)
|
||||
);
|
||||
|
||||
const initSearchParameters = {
|
||||
method: methodForUserInfo,
|
||||
format: 'JSON',
|
||||
timestamp: dayjs().format(timestampFormat),
|
||||
version: '1.0',
|
||||
grant_type: 'authorization_code',
|
||||
auth_token: accessToken,
|
||||
biz_content: JSON.stringify({}),
|
||||
...config,
|
||||
};
|
||||
const signedSearchParameters = signingParameters(initSearchParameters);
|
||||
|
||||
const httpResponse = await got.post(alipayEndpoint, {
|
||||
searchParams: signedSearchParameters,
|
||||
timeout: { request: defaultTimeout },
|
||||
});
|
||||
|
||||
const { body: rawBody } = httpResponse;
|
||||
|
||||
const result = userInfoResponseGuard.safeParse(parseJson(rawBody));
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error);
|
||||
}
|
||||
|
||||
const { alipay_user_info_share_response } = result.data;
|
||||
|
||||
errorHandler(alipay_user_info_share_response);
|
||||
|
||||
const { user_id: id, avatar, nick_name: name } = alipay_user_info_share_response;
|
||||
|
||||
if (!id) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse);
|
||||
}
|
||||
|
||||
return { id, avatar, name };
|
||||
};
|
||||
|
||||
const errorHandler: ErrorHandler = ({ code, msg, sub_code, sub_msg }) => {
|
||||
if (invalidAccessTokenCode.includes(code)) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, msg);
|
||||
}
|
||||
|
||||
if (sub_code) {
|
||||
assert(
|
||||
!invalidAccessTokenSubCode.includes(sub_code),
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, msg)
|
||||
);
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, {
|
||||
errorDescription: msg,
|
||||
code,
|
||||
sub_code,
|
||||
sub_msg,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const authorizationCallbackHandler = async (parameterObject: unknown) => {
|
||||
const dataGuard = z.object({ auth_code: z.string() });
|
||||
|
||||
const result = dataGuard.safeParse(parameterObject);
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, JSON.stringify(parameterObject));
|
||||
}
|
||||
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const createAlipayConnector: CreateConnector<SocialConnector> = async ({ getConfig }) => {
|
||||
return {
|
||||
metadata: defaultMetadata,
|
||||
type: ConnectorType.Social,
|
||||
configGuard: alipayConfigGuard,
|
||||
getAuthorizationUri: getAuthorizationUri(getConfig),
|
||||
getUserInfo: getUserInfo(getConfig),
|
||||
};
|
||||
};
|
||||
|
||||
export default createAlipayConnector;
|
26
packages/connectors/connector-alipay-web/src/mock.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import type { AlipayConfig } from './types.js';
|
||||
|
||||
export const mockedTimestamp = '2022-02-22 22:22:22';
|
||||
|
||||
export const mockedAlipayConfig: AlipayConfig = {
|
||||
appId: '2021000000000000',
|
||||
signType: 'RSA2',
|
||||
privateKey: '<private-key>',
|
||||
charset: 'utf8',
|
||||
};
|
||||
|
||||
export const mockedAlipayConfigWithValidPrivateKey: AlipayConfig = {
|
||||
appId: '2021000000000000',
|
||||
signType: 'RSA2',
|
||||
privateKey:
|
||||
'-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC52SvnlRfzJJDR\nA1h4MX2JWV7Yt1j+1gvtQuLh0RYbE0AgRyz8CXFcJegO8gNyUQ05vrc1RMVzvNh8\njfjLpIX8an88KE4FyoG5P8NWrwPw5ZXOnzdvNxAV8QWOU+rT4WAdCsx4++mLlb5v\nGL18R77f3WLgY23bFtcGr9q7/qOaLzNxEe4idX1eLf7Ba/gQRY0awA55/Epd1Mi7\nLqTfxTd11PoBZQPe0vnuChp3P2l1MNpIJ5G1eQ4RXgI4UMClEbGRlBN7GUlXy5p7\ng6RtvOcwmBNoE4i0/HbvaanY3u7oenST3iSzEXa2hXMjnZPvg0G4Y5mq/V6XJPTh\nJrFc9XzFAgMBAAECggEAXfmNtN10LdN4kugBLU3BL9mMF0Om8b1kbIXc2djzN5+l\nVm0HNy7DLphQXnZL/ds0N9XTKFFtEpgUU+8qNjcsNTXYvp+WzGDY9cZjTQrUkFRX\nSxLBYjBSpvWoHI8ceCVHh4f1Wtvu/VEr6Vt2PUi+IM7+d35vh1BmTJBRp6wcKBMH\nXdfjWIi5z37pTXD3OTfUjBCtzA2DX0vY6UTsmD9UI0Mb6IJdT6qugiGODFdlsduA\nWJoZlXV1VbHcvGt7DoeQgzA45sr5siUnm+ntTVBHOR/hoZQrr0DY/O/MLKYUj/+r\nZMKKpx/7VHnWfMia2EOHfjW8vUlnraUzI+5E2/FzIQKBgQDgi7S7pfRux8YONGP2\nRtHPkF8d0YllsfKedhqF3cQlJ1dhxzVqHOi1IFn6ttuuYy5UsP5apYa2kj2UUPCa\nZGGi19Vnc+RHThpR4K6/OGFrpbINAgiVJLj7F8GXzqeA7W2ZHMp1R+oB+oTxih6t\nU0dbeTP01kbBV1/7+ZUKPhLE6QKBgQDT4cMgq01F/WIGGd1GUHZQjH5bqtNiJpIf\n2Q2OTw/gn1DVnwDXpHuXPxtC3NRoaRW/dTqsF6AAkMja3voPM3sYJurGBdU8pZPC\nquc9mqqu6TR5gX3KL1lSESvMBEgfLUy/f0gI3JNw1mG17pIhnXmOB2be3HfZPcj3\nwKWlluY/fQKBgDLll97c3A3sPGll2K6vGMmqmNTCdRlW/36JmLN1NAuT4kuoguP9\nj4XWwm6A2kSp+It73vue/20MsuaWfiMQ08y8jYO4kirTekXK3vE7D2H+GeC28EkW\nHNPVa61ES1V++9Oz4fQ5i8JNDatOOmvhL5B9ZZh+pWUXsAsGZJEAxvJZAoGAMPHO\n5GYN1KQil6wz3EFMA3Fg4wYEDIFCcg7uvbfvwACtaJtxU18QmbCfOIPQoUndFzwa\nUJSohljrvPuTIh3PSpX618GTL45EIszd2/I1iXAfig3qo+DqLjX/OwKmMmWBfB8H\n4dwqRv+O1LsGkLNS2AdHsSWWnd1S5kBfQ3AnQfUCgYACM8ldXZv7uGt9uZBmxile\nB0Hg5w7F1v9VD/m9ko+INAISz8OVkD83pCEoyHwlr20JjiF+yzAakOuq6rBi+l/V\n1veSiTDUcZhciuq1G178dFYepJqisFBu7bAM+WBS4agTTtxdSLZkHeS4VX+H3DOc\ntri43NXw6QS7uQ5/+2TsEw==\n-----END PRIVATE KEY-----',
|
||||
charset: 'utf8',
|
||||
};
|
||||
|
||||
export const mockedAlipayPublicParameters = {
|
||||
format: 'JSON',
|
||||
grantType: 'authorization_code',
|
||||
timestamp: mockedTimestamp,
|
||||
version: '1.0',
|
||||
method: '<method-placeholder>',
|
||||
};
|
65
packages/connectors/connector-alipay-web/src/types.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { fallback } from '@logto/core-kit';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { alipaySigningAlgorithms, charsetEnum, fallbackCharset } from './constant.js';
|
||||
|
||||
const charsetGuard = z.enum(charsetEnum);
|
||||
|
||||
type Charset = z.infer<typeof charsetGuard>;
|
||||
|
||||
export const alipayConfigGuard = z.object({
|
||||
appId: z.string(),
|
||||
privateKey: z.string(),
|
||||
signType: z.enum(alipaySigningAlgorithms),
|
||||
charset: charsetGuard.or(fallback<Charset>(fallbackCharset)),
|
||||
});
|
||||
|
||||
export type AlipayConfig = z.infer<typeof alipayConfigGuard>;
|
||||
|
||||
// `error_response` and `alipay_system_oauth_token_response` are mutually exclusive.
|
||||
export const errorResponseGuard = z.object({
|
||||
code: z.string(),
|
||||
msg: z.string(), // To know `code` and `msg` details, see: https://opendocs.alipay.com/common/02km9f
|
||||
sub_code: z.string().optional(),
|
||||
sub_msg: z.string().optional(),
|
||||
});
|
||||
|
||||
export const alipaySystemOauthTokenResponseGuard = z.object({
|
||||
user_id: z.string(), // Unique Alipay ID, 16 digits starts with '2088'
|
||||
access_token: z.string(),
|
||||
expires_in: z.number(), // In seconds (is string type in docs which is not true)
|
||||
refresh_token: z.string(),
|
||||
re_expires_in: z.number(), // Expiration timeout of refresh token, in seconds (is string type in docs which is not true)
|
||||
});
|
||||
|
||||
export const accessTokenResponseGuard = z.object({
|
||||
sign: z.string(), // To know `sign` details, see: https://opendocs.alipay.com/common/02kf5q
|
||||
error_response: z.optional(errorResponseGuard),
|
||||
alipay_system_oauth_token_response: z.optional(alipaySystemOauthTokenResponseGuard),
|
||||
});
|
||||
|
||||
export type AccessTokenResponse = z.infer<typeof accessTokenResponseGuard>;
|
||||
|
||||
export const alipayUserInfoShareResponseGuard = z.object({
|
||||
user_id: z.string().optional(), // String of digits with max length of 16
|
||||
avatar: z.string().optional(), // URL of avatar
|
||||
province: z.string().optional(),
|
||||
city: z.string().optional(),
|
||||
nick_name: z.string().optional(),
|
||||
gender: z.string().optional(), // Enum type: 'F' for female, 'M' for male
|
||||
code: z.string(),
|
||||
msg: z.string(), // To know `code` and `msg` details, see: https://opendocs.alipay.com/common/02km9f
|
||||
sub_code: z.string().optional(),
|
||||
sub_msg: z.string().optional(),
|
||||
});
|
||||
|
||||
type AlipayUserInfoShareResponse = z.infer<typeof alipayUserInfoShareResponseGuard>;
|
||||
|
||||
export const userInfoResponseGuard = z.object({
|
||||
sign: z.string(), // To know `sign` details, see: https://opendocs.alipay.com/common/02kf5q
|
||||
alipay_user_info_share_response: alipayUserInfoShareResponseGuard,
|
||||
});
|
||||
|
||||
export type UserInfoResponse = z.infer<typeof userInfoResponseGuard>;
|
||||
|
||||
export type ErrorHandler = (response: AlipayUserInfoShareResponse) => void;
|
59
packages/connectors/connector-alipay-web/src/utils.test.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { methodForAccessToken } from './constant.js';
|
||||
import { mockedAlipayConfigWithValidPrivateKey, mockedAlipayPublicParameters } from './mock.js';
|
||||
import { signingParameters } from './utils.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const listenJSONParse = jest.spyOn(JSON, 'parse');
|
||||
const listenJSONStringify = jest.spyOn(JSON, 'stringify');
|
||||
|
||||
describe('signingParameters', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const testingParameters = {
|
||||
...mockedAlipayPublicParameters,
|
||||
...mockedAlipayConfigWithValidPrivateKey,
|
||||
method: methodForAccessToken,
|
||||
code: '7ffeb112fbb6495c9e7dfb720380DD39',
|
||||
};
|
||||
|
||||
it('should return exact signature with the given parameters (functionality check)', () => {
|
||||
const decamelizedParameters = signingParameters(testingParameters);
|
||||
expect(decamelizedParameters.sign).toBe(
|
||||
'jqVzRnwdvBEIocvKGZlZ4X3CK0pEsm8HpRWL9FtGS+P8ZRehh+Wvb3lmXWf0fhTIHmcZahQMAnLFO3OmqcwlUrs4PuRgPVmLG6mK087tkw/GP18hlstnD1hN3DS98eZZQsn8psxdHQ1qtzuik1fM0hiZvR7d/Pr72yNhIzgzWa66wBXJGYc6cmSQzB7g5hFg7L/SC55Xk205tkXkenPO9ti2TY8+bWOEZ4hAteWGftwCROz+1ne3EVrt2e/LpQQvRmDPhMIRVEShmcGTNj0ovnjN2K4Uo/YB7+hPLJkrGpYBV4hDEV91KQ9RybmE927xgIzXl7xbiHvK+BayFGNzFA=='
|
||||
);
|
||||
});
|
||||
|
||||
it('should return exact signature with the given parameters (with empty property in testingParameters)', () => {
|
||||
const decamelizedParameters = signingParameters({
|
||||
...testingParameters,
|
||||
emptyProperty: '',
|
||||
});
|
||||
expect(decamelizedParameters.sign).toBe(
|
||||
'jqVzRnwdvBEIocvKGZlZ4X3CK0pEsm8HpRWL9FtGS+P8ZRehh+Wvb3lmXWf0fhTIHmcZahQMAnLFO3OmqcwlUrs4PuRgPVmLG6mK087tkw/GP18hlstnD1hN3DS98eZZQsn8psxdHQ1qtzuik1fM0hiZvR7d/Pr72yNhIzgzWa66wBXJGYc6cmSQzB7g5hFg7L/SC55Xk205tkXkenPO9ti2TY8+bWOEZ4hAteWGftwCROz+1ne3EVrt2e/LpQQvRmDPhMIRVEShmcGTNj0ovnjN2K4Uo/YB7+hPLJkrGpYBV4hDEV91KQ9RybmE927xgIzXl7xbiHvK+BayFGNzFA=='
|
||||
);
|
||||
});
|
||||
|
||||
it('should not call JSON.parse() when biz_content is empty', () => {
|
||||
signingParameters(testingParameters);
|
||||
expect(listenJSONParse).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call JSON.parse() when biz_content is not empty', () => {
|
||||
signingParameters({
|
||||
...testingParameters,
|
||||
biz_content: JSON.stringify({ AB: 'AB' }),
|
||||
});
|
||||
expect(listenJSONParse).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call JSON.stringify() when some value is object string', () => {
|
||||
signingParameters({
|
||||
...testingParameters,
|
||||
testObject: JSON.stringify({ AB: 'AB' }),
|
||||
});
|
||||
expect(listenJSONStringify).toHaveBeenCalled();
|
||||
});
|
||||
});
|
52
packages/connectors/connector-alipay-web/src/utils.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import crypto from 'node:crypto';
|
||||
|
||||
import { parseJson } from '@logto/connector-kit';
|
||||
import iconv from 'iconv-lite';
|
||||
import snakeCaseKeys from 'snakecase-keys';
|
||||
|
||||
import { alipaySigningAlgorithmMapping } from './constant.js';
|
||||
import type { AlipayConfig } from './types.js';
|
||||
|
||||
export type SigningParameters = (
|
||||
parameters: AlipayConfig & Record<string, string | undefined>
|
||||
) => Record<string, string>;
|
||||
|
||||
// Reference: https://github.com/alipay/alipay-sdk-nodejs-all/blob/10d78e0adc7f310d5b07567ce7e4c13a3f6c768f/lib/util.ts
|
||||
export const signingParameters: SigningParameters = (
|
||||
parameters: AlipayConfig & Record<string, string | undefined>
|
||||
): Record<string, string> => {
|
||||
const { biz_content, privateKey, ...rest } = parameters;
|
||||
const signParameters = snakeCaseKeys(
|
||||
biz_content
|
||||
? {
|
||||
...rest,
|
||||
bizContent: JSON.stringify(snakeCaseKeys(parseJson(biz_content))),
|
||||
}
|
||||
: rest
|
||||
);
|
||||
|
||||
const decamelizeParameters = snakeCaseKeys(signParameters);
|
||||
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
|
||||
const sortedParametersAsString = Object.entries(decamelizeParameters)
|
||||
.map(([key, value]) => {
|
||||
// Supported Encodings can be found at https://github.com/ashtuchkin/iconv-lite/wiki/Supported-Encodings
|
||||
|
||||
if (value) {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
return `${key}=${iconv.encode(value, rest.charset)}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
.join('&');
|
||||
|
||||
const sign = crypto
|
||||
.createSign(alipaySigningAlgorithmMapping[rest.signType])
|
||||
.update(sortedParametersAsString, 'utf8')
|
||||
.sign(privateKey, 'base64');
|
||||
|
||||
return { ...decamelizeParameters, sign };
|
||||
};
|
202
packages/connectors/connector-aliyun-dm/CHANGELOG.md
Normal file
|
@ -0,0 +1,202 @@
|
|||
# Change Log
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 4ed07d2: bump connector-kit to v1.0.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 8c0654a: - Add "Generic" verification code type, remove deprecated "Continue" code type. Generic type verification code is used when user needs to send and verify verification code through our management APIs. Correspondingly, a "Generic" type mail or SMS template should be configured in the connector config.
|
||||
- Replace the term "passcode" with "verification code".
|
||||
- 269d701: The console connector configuration page has been changed from JSON format to form view.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9ff0638: update connector-kit version
|
||||
- 7b0bf69: Bump version to upgrade connector kit
|
||||
- 5fab4c2: Update the format of parameters passed into got.post to fit the change of got v12.
|
||||
- 4ec0889: bump connector-kit version
|
||||
- d8b9dea: 1. Update `@logto/connector-kit` from `1.0.0-beta.32` to `1.0.0-beta.33`.
|
||||
- d183d6d: Upgrade connector-kit
|
||||
- a5f57f8: Update README, default value and type guard of passwordless connectors' template field since we will use Generic template for all other cases rather than Sign-in, Register and ForgotPassword.
|
||||
|
||||
## 1.0.0-beta.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4ec0889: bump connector-kit version
|
||||
|
||||
## 1.0.0-beta.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a5f57f8: Update README, default value and type guard of passwordless connectors' template field since we will use Generic template for all other cases rather than Sign-in, Register and ForgotPassword.
|
||||
|
||||
## 1.0.0-beta.21
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 269d701: The console connector configuration page has been changed from JSON format to form view.
|
||||
|
||||
## 1.0.0-beta.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7b0bf69: Bump version to upgrade connector kit
|
||||
|
||||
## 1.0.0-beta.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5fab4c2: Update the format of parameters passed into got.post to fit the change of got v12.
|
||||
|
||||
## 1.0.0-beta.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9ff0638: update connector-kit version
|
||||
|
||||
## 1.0.0-beta.17
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 8c0654a: - Add "Generic" verification code type, remove deprecated "Continue" code type. Generic type verification code is used when user needs to send and verify verification code through our management APIs. Correspondingly, a "Generic" type mail or SMS template should be configured in the connector config.
|
||||
- Replace the term "passcode" with "verification code".
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d8b9dea: 1. Update `@logto/connector-kit` from `1.0.0-beta.32` to `1.0.0-beta.33`.
|
||||
|
||||
## 1.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d183d6d: Upgrade connector-kit
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.0-beta.13](https://github.com/logto-io/connectors/compare/v1.0.0-beta.12...v1.0.0-beta.13) (2022-11-29)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
## [1.0.0-beta.12](https://github.com/logto-io/connectors/compare/v1.0.0-beta.11...v1.0.0-beta.12) (2022-11-29)
|
||||
|
||||
### Features
|
||||
|
||||
- add forgot password and continue templates for sms and email ([#36](https://github.com/logto-io/connectors/issues/36)) ([4ad3551](https://github.com/logto-io/connectors/commit/4ad35516c0770ec344caf0bcc68b572c832b30a0))
|
||||
- add mock standard email connector ([#35](https://github.com/logto-io/connectors/issues/35)) ([479114e](https://github.com/logto-io/connectors/commit/479114e847fb4b11c6fbd697a36b7f5eb56305ed))
|
||||
|
||||
## [1.0.0-beta.11](https://github.com/logto-io/connectors/compare/v1.0.0-beta.10...v1.0.0-beta.11) (2022-11-06)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
## [1.0.0-beta.10](https://github.com/logto-io/connectors/compare/v1.0.0-beta.9...v1.0.0-beta.10) (2022-09-27)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
## 1.0.0-beta.9 (2022-09-18)
|
||||
|
||||
### Features
|
||||
|
||||
- add connectors ([#2](https://github.com/logto-io/connectors/issues/2)) ([2fbb578](https://github.com/logto-io/connectors/commit/2fbb57815406bace113617a6304eafcfc5db2d61))
|
||||
|
||||
## [1.0.0-beta.8](https://github.com/logto-io/logto/compare/v1.0.0-beta.6...v1.0.0-beta.8) (2022-09-01)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
## [1.0.0-beta.6](https://github.com/logto-io/logto/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-08-30)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
## [1.0.0-beta.5](https://github.com/logto-io/logto/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2022-08-19)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
## [1.0.0-beta.4](https://github.com/logto-io/logto/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-08-11)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
## [1.0.0-beta.3](https://github.com/logto-io/logto/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-08-01)
|
||||
|
||||
### Features
|
||||
|
||||
- **phrases:** tr language ([#1707](https://github.com/logto-io/logto/issues/1707)) ([411a8c2](https://github.com/logto-io/logto/commit/411a8c2fa2bfb16c4fef5f0a55c3c1dc5ead1124))
|
||||
|
||||
## [1.0.0-beta.2](https://github.com/logto-io/logto/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-07-25)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
## [1.0.0-beta.1](https://github.com/logto-io/logto/compare/v1.0.0-beta.0...v1.0.0-beta.1) (2022-07-19)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
## [1.0.0-beta.0](https://github.com/logto-io/logto/compare/v1.0.0-alpha.4...v1.0.0-beta.0) (2022-07-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **connector:** fix connector getConfig and validateConfig type ([#1530](https://github.com/logto-io/logto/issues/1530)) ([88a54aa](https://github.com/logto-io/logto/commit/88a54aaa9ebce419c149a33150a4927296cb705b))
|
||||
- **connector:** passwordless connector send test msg with unsaved config ([#1539](https://github.com/logto-io/logto/issues/1539)) ([0297f6c](https://github.com/logto-io/logto/commit/0297f6c52f7b5d730de44fbb08f88c2e9b951874))
|
||||
- **connector:** refactor ConnectorInstance as class ([#1541](https://github.com/logto-io/logto/issues/1541)) ([6b9ad58](https://github.com/logto-io/logto/commit/6b9ad580ae86fbcc100a100aab1d834090e682a3))
|
||||
|
||||
## [1.0.0-alpha.4](https://github.com/logto-io/logto/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) (2022-07-08)
|
||||
|
||||
### Features
|
||||
|
||||
- **connector:** connector error handler, throw errmsg on general errors ([#1458](https://github.com/logto-io/logto/issues/1458)) ([7da1de3](https://github.com/logto-io/logto/commit/7da1de33e97de4aeeec9f9b6cea59d1bf90ba623))
|
||||
|
||||
## [1.0.0-alpha.3](https://github.com/logto-io/logto/compare/v1.0.0-alpha.2...v1.0.0-alpha.3) (2022-07-07)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
## [1.0.0-alpha.2](https://github.com/logto-io/logto/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2022-07-07)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
## [1.0.0-alpha.1](https://github.com/logto-io/logto/compare/v1.0.0-alpha.0...v1.0.0-alpha.1) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
## [1.0.0-alpha.0](https://github.com/logto-io/logto/compare/v0.1.2-alpha.5...v1.0.0-alpha.0) (2022-07-04)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
### [0.1.2-alpha.5](https://github.com/logto-io/logto/compare/v0.1.2-alpha.4...v0.1.2-alpha.5) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
### [0.1.2-alpha.4](https://github.com/logto-io/logto/compare/v0.1.2-alpha.3...v0.1.2-alpha.4) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
### [0.1.2-alpha.3](https://github.com/logto-io/logto/compare/v0.1.2-alpha.2...v0.1.2-alpha.3) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
### [0.1.2-alpha.2](https://github.com/logto-io/logto/compare/v0.1.2-alpha.1...v0.1.2-alpha.2) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
### [0.1.2-alpha.1](https://github.com/logto-io/logto/compare/v0.1.2-alpha.0...v0.1.2-alpha.1) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-dm
|
||||
|
||||
### [0.1.1-alpha.0](https://github.com/logto-io/logto/compare/v0.1.0-internal...v0.1.1-alpha.0) (2022-07-01)
|
||||
|
||||
### Features
|
||||
|
||||
- **connector-sendgrid-email:** add sendgrid email connector ([#850](https://github.com/logto-io/logto/issues/850)) ([b887655](https://github.com/logto-io/logto/commit/b8876558275e28ca921d4eeea6c38f8559810a11))
|
||||
- **connectors:** add logo for connectors ([#914](https://github.com/logto-io/logto/issues/914)) ([a3a7c52](https://github.com/logto-io/logto/commit/a3a7c52a91dba3603617a68e5ce47e0017081a91))
|
||||
- **core,connectors:** update Aliyun logo and add logo_dark to Apple, Github ([#1194](https://github.com/logto-io/logto/issues/1194)) ([98f8083](https://github.com/logto-io/logto/commit/98f808320b1c79c51f8bd6f49e35ca44363ea560))
|
||||
- **core:** serve connector logo ([#931](https://github.com/logto-io/logto/issues/931)) ([5b44b71](https://github.com/logto-io/logto/commit/5b44b7194ed4f98c6c2e77aae828a39b477b6010))
|
||||
- **core:** update connector db schema ([#732](https://github.com/logto-io/logto/issues/732)) ([8e1533a](https://github.com/logto-io/logto/commit/8e1533a70267d459feea4e5174296b17bef84d48))
|
||||
- **core:** wrap aliyun direct mail connector ([#660](https://github.com/logto-io/logto/issues/660)) ([54b6209](https://github.com/logto-io/logto/commit/54b62094c8d8af0611cf64e39306c4f1a216e8f6))
|
||||
- **core:** wrap aliyun short message service connector ([#670](https://github.com/logto-io/logto/issues/670)) ([a06d3ee](https://github.com/logto-io/logto/commit/a06d3ee73ccc59f6aaef1dab4f45d6c118aab40d))
|
||||
- remove target, platform from connector schema and add id to metadata ([#930](https://github.com/logto-io/logto/issues/930)) ([054b0f7](https://github.com/logto-io/logto/commit/054b0f7b6a6dfed66540042ea69b0721126fe695))
|
||||
- **sms/email-connectors:** expose third-party API request error message ([#1059](https://github.com/logto-io/logto/issues/1059)) ([4cfd578](https://github.com/logto-io/logto/commit/4cfd5788d24d55017a8ace53fed99082f87691cb))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- `lint:report` script ([#730](https://github.com/logto-io/logto/issues/730)) ([3b17324](https://github.com/logto-io/logto/commit/3b17324d189b2fe47985d0bee8b37b4ef1dbdd2b))
|
148
packages/connectors/connector-aliyun-dm/README.md
Normal file
|
@ -0,0 +1,148 @@
|
|||
# Aliyun direct mail connector
|
||||
|
||||
The official Logto connector for Aliyun connector for direct mail service.
|
||||
|
||||
阿里云邮件推送服务 Logto 官方连接器 [中文文档](#阿里云邮件连接器)
|
||||
|
||||
**Table of contents**
|
||||
|
||||
- [Aliyun direct mail connector](#aliyun-direct-mail-connector)
|
||||
- [Get started](#get-started)
|
||||
- [Set up an email service in Aliyun DirectMail Console](#set-up-an-email-service-in-aliyun-directmail-console)
|
||||
- [Create an Aliyun account](#create-an-aliyun-account)
|
||||
- [Enable and configure Aliyun Direct Mail](#enable-and-configure-aliyun-direct-mail)
|
||||
- [Compose the connector JSON](#compose-the-connector-json)
|
||||
- [Test Aliyun DM connector](#test-aliyun-dm-connector)
|
||||
- [Config types](#config-types)
|
||||
- [阿里云邮件连接器](#阿里云邮件连接器)
|
||||
- [开始上手](#开始上手)
|
||||
- [在阿里云邮件服务控制台中配置一个邮件服务](#在阿里云邮件服务控制台中配置一个邮件服务)
|
||||
- [注册阿里云帐号](#注册阿里云帐号)
|
||||
- [启用并配置阿里云邮件服务](#启用并配置阿里云邮件服务)
|
||||
- [编写连接器的 JSON](#编写连接器的-json)
|
||||
- [测试阿里云邮件连接器](#测试阿里云邮件连接器)
|
||||
- [配置类型](#配置类型)
|
||||
|
||||
## Get started
|
||||
|
||||
Aliyun is a primary cloud service provider in Asia, offering many cloud services, including DM (direct mail). Aliyun DM Connector is a plugin provided by the Logto team to call the Aliyun DM service APIs, with the help of which Logto end-users can register and sign in to their Logto account via mail verification code (or in other words, verification code).
|
||||
|
||||
## Set up an email service in Aliyun DirectMail Console
|
||||
|
||||
> 💡 **Tip**
|
||||
>
|
||||
> You can skip some sections if you have already finished.
|
||||
|
||||
### Create an Aliyun account
|
||||
|
||||
Head to [Aliyun](https://aliyun.com/) and create your Aliyun account if you don't have one.
|
||||
|
||||
### Enable and configure Aliyun Direct Mail
|
||||
|
||||
Go to the [DM service console page](https://www.aliyun.com/product/directmail) and sign in. Enable the Direct Mail service by clicking the "Apply to enable" (申请开通) button on the top left of the page and begin the configuration process.
|
||||
|
||||
Starting from the [DM admin console page](https://dm.console.aliyun.com/), you should:
|
||||
1. Go to "Email Domains" (发信域名) from the sidebar and add "New Domain" (新建域名) following the instructions.
|
||||
2. Customize "Sender Addresses" (发信地址) and "Email Tags" (邮件标签) respectively.
|
||||
|
||||
After finishing setup, there are two different ways to test:
|
||||
- Go to the [DirectMail Overview page](https://dm.console.aliyun.com/), find "Operation Guide" (操作引导) at the bottom of the page, and click on "Send Emails" (发送邮件). You will find all the different kinds of testing methods.
|
||||
- Follow the path "Send Emails" (发送邮件) -> "Email Tasks" (发送邮件) in the sidebar to create a testing task.
|
||||
|
||||
## Compose the connector JSON
|
||||
|
||||
1. From the [DM admin console page](https://dm.console.aliyun.com/), hover on your avatar in the top right corner and go to "AccessKey Management" (AccessKey 管理), and click "Create AccessKey" (创建 AccessKey). You will get an "AccessKey ID" and "AccessKey Secret" pair after finishing security verification. Please keep them properly.
|
||||
2. Go to the "Sender Addresses" (发信地址) or "Email Tags" (邮件标签) tab you just visited from the [DM admin console page](https://dm.console.aliyun.com/), you can find _Sender Address_ or _Email Tag_ easily.
|
||||
3. Fill out the Aliyun DM Connector settings:
|
||||
- Fill out the `accessKeyId` and `accessKeySecret` fields with access key pairs you've got from step 1.
|
||||
- Fill out the `accountName` and `fromAlias` field with "Sender Address" and "Email Tag" which were found in step 2. All templates will share this signature name. (You can leave `fromAlias` blank as it is OPTIONAL.)
|
||||
- You can add multiple DM connector templates for different cases. Here is an example of adding a single template:
|
||||
- Fill out the `subject` field, which will work as title of the sending email.
|
||||
- Fill out the `content` field with arbitrary string-type contents. Do not forget to leave `{{code}}` placeholder for random verification code.
|
||||
- Fill out `usageType` field with either `Register`, `SignIn`, `ForgotPassword` or `Generic` for different use cases. (`usageType` is a Logto property to identify the proper use case.) In order to enable full user flows, templates with usageType `Register`, `SignIn`, `ForgotPassword` and `Generic` are required.
|
||||
|
||||
### Test Aliyun DM connector
|
||||
|
||||
You can type in an email address and click on "Send" to see whether the settings can work before "Save and Done".
|
||||
|
||||
That's it. Don't forget to [Enable connector in sign-in experience](https://docs.logto.io/docs/tutorials/get-started/enable-passcode-sign-in/#enable-connector-in-sign-in-experience).
|
||||
|
||||
### Config types
|
||||
|
||||
| Name | Type |
|
||||
|-----------------|-------------------|
|
||||
| accessKeyId | string |
|
||||
| accessKeySecret | string |
|
||||
| accountName | string |
|
||||
| fromAlias | string (OPTIONAL) |
|
||||
| templates | Template[] |
|
||||
|
||||
| Template Properties | Type | Enum values |
|
||||
|---------------------|-------------|------------------------------------------------------|
|
||||
| subject | string | N/A |
|
||||
| content | string | N/A |
|
||||
| usageType | enum string | 'Register' \| 'SignIn' \| 'ForgotPassword' \| 'Generic' |
|
||||
|
||||
# 阿里云邮件连接器
|
||||
|
||||
## 开始上手
|
||||
|
||||
阿里云是亚洲地区一个重要的云服务厂商,提供了包括邮件服务在内的诸多云服务。
|
||||
|
||||
本连接器是 Logto 官方提供的阿里云邮件连接器,帮助终端用户通过邮件验证码进行登录注册。
|
||||
|
||||
## 在阿里云邮件服务控制台中配置一个邮件服务
|
||||
|
||||
> 💡 **Tip**
|
||||
>
|
||||
> 你可以跳过已经完成的部分。
|
||||
|
||||
### 注册阿里云帐号
|
||||
|
||||
前往 [阿里云](https://aliyun.com/) 并完成帐号的注册。
|
||||
|
||||
### 启用并配置阿里云邮件服务
|
||||
|
||||
来到 [阿里云邮件服务](https://www.aliyun.com/product/directmail) 然后登录。点按页面左上的「申请开通」按钮以开通邮件服务并开始配置流程。
|
||||
|
||||
从 [邮件服务管理控制台](https://dm.console.aliyun.com/) 开始:
|
||||
1. 从侧边栏进入到「发信域名」,点按「新建域名」并完成指引。
|
||||
2. 依次配置好「发信地址」和「邮件标签」。
|
||||
|
||||
在完成了设置之后,这里提供了两种测试的方法:
|
||||
- 前往 [邮件服务管理控制台概览](https://dm.console.aliyun.com/),在该页面底部找到「操作引导」框并点按「发送邮件」。你可以找到很多不同的测试方法。
|
||||
- 在侧边栏中选择「发送邮件」->「发送邮件」,在这里你可以「新建发送任务」来测试。
|
||||
|
||||
## 编写连接器的 JSON
|
||||
|
||||
1. 在 [邮件服务管理控制台](https://dm.console.aliyun.com/),鼠标停在右上角你的头像上,进入「AccessKey 管理」,点按「创建 AccessKey」。完成了安全验证之后,你会得到一对「AccessKey ID」和「AccessKey Secret」,请妥善保管他们。
|
||||
2. 从 [邮件服务管理控制台](https://dm.console.aliyun.com/) 的侧边栏,分别进入「发信地址」和「邮件标签」。这里你可以找到之前创建的 _发信地址_ 和 _邮件标签_。
|
||||
3. 完成阿里云邮件服务连接器的设置:
|
||||
- 用你在步骤 1 中拿到的一对「AccessKey ID」和「AccessKey Secret」来分别填入 `accessKeyId` 和 `accessKeySecret`。
|
||||
- 用步骤 2 中的 _发信地址_ 和 _邮件标签_ 填写 `accountName` 和 `fromAlias`。(`fromAlias` 可以不填写,它是 **可选的**。)
|
||||
- 你可以添加多个邮件服务模板以应对不同的用户场景。这里展示填写单个模板的例子:
|
||||
- 在 `subject` 栏填写发送邮件的 _标题_。
|
||||
- 在 `content` 栏中填写字符形式的内容。不要忘了在内容中插入 `{{code}}` 占位符,在真实发送时他会被替换成随机生成的验证码。
|
||||
- `usageType` 栏填写 `Register`,`SignIn`,`ForgotPassword` 或者`Generic` 其中之一以分别对应 _注册_,_登录_,_忘记密码_ 和 _通用_ 的不同场景。(`usageType` 是 Logto 的属性,用来确定使用场景。)为了能够使用完成的流程,需要配置 `usageType` 为 `Register`,`SignIn`, `ForgotPassword` 以及 `Generic` 的模板。
|
||||
|
||||
### 测试阿里云邮件连接器
|
||||
|
||||
你可以在「保存并完成」之前输入一个邮件地址并点按「发送」来测试配置是否可以正常工作。
|
||||
|
||||
大功告成!快去 [启用短信或邮件验证码登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-passcode-sign-in/#%E5%9C%A8%E7%99%BB%E5%BD%95%E4%BD%93%E9%AA%8C%E4%B8%AD%E5%90%AF%E7%94%A8%E8%BF%9E%E6%8E%A5%E5%99%A8) 吧。
|
||||
|
||||
### 配置类型
|
||||
|
||||
| 名称 | 类型 |
|
||||
|-----------------|-------------------|
|
||||
| accessKeyId | string |
|
||||
| accessKeySecret | string |
|
||||
| accountName | string |
|
||||
| fromAlias | string (OPTIONAL) |
|
||||
| templates | Template[] |
|
||||
|
||||
| 模板属性 | 类型 | 枚举值 |
|
||||
|-----------|-------------|------------------------------------------------------|
|
||||
| subject | string | N/A |
|
||||
| content | string | N/A |
|
||||
| usageType | enum string | 'Register' \| 'SignIn' \| 'ForgotPassword' \| 'Generic' |
|
3
packages/connectors/connector-aliyun-dm/logo.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.2633 4.58495C2.16941 4.80996 0.994516 5.68373 0.506258 6.6415C0.0180004 7.59927 0 7.83478 0 11.8665C0 14.0179 0.0360005 15.7898 0.0900013 16.0666C0.411006 17.7076 1.69428 18.9909 3.3353 19.3119C3.83256 19.4019 10.3048 19.4477 10.3048 19.3479C10.3048 19.2755 9.75353 17.1061 9.72615 17.0791C9.71715 17.0611 8.56888 16.808 7.17686 16.5091C3.93156 15.8041 4.02681 15.8311 3.75981 15.5056L3.52468 15.2254V8.66279L3.75981 8.37779C4.03094 8.05228 3.93156 8.07966 7.17686 7.37426L9.72615 6.80425C9.7539 6.7855 10.3052 4.6157 10.3052 4.54745C10.3052 4.4612 3.69756 4.50245 3.2633 4.59245V4.58495ZM13.7402 4.59845C13.7492 4.76121 14.2555 6.74988 14.3102 6.80425C14.3282 6.83125 15.5665 7.11176 17.0493 7.42789C18.532 7.74402 19.8517 8.06541 19.9691 8.13291C20.1281 8.22516 20.2616 8.35566 20.3579 8.51242C20.5027 8.76555 20.5207 9.03668 20.5207 11.9475C20.5207 14.8583 20.5027 15.1294 20.3579 15.3826C20.2594 15.5374 20.1263 15.6673 19.9691 15.7621C19.8517 15.8258 18.544 16.1416 17.0493 16.4671L14.3102 17.0952C14.2558 17.1496 13.7496 19.1383 13.7402 19.301C13.7402 19.4004 14.3912 19.4184 17.2476 19.391C21.1852 19.355 21.234 19.346 22.2375 18.668C22.9582 18.1719 23.5027 17.4597 23.7922 16.634C24 16.0283 24 15.974 24 11.9512C24 7.92853 24 7.87415 23.7922 7.26851C23.5026 6.44288 22.9582 5.73061 22.2375 5.23447C21.234 4.5572 21.189 4.54745 17.2476 4.51183C14.3912 4.48483 13.7402 4.49833 13.7402 4.60183V4.59845ZM9.70515 11.7619C9.67618 11.8585 9.67002 11.9606 9.68715 12.06C9.71415 12.21 9.8769 12.2227 12.0009 12.2497L14.2791 12.2677V11.6351H12.0227C10.1334 11.6351 9.74453 11.6531 9.7089 11.7615L9.70515 11.7619Z" fill="#FF6A00"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package",
|
||||
"name": "@logto/connector-aliyun-dm",
|
||||
"version": "1.0.0",
|
||||
"description": "Aliyun DM connector implementation."
|
||||
}
|
94
packages/connectors/connector-aliyun-dm/src/constant.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
import type { ConnectorMetadata } from '@logto/connector-kit';
|
||||
import { ConnectorConfigFormItemType } from '@logto/connector-kit';
|
||||
|
||||
export const endpoint = 'https://dm.aliyuncs.com/';
|
||||
|
||||
export const staticConfigs = {
|
||||
Format: 'json',
|
||||
SignatureMethod: 'HMAC-SHA1',
|
||||
SignatureVersion: '1.0',
|
||||
Version: '2015-11-23',
|
||||
};
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'aliyun-direct-mail',
|
||||
target: 'aliyun-dm',
|
||||
platform: null,
|
||||
name: {
|
||||
en: 'Aliyun Direct Mail',
|
||||
'zh-CN': '阿里云邮件推送',
|
||||
'tr-TR': 'Aliyun Direct Mail',
|
||||
ko: 'Aliyun 다이렉트 메일',
|
||||
},
|
||||
logo: './logo.svg',
|
||||
logoDark: null,
|
||||
description: {
|
||||
en: 'Aliyun provides cloud computing services to online businesses.',
|
||||
'zh-CN': '阿里云是全球性的云服务提供商。',
|
||||
'tr-TR': 'Aliyun, çevrimiçi işletmelere bulut bilişim hizmetleri sunmaktadır.',
|
||||
ko: 'Aliyun는 온라인 비지니스를 위해 클라우딩 컴퓨팅 서비스를 제공합니다.',
|
||||
},
|
||||
readme: './README.md',
|
||||
formItems: [
|
||||
{
|
||||
key: 'accessKeyId',
|
||||
label: 'Access Key ID',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: true,
|
||||
placeholder: '<access-key-id>',
|
||||
},
|
||||
{
|
||||
key: 'accessKeySecret',
|
||||
label: 'Access Key Secret',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: true,
|
||||
placeholder: '<access-key-secret>',
|
||||
},
|
||||
{
|
||||
key: 'accountName',
|
||||
label: 'Account Name',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: true,
|
||||
placeholder: '<account-name>',
|
||||
},
|
||||
{
|
||||
key: 'fromAlias',
|
||||
label: 'From Alias',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: false,
|
||||
placeholder: '<from-alias>',
|
||||
},
|
||||
{
|
||||
key: 'templates',
|
||||
label: 'Templates',
|
||||
type: ConnectorConfigFormItemType.Json,
|
||||
required: true,
|
||||
defaultValue: [
|
||||
{
|
||||
usageType: 'SignIn',
|
||||
subject: '<sign-in-template-subject>',
|
||||
content:
|
||||
'Your Logto sign-in verification code is {{code}}. The code will remain active for 10 minutes.',
|
||||
},
|
||||
{
|
||||
usageType: 'Register',
|
||||
subject: '<register-template-subject>',
|
||||
content:
|
||||
'Your Logto sign-up verification code is {{code}}. The code will remain active for 10 minutes.',
|
||||
},
|
||||
{
|
||||
usageType: 'ForgotPassword',
|
||||
subject: '<forgot-password-template-subject>',
|
||||
content:
|
||||
'Your Logto password change verification code is {{code}}. The code will remain active for 10 minutes.',
|
||||
},
|
||||
{
|
||||
usageType: 'Generic',
|
||||
subject: '<generic-template-subject>',
|
||||
content:
|
||||
'Your Logto verification code is {{code}}. The code will remain active for 10 minutes.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
50
packages/connectors/connector-aliyun-dm/src/index.test.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { VerificationCodeType } from '@logto/connector-kit';
|
||||
|
||||
import { mockedConfigWithAllRequiredTemplates } from './mock.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const getConfig = jest.fn().mockResolvedValue(mockedConfigWithAllRequiredTemplates);
|
||||
|
||||
const singleSendMail = jest.fn(() => ({
|
||||
body: JSON.stringify({ EnvId: 'env-id', RequestId: 'request-id' }),
|
||||
statusCode: 200,
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('./single-send-mail.js', () => ({
|
||||
singleSendMail,
|
||||
}));
|
||||
|
||||
const { default: createConnector } = await import('./index.js');
|
||||
|
||||
describe('sendMessage()', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call singleSendMail() and replace code in content', async () => {
|
||||
const connector = await createConnector({ getConfig });
|
||||
await connector.sendMessage({
|
||||
to: 'to@email.com',
|
||||
type: VerificationCodeType.SignIn,
|
||||
payload: { code: '1234' },
|
||||
});
|
||||
expect(singleSendMail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
HtmlBody: 'Your code is 1234, 1234 is your code',
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if template is missing', async () => {
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(
|
||||
connector.sendMessage({
|
||||
to: 'to@email.com',
|
||||
type: VerificationCodeType.Test,
|
||||
payload: { code: '1234' },
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
107
packages/connectors/connector-aliyun-dm/src/index.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
import type {
|
||||
CreateConnector,
|
||||
EmailConnector,
|
||||
GetConnectorConfig,
|
||||
SendMessageFunction,
|
||||
} from '@logto/connector-kit';
|
||||
import {
|
||||
ConnectorError,
|
||||
ConnectorErrorCodes,
|
||||
ConnectorType,
|
||||
validateConfig,
|
||||
parseJson,
|
||||
} from '@logto/connector-kit';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import { HTTPError } from 'got';
|
||||
|
||||
import { defaultMetadata } from './constant.js';
|
||||
import { singleSendMail } from './single-send-mail.js';
|
||||
import type { AliyunDmConfig } from './types.js';
|
||||
import {
|
||||
aliyunDmConfigGuard,
|
||||
sendEmailResponseGuard,
|
||||
sendMailErrorResponseGuard,
|
||||
} from './types.js';
|
||||
|
||||
const sendMessage =
|
||||
(getConfig: GetConnectorConfig): SendMessageFunction =>
|
||||
async (data, inputConfig) => {
|
||||
const { to, type, payload } = data;
|
||||
const config = inputConfig ?? (await getConfig(defaultMetadata.id));
|
||||
validateConfig<AliyunDmConfig>(config, aliyunDmConfigGuard);
|
||||
const { accessKeyId, accessKeySecret, accountName, fromAlias, templates } = config;
|
||||
const template = templates.find((template) => template.usageType === type);
|
||||
|
||||
assert(
|
||||
template,
|
||||
new ConnectorError(
|
||||
ConnectorErrorCodes.TemplateNotFound,
|
||||
`Cannot find template for type: ${type}`
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
const httpResponse = await singleSendMail(
|
||||
{
|
||||
AccessKeyId: accessKeyId,
|
||||
AccountName: accountName,
|
||||
ReplyToAddress: 'false',
|
||||
AddressType: '1',
|
||||
ToAddress: to,
|
||||
FromAlias: fromAlias,
|
||||
Subject: template.subject,
|
||||
HtmlBody:
|
||||
typeof payload.code === 'string'
|
||||
? template.content.replace(/{{code}}/g, payload.code)
|
||||
: template.content,
|
||||
},
|
||||
accessKeySecret
|
||||
);
|
||||
|
||||
const result = sendEmailResponseGuard.safeParse(parseJson(httpResponse.body));
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof HTTPError) {
|
||||
const {
|
||||
response: { body: rawBody },
|
||||
} = error;
|
||||
|
||||
assert(
|
||||
typeof rawBody === 'string',
|
||||
new ConnectorError(ConnectorErrorCodes.InvalidResponse)
|
||||
);
|
||||
|
||||
errorHandler(rawBody);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const errorHandler = (errorResponseBody: string) => {
|
||||
const result = sendMailErrorResponseGuard.safeParse(parseJson(errorResponseBody));
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error);
|
||||
}
|
||||
|
||||
const { Message: errorDescription, ...rest } = result.data;
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, { errorDescription, ...rest });
|
||||
};
|
||||
|
||||
const createAliyunDmConnector: CreateConnector<EmailConnector> = async ({ getConfig }) => {
|
||||
return {
|
||||
metadata: defaultMetadata,
|
||||
type: ConnectorType.Email,
|
||||
configGuard: aliyunDmConfigGuard,
|
||||
sendMessage: sendMessage(getConfig),
|
||||
};
|
||||
};
|
||||
|
||||
export default createAliyunDmConnector;
|
57
packages/connectors/connector-aliyun-dm/src/mock.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
export const mockedParameters = {
|
||||
AccessKeyId: 'testid',
|
||||
AccountName: "<a%b'>",
|
||||
Action: 'SingleSendMail',
|
||||
AddressType: '1',
|
||||
Format: 'XML',
|
||||
HtmlBody: '4',
|
||||
RegionId: 'cn-hangzhou',
|
||||
ReplyToAddress: 'true',
|
||||
SignatureMethod: 'HMAC-SHA1',
|
||||
SignatureVersion: '1.0',
|
||||
Subject: '3',
|
||||
TagName: '2',
|
||||
ToAddress: '1@test.com',
|
||||
Version: '2015-11-23',
|
||||
};
|
||||
|
||||
export const mockedConfig = {
|
||||
accessKeyId: 'accessKeyId',
|
||||
accessKeySecret: 'accessKeySecret',
|
||||
accountName: 'accountName',
|
||||
templates: [
|
||||
{
|
||||
usageType: 'SignIn',
|
||||
content: 'Your code is {{code}}, {{code}} is your code',
|
||||
subject: 'subject',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const mockedConfigWithAllRequiredTemplates = {
|
||||
accessKeyId: 'accessKeyId',
|
||||
accessKeySecret: 'accessKeySecret',
|
||||
accountName: 'accountName',
|
||||
templates: [
|
||||
{
|
||||
usageType: 'SignIn',
|
||||
content: 'Your code is {{code}}, {{code}} is your code',
|
||||
subject: 'subject',
|
||||
},
|
||||
{
|
||||
usageType: 'Register',
|
||||
content: 'Your code is {{code}}, {{code}} is your code',
|
||||
subject: 'subject',
|
||||
},
|
||||
{
|
||||
usageType: 'ForgotPassword',
|
||||
content: 'Your code is {{code}}, {{code}} is your code',
|
||||
subject: 'subject',
|
||||
},
|
||||
{
|
||||
usageType: 'Generic',
|
||||
content: 'Your code is {{code}}, {{code}} is your code',
|
||||
subject: 'subject',
|
||||
},
|
||||
],
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
const { jest } = import.meta;
|
||||
|
||||
const request = jest.fn();
|
||||
|
||||
jest.unstable_mockModule('./utils.js', () => ({
|
||||
request,
|
||||
}));
|
||||
|
||||
const { singleSendMail } = await import('./single-send-mail.js');
|
||||
|
||||
describe('singleSendMail', () => {
|
||||
it('should call request with action SingleSendMail', async () => {
|
||||
await singleSendMail(
|
||||
{
|
||||
AccessKeyId: '<access-key-id>',
|
||||
AccountName: 'noreply@example.com',
|
||||
AddressType: '1',
|
||||
FromAlias: 'CompanyName',
|
||||
HtmlBody: 'test from logto',
|
||||
ReplyToAddress: 'false',
|
||||
Subject: 'test',
|
||||
ToAddress: 'user@example.com',
|
||||
},
|
||||
'<access-key-secret>'
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const calledData = request.mock.calls[0];
|
||||
expect(calledData).not.toBeUndefined();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const payload = calledData?.[1];
|
||||
expect(payload).toHaveProperty('Action', 'SingleSendMail');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
import { endpoint, staticConfigs } from './constant.js';
|
||||
import type { PublicParameters, SingleSendMail } from './types.js';
|
||||
import { request } from './utils.js';
|
||||
|
||||
/**
|
||||
* @doc https://help.aliyun.com/document_detail/29444.html
|
||||
*/
|
||||
export const singleSendMail = async (
|
||||
parameters: PublicParameters & SingleSendMail,
|
||||
accessKeySecret: string
|
||||
) => {
|
||||
return request(
|
||||
endpoint,
|
||||
{ Action: 'SingleSendMail', ...staticConfigs, ...parameters },
|
||||
accessKeySecret
|
||||
);
|
||||
};
|
14
packages/connectors/connector-aliyun-dm/src/types.test.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { mockedConfig, mockedConfigWithAllRequiredTemplates } from './mock.js';
|
||||
import { aliyunDmConfigGuard } from './types.js';
|
||||
|
||||
describe('aliyunDmConfigGuard', () => {
|
||||
it('throws when required templates not provided', () => {
|
||||
const result = aliyunDmConfigGuard.safeParse(mockedConfig);
|
||||
expect(result.success).toEqual(false);
|
||||
});
|
||||
|
||||
it('passes when all required templated are presented', () => {
|
||||
const result = aliyunDmConfigGuard.safeParse(mockedConfigWithAllRequiredTemplates);
|
||||
expect(result.success).toEqual(true);
|
||||
});
|
||||
});
|
83
packages/connectors/connector-aliyun-dm/src/types.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const sendEmailResponseGuard = z.object({
|
||||
EnvId: z.string(),
|
||||
RequestId: z.string(),
|
||||
});
|
||||
|
||||
export type SendEmailResponse = z.infer<typeof sendEmailResponseGuard>;
|
||||
|
||||
/**
|
||||
* UsageType here is used to specify the use case of the template, can be either
|
||||
* 'Register', 'SignIn', 'ForgotPassword' or 'Generic'.
|
||||
*/
|
||||
const requiredTemplateUsageTypes = ['Register', 'SignIn', 'ForgotPassword', 'Generic'];
|
||||
|
||||
const templateGuard = z.object({
|
||||
usageType: z.string(),
|
||||
subject: z.string(),
|
||||
content: z.string(), // With variable {{code}}, support HTML
|
||||
});
|
||||
|
||||
export const aliyunDmConfigGuard = z.object({
|
||||
accessKeyId: z.string(),
|
||||
accessKeySecret: z.string(),
|
||||
accountName: z.string(),
|
||||
fromAlias: z.string().optional(),
|
||||
templates: z.array(templateGuard).refine(
|
||||
(templates) =>
|
||||
requiredTemplateUsageTypes.every((requiredType) =>
|
||||
templates.map((template) => template.usageType).includes(requiredType)
|
||||
),
|
||||
(templates) => ({
|
||||
message: `Template with UsageType (${requiredTemplateUsageTypes
|
||||
.filter(
|
||||
(requiredType) => !templates.map((template) => template.usageType).includes(requiredType)
|
||||
)
|
||||
.join(', ')}) should be provided!`,
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
export type AliyunDmConfig = z.infer<typeof aliyunDmConfigGuard>;
|
||||
|
||||
/**
|
||||
* @doc https://help.aliyun.com/document_detail/29444.html
|
||||
*/
|
||||
export type SingleSendMail = {
|
||||
AccountName: string;
|
||||
AddressType: '0' | '1';
|
||||
ClickTrace?: '0' | '1';
|
||||
FromAlias?: string;
|
||||
HtmlBody?: string;
|
||||
ReplyToAddress: 'true' | 'false';
|
||||
Subject: string;
|
||||
TagName?: string;
|
||||
TextBody?: string;
|
||||
ToAddress: string;
|
||||
};
|
||||
|
||||
export type PublicParameters = {
|
||||
AccessKeyId: string;
|
||||
Format?: string; // 'json' or 'xml', default: 'json'
|
||||
RegionId?: string; // 'cn-hangzhou' | 'ap-southeast-1' | 'ap-southeast-2'
|
||||
Signature?: string;
|
||||
SignatureMethod?: string;
|
||||
SignatureNonce?: string;
|
||||
SignatureVersion?: string;
|
||||
Timestamp?: string;
|
||||
Version?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @doc https://next.api.aliyun.com/troubleshoot
|
||||
*/
|
||||
export const sendMailErrorResponseGuard = z.object({
|
||||
Code: z.string(),
|
||||
Message: z.string(),
|
||||
RequestId: z.string().optional(),
|
||||
HostId: z.string().optional(),
|
||||
Recommend: z.string().optional(),
|
||||
});
|
||||
|
||||
export type SendMailErrorResponse = z.infer<typeof sendMailErrorResponseGuard>;
|
38
packages/connectors/connector-aliyun-dm/src/utils.test.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { mockedParameters } from './mock.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const post = jest.fn();
|
||||
|
||||
jest.unstable_mockModule('got', () => ({
|
||||
got: { post },
|
||||
}));
|
||||
|
||||
const { getSignature, request } = await import('./utils.js');
|
||||
|
||||
describe('getSignature', () => {
|
||||
it('should get valid signature', () => {
|
||||
const parameters = {
|
||||
...mockedParameters,
|
||||
SignatureNonce: 'c1b2c332-4cfb-4a0f-b8cc-ebe622aa0a5c',
|
||||
Timestamp: '2016-10-20T06:27:56Z',
|
||||
};
|
||||
const signature = getSignature(parameters, 'testsecret', 'POST');
|
||||
expect(signature).toEqual('llJfXJjBW3OacrVgxxsITgYaYm0=');
|
||||
});
|
||||
});
|
||||
|
||||
describe('request', () => {
|
||||
it('should call got.post with extended params', async () => {
|
||||
const parameters = mockedParameters;
|
||||
await request('http://test.endpoint.com', parameters, 'testsecret');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const calledData = post.mock.calls[0];
|
||||
expect(calledData).not.toBeUndefined();
|
||||
const payload = calledData?.[0].form as Record<string, unknown>;
|
||||
expect(payload.AccessKeyId).toEqual('testid');
|
||||
expect(payload.Timestamp).not.toBeNull();
|
||||
expect(payload.SignatureNonce).not.toBeNull();
|
||||
expect(payload.Signature).not.toBeNull();
|
||||
});
|
||||
});
|
59
packages/connectors/connector-aliyun-dm/src/utils.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { createHmac } from 'crypto';
|
||||
|
||||
import { got } from 'got';
|
||||
|
||||
import type { PublicParameters } from './types.js';
|
||||
|
||||
// Aliyun has special escape rules.
|
||||
// https://help.aliyun.com/document_detail/29442.html
|
||||
const escaper = (string_: string) =>
|
||||
encodeURIComponent(string_)
|
||||
.replace(/\*/g, '%2A')
|
||||
.replace(/'/g, '%27')
|
||||
.replace(/!/g, '%21')
|
||||
.replace(/"/g, '%22')
|
||||
.replace(/\(/g, '%28')
|
||||
.replace(/\)/g, '%29')
|
||||
.replace(/\+/g, '%2B');
|
||||
|
||||
export const getSignature = (
|
||||
parameters: Record<string, string>,
|
||||
secret: string,
|
||||
method: string
|
||||
) => {
|
||||
const canonicalizedQuery = Object.keys(parameters)
|
||||
.map((key) => {
|
||||
const value = parameters[key];
|
||||
|
||||
return value === undefined ? '' : `${escaper(key)}=${escaper(value)}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.slice()
|
||||
.sort()
|
||||
.join('&');
|
||||
|
||||
const stringToSign = `${method.toUpperCase()}&${escaper('/')}&${escaper(canonicalizedQuery)}`;
|
||||
|
||||
return createHmac('sha1', `${secret}&`).update(stringToSign).digest('base64');
|
||||
};
|
||||
|
||||
export const request = async (
|
||||
url: string,
|
||||
parameters: PublicParameters & Record<string, string>,
|
||||
accessKeySecret: string
|
||||
) => {
|
||||
const finalParameters: Record<string, string> = {
|
||||
...parameters,
|
||||
SignatureNonce: String(Math.random()),
|
||||
Timestamp: new Date().toISOString(),
|
||||
};
|
||||
const signature = getSignature(finalParameters, accessKeySecret, 'POST');
|
||||
|
||||
return got.post({
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
form: { ...finalParameters, Signature: signature },
|
||||
});
|
||||
};
|
213
packages/connectors/connector-aliyun-sms/CHANGELOG.md
Normal file
|
@ -0,0 +1,213 @@
|
|||
# Change Log
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 4ed07d2: bump connector-kit to v1.0.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 8c0654a: - Add "Generic" verification code type, remove deprecated "Continue" code type. Generic type verification code is used when user needs to send and verify verification code through our management APIs. Correspondingly, a "Generic" type mail or SMS template should be configured in the connector config.
|
||||
- Replace the term "passcode" with "verification code".
|
||||
- e6dd3d3: Support separating global and China template
|
||||
- 269d701: The console connector configuration page has been changed from JSON format to form view.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9ff0638: update connector-kit version
|
||||
- 7b0bf69: Bump version to upgrade connector kit
|
||||
- 5fab4c2: Update the format of parameters passed into got.post to fit the change of got v12.
|
||||
- 4ec0889: bump connector-kit version
|
||||
- d8b9dea: 1. Update `@logto/connector-kit` from `1.0.0-beta.32` to `1.0.0-beta.33`.
|
||||
- d183d6d: Upgrade connector-kit
|
||||
- a5f57f8: Update README, default value and type guard of passwordless connectors' template field since we will use Generic template for all other cases rather than Sign-in, Register and ForgotPassword.
|
||||
|
||||
## 1.0.0-beta.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4ec0889: bump connector-kit version
|
||||
|
||||
## 1.0.0-beta.22
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- e6dd3d3: Support separating global and China template
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a5f57f8: Update README, default value and type guard of passwordless connectors' template field since we will use Generic template for all other cases rather than Sign-in, Register and ForgotPassword.
|
||||
|
||||
## 1.0.0-beta.21
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 269d701: The console connector configuration page has been changed from JSON format to form view.
|
||||
|
||||
## 1.0.0-beta.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7b0bf69: Bump version to upgrade connector kit
|
||||
|
||||
## 1.0.0-beta.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5fab4c2: Update the format of parameters passed into got.post to fit the change of got v12.
|
||||
|
||||
## 1.0.0-beta.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9ff0638: update connector-kit version
|
||||
|
||||
## 1.0.0-beta.17
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 8c0654a: - Add "Generic" verification code type, remove deprecated "Continue" code type. Generic type verification code is used when user needs to send and verify verification code through our management APIs. Correspondingly, a "Generic" type mail or SMS template should be configured in the connector config.
|
||||
- Replace the term "passcode" with "verification code".
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d8b9dea: 1. Update `@logto/connector-kit` from `1.0.0-beta.32` to `1.0.0-beta.33`.
|
||||
|
||||
## 1.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d183d6d: Upgrade connector-kit
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.0-beta.13](https://github.com/logto-io/connectors/compare/v1.0.0-beta.12...v1.0.0-beta.13) (2022-11-29)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
## [1.0.0-beta.12](https://github.com/logto-io/connectors/compare/v1.0.0-beta.11...v1.0.0-beta.12) (2022-11-29)
|
||||
|
||||
### Features
|
||||
|
||||
- add forgot password and continue templates for sms and email ([#36](https://github.com/logto-io/connectors/issues/36)) ([4ad3551](https://github.com/logto-io/connectors/commit/4ad35516c0770ec344caf0bcc68b572c832b30a0))
|
||||
- add mock standard email connector ([#35](https://github.com/logto-io/connectors/issues/35)) ([479114e](https://github.com/logto-io/connectors/commit/479114e847fb4b11c6fbd697a36b7f5eb56305ed))
|
||||
|
||||
## [1.0.0-beta.11](https://github.com/logto-io/connectors/compare/v1.0.0-beta.10...v1.0.0-beta.11) (2022-11-06)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
## [1.0.0-beta.10](https://github.com/logto-io/connectors/compare/v1.0.0-beta.9...v1.0.0-beta.10) (2022-09-27)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
## 1.0.0-beta.9 (2022-09-18)
|
||||
|
||||
### Features
|
||||
|
||||
- add connectors ([#2](https://github.com/logto-io/connectors/issues/2)) ([2fbb578](https://github.com/logto-io/connectors/commit/2fbb57815406bace113617a6304eafcfc5db2d61))
|
||||
|
||||
## [1.0.0-beta.8](https://github.com/logto-io/logto/compare/v1.0.0-beta.6...v1.0.0-beta.8) (2022-09-01)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
## [1.0.0-beta.6](https://github.com/logto-io/logto/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-08-30)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
## [1.0.0-beta.5](https://github.com/logto-io/logto/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2022-08-19)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
## [1.0.0-beta.4](https://github.com/logto-io/logto/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-08-11)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
## [1.0.0-beta.3](https://github.com/logto-io/logto/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-08-01)
|
||||
|
||||
### Features
|
||||
|
||||
- **phrases:** tr language ([#1707](https://github.com/logto-io/logto/issues/1707)) ([411a8c2](https://github.com/logto-io/logto/commit/411a8c2fa2bfb16c4fef5f0a55c3c1dc5ead1124))
|
||||
|
||||
## [1.0.0-beta.2](https://github.com/logto-io/logto/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-07-25)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
## [1.0.0-beta.1](https://github.com/logto-io/logto/compare/v1.0.0-beta.0...v1.0.0-beta.1) (2022-07-19)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
## [1.0.0-beta.0](https://github.com/logto-io/logto/compare/v1.0.0-alpha.4...v1.0.0-beta.0) (2022-07-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **connector:** fix connector getConfig and validateConfig type ([#1530](https://github.com/logto-io/logto/issues/1530)) ([88a54aa](https://github.com/logto-io/logto/commit/88a54aaa9ebce419c149a33150a4927296cb705b))
|
||||
- **connector:** passwordless connector send test msg with unsaved config ([#1539](https://github.com/logto-io/logto/issues/1539)) ([0297f6c](https://github.com/logto-io/logto/commit/0297f6c52f7b5d730de44fbb08f88c2e9b951874))
|
||||
- **connector:** refactor ConnectorInstance as class ([#1541](https://github.com/logto-io/logto/issues/1541)) ([6b9ad58](https://github.com/logto-io/logto/commit/6b9ad580ae86fbcc100a100aab1d834090e682a3))
|
||||
|
||||
## [1.0.0-alpha.4](https://github.com/logto-io/logto/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) (2022-07-08)
|
||||
|
||||
### Features
|
||||
|
||||
- **connector:** connector error handler, throw errmsg on general errors ([#1458](https://github.com/logto-io/logto/issues/1458)) ([7da1de3](https://github.com/logto-io/logto/commit/7da1de33e97de4aeeec9f9b6cea59d1bf90ba623))
|
||||
|
||||
## [1.0.0-alpha.3](https://github.com/logto-io/logto/compare/v1.0.0-alpha.2...v1.0.0-alpha.3) (2022-07-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **connector:** fix Aliyun SMS connector error handling ([#1227](https://github.com/logto-io/logto/issues/1227)) ([d9ba729](https://github.com/logto-io/logto/commit/d9ba72985d016a762b3946dcbb6917db562e9b0b))
|
||||
|
||||
## [1.0.0-alpha.2](https://github.com/logto-io/logto/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2022-07-07)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
## [1.0.0-alpha.1](https://github.com/logto-io/logto/compare/v1.0.0-alpha.0...v1.0.0-alpha.1) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
## [1.0.0-alpha.0](https://github.com/logto-io/logto/compare/v0.1.2-alpha.5...v1.0.0-alpha.0) (2022-07-04)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
### [0.1.2-alpha.5](https://github.com/logto-io/logto/compare/v0.1.2-alpha.4...v0.1.2-alpha.5) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
### [0.1.2-alpha.4](https://github.com/logto-io/logto/compare/v0.1.2-alpha.3...v0.1.2-alpha.4) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
### [0.1.2-alpha.3](https://github.com/logto-io/logto/compare/v0.1.2-alpha.2...v0.1.2-alpha.3) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
### [0.1.2-alpha.2](https://github.com/logto-io/logto/compare/v0.1.2-alpha.1...v0.1.2-alpha.2) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
### [0.1.2-alpha.1](https://github.com/logto-io/logto/compare/v0.1.2-alpha.0...v0.1.2-alpha.1) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
### [0.1.2-alpha.0](https://github.com/logto-io/logto/compare/v0.1.1-alpha.0...v0.1.2-alpha.0) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-aliyun-sms
|
||||
|
||||
### [0.1.1-alpha.0](https://github.com/logto-io/logto/compare/v0.1.0-internal...v0.1.1-alpha.0) (2022-07-01)
|
||||
|
||||
### Features
|
||||
|
||||
- **connector-sendgrid-email:** add sendgrid email connector ([#850](https://github.com/logto-io/logto/issues/850)) ([b887655](https://github.com/logto-io/logto/commit/b8876558275e28ca921d4eeea6c38f8559810a11))
|
||||
- **connectors:** add logo for connectors ([#914](https://github.com/logto-io/logto/issues/914)) ([a3a7c52](https://github.com/logto-io/logto/commit/a3a7c52a91dba3603617a68e5ce47e0017081a91))
|
||||
- **core,connectors:** update Aliyun logo and add logo_dark to Apple, Github ([#1194](https://github.com/logto-io/logto/issues/1194)) ([98f8083](https://github.com/logto-io/logto/commit/98f808320b1c79c51f8bd6f49e35ca44363ea560))
|
||||
- **core:** serve connector logo ([#931](https://github.com/logto-io/logto/issues/931)) ([5b44b71](https://github.com/logto-io/logto/commit/5b44b7194ed4f98c6c2e77aae828a39b477b6010))
|
||||
- **core:** update connector db schema ([#732](https://github.com/logto-io/logto/issues/732)) ([8e1533a](https://github.com/logto-io/logto/commit/8e1533a70267d459feea4e5174296b17bef84d48))
|
||||
- **core:** wrap aliyun short message service connector ([#670](https://github.com/logto-io/logto/issues/670)) ([a06d3ee](https://github.com/logto-io/logto/commit/a06d3ee73ccc59f6aaef1dab4f45d6c118aab40d))
|
||||
- remove target, platform from connector schema and add id to metadata ([#930](https://github.com/logto-io/logto/issues/930)) ([054b0f7](https://github.com/logto-io/logto/commit/054b0f7b6a6dfed66540042ea69b0721126fe695))
|
||||
- **sms/email-connectors:** expose third-party API request error message ([#1059](https://github.com/logto-io/logto/issues/1059)) ([4cfd578](https://github.com/logto-io/logto/commit/4cfd5788d24d55017a8ace53fed99082f87691cb))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- `lint:report` script ([#730](https://github.com/logto-io/logto/issues/730)) ([3b17324](https://github.com/logto-io/logto/commit/3b17324d189b2fe47985d0bee8b37b4ef1dbdd2b))
|
||||
- **connector-aliyun-sms:** fix config guard, remove unnecessary fields ([#1229](https://github.com/logto-io/logto/issues/1229)) ([4ee7752](https://github.com/logto-io/logto/commit/4ee775273ac6c97b6580a40ec20cb3f5df8285f4))
|
156
packages/connectors/connector-aliyun-sms/README.md
Normal file
|
@ -0,0 +1,156 @@
|
|||
# Aliyun short message service connector
|
||||
|
||||
The official Logto connector for Aliyun short message service.
|
||||
|
||||
阿里云短信服务 Logto 官方连接器 [中文文档](#阿里云短信连接器)
|
||||
|
||||
**Table of contents**
|
||||
|
||||
- [Aliyun short message service connector](#aliyun-short-message-service-connector)
|
||||
- [Get started](#get-started)
|
||||
- [Set up a short message service in Aliyun SMS Console](#set-up-a-short-message-service-in-aliyun-sms-console)
|
||||
- [Create an Aliyun account](#create-an-aliyun-account)
|
||||
- [Enable and Configure Aliyun Short Message Service](#enable-and-configure-aliyun-short-message-service)
|
||||
- [Compose the connector JSON](#compose-the-connector-json)
|
||||
- [Test Aliyun SMS connector](#test-aliyun-sms-connector)
|
||||
- [Config types](#config-types)
|
||||
- [References](#references)
|
||||
- [阿里云短信连接器](#阿里云短信连接器)
|
||||
- [在阿里云短信服务控制台中配置一个短信服务](#在阿里云短信服务控制台中配置一个短信服务)
|
||||
- [创建阿里云账号](#创建阿里云账号)
|
||||
- [启用并配置阿里云短信服务](#启用并配置阿里云短信服务)
|
||||
- [编写连接器的 JSON](#编写连接器的-json)
|
||||
- [测试阿里云短信连接器](#测试阿里云短信连接器)
|
||||
- [配置类型](#配置类型)
|
||||
- [参考](#参考)
|
||||
|
||||
## Get started
|
||||
|
||||
Aliyun is a primary cloud service provider in Asia, offering many cloud services, including SMS (short message service). Aliyun SMS Connector is a plugin provided by the Logto team to call the Aliyun SMS service, with the help of which Logto end-users can register and sign in to their Logto account via SMS verification code.
|
||||
|
||||
## Set up a short message service in Aliyun SMS Console
|
||||
|
||||
> 💡 **Tip**
|
||||
>
|
||||
> You can skip some sections if you have already finished.
|
||||
|
||||
### Create an Aliyun account
|
||||
|
||||
Go to the [Aliyun website](https://cn.aliyun.com/) and register your Aliyun account if you don't have one.
|
||||
|
||||
### Enable and Configure Aliyun Short Message Service
|
||||
|
||||
1. Sign-in with your Aliyun account at the [Aliyun website](https://cn.aliyun.com/) and go to the [SMS service console page](https://www.aliyun.com/product/sms).
|
||||
2. Click the "Open for free" (免费开通) button on the top left of the SMS service page and begin the configuration process.
|
||||
3. Read and agree to the "SMS service activation Agreement" (短信服务开通条款) and click "Subscribe to a service" (开通服务) to move on.
|
||||
4. You are now on the [SMS service console page](https://dysms.console.aliyun.com/overview), go to either "Mainland China" (国内消息) or "Outside Mainland China" (国际/港澳台消息) button on the sidebar per your use case.
|
||||
5. Add signature and template following the guidelines, and provide the materials or information required for review.
|
||||
- Remember to select "Verification Code Message" (验证码) as "Scenario" (适用场景) when filling out the signature application and also "Verification Code Message" (验证码) for "Type" (模板类型) when applying for a template review because we are using these signatures and templates to send verification code. Currently, we do not support sending SMS messages other than verification-code-related text messages.
|
||||
- Also, use `{{code}}` as a placeholder where you want to place your digital verification code in template contents.
|
||||
6. After submitting your SMS signature and template application, you need to wait for it to take effect. At this point, we can go back to the [SMS service console page](https://dysms.console.aliyun.com/overview) and send a test SMS. If your signatures and templates are ready for use, you can try them directly; if they are not taking effect yet, Aliyun also provides test templates.
|
||||
- You may need to recharge a small amount of money before sending test messages.
|
||||
- You may also be asked to bind a test phone number before sending test messages. For more details, go to "Quick Start" (快速学习) tab from the sidebar of the [SMS service console page](https://dysms.console.aliyun.com/overview).
|
||||
|
||||
## Compose the connector JSON
|
||||
|
||||
1. From the [SMS service console page](https://dysms.console.aliyun.com/overview), hover on your avatar in the top right corner and go to "AccessKey Management" (AccessKey 管理), and click "Create AccessKey" (创建 AccessKey). You will get an "AccessKey ID" and "AccessKey Secret" pair after finishing security verification. Please keep them properly.
|
||||
2. Go to the "Mainland China" (国内消息) or "Outside Mainland China" (国际/港澳台消息) tab you just visited, you can find "Signature" (签名名称) and "Template Code" (模板 CODE) easily.
|
||||
- If you want to use the test-only signature and template, go to the "Quick Start" (快速学习) tab instead, and you will find them below "Signature & Templates (For Test Only)".
|
||||
3. Fill out the Aliyun SMS Connector settings:
|
||||
- Fill out the `accessKeyId` and `accessKeySecret` fields with access key pairs you've got from step 1.
|
||||
- Fill out the `signName` field with "Signature" (签名名称) which is mentioned in step 2. All templates will share this signature name.
|
||||
- You can add multiple SMS connector templates for different cases. Here is an example of adding a single template:
|
||||
- Fill the `templateCode` field, which is how you can control SMS context, with "Template Code" (模板 CODE) from step 2.
|
||||
- Fill out `usageType` field with either `Register`, `SignIn`, `ForgotPassword` or `Generic` for different use cases. (`usageType` is a Logto property to identify the proper use case.) In order to enable full user flows, templates with usageType `Register`, `SignIn`, `ForgotPassword` and `Generic` are required.
|
||||
|
||||
### Test Aliyun SMS connector
|
||||
|
||||
You can type in a phone number and click on "Send" to see whether the settings can work before "Save and Done".
|
||||
|
||||
That's it. Don't forget to [Enable connector in sign-in experience](https://docs.logto.io/docs/tutorials/get-started/enable-passcode-sign-in/#enable-connector-in-sign-in-experience).
|
||||
|
||||
### Config types
|
||||
|
||||
| Name | Type |
|
||||
|-----------------|------------|
|
||||
| accessKeyId | string |
|
||||
| accessKeySecret | string |
|
||||
| signName | string |
|
||||
| templates | Template[] |
|
||||
|
||||
| Template Properties | Type | Enum values |
|
||||
|---------------------|-------------|------------------------------------------------------|
|
||||
| templateCode | string | N/A |
|
||||
| usageType | enum string | 'Register' \| 'SignIn' \| 'ForgotPassword' \| 'Generic' |
|
||||
|
||||
|
||||
## References
|
||||
|
||||
- [Aliyun SMS - Quick Start](https://dysms.console.aliyun.com/quickstart)
|
||||
|
||||
# 阿里云短信连接器
|
||||
|
||||
阿里云是亚洲地区一个重要的云服务厂商,提供了包括短信服务在内的诸多云服务。
|
||||
|
||||
本连接器是 Logto 官方提供的阿里云短信连接器,帮助终端用户通过短信验证码进行登录注册。
|
||||
|
||||
## 在阿里云短信服务控制台中配置一个短信服务
|
||||
|
||||
> 💡 **Tip**
|
||||
>
|
||||
> 你可以跳过已经完成的部分。
|
||||
|
||||
### 创建阿里云账号
|
||||
|
||||
前往 [阿里云](https://cn.aliyun.com/) 并完成账号注册。
|
||||
|
||||
### 启用并配置阿里云短信服务
|
||||
|
||||
1. 用刚刚在 [阿里云](https://cn.aliyun.com/) 注册额账号登录并前往 [短信服务控制台](https://www.aliyun.com/product/sms)。
|
||||
2. 点按短信服务页面左上角的「免费开通」按钮并开始配置的流程。
|
||||
3. 阅读并同意「短信服务开通条款」和「开通服务」以继续。
|
||||
4. 你现在处于「[短信服务控制台概览](https://dysms.console.aliyun.com/overview)」,根据你的用户场景,点击侧边栏中的「国内消息」或者「国际/港澳台消息」。
|
||||
5. 跟随指引添加签名和模板,并提供相应的材料和信息以便审核:
|
||||
- 注意:添加 **签名** 时要在「适用场景」栏选择「验证码」,添加 **模板** 时「模板类型」也要选择「验证码」,因为我们的使用这些签名和模板就是用来发送验证码的。目前我们暂不支持除了发送验证码之外别的类型的文字短信。
|
||||
- 请同时注意要在模板的内容中加上 `{{code}}` 的占位符,在发送短信是会被随机生成的验证码所替代。
|
||||
6. 提交了短信签名和模板的申请之后,需要等待它们生效。这时候我们可以回到 [短信服务控制台概览](https://dysms.console.aliyun.com/overview) 发送测试短信。如果你的签名和模板都已经通过审核,你可以直接使用它们测试;如果它们还没有通过审核,阿里云也提供了测试模板供使用。
|
||||
- 在发送测试短信之前,可能你需要对账户进行小额的充值。
|
||||
- 测试时也需要提前绑定测试使用的手机号码以成功收到测试短信。点击 [短信服务控制台概览](https://dysms.console.aliyun.com/overview) 侧边栏上的「快速学习」标签页以了解更多。
|
||||
|
||||
## 编写连接器的 JSON
|
||||
|
||||
1. 前往 [短信服务控制台概览](https://dysms.console.aliyun.com/overview),将鼠标悬停在页面右上角的头像处,进入「AccessKey 管理」并点按「创建 AccessKey」。完成了安全验证之后,你会得到一对「AccessKey ID」和「AccessKey Secret」,请妥善保管他们。
|
||||
2. 前往你之前访问过的「国内消息」或「国际/港澳台消息」标签页,可以很快找到「签名名称」和「模板 CODE」。
|
||||
- 如果你想使用测试专用的签名模板, 则前往「快速开始」标签页,你就能在「测试专用签名模版」下方找到它们。
|
||||
3. 完成阿里云短信服务连接器的设置:
|
||||
- 用你在步骤 1 中拿到的一对「AccessKey ID」和「AccessKey Secret」来分别填入 `accessKeyId` 和 `accessKeySecret`。
|
||||
- 用你在步骤 2 中拿到的「签名名称」填入 `signName` 栏。所有的模板都会共用这个签名。
|
||||
- 你可以添加多个短信服务模板以应对不同的用户场景。这里展示填写单个模板的例子:
|
||||
- `templateCode` 栏是你可以用来控制所发送短信内容的属性。它们的值从步骤 2 中的「模板 CODE」获取。
|
||||
- `usageType` 栏填写 `Register`,`SignIn`,`ForgotPassword` 或者 `Generic` 其中之一以分别对应 _注册_,_登录_,_忘记密码_,_通用_ 的不同场景。(`usageType` 是 Logto 的属性,用来确定使用场景。)为了能够使用完成的流程,需要配置 `usageType` 为 `Register`,`SignIn` 以及 `ForgotPassword` 的模板。
|
||||
|
||||
### 测试阿里云短信连接器
|
||||
|
||||
你可以在「保存并完成」之前输入一个手机号码并点按「发送」来测试配置是否可以正常工作。
|
||||
|
||||
大功告成!快去 [启用短信或邮件验证码登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-passcode-sign-in/#%E5%9C%A8%E7%99%BB%E5%BD%95%E4%BD%93%E9%AA%8C%E4%B8%AD%E5%90%AF%E7%94%A8%E8%BF%9E%E6%8E%A5%E5%99%A8) 吧。
|
||||
|
||||
### 配置类型
|
||||
|
||||
| 名称 | 类型 |
|
||||
|-----------------|------------|
|
||||
| accessKeyId | string |
|
||||
| accessKeySecret | string |
|
||||
| signName | string |
|
||||
| templates | Template[] |
|
||||
|
||||
| 模板属性 | 类型 | 枚举值 |
|
||||
|--------------|-------------|------------------------------------------------------|
|
||||
| templateCode | string | N/A |
|
||||
| usageType | enum string | 'Register' \| 'SignIn' \| 'ForgotPassword' \| 'Generic' |
|
||||
|
||||
|
||||
## 参考
|
||||
|
||||
- [阿里云短信服务 - 快速学习](https://dysms.console.aliyun.com/quickstart)
|
||||
|
3
packages/connectors/connector-aliyun-sms/logo.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.2633 4.58495C2.16941 4.80996 0.994516 5.68373 0.506258 6.6415C0.0180004 7.59927 0 7.83478 0 11.8665C0 14.0179 0.0360005 15.7898 0.0900013 16.0666C0.411006 17.7076 1.69428 18.9909 3.3353 19.3119C3.83256 19.4019 10.3048 19.4477 10.3048 19.3479C10.3048 19.2755 9.75353 17.1061 9.72615 17.0791C9.71715 17.0611 8.56888 16.808 7.17686 16.5091C3.93156 15.8041 4.02681 15.8311 3.75981 15.5056L3.52468 15.2254V8.66279L3.75981 8.37779C4.03094 8.05228 3.93156 8.07966 7.17686 7.37426L9.72615 6.80425C9.7539 6.7855 10.3052 4.6157 10.3052 4.54745C10.3052 4.4612 3.69756 4.50245 3.2633 4.59245V4.58495ZM13.7402 4.59845C13.7492 4.76121 14.2555 6.74988 14.3102 6.80425C14.3282 6.83125 15.5665 7.11176 17.0493 7.42789C18.532 7.74402 19.8517 8.06541 19.9691 8.13291C20.1281 8.22516 20.2616 8.35566 20.3579 8.51242C20.5027 8.76555 20.5207 9.03668 20.5207 11.9475C20.5207 14.8583 20.5027 15.1294 20.3579 15.3826C20.2594 15.5374 20.1263 15.6673 19.9691 15.7621C19.8517 15.8258 18.544 16.1416 17.0493 16.4671L14.3102 17.0952C14.2558 17.1496 13.7496 19.1383 13.7402 19.301C13.7402 19.4004 14.3912 19.4184 17.2476 19.391C21.1852 19.355 21.234 19.346 22.2375 18.668C22.9582 18.1719 23.5027 17.4597 23.7922 16.634C24 16.0283 24 15.974 24 11.9512C24 7.92853 24 7.87415 23.7922 7.26851C23.5026 6.44288 22.9582 5.73061 22.2375 5.23447C21.234 4.5572 21.189 4.54745 17.2476 4.51183C14.3912 4.48483 13.7402 4.49833 13.7402 4.60183V4.59845ZM9.70515 11.7619C9.67618 11.8585 9.67002 11.9606 9.68715 12.06C9.71415 12.21 9.8769 12.2227 12.0009 12.2497L14.2791 12.2677V11.6351H12.0227C10.1334 11.6351 9.74453 11.6531 9.7089 11.7615L9.70515 11.7619Z" fill="#FF6A00"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package",
|
||||
"name": "@logto/connector-aliyun-sms",
|
||||
"version": "1.0.0",
|
||||
"description": "Aliyun SMS connector implementation."
|
||||
}
|
95
packages/connectors/connector-aliyun-sms/src/constant.ts
Normal file
|
@ -0,0 +1,95 @@
|
|||
import type { ConnectorMetadata } from '@logto/connector-kit';
|
||||
import { ConnectorConfigFormItemType } from '@logto/connector-kit';
|
||||
|
||||
export const endpoint = 'https://dysmsapi.aliyuncs.com/';
|
||||
|
||||
export const staticConfigs = {
|
||||
Format: 'json',
|
||||
RegionId: 'cn-hangzhou',
|
||||
SignatureMethod: 'HMAC-SHA1',
|
||||
SignatureVersion: '1.0',
|
||||
Version: '2017-05-25',
|
||||
};
|
||||
|
||||
/**
|
||||
* Details of SmsTemplateType can be found at:
|
||||
* https://next.api.aliyun.com/document/Dysmsapi/2017-05-25/QuerySmsTemplateList.
|
||||
*
|
||||
* In our use case, it is to send verification code SMS for passwordless sign-in/up as well as
|
||||
* reset password. The default value of type code is set to 2.
|
||||
*/
|
||||
export enum SmsTemplateType {
|
||||
Notification = 0,
|
||||
Promotion = 1,
|
||||
VerificationCode = 2,
|
||||
InternationalMessage = 6,
|
||||
PureNumber = 7,
|
||||
}
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'aliyun-short-message-service',
|
||||
target: 'aliyun-sms',
|
||||
platform: null,
|
||||
name: {
|
||||
en: 'Aliyun Short Message Service',
|
||||
'zh-CN': '阿里云短信服务',
|
||||
'tr-TR': 'Aliyun SMS Servisi',
|
||||
ko: 'Aliyun Short 메세지 서비스',
|
||||
},
|
||||
logo: './logo.svg',
|
||||
logoDark: null,
|
||||
description: {
|
||||
en: 'Aliyun provides cloud computing services to online businesses.',
|
||||
'zh-CN': '阿里云是全球性的云服务提供商。',
|
||||
'tr-TR': 'Aliyun, çevrimiçi işletmelere bulut bilişim hizmetleri sunmaktadır.',
|
||||
ko: 'Aliyun는 온라인 비지니스를 위해 클라우딩 컴퓨팅 서비스를 제공합니다.',
|
||||
},
|
||||
readme: './README.md',
|
||||
formItems: [
|
||||
{
|
||||
key: 'accessKeyId',
|
||||
label: 'Access Key ID',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: true,
|
||||
placeholder: '<access-key-id>',
|
||||
},
|
||||
{
|
||||
key: 'accessKeySecret',
|
||||
label: 'Access Key Secret',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: true,
|
||||
placeholder: '<access-key-secret>',
|
||||
},
|
||||
{
|
||||
key: 'signName',
|
||||
label: 'Signature Name',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: true,
|
||||
placeholder: '<signature-name>',
|
||||
},
|
||||
{
|
||||
key: 'templates',
|
||||
label: 'Templates',
|
||||
type: ConnectorConfigFormItemType.Json,
|
||||
required: true,
|
||||
defaultValue: [
|
||||
{
|
||||
usageType: 'SignIn',
|
||||
templateCode: '<template-code>',
|
||||
},
|
||||
{
|
||||
usageType: 'Register',
|
||||
templateCode: '<template-code>',
|
||||
},
|
||||
{
|
||||
usageType: 'ForgotPassword',
|
||||
templateCode: '<template-code>',
|
||||
},
|
||||
{
|
||||
usageType: 'Generic',
|
||||
templateCode: '<template-code>',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
87
packages/connectors/connector-aliyun-sms/src/index.test.ts
Normal file
|
@ -0,0 +1,87 @@
|
|||
import { VerificationCodeType } from '@logto/connector-kit';
|
||||
|
||||
import { mockedConnectorConfig, phoneTest, codeTest } from './mock.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const getConfig = jest.fn().mockResolvedValue(mockedConnectorConfig);
|
||||
|
||||
const sendSms = jest.fn().mockResolvedValue({
|
||||
body: JSON.stringify({ Code: 'OK', RequestId: 'request-id', Message: 'OK' }),
|
||||
statusCode: 200,
|
||||
});
|
||||
|
||||
jest.unstable_mockModule('./single-send-text.js', () => ({
|
||||
sendSms,
|
||||
}));
|
||||
|
||||
const { default: createConnector } = await import('./index.js');
|
||||
|
||||
describe('sendMessage()', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call singleSendMail() and replace code in content', async () => {
|
||||
const connector = await createConnector({ getConfig });
|
||||
await connector.sendMessage({
|
||||
to: phoneTest,
|
||||
type: VerificationCodeType.SignIn,
|
||||
payload: { code: codeTest },
|
||||
});
|
||||
expect(sendSms).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
AccessKeyId: mockedConnectorConfig.accessKeyId,
|
||||
PhoneNumbers: phoneTest,
|
||||
SignName: mockedConnectorConfig.signName,
|
||||
TemplateCode: 'TemplateCode',
|
||||
TemplateParam: `{"code":"${codeTest}"}`,
|
||||
}),
|
||||
mockedConnectorConfig.accessKeySecret
|
||||
);
|
||||
});
|
||||
|
||||
it('should use template per region', async () => {
|
||||
const connector = await createConnector({ getConfig });
|
||||
|
||||
for (const to of [phoneTest, `86${phoneTest}`, `0086${phoneTest}`, `+86${phoneTest}`]) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await connector.sendMessage({
|
||||
to,
|
||||
type: VerificationCodeType.Register,
|
||||
payload: { code: codeTest },
|
||||
});
|
||||
expect(sendSms).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
TemplateCode: 'TemplateCode1',
|
||||
}),
|
||||
mockedConnectorConfig.accessKeySecret
|
||||
);
|
||||
|
||||
sendSms.mockClear();
|
||||
}
|
||||
|
||||
await connector.sendMessage({
|
||||
to: '+1123123123',
|
||||
type: VerificationCodeType.Register,
|
||||
payload: { code: codeTest },
|
||||
});
|
||||
expect(sendSms).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
TemplateCode: 'TemplateCode2',
|
||||
}),
|
||||
mockedConnectorConfig.accessKeySecret
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if template is missing', async () => {
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(
|
||||
connector.sendMessage({
|
||||
to: phoneTest,
|
||||
type: VerificationCodeType.Test,
|
||||
payload: { code: codeTest },
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
111
packages/connectors/connector-aliyun-sms/src/index.ts
Normal file
|
@ -0,0 +1,111 @@
|
|||
import type {
|
||||
GetConnectorConfig,
|
||||
SendMessageFunction,
|
||||
SmsConnector,
|
||||
CreateConnector,
|
||||
} from '@logto/connector-kit';
|
||||
import {
|
||||
ConnectorError,
|
||||
ConnectorErrorCodes,
|
||||
validateConfig,
|
||||
ConnectorType,
|
||||
parseJson,
|
||||
} from '@logto/connector-kit';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import { HTTPError } from 'got';
|
||||
|
||||
import { defaultMetadata } from './constant.js';
|
||||
import { sendSms } from './single-send-text.js';
|
||||
import type { AliyunSmsConfig, Template } from './types.js';
|
||||
import { aliyunSmsConfigGuard, sendSmsResponseGuard } from './types.js';
|
||||
|
||||
const isChinaNumber = (to: string) => /^(\+86|0086|86)?\d{11}$/.test(to);
|
||||
|
||||
const getTemplateCode = ({ templateCode }: Template, to: string) => {
|
||||
if (typeof templateCode === 'string') {
|
||||
return templateCode;
|
||||
}
|
||||
|
||||
return isChinaNumber(to) ? templateCode.china : templateCode.overseas;
|
||||
};
|
||||
|
||||
const sendMessage =
|
||||
(getConfig: GetConnectorConfig): SendMessageFunction =>
|
||||
async (data, inputConfig) => {
|
||||
const { to, type, payload } = data;
|
||||
const config = inputConfig ?? (await getConfig(defaultMetadata.id));
|
||||
validateConfig<AliyunSmsConfig>(config, aliyunSmsConfigGuard);
|
||||
const { accessKeyId, accessKeySecret, signName, templates } = config;
|
||||
const template = templates.find(({ usageType }) => usageType === type);
|
||||
|
||||
assert(
|
||||
template,
|
||||
new ConnectorError(ConnectorErrorCodes.TemplateNotFound, `Cannot find template!`)
|
||||
);
|
||||
|
||||
try {
|
||||
const httpResponse = await sendSms(
|
||||
{
|
||||
AccessKeyId: accessKeyId,
|
||||
PhoneNumbers: to,
|
||||
SignName: signName,
|
||||
TemplateCode: getTemplateCode(template, to),
|
||||
TemplateParam: JSON.stringify(payload),
|
||||
},
|
||||
accessKeySecret
|
||||
);
|
||||
|
||||
const { body: rawBody } = httpResponse;
|
||||
|
||||
const { Code, Message, ...rest } = parseResponseString(rawBody);
|
||||
|
||||
if (Code !== 'OK') {
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, {
|
||||
errorDescription: Message,
|
||||
Code,
|
||||
...rest,
|
||||
});
|
||||
}
|
||||
|
||||
return httpResponse;
|
||||
} catch (error: unknown) {
|
||||
if (!(error instanceof HTTPError)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const {
|
||||
response: { body: rawBody },
|
||||
} = error;
|
||||
|
||||
assert(typeof rawBody === 'string', new ConnectorError(ConnectorErrorCodes.InvalidResponse));
|
||||
|
||||
const { Code, Message, ...rest } = parseResponseString(rawBody);
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, {
|
||||
errorDescription: Message,
|
||||
Code,
|
||||
...rest,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const parseResponseString = (response: string) => {
|
||||
const result = sendSmsResponseGuard.safeParse(parseJson(response));
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const createAliyunSmsConnector: CreateConnector<SmsConnector> = async ({ getConfig }) => {
|
||||
return {
|
||||
metadata: defaultMetadata,
|
||||
type: ConnectorType.Sms,
|
||||
configGuard: aliyunSmsConfigGuard,
|
||||
sendMessage: sendMessage(getConfig),
|
||||
};
|
||||
};
|
||||
|
||||
export default createAliyunSmsConnector;
|
51
packages/connectors/connector-aliyun-sms/src/mock.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
export const mockedConnectorConfig = {
|
||||
accessKeyId: 'accessKeyId',
|
||||
accessKeySecret: 'accessKeySecret',
|
||||
signName: 'signName',
|
||||
templates: [
|
||||
{
|
||||
type: 2,
|
||||
usageType: 'SignIn',
|
||||
templateCode: 'TemplateCode',
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
usageType: 'Register',
|
||||
templateCode: {
|
||||
china: 'TemplateCode1',
|
||||
overseas: 'TemplateCode2',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
usageType: 'ForgotPassword',
|
||||
templateCode: 'TemplateCode',
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
usageType: 'Generic',
|
||||
templateCode: 'TemplateCode',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const phoneTest = '13012345678';
|
||||
export const codeTest = '1234';
|
||||
|
||||
export const mockedParameters = {
|
||||
AccessKeyId: 'testid',
|
||||
AccountName: "<a%b'>",
|
||||
Action: 'SingleSendMail',
|
||||
AddressType: '1',
|
||||
Format: 'XML',
|
||||
HtmlBody: '4',
|
||||
RegionId: 'cn-hangzhou',
|
||||
ReplyToAddress: 'true',
|
||||
SignatureMethod: 'HMAC-SHA1',
|
||||
SignatureVersion: '1.0',
|
||||
Subject: '3',
|
||||
TagName: '2',
|
||||
ToAddress: '1@test.com',
|
||||
Version: '2015-11-23',
|
||||
};
|
||||
export const mockedRandomCode = 1235;
|
|
@ -0,0 +1,32 @@
|
|||
import { mockedRandomCode } from './mock.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const request = jest.fn();
|
||||
|
||||
jest.unstable_mockModule('./utils.js', () => ({ request }));
|
||||
|
||||
const { sendSms } = await import('./single-send-text.js');
|
||||
|
||||
describe('sendSms', () => {
|
||||
it('should call request with action sendSms', async () => {
|
||||
const code = mockedRandomCode;
|
||||
|
||||
await sendSms(
|
||||
{
|
||||
AccessKeyId: '<access-key-id>',
|
||||
PhoneNumbers: '13912345678',
|
||||
SignName: '阿里云短信测试',
|
||||
TemplateCode: ' SMS_154950909',
|
||||
TemplateParam: JSON.stringify({ code }),
|
||||
},
|
||||
'<access-key-secret>'
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const calledData = request.mock.calls[0];
|
||||
expect(calledData).not.toBeUndefined();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const payload = calledData?.[1];
|
||||
expect(payload).toHaveProperty('Action', 'SendSms');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
import { endpoint, staticConfigs } from './constant.js';
|
||||
import type { PublicParameters, SendSms } from './types.js';
|
||||
import { request } from './utils.js';
|
||||
|
||||
/**
|
||||
* @doc https://help.aliyun.com/document_detail/101414.html
|
||||
*/
|
||||
export const sendSms = async (parameters: PublicParameters & SendSms, accessKeySecret: string) => {
|
||||
return request(endpoint, { Action: 'SendSms', ...staticConfigs, ...parameters }, accessKeySecret);
|
||||
};
|
75
packages/connectors/connector-aliyun-sms/src/types.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { SmsTemplateType } from './constant.js';
|
||||
|
||||
export const sendSmsResponseGuard = z.object({
|
||||
BizId: z.string().optional(),
|
||||
Code: z.string(),
|
||||
Message: z.string(),
|
||||
RequestId: z.string(),
|
||||
});
|
||||
|
||||
export type SendSmsResponse = z.infer<typeof sendSmsResponseGuard>;
|
||||
|
||||
/**
|
||||
* @doc https://help.aliyun.com/document_detail/101414.html
|
||||
*/
|
||||
export type SendSms = {
|
||||
OutId?: string;
|
||||
PhoneNumbers: string; // 11 digits w/o prefix (can be multiple phone numbers with separator `,`)
|
||||
SignName: string; // Name of SMS signature
|
||||
SmsUpExtendCode?: string;
|
||||
TemplateCode: string; // Text message template ID
|
||||
TemplateParam?: string; // Stringified JSON (used to fill in text template)
|
||||
};
|
||||
|
||||
export type PublicParameters = {
|
||||
AccessKeyId: string;
|
||||
Format?: string; // 'json' or 'xml', default: 'json'
|
||||
RegionId?: string; // 'cn-hangzhou' | 'ap-southeast-1' | 'ap-southeast-2'
|
||||
Signature?: string;
|
||||
SignatureMethod?: string;
|
||||
SignatureNonce?: string;
|
||||
SignatureVersion?: string;
|
||||
Timestamp?: string;
|
||||
Version?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* UsageType here is used to specify the use case of the template, can be either
|
||||
* 'Register', 'SignIn', 'ForgotPassword', 'Generic'.
|
||||
*
|
||||
* Type here in the template is used to specify the purpose of sending the SMS,
|
||||
* can be either item in SmsTemplateType.
|
||||
* As the SMS is applied for sending verification code, the value should always be 2 in our case.
|
||||
*/
|
||||
const requiredTemplateUsageTypes = ['Register', 'SignIn', 'ForgotPassword', 'Generic'];
|
||||
|
||||
export const templateGuard = z.object({
|
||||
type: z.nativeEnum(SmsTemplateType).default(2),
|
||||
usageType: z.string(),
|
||||
templateCode: z.string().or(z.object({ china: z.string(), overseas: z.string() })),
|
||||
});
|
||||
|
||||
export type Template = z.infer<typeof templateGuard>;
|
||||
|
||||
export const aliyunSmsConfigGuard = z.object({
|
||||
accessKeyId: z.string(),
|
||||
accessKeySecret: z.string(),
|
||||
signName: z.string(),
|
||||
templates: z.array(templateGuard).refine(
|
||||
(templates) =>
|
||||
requiredTemplateUsageTypes.every((requiredType) =>
|
||||
templates.map((template) => template.usageType).includes(requiredType)
|
||||
),
|
||||
(templates) => ({
|
||||
message: `UsageType (${requiredTemplateUsageTypes
|
||||
.filter(
|
||||
(requiredType) => !templates.map((template) => template.usageType).includes(requiredType)
|
||||
)
|
||||
.join(', ')}) should be provided in templates.`,
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
export type AliyunSmsConfig = z.infer<typeof aliyunSmsConfigGuard>;
|
38
packages/connectors/connector-aliyun-sms/src/utils.test.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { mockedParameters } from './mock.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const post = jest.fn();
|
||||
|
||||
jest.unstable_mockModule('got', () => ({
|
||||
got: { post },
|
||||
}));
|
||||
|
||||
const { getSignature, request } = await import('./utils.js');
|
||||
|
||||
describe('getSignature', () => {
|
||||
it('should get valid signature', () => {
|
||||
const parameters = {
|
||||
...mockedParameters,
|
||||
SignatureNonce: 'c1b2c332-4cfb-4a0f-b8cc-ebe622aa0a5c',
|
||||
Timestamp: '2016-10-20T06:27:56Z',
|
||||
};
|
||||
const signature = getSignature(parameters, 'testsecret', 'POST');
|
||||
expect(signature).toEqual('llJfXJjBW3OacrVgxxsITgYaYm0=');
|
||||
});
|
||||
});
|
||||
|
||||
describe('request', () => {
|
||||
it('should call got.post with extended params', async () => {
|
||||
const parameters = mockedParameters;
|
||||
await request('http://test.endpoint.com', parameters, 'testsecret');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const calledData = post.mock.calls[0];
|
||||
expect(calledData).not.toBeUndefined();
|
||||
const payload = calledData?.[0].form as Record<string, unknown>;
|
||||
expect(payload.AccessKeyId).toEqual('testid');
|
||||
expect(payload.Timestamp).not.toBeNull();
|
||||
expect(payload.SignatureNonce).not.toBeNull();
|
||||
expect(payload.Signature).not.toBeNull();
|
||||
});
|
||||
});
|
59
packages/connectors/connector-aliyun-sms/src/utils.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { createHmac } from 'crypto';
|
||||
|
||||
import { got } from 'got';
|
||||
|
||||
import type { PublicParameters } from './types.js';
|
||||
|
||||
// Aliyun has special escape rules.
|
||||
// https://help.aliyun.com/document_detail/29442.html
|
||||
const escaper = (string_: string) =>
|
||||
encodeURIComponent(string_)
|
||||
.replace(/\*/g, '%2A')
|
||||
.replace(/'/g, '%27')
|
||||
.replace(/!/g, '%21')
|
||||
.replace(/"/g, '%22')
|
||||
.replace(/\(/g, '%28')
|
||||
.replace(/\)/g, '%29')
|
||||
.replace(/\+/g, '%2B');
|
||||
|
||||
export const getSignature = (
|
||||
parameters: Record<string, string>,
|
||||
secret: string,
|
||||
method: string
|
||||
) => {
|
||||
const canonicalizedQuery = Object.keys(parameters)
|
||||
.map((key) => {
|
||||
const value = parameters[key];
|
||||
|
||||
return value === undefined ? '' : `${escaper(key)}=${escaper(value)}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.slice()
|
||||
.sort()
|
||||
.join('&');
|
||||
|
||||
const stringToSign = `${method.toUpperCase()}&${escaper('/')}&${escaper(canonicalizedQuery)}`;
|
||||
|
||||
return createHmac('sha1', `${secret}&`).update(stringToSign).digest('base64');
|
||||
};
|
||||
|
||||
export const request = async (
|
||||
url: string,
|
||||
parameters: PublicParameters & Record<string, string>,
|
||||
accessKeySecret: string
|
||||
) => {
|
||||
const finalParameters: Record<string, string> = {
|
||||
...parameters,
|
||||
SignatureNonce: String(Math.random()),
|
||||
Timestamp: new Date().toISOString(),
|
||||
};
|
||||
const signature = getSignature(finalParameters, accessKeySecret, 'POST');
|
||||
|
||||
return got.post({
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
form: { ...finalParameters, Signature: signature },
|
||||
});
|
||||
};
|
178
packages/connectors/connector-apple/CHANGELOG.md
Normal file
|
@ -0,0 +1,178 @@
|
|||
# Change Log
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 4ed07d2: bump connector-kit to v1.0.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- d183d6d: Support nonce
|
||||
- 269d701: The console connector configuration page has been changed from JSON format to form view.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9ff0638: update connector-kit version
|
||||
- 7b0bf69: Bump version to upgrade connector kit
|
||||
- 4ec0889: bump connector-kit version
|
||||
- d8b9dea: 1. Update `@logto/connector-kit` from `1.0.0-beta.32` to `1.0.0-beta.33`.
|
||||
- d183d6d: Upgrade connector-kit
|
||||
|
||||
## 1.0.0-beta.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4ec0889: bump connector-kit version
|
||||
|
||||
## 1.0.0-beta.20
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 269d701: The console connector configuration page has been changed from JSON format to form view.
|
||||
|
||||
## 1.0.0-beta.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7b0bf69: Bump version to upgrade connector kit
|
||||
|
||||
## 1.0.0-beta.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9ff0638: update connector-kit version
|
||||
|
||||
## 1.0.0-beta.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d8b9dea: 1. Update `@logto/connector-kit` from `1.0.0-beta.32` to `1.0.0-beta.33`.
|
||||
|
||||
## 1.0.0-beta.16
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- d183d6d: Support nonce
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d183d6d: Upgrade connector-kit
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.0-beta.12](https://github.com/logto-io/connectors/compare/v1.0.0-beta.11...v1.0.0-beta.12) (2022-11-29)
|
||||
|
||||
### Features
|
||||
|
||||
- add mock standard email connector ([#35](https://github.com/logto-io/connectors/issues/35)) ([479114e](https://github.com/logto-io/connectors/commit/479114e847fb4b11c6fbd697a36b7f5eb56305ed))
|
||||
|
||||
## [1.0.0-beta.11](https://github.com/logto-io/connectors/compare/v1.0.0-beta.10...v1.0.0-beta.11) (2022-11-06)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
## [1.0.0-beta.10](https://github.com/logto-io/connectors/compare/v1.0.0-beta.9...v1.0.0-beta.10) (2022-09-27)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
## 1.0.0-beta.9 (2022-09-18)
|
||||
|
||||
### Features
|
||||
|
||||
- add connectors ([#2](https://github.com/logto-io/connectors/issues/2)) ([2fbb578](https://github.com/logto-io/connectors/commit/2fbb57815406bace113617a6304eafcfc5db2d61))
|
||||
|
||||
## [1.0.0-beta.8](https://github.com/logto-io/logto/compare/v1.0.0-beta.6...v1.0.0-beta.8) (2022-09-01)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
## [1.0.0-beta.6](https://github.com/logto-io/logto/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-08-30)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
## [1.0.0-beta.5](https://github.com/logto-io/logto/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2022-08-19)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
## [1.0.0-beta.4](https://github.com/logto-io/logto/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-08-11)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
## [1.0.0-beta.3](https://github.com/logto-io/logto/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-08-01)
|
||||
|
||||
### Features
|
||||
|
||||
- **phrases:** tr language ([#1707](https://github.com/logto-io/logto/issues/1707)) ([411a8c2](https://github.com/logto-io/logto/commit/411a8c2fa2bfb16c4fef5f0a55c3c1dc5ead1124))
|
||||
|
||||
## [1.0.0-beta.2](https://github.com/logto-io/logto/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-07-25)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
## [1.0.0-beta.1](https://github.com/logto-io/logto/compare/v1.0.0-beta.0...v1.0.0-beta.1) (2022-07-19)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
## [1.0.0-beta.0](https://github.com/logto-io/logto/compare/v1.0.0-alpha.4...v1.0.0-beta.0) (2022-07-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **connector:** fix connector getConfig and validateConfig type ([#1530](https://github.com/logto-io/logto/issues/1530)) ([88a54aa](https://github.com/logto-io/logto/commit/88a54aaa9ebce419c149a33150a4927296cb705b))
|
||||
- **connector:** refactor ConnectorInstance as class ([#1541](https://github.com/logto-io/logto/issues/1541)) ([6b9ad58](https://github.com/logto-io/logto/commit/6b9ad580ae86fbcc100a100aab1d834090e682a3))
|
||||
|
||||
## [1.0.0-alpha.4](https://github.com/logto-io/logto/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) (2022-07-08)
|
||||
|
||||
### Features
|
||||
|
||||
- expose zod error ([#1474](https://github.com/logto-io/logto/issues/1474)) ([81b63f0](https://github.com/logto-io/logto/commit/81b63f07bb412abf1f2b42059bac2ffcfc86272c))
|
||||
|
||||
## [1.0.0-alpha.3](https://github.com/logto-io/logto/compare/v1.0.0-alpha.2...v1.0.0-alpha.3) (2022-07-07)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
## [1.0.0-alpha.2](https://github.com/logto-io/logto/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2022-07-07)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
## [1.0.0-alpha.1](https://github.com/logto-io/logto/compare/v1.0.0-alpha.0...v1.0.0-alpha.1) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
## [1.0.0-alpha.0](https://github.com/logto-io/logto/compare/v0.1.2-alpha.5...v1.0.0-alpha.0) (2022-07-04)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
### [0.1.2-alpha.5](https://github.com/logto-io/logto/compare/v0.1.2-alpha.4...v0.1.2-alpha.5) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
### [0.1.2-alpha.4](https://github.com/logto-io/logto/compare/v0.1.2-alpha.3...v0.1.2-alpha.4) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
### [0.1.2-alpha.3](https://github.com/logto-io/logto/compare/v0.1.2-alpha.2...v0.1.2-alpha.3) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
### [0.1.2-alpha.2](https://github.com/logto-io/logto/compare/v0.1.2-alpha.1...v0.1.2-alpha.2) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
### [0.1.2-alpha.1](https://github.com/logto-io/logto/compare/v0.1.2-alpha.0...v0.1.2-alpha.1) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
### [0.1.2-alpha.0](https://github.com/logto-io/logto/compare/v0.1.1-alpha.0...v0.1.2-alpha.0) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-apple
|
||||
|
||||
### [0.1.1-alpha.0](https://github.com/logto-io/logto/compare/v0.1.0-internal...v0.1.1-alpha.0) (2022-07-01)
|
||||
|
||||
### Features
|
||||
|
||||
- **connector:** apple ([#966](https://github.com/logto-io/logto/issues/966)) ([7400ed8](https://github.com/logto-io/logto/commit/7400ed8896fdceda6165a0540413efb4e3a47438))
|
||||
- **connectors:** handle authorization callback parameters in each connector respectively ([#1166](https://github.com/logto-io/logto/issues/1166)) ([097aade](https://github.com/logto-io/logto/commit/097aade2e2e1b1ea1531bcb4c1cca8d24961a9b9))
|
||||
- **core,connectors:** update Aliyun logo and add logo_dark to Apple, Github ([#1194](https://github.com/logto-io/logto/issues/1194)) ([98f8083](https://github.com/logto-io/logto/commit/98f808320b1c79c51f8bd6f49e35ca44363ea560))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **connector:** test ([d327c6f](https://github.com/logto-io/logto/commit/d327c6fdf5f4a3fbc68618f46df7ac213d77aed5))
|
142
packages/connectors/connector-apple/README.md
Normal file
|
@ -0,0 +1,142 @@
|
|||
# Apple connector
|
||||
|
||||
The official Logto connector for Apple social sign-in.
|
||||
|
||||
Apple 社交登录 Logto 官方连接器 [中文文档](#apple-连接器)
|
||||
|
||||
**Table of contents**
|
||||
|
||||
- [Apple connector](#apple-connector)
|
||||
- [Get started](#get-started)
|
||||
- [Enable Sign in with Apple for your app](#enable-sign-in-with-apple-for-your-app)
|
||||
- [Create an identifier](#create-an-identifier)
|
||||
- [Enable Sign in with Apple for your identifier](#enable-sign-in-with-apple-for-your-identifier)
|
||||
- [Test Apple connector](#test-apple-connector)
|
||||
- [Apple 连接器](#apple-连接器)
|
||||
- [开始上手](#开始上手)
|
||||
- [为你的应用启用「通过 Apple 登录」](#为你的应用启用通过-apple-登录)
|
||||
- [创建一个 identifier](#创建一个-identifier)
|
||||
- [为你的 identifier 启用「通过 Apple 登录」](#为你的-identifier-启用通过-apple-登录)
|
||||
- [测试 Apple 连接器](#测试-apple-连接器)
|
||||
|
||||
## Get started
|
||||
|
||||
If you don't know the concept of the connector or don't know how to add this connector to your Sign-in experience, please see [Logto tutorial](https://docs.logto.io/docs/tutorials/get-started/enable-social-sign-in).
|
||||
|
||||
> ℹ️ **Note**
|
||||
>
|
||||
> Apple sign-in is required for AppStore if you have other social sign-in methods in your app.
|
||||
> Having Apple sign-in on Android devices is great if you also provide an Android app.
|
||||
|
||||
You need to enroll [Apple Developer Program](https://developer.apple.com/programs/) before continuing.
|
||||
|
||||
### Enable Sign in with Apple for your app
|
||||
|
||||
> ⚠️ **Caution**
|
||||
>
|
||||
> Even if you want to implement Sign in with Apple on a web app only, you still need to have an existing app that embraces the AppStore ecosystem (i.e., have a valid App ID).
|
||||
|
||||
You can do it via Xcode -> Project settings -> Signing & Capabilities, or visit [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list/bundleId).
|
||||
|
||||

|
||||
|
||||
See the "Enable an App ID" section in [Apple official docs](https://developer.apple.com/documentation/sign_in_with_apple/configuring_your_environment_for_sign_in_with_apple) for more info.
|
||||
|
||||
### Create an identifier
|
||||
|
||||
1. Visit [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list/serviceId), then click the "+" button next to "Identifier".
|
||||
2. In the "Register a new identifier" page, choose "Services IDs" and click "Continue".
|
||||
3. Fill out "Description" and "Identifier" (E.g., `Logto Test` and `io.logto.test`), then click "Continue".
|
||||
4. Double-check the info and click "Register".
|
||||
|
||||
### Enable Sign in with Apple for your identifier
|
||||
|
||||
Click the identifier you just created. Check "Sign in with Apple" on the details page and click "Configure".
|
||||
|
||||

|
||||
|
||||
In the opening modal, select the App ID you just enabled Sign in with Apple.
|
||||
|
||||
Enter the domain of your Logto instance without protocol and port, e.g., `your.logto.domain`; then enter the "Return URL" (i.e., Redirect URI), which is the Logto URL with `/callback/${connector_id}`, e.g., `https://your.logto.domain/callback/apple-universal`. You can get the randomly generated `connector_id` after creating Apple connector in Admin Console.
|
||||
|
||||

|
||||
|
||||
Click "Next" then "Done" to close the modal. Click "Continue" on the top-right corner, then click "Save" to save your configuration.
|
||||
|
||||
> ⚠️ **Caution**
|
||||
>
|
||||
> Apple does NOT allow Return URLs with HTTP protocol and `localhost` domain.
|
||||
>
|
||||
> If you want to test locally, you need to edit `/etc/hosts` file to map localhost to a custom domain and set up a local HTTPS environment. [mkcert](https://github.com/FiloSottile/mkcert) can help you for setting up local HTTPS.
|
||||
|
||||
> ℹ️ **Note**
|
||||
>
|
||||
> This connector doesn't support customizing `scope` (e.g., name, email) yet since Apple requires `form_post` response mode when `scope` is not empty, which is incompatible with the current connector design.
|
||||
>
|
||||
> We'll figure out this later.
|
||||
|
||||
## Test Apple connector
|
||||
|
||||
That's it. The Apple connector should be available in both web and native apps. Don't forget to [Enable connector in sign-in experience](https://docs.logto.io/docs/tutorials/get-started/enable-social-sign-in#enable-connector-in-sign-in-experience).
|
||||
|
||||
# Apple 连接器
|
||||
|
||||
## 开始上手
|
||||
|
||||
如果你还不知道连接器的概念,或者还不知道如何将本连接器添加至你的「登录体验」,请先参见 [Logto 教程](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in)。
|
||||
|
||||
> ℹ️ **Note**
|
||||
>
|
||||
> 如果你的应用有其他的社交登录方式,AppStore 要求必须同时有 Apple 登录。
|
||||
> 如果同时提供 Android 应用,在 Android 设备上同时提供 Apple 登录会让用户体验很棒。
|
||||
|
||||
在继续前,你需要加入 [Apple Developer Program](https://developer.apple.com/programs/)。
|
||||
|
||||
### 为你的应用启用「通过 Apple 登录」
|
||||
|
||||
> ⚠️ **Caution**
|
||||
>
|
||||
> 即使你只想在 web 应用中实现「通过 Apple 登录」,你仍需要拥有一个拥抱 AppStore 生态的应用(即:有一个有效的 App ID)。
|
||||
|
||||
你可以通过 Xcode -> Project settings -> Signing & Capabilities 来启用,或者访问 [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list/bundleId)。
|
||||
|
||||

|
||||
|
||||
参见 [Apple 官方文档](https://developer.apple.com/documentation/sign_in_with_apple/configuring_your_environment_for_sign_in_with_apple) 里的「Enable an App ID」章节以了解更多。
|
||||
|
||||
### 创建一个 identifier
|
||||
|
||||
1. 访问 [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list/serviceId),并点按在「Identifier」旁边的「+」按钮。
|
||||
2. 在「Register a new identifier」页面,选择「Services IDs」并点按「Continue」。
|
||||
3. 填写「Description」与「Identifier」(例如 `Logto Test` 和 `io.logto.test`),并点按「Continue」。
|
||||
4. 再次检查相关信息并点按「Register」。
|
||||
|
||||
### 为你的 identifier 启用「通过 Apple 登录」
|
||||
|
||||
点按你刚刚创建的 identifier。在详情页勾选「Sign in with Apple」并点按「Configure」。
|
||||
|
||||

|
||||
|
||||
在打开的对话框中,选择刚刚启用了「通过 Apple 登录」的 App ID。
|
||||
|
||||
输入你的 Logto 实例域名(不含协议和端口),例如 `your.logto.domain`;并输入「Return URL」(即 Redirect URI)。Return URL 的值是 Logto URL 加上 `/callback/${connector_id}`,例如 `https://your.logto.domain/callback/apple-universal`。在管理控制台创建了 Apple 连接器之后,在详情页可以找到生成的随机 `connector_id`。
|
||||
|
||||

|
||||
|
||||
点按「Next」以及「Done」以关闭对话框。点按右上角的「Continue」,接着点按「Save」以保存你的配置。
|
||||
|
||||
> ⚠️ **Caution**
|
||||
>
|
||||
> Apple _不允许_ HTTP 协议或 `localhost` 域名作为 Return URL。
|
||||
>
|
||||
> 如果你想在本地进行测试,你需要编辑 `/etc/hosts` 文件以映射 localhost 到一个自定义域名,并设置一个本地的 HTTPS 环境。[mkcert](https://github.com/FiloSottile/mkcert) 可以帮助你设置本地 HTTPS。
|
||||
|
||||
> ℹ️ **Note**
|
||||
>
|
||||
> 本连接器暂时不支持自定义 `scope`(例如 name,email)。因为在 `scope` 非空时,Apple 要求 `response_mode` 为 `form_post`,与现在连接器的设计不兼容。
|
||||
>
|
||||
> 我们将稍后解决这个问题。
|
||||
|
||||
## 测试 Apple 连接器
|
||||
|
||||
大功告成。Apple 连接器应该在 web 和原生应用中都可用了。别忘了 [在登录体验中启用本连接器](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in/#%E5%9C%A8%E7%99%BB%E5%BD%95%E4%BD%93%E9%AA%8C%E4%B8%AD%E5%90%AF%E7%94%A8%E8%BF%9E%E6%8E%A5%E5%99%A8)。
|
BIN
packages/connectors/connector-apple/docs/domain-and-url.png
Normal file
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 15 KiB |
3
packages/connectors/connector-apple/logo-dark.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.548 12.7505C18.5586 11.832 18.8022 10.9313 19.2558 10.1326C19.7094 9.3339 20.3583 8.6634 21.1417 8.18388C20.6437 7.47486 19.9882 6.89091 19.2266 6.47786C18.465 6.06481 17.618 5.83391 16.7521 5.80328C14.8829 5.6138 13.1049 6.90359 12.1578 6.90359C11.2101 6.90359 9.74697 5.83111 8.19675 5.8607C7.17936 5.88898 6.18677 6.18109 5.31623 6.70839C4.44569 7.23569 3.72706 7.9801 3.23077 8.86868C1.1159 12.5409 2.69095 17.9811 4.75194 20.9608C5.76048 22.4175 6.96267 24.0582 8.54126 23.9984C10.0625 23.9386 10.6366 23.0159 12.4733 23.0159C14.3105 23.0159 14.8273 23.9984 16.4343 23.9688C18.0698 23.9386 19.1079 22.482 20.1082 21.0199C20.8242 19.9685 21.3837 18.8187 21.7692 17.6065C20.8169 17.1998 20.0045 16.5233 19.4321 15.6604C18.8597 14.7975 18.5524 13.7859 18.548 12.7505ZM15.5278 3.83196C16.4336 2.76959 16.8824 1.39205 16.7762 0C15.4087 0.132651 14.1437 0.783462 13.2408 1.81897C12.7902 2.32062 12.4453 2.90785 12.2266 3.54565C12.0078 4.18345 11.9197 4.85876 11.9676 5.53133C12.6517 5.54078 13.3288 5.39237 13.9463 5.09765C14.5637 4.80292 15.1049 4.36981 15.5278 3.83196Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
3
packages/connectors/connector-apple/logo.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.548 12.7505C18.5586 11.832 18.8022 10.9313 19.2558 10.1326C19.7094 9.3339 20.3583 8.6634 21.1417 8.18388C20.6437 7.47486 19.9882 6.89091 19.2266 6.47786C18.465 6.06481 17.618 5.83391 16.7521 5.80328C14.8829 5.6138 13.1049 6.90359 12.1578 6.90359C11.2101 6.90359 9.74697 5.83111 8.19675 5.8607C7.17936 5.88898 6.18677 6.18109 5.31623 6.70839C4.44569 7.23569 3.72706 7.9801 3.23077 8.86868C1.1159 12.5409 2.69095 17.9811 4.75194 20.9608C5.76048 22.4175 6.96267 24.0582 8.54126 23.9984C10.0625 23.9386 10.6366 23.0159 12.4733 23.0159C14.3105 23.0159 14.8273 23.9984 16.4343 23.9688C18.0698 23.9386 19.1079 22.482 20.1082 21.0199C20.8242 19.9685 21.3837 18.8187 21.7692 17.6065C20.8169 17.1998 20.0045 16.5233 19.4321 15.6604C18.8597 14.7975 18.5524 13.7859 18.548 12.7505ZM15.5278 3.83196C16.4336 2.76959 16.8824 1.39205 16.7762 0C15.4087 0.132651 14.1437 0.783462 13.2408 1.81897C12.7902 2.32062 12.4453 2.90785 12.2266 3.54565C12.0078 4.18345 11.9197 4.85876 11.9676 5.53133C12.6517 5.54078 13.3288 5.39237 13.9463 5.09765C14.5637 4.80292 15.1049 4.36981 15.5278 3.83196Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
10
packages/connectors/connector-apple/package.extend.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package",
|
||||
"name": "@logto/connector-apple",
|
||||
"version": "1.0.0",
|
||||
"description": "Apple web connector implementation.",
|
||||
"dependencies": {
|
||||
"@logto/core-kit": "1.0.0-beta.30",
|
||||
"jose": "^4.3.8"
|
||||
}
|
||||
}
|
43
packages/connectors/connector-apple/src/constant.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import type { ConnectorMetadata } from '@logto/connector-kit';
|
||||
import { ConnectorPlatform, ConnectorConfigFormItemType } from '@logto/connector-kit';
|
||||
|
||||
// https://appleid.apple.com/.well-known/openid-configuration
|
||||
export const issuer = 'https://appleid.apple.com';
|
||||
export const authorizationEndpoint = `${issuer}/auth/authorize`;
|
||||
export const accessTokenEndpoint = `${issuer}/auth/token`;
|
||||
export const jwksUri = `${issuer}/auth/keys`;
|
||||
|
||||
// Note: only support fixed scope for v1.
|
||||
export const scope = ''; // Note: `openid` is required when adding more scope(s)
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'apple-universal',
|
||||
target: 'apple',
|
||||
platform: ConnectorPlatform.Universal,
|
||||
name: {
|
||||
en: 'Apple',
|
||||
'zh-CN': 'Apple',
|
||||
'tr-TR': 'Apple',
|
||||
ko: 'Apple',
|
||||
},
|
||||
logo: './logo.svg',
|
||||
logoDark: './logo-dark.svg',
|
||||
description: {
|
||||
en: 'Apple is a multinational high-end provider of hardware and software.',
|
||||
'zh-CN': 'Apple 是全球领先的高端消费者软硬件提供商。',
|
||||
'tr-TR': 'Apple, çok uluslu bir üst düzey donanım ve yazılım sağlayıcısıdır.',
|
||||
ko: 'Apple은 하드웨어와 소프트웨어의 다국적 공급자 입니다.',
|
||||
},
|
||||
readme: './README.md',
|
||||
formItems: [
|
||||
{
|
||||
key: 'clientId',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: true,
|
||||
label: 'Client ID',
|
||||
placeholder: '<client-id>',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const defaultTimeout = 5000;
|
93
packages/connectors/connector-apple/src/index.test.ts
Normal file
|
@ -0,0 +1,93 @@
|
|||
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit';
|
||||
|
||||
import { mockedConfig } from './mock.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const getConfig = jest.fn().mockResolvedValue(mockedConfig);
|
||||
|
||||
const jwtVerify = jest.fn();
|
||||
|
||||
jest.unstable_mockModule('jose', () => ({
|
||||
jwtVerify,
|
||||
createRemoteJWKSet: jest.fn(),
|
||||
}));
|
||||
|
||||
const { authorizationEndpoint } = await import('./constant.js');
|
||||
const { default: createConnector } = await import('./index.js');
|
||||
|
||||
describe('getAuthorizationUri', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should get a valid uri by redirectUri and state', async () => {
|
||||
const connector = await createConnector({ getConfig });
|
||||
const setSession = jest.fn();
|
||||
const authorizationUri = await connector.getAuthorizationUri(
|
||||
{
|
||||
state: 'some_state',
|
||||
redirectUri: 'http://localhost:3000/callback',
|
||||
connectorId: 'some_connector_id',
|
||||
connectorFactoryId: 'some_connector_factory_id',
|
||||
jti: 'some_jti',
|
||||
headers: {},
|
||||
},
|
||||
setSession
|
||||
);
|
||||
|
||||
const { origin, pathname, searchParams } = new URL(authorizationUri);
|
||||
expect(origin + pathname).toEqual(authorizationEndpoint);
|
||||
expect(searchParams.get('client_id')).toEqual('<client-id>');
|
||||
expect(searchParams.get('redirect_uri')).toEqual('http://localhost:3000/callback');
|
||||
expect(searchParams.get('state')).toEqual('some_state');
|
||||
expect(searchParams.get('response_type')).toEqual('code id_token');
|
||||
expect(searchParams.get('response_mode')).toEqual('fragment');
|
||||
expect(searchParams.has('scope')).toBeTruthy();
|
||||
expect(searchParams.has('nonce')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserInfo', () => {
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should get user info from id token payload', async () => {
|
||||
const userId = 'userId';
|
||||
const mockJwtVerify = jwtVerify;
|
||||
mockJwtVerify.mockImplementationOnce(() => ({ payload: { sub: userId } }));
|
||||
const connector = await createConnector({ getConfig });
|
||||
const userInfo = await connector.getUserInfo({ id_token: 'idToken' }, jest.fn());
|
||||
expect(userInfo).toEqual({ id: userId });
|
||||
});
|
||||
|
||||
it('should throw if id token is missing', async () => {
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(connector.getUserInfo({}, jest.fn())).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.General, '{}')
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if verify id token failed', async () => {
|
||||
const mockJwtVerify = jwtVerify;
|
||||
mockJwtVerify.mockImplementationOnce(() => {
|
||||
throw new Error('jwtVerify failed');
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(connector.getUserInfo({ id_token: 'id_token' }, jest.fn())).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if the id token payload does not contains sub', async () => {
|
||||
const mockJwtVerify = jwtVerify;
|
||||
mockJwtVerify.mockImplementationOnce(() => ({
|
||||
payload: { iat: 123_456 },
|
||||
}));
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(connector.getUserInfo({ id_token: 'id_token' }, jest.fn())).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid)
|
||||
);
|
||||
});
|
||||
});
|
132
packages/connectors/connector-apple/src/index.ts
Normal file
|
@ -0,0 +1,132 @@
|
|||
import type {
|
||||
GetAuthorizationUri,
|
||||
GetUserInfo,
|
||||
GetConnectorConfig,
|
||||
CreateConnector,
|
||||
SocialConnector,
|
||||
} from '@logto/connector-kit';
|
||||
import {
|
||||
ConnectorError,
|
||||
ConnectorErrorCodes,
|
||||
validateConfig,
|
||||
ConnectorType,
|
||||
} from '@logto/connector-kit';
|
||||
import { generateStandardId } from '@logto/core-kit';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
||||
|
||||
import { scope, defaultMetadata, jwksUri, issuer, authorizationEndpoint } from './constant.js';
|
||||
import type { AppleConfig } from './types.js';
|
||||
import { appleConfigGuard, dataGuard } from './types.js';
|
||||
|
||||
const generateNonce = () => generateStandardId();
|
||||
|
||||
const getAuthorizationUri =
|
||||
(getConfig: GetConnectorConfig): GetAuthorizationUri =>
|
||||
async ({ state, redirectUri }, setSession) => {
|
||||
const config = await getConfig(defaultMetadata.id);
|
||||
|
||||
validateConfig<AppleConfig>(config, appleConfigGuard);
|
||||
|
||||
const nonce = generateNonce();
|
||||
|
||||
const queryParameters = new URLSearchParams({
|
||||
client_id: config.clientId,
|
||||
redirect_uri: redirectUri,
|
||||
scope,
|
||||
state,
|
||||
nonce,
|
||||
// https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms#3332113
|
||||
response_type: 'code id_token',
|
||||
response_mode: 'fragment',
|
||||
});
|
||||
|
||||
assert(
|
||||
setSession,
|
||||
new ConnectorError(ConnectorErrorCodes.NotImplemented, {
|
||||
message: "'setSession' is not implemented.",
|
||||
})
|
||||
);
|
||||
await setSession({ nonce });
|
||||
|
||||
return `${authorizationEndpoint}?${queryParameters.toString()}`;
|
||||
};
|
||||
|
||||
const getUserInfo =
|
||||
(getConfig: GetConnectorConfig): GetUserInfo =>
|
||||
async (data, getSession) => {
|
||||
const { id_token: idToken } = await authorizationCallbackHandler(data);
|
||||
|
||||
if (!idToken) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid);
|
||||
}
|
||||
|
||||
const config = await getConfig(defaultMetadata.id);
|
||||
validateConfig<AppleConfig>(config, appleConfigGuard);
|
||||
|
||||
const { clientId } = config;
|
||||
|
||||
try {
|
||||
const { payload } = await jwtVerify(idToken, createRemoteJWKSet(new URL(jwksUri)), {
|
||||
issuer,
|
||||
audience: clientId,
|
||||
});
|
||||
|
||||
if (payload.nonce) {
|
||||
// TODO @darcy: need to specify error code
|
||||
assert(
|
||||
getSession,
|
||||
new ConnectorError(ConnectorErrorCodes.NotImplemented, {
|
||||
message: "'getSession' is not implemented.",
|
||||
})
|
||||
);
|
||||
const { nonce: validationNonce } = await getSession();
|
||||
|
||||
assert(
|
||||
validationNonce,
|
||||
new ConnectorError(ConnectorErrorCodes.General, {
|
||||
message: "'nonce' not presented in session storage.",
|
||||
})
|
||||
);
|
||||
|
||||
assert(
|
||||
validationNonce === payload.nonce,
|
||||
new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid, {
|
||||
message: "IdToken validation failed due to 'nonce' mismatch.",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (!payload.sub) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid);
|
||||
}
|
||||
|
||||
return {
|
||||
id: payload.sub,
|
||||
};
|
||||
} catch {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid);
|
||||
}
|
||||
};
|
||||
|
||||
const authorizationCallbackHandler = async (parameterObject: unknown) => {
|
||||
const result = dataGuard.safeParse(parameterObject);
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(parameterObject));
|
||||
}
|
||||
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const createAppleConnector: CreateConnector<SocialConnector> = async ({ getConfig }) => {
|
||||
return {
|
||||
metadata: defaultMetadata,
|
||||
type: ConnectorType.Social,
|
||||
configGuard: appleConfigGuard,
|
||||
getAuthorizationUri: getAuthorizationUri(getConfig),
|
||||
getUserInfo: getUserInfo(getConfig),
|
||||
};
|
||||
};
|
||||
|
||||
export default createAppleConnector;
|
4
packages/connectors/connector-apple/src/mock.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export const mockedConfig = {
|
||||
clientId: '<client-id>',
|
||||
clientSecret: '<client-secret>',
|
||||
};
|
12
packages/connectors/connector-apple/src/types.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const appleConfigGuard = z.object({
|
||||
clientId: z.string(),
|
||||
});
|
||||
|
||||
export type AppleConfig = z.infer<typeof appleConfigGuard>;
|
||||
|
||||
// https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/configuring_your_webpage_for_sign_in_with_apple#3331292
|
||||
export const dataGuard = z.object({
|
||||
id_token: z.string(),
|
||||
});
|
69
packages/connectors/connector-aws-ses/CHANGELOG.md
Normal file
|
@ -0,0 +1,69 @@
|
|||
# @logto/connector-aws-ses
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 4ed07d2: bump connector-kit to v1.0.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 8c0654a: - Add "Generic" verification code type, remove deprecated "Continue" code type. Generic type verification code is used when user needs to send and verify verification code through our management APIs. Correspondingly, a "Generic" type mail or SMS template should be configured in the connector config.
|
||||
- Replace the term "passcode" with "verification code".
|
||||
- 269d701: The console connector configuration page has been changed from JSON format to form view.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9ff0638: update connector-kit version
|
||||
- 7b0bf69: Bump version to upgrade connector kit
|
||||
- 4ec0889: bump connector-kit version
|
||||
- d8b9dea: 1. Update `@logto/connector-kit` from `1.0.0-beta.32` to `1.0.0-beta.33`.
|
||||
- d183d6d: Upgrade connector-kit
|
||||
- a5f57f8: Update README, default value and type guard of passwordless connectors' template field since we will use Generic template for all other cases rather than Sign-in, Register and ForgotPassword.
|
||||
|
||||
## 1.0.0-beta.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4ec0889: bump connector-kit version
|
||||
|
||||
## 1.0.0-beta.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a5f57f8: Update README, default value and type guard of passwordless connectors' template field since we will use Generic template for all other cases rather than Sign-in, Register and ForgotPassword.
|
||||
|
||||
## 1.0.0-beta.20
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 269d701: The console connector configuration page has been changed from JSON format to form view.
|
||||
|
||||
## 1.0.0-beta.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7b0bf69: Bump version to upgrade connector kit
|
||||
|
||||
## 1.0.0-beta.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9ff0638: update connector-kit version
|
||||
|
||||
## 1.0.0-beta.17
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 8c0654a: - Add "Generic" verification code type, remove deprecated "Continue" code type. Generic type verification code is used when user needs to send and verify verification code through our management APIs. Correspondingly, a "Generic" type mail or SMS template should be configured in the connector config.
|
||||
- Replace the term "passcode" with "verification code".
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d8b9dea: 1. Update `@logto/connector-kit` from `1.0.0-beta.32` to `1.0.0-beta.33`.
|
||||
|
||||
## 1.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d183d6d: Upgrade connector-kit
|
75
packages/connectors/connector-aws-ses/README.md
Normal file
|
@ -0,0 +1,75 @@
|
|||
# AWS direct mail connector
|
||||
|
||||
The official Logto connector for AWS connector for direct mail service.
|
||||
|
||||
- [AWS direct mail connector](#aws-direct-mail-connector)
|
||||
- [Get started](#get-started)
|
||||
- [Configure a mail service in the AWS service console](#configure-a-mail-service-in-the-aws-service-console)
|
||||
- [Register AWS account](#register-aws-account)
|
||||
- [Create a identity](#create-a-identity)
|
||||
- [Configuration of the connector](#configuration-of-the-connector)
|
||||
- [Test the Amazon SES connector](#test-the-amazon-ses-connector)
|
||||
- [Configure types](#configure-types)
|
||||
|
||||
## Get started
|
||||
Amazon SES is a cloud email service provider that can integrate into any application for bulk email sending.
|
||||
|
||||
Logto team to call the Amazon Simple Email Service APIs, with the help of which Logto end-users can register and sign in to their Logto account via mail verification code.
|
||||
|
||||
## Configure a mail service in the AWS service console
|
||||
|
||||
> 💡 **Tip**
|
||||
>
|
||||
> You can skip some sections if you have already finished.
|
||||
|
||||
### Register AWS account
|
||||
|
||||
Go to [AWS](https://aws.amazon.com/) and register an account.
|
||||
|
||||
### Create a identity
|
||||
|
||||
- Go to `Amazon Simple Email Service` Console
|
||||
- Create an identity, choose one of the following options
|
||||
- Create an domain
|
||||
- Create an email address
|
||||
|
||||
|
||||
### Configuration of the connector
|
||||
|
||||
1. Click your username in the upper right corner of the Amazon console to enter `Security Credentials`. If you don't have one, create an `AccessKey` and save it carefully.
|
||||
2. Complete the settings of the `Amazon Simple Email Service` connector:
|
||||
- Use the `AccessKey ID` and `AccessKey Secret` obtained in step 1 to fill in `accessKeyId` and `accessKeySecret` respectively.
|
||||
- `region`: Fill in the `region` field with the region of the identity you use to send mail.
|
||||
- `emailAddress`: The email address you use to send mail, in the format of `Logto<noreply@logto.io>` or `<noreply@logto.io>`
|
||||
|
||||
the following parameters are optional; parameters description can be found in the [AWS SES API documentation](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_SendEmail.html).
|
||||
|
||||
- `feedbackForwardingEmailAddress`
|
||||
- `feedbackForwardingEmailAddressIdentityArn`
|
||||
- `configurationSetName`
|
||||
|
||||
### Test the Amazon SES connector
|
||||
|
||||
You can type in an email address and click on "Send" to see whether the settings work before "Save and Done".
|
||||
|
||||
That's it. Don't forget to [Enable connector in sign-in experience](https://docs.logto.io/docs/tutorials/get-started/enable-passcode-sign-in/#enable-connector-in-sign-in-experience).
|
||||
|
||||
### Configure types
|
||||
|
||||
| Name | Type |
|
||||
| ----------------------------------------- | ----------------- |
|
||||
| accessKeyId | string |
|
||||
| accessKeySecret | string |
|
||||
| region | string |
|
||||
| emailAddress | string (OPTIONAL) |
|
||||
| emailAddressIdentityArn | string (OPTIONAL) |
|
||||
| feedbackForwardingEmailAddress | string (OPTIONAL) |
|
||||
| feedbackForwardingEmailAddressIdentityArn | string (OPTIONAL) |
|
||||
| configurationSetName | string (OPTIONAL) |
|
||||
| templates | Template[] |
|
||||
|
||||
| Template Properties | Type | Enum values |
|
||||
| ------------------- | ----------- | -----------------------------------------------------|
|
||||
| subject | string | N/A |
|
||||
| content | string | N/A |
|
||||
| usageType | enum string | 'Register' \| 'SignIn' \| 'ForgotPassword' \| 'Generic' |
|
5
packages/connectors/connector-aws-ses/logo-dark.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.76255 10.7123C6.76255 11.0077 6.79448 11.2472 6.85036 11.4228C6.91421 11.5984 6.99403 11.7899 7.10578 11.9975C7.1457 12.0613 7.16166 12.1252 7.16166 12.1811C7.16166 12.2609 7.11377 12.3407 7.01 12.4205L6.50712 12.7558C6.43528 12.8037 6.36344 12.8276 6.29959 12.8276C6.21977 12.8276 6.13994 12.7877 6.06012 12.7159C5.94837 12.5961 5.85259 12.4684 5.77277 12.3407C5.69294 12.205 5.61312 12.0533 5.52532 11.8698C4.90271 12.6041 4.12046 12.9713 3.17857 12.9713C2.50806 12.9713 1.97326 12.7797 1.58213 12.3966C1.19101 12.0134 0.991455 11.5026 0.991455 10.864C0.991455 10.1855 1.23092 9.63476 1.71783 9.21968C2.20474 8.80461 2.8513 8.59708 3.67346 8.59708C3.94485 8.59708 4.22423 8.62102 4.51957 8.66093C4.81491 8.70084 5.11823 8.7647 5.43752 8.83654V8.25384C5.43752 7.6472 5.3098 7.22415 5.06235 6.9767C4.80692 6.72925 4.37589 6.60952 3.76126 6.60952C3.48189 6.60952 3.19453 6.64145 2.89919 6.71329C2.60385 6.78513 2.31649 6.87293 2.03712 6.98468C1.9094 7.04056 1.81362 7.07248 1.75774 7.08845C1.70187 7.10441 1.66196 7.1124 1.63003 7.1124C1.51828 7.1124 1.4624 7.03257 1.4624 6.86495V6.47382C1.4624 6.34611 1.47837 6.25032 1.51828 6.19445C1.55819 6.13857 1.63003 6.0827 1.74178 6.02682C2.02115 5.88314 2.3564 5.76341 2.74753 5.66763C3.13865 5.56386 3.55373 5.51596 3.99274 5.51596C4.94262 5.51596 5.63707 5.73148 6.08407 6.16252C6.52309 6.59356 6.74659 7.24809 6.74659 8.12613V10.7123H6.76255ZM3.5218 11.9256C3.78521 11.9256 4.0566 11.8777 4.34396 11.782C4.63132 11.6862 4.88675 11.5106 5.10226 11.2711C5.22998 11.1194 5.32577 10.9518 5.37366 10.7602C5.42155 10.5687 5.45348 10.3372 5.45348 10.0658V9.73054C5.222 9.67467 4.97455 9.62677 4.71912 9.59485C4.46369 9.56292 4.21625 9.54695 3.9688 9.54695C3.43399 9.54695 3.04287 9.65072 2.77946 9.86624C2.51605 10.0818 2.38833 10.3851 2.38833 10.7842C2.38833 11.1593 2.48412 11.4387 2.68367 11.6303C2.87524 11.8298 3.15462 11.9256 3.5218 11.9256ZM9.93147 12.7877C9.78779 12.7877 9.692 12.7638 9.62815 12.7079C9.56429 12.66 9.50841 12.5482 9.46052 12.3966L7.58471 6.22638C7.53682 6.06673 7.51288 5.96297 7.51288 5.90709C7.51288 5.77938 7.57673 5.70754 7.70445 5.70754H8.4867C8.63836 5.70754 8.74213 5.73148 8.798 5.78736C8.86186 5.83525 8.90975 5.947 8.95765 6.09866L10.2986 11.3828L11.5439 6.09866C11.5838 5.93902 11.6317 5.83525 11.6955 5.78736C11.7594 5.73946 11.8711 5.70754 12.0148 5.70754H12.6534C12.805 5.70754 12.9088 5.73148 12.9727 5.78736C13.0365 5.83525 13.0924 5.947 13.1243 6.09866L14.3855 11.4467L15.7664 6.09866C15.8143 5.93902 15.8702 5.83525 15.9261 5.78736C15.9899 5.73946 16.0937 5.70754 16.2374 5.70754H16.9797C17.1074 5.70754 17.1793 5.77139 17.1793 5.90709C17.1793 5.947 17.1713 5.98691 17.1633 6.0348C17.1553 6.0827 17.1394 6.14655 17.1074 6.23436L15.1837 12.4046C15.1358 12.5642 15.08 12.668 15.0161 12.7159C14.9522 12.7638 14.8485 12.7957 14.7128 12.7957H14.0263C13.8747 12.7957 13.7709 12.7717 13.707 12.7159C13.6432 12.66 13.5873 12.5562 13.5554 12.3966L12.3181 7.24809L11.0889 12.3886C11.049 12.5482 11.0011 12.652 10.9372 12.7079C10.8734 12.7638 10.7616 12.7877 10.6179 12.7877H9.93147ZM20.1885 13.0032C19.7735 13.0032 19.3584 12.9553 18.9593 12.8595C18.5602 12.7638 18.2489 12.66 18.0413 12.5403C17.9136 12.4684 17.8258 12.3886 17.7939 12.3168C17.762 12.2449 17.746 12.1651 17.746 12.0933V11.6862C17.746 11.5185 17.8099 11.4387 17.9296 11.4387C17.9775 11.4387 18.0254 11.4467 18.0733 11.4627C18.1212 11.4786 18.193 11.5106 18.2728 11.5425C18.5442 11.6622 18.8396 11.758 19.1509 11.8219C19.4701 11.8857 19.7814 11.9177 20.1007 11.9177C20.6036 11.9177 20.9947 11.8298 21.2661 11.6542C21.5375 11.4786 21.6812 11.2232 21.6812 10.8959C21.6812 10.6724 21.6094 10.4888 21.4657 10.3372C21.322 10.1855 21.0506 10.0498 20.6595 9.92211L19.5021 9.56292C18.9194 9.37933 18.4883 9.10793 18.2249 8.74874C17.9615 8.39752 17.8258 8.0064 17.8258 7.59132C17.8258 7.25607 17.8977 6.96073 18.0413 6.70531C18.185 6.44988 18.3766 6.22638 18.6161 6.05077C18.8555 5.86718 19.1269 5.73148 19.4462 5.6357C19.7655 5.53991 20.1007 5.5 20.4519 5.5C20.6276 5.5 20.8111 5.50798 20.9868 5.53193C21.1703 5.55588 21.338 5.5878 21.5056 5.61973C21.6652 5.65964 21.8169 5.69955 21.9606 5.74745C22.1043 5.79534 22.216 5.84323 22.2958 5.89113C22.4076 5.95498 22.4874 6.01884 22.5353 6.09068C22.5832 6.15454 22.6071 6.24234 22.6071 6.35409V6.72925C22.6071 6.89688 22.5433 6.98468 22.4235 6.98468C22.3597 6.98468 22.2559 6.95275 22.1202 6.8889C21.6652 6.68136 21.1544 6.57759 20.5876 6.57759C20.1327 6.57759 19.7735 6.64943 19.526 6.80109C19.2786 6.95275 19.1509 7.18423 19.1509 7.5115C19.1509 7.735 19.2307 7.92658 19.3903 8.07824C19.55 8.2299 19.8453 8.38156 20.2684 8.51726L21.4018 8.87645C21.9765 9.06004 22.3916 9.31547 22.6391 9.64274C22.8865 9.97001 23.0062 10.3452 23.0062 10.7602C23.0062 11.1035 22.9344 11.4148 22.7987 11.6862C22.655 11.9576 22.4634 12.197 22.216 12.3886C21.9686 12.5882 21.6732 12.7318 21.33 12.8356C20.9708 12.9474 20.5956 13.0032 20.1885 13.0032Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.6981 16.8824C19.0719 18.8221 15.2565 19.8518 11.9758 19.8518C7.37807 19.8518 3.23533 18.1516 0.106324 15.3259C-0.141123 15.1024 0.0823772 14.7991 0.377717 14.9747C3.76215 16.9383 7.93682 18.1276 12.2552 18.1276C15.1687 18.1276 18.3695 17.521 21.3149 16.2758C21.7539 16.0762 22.1291 16.5631 21.6981 16.8824Z" fill="#FF9900"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.792 15.6372C22.4568 15.2062 20.573 15.4297 19.7189 15.5335C19.4635 15.5654 19.4236 15.3419 19.655 15.1743C21.1557 14.1206 23.6222 14.4239 23.9095 14.7752C24.1969 15.1344 23.8297 17.6008 22.4248 18.7822C22.2093 18.9658 22.0018 18.87 22.0976 18.6305C22.4169 17.8403 23.1273 16.0603 22.792 15.6372Z" fill="#FF9900"/>
|
||||
</svg>
|
After Width: | Height: | Size: 5.6 KiB |
5
packages/connectors/connector-aws-ses/logo.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.76255 10.7123C6.76255 11.0077 6.79448 11.2472 6.85036 11.4228C6.91421 11.5984 6.99403 11.7899 7.10578 11.9975C7.1457 12.0613 7.16166 12.1252 7.16166 12.1811C7.16166 12.2609 7.11377 12.3407 7.01 12.4205L6.50712 12.7558C6.43528 12.8037 6.36344 12.8276 6.29959 12.8276C6.21977 12.8276 6.13994 12.7877 6.06012 12.7159C5.94837 12.5961 5.85259 12.4684 5.77277 12.3407C5.69294 12.205 5.61312 12.0533 5.52532 11.8698C4.90271 12.6041 4.12046 12.9713 3.17857 12.9713C2.50806 12.9713 1.97326 12.7797 1.58213 12.3966C1.19101 12.0134 0.991455 11.5026 0.991455 10.864C0.991455 10.1855 1.23092 9.63476 1.71783 9.21968C2.20474 8.80461 2.8513 8.59708 3.67346 8.59708C3.94485 8.59708 4.22423 8.62102 4.51957 8.66093C4.81491 8.70084 5.11823 8.7647 5.43752 8.83654V8.25384C5.43752 7.6472 5.3098 7.22415 5.06235 6.9767C4.80692 6.72925 4.37589 6.60952 3.76126 6.60952C3.48189 6.60952 3.19453 6.64145 2.89919 6.71329C2.60385 6.78513 2.31649 6.87293 2.03712 6.98468C1.9094 7.04056 1.81362 7.07248 1.75774 7.08845C1.70187 7.10441 1.66196 7.1124 1.63003 7.1124C1.51828 7.1124 1.4624 7.03257 1.4624 6.86495V6.47382C1.4624 6.34611 1.47837 6.25032 1.51828 6.19445C1.55819 6.13857 1.63003 6.0827 1.74178 6.02682C2.02115 5.88314 2.3564 5.76341 2.74753 5.66763C3.13865 5.56386 3.55373 5.51596 3.99274 5.51596C4.94262 5.51596 5.63707 5.73148 6.08407 6.16252C6.52309 6.59356 6.74659 7.24809 6.74659 8.12613V10.7123H6.76255ZM3.5218 11.9256C3.78521 11.9256 4.0566 11.8777 4.34396 11.782C4.63132 11.6862 4.88675 11.5106 5.10226 11.2711C5.22998 11.1194 5.32577 10.9518 5.37366 10.7602C5.42155 10.5687 5.45348 10.3372 5.45348 10.0658V9.73054C5.222 9.67467 4.97455 9.62677 4.71912 9.59485C4.46369 9.56292 4.21625 9.54695 3.9688 9.54695C3.43399 9.54695 3.04287 9.65072 2.77946 9.86624C2.51605 10.0818 2.38833 10.3851 2.38833 10.7842C2.38833 11.1593 2.48412 11.4387 2.68367 11.6303C2.87524 11.8298 3.15462 11.9256 3.5218 11.9256ZM9.93147 12.7877C9.78779 12.7877 9.692 12.7638 9.62815 12.7079C9.56429 12.66 9.50841 12.5482 9.46052 12.3966L7.58471 6.22638C7.53682 6.06673 7.51288 5.96297 7.51288 5.90709C7.51288 5.77938 7.57673 5.70754 7.70445 5.70754H8.4867C8.63836 5.70754 8.74213 5.73148 8.798 5.78736C8.86186 5.83525 8.90975 5.947 8.95765 6.09866L10.2986 11.3828L11.5439 6.09866C11.5838 5.93902 11.6317 5.83525 11.6955 5.78736C11.7594 5.73946 11.8711 5.70754 12.0148 5.70754H12.6534C12.805 5.70754 12.9088 5.73148 12.9727 5.78736C13.0365 5.83525 13.0924 5.947 13.1243 6.09866L14.3855 11.4467L15.7664 6.09866C15.8143 5.93902 15.8702 5.83525 15.9261 5.78736C15.9899 5.73946 16.0937 5.70754 16.2374 5.70754H16.9797C17.1074 5.70754 17.1793 5.77139 17.1793 5.90709C17.1793 5.947 17.1713 5.98691 17.1633 6.0348C17.1553 6.0827 17.1394 6.14655 17.1074 6.23436L15.1837 12.4046C15.1358 12.5642 15.08 12.668 15.0161 12.7159C14.9522 12.7638 14.8485 12.7957 14.7128 12.7957H14.0263C13.8747 12.7957 13.7709 12.7717 13.707 12.7159C13.6432 12.66 13.5873 12.5562 13.5554 12.3966L12.3181 7.24809L11.0889 12.3886C11.049 12.5482 11.0011 12.652 10.9372 12.7079C10.8734 12.7638 10.7616 12.7877 10.6179 12.7877H9.93147ZM20.1885 13.0032C19.7735 13.0032 19.3584 12.9553 18.9593 12.8595C18.5602 12.7638 18.2489 12.66 18.0413 12.5403C17.9136 12.4684 17.8258 12.3886 17.7939 12.3168C17.762 12.2449 17.746 12.1651 17.746 12.0933V11.6862C17.746 11.5185 17.8099 11.4387 17.9296 11.4387C17.9775 11.4387 18.0254 11.4467 18.0733 11.4627C18.1212 11.4786 18.193 11.5106 18.2728 11.5425C18.5442 11.6622 18.8396 11.758 19.1509 11.8219C19.4701 11.8857 19.7814 11.9177 20.1007 11.9177C20.6036 11.9177 20.9947 11.8298 21.2661 11.6542C21.5375 11.4786 21.6812 11.2232 21.6812 10.8959C21.6812 10.6724 21.6094 10.4888 21.4657 10.3372C21.322 10.1855 21.0506 10.0498 20.6595 9.92211L19.5021 9.56292C18.9194 9.37933 18.4883 9.10793 18.2249 8.74874C17.9615 8.39752 17.8258 8.0064 17.8258 7.59132C17.8258 7.25607 17.8977 6.96073 18.0413 6.70531C18.185 6.44988 18.3766 6.22638 18.6161 6.05077C18.8555 5.86718 19.1269 5.73148 19.4462 5.6357C19.7655 5.53991 20.1007 5.5 20.4519 5.5C20.6276 5.5 20.8111 5.50798 20.9868 5.53193C21.1703 5.55588 21.338 5.5878 21.5056 5.61973C21.6652 5.65964 21.8169 5.69955 21.9606 5.74745C22.1043 5.79534 22.216 5.84323 22.2958 5.89113C22.4076 5.95498 22.4874 6.01884 22.5353 6.09068C22.5832 6.15454 22.6071 6.24234 22.6071 6.35409V6.72925C22.6071 6.89688 22.5433 6.98468 22.4235 6.98468C22.3597 6.98468 22.2559 6.95275 22.1202 6.8889C21.6652 6.68136 21.1544 6.57759 20.5876 6.57759C20.1327 6.57759 19.7735 6.64943 19.526 6.80109C19.2786 6.95275 19.1509 7.18423 19.1509 7.5115C19.1509 7.735 19.2307 7.92658 19.3903 8.07824C19.55 8.2299 19.8453 8.38156 20.2684 8.51726L21.4018 8.87645C21.9765 9.06004 22.3916 9.31547 22.6391 9.64274C22.8865 9.97001 23.0062 10.3452 23.0062 10.7602C23.0062 11.1035 22.9344 11.4148 22.7987 11.6862C22.655 11.9576 22.4634 12.197 22.216 12.3886C21.9686 12.5882 21.6732 12.7318 21.33 12.8356C20.9708 12.9474 20.5956 13.0032 20.1885 13.0032Z" fill="#252F3E"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.6981 16.8824C19.0719 18.8221 15.2565 19.8518 11.9758 19.8518C7.37807 19.8518 3.23533 18.1516 0.106324 15.3259C-0.141123 15.1024 0.0823772 14.7991 0.377717 14.9747C3.76215 16.9383 7.93682 18.1276 12.2552 18.1276C15.1687 18.1276 18.3695 17.521 21.3149 16.2758C21.7539 16.0762 22.1291 16.5631 21.6981 16.8824Z" fill="#FF9900"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.792 15.6372C22.4568 15.2062 20.573 15.4297 19.7189 15.5335C19.4635 15.5654 19.4236 15.3419 19.655 15.1743C21.1557 14.1206 23.6222 14.4239 23.9095 14.7752C24.1969 15.1344 23.8297 17.6008 22.4248 18.7822C22.2093 18.9658 22.0018 18.87 22.0976 18.6305C22.4169 17.8403 23.1273 16.0603 22.792 15.6372Z" fill="#FF9900"/>
|
||||
</svg>
|
After Width: | Height: | Size: 5.6 KiB |
10
packages/connectors/connector-aws-ses/package.extend.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "@logto/connector-aws-ses",
|
||||
"version": "1.0.0",
|
||||
"description": "Logto Connector for Amazon SES",
|
||||
"author": "Jeff <admin@breadth.app>",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sesv2": "^3.224.0",
|
||||
"@aws-sdk/types": "^3.226.0"
|
||||
}
|
||||
}
|
115
packages/connectors/connector-aws-ses/src/constant.ts
Normal file
|
@ -0,0 +1,115 @@
|
|||
import type { ConnectorMetadata } from '@logto/connector-kit';
|
||||
import { ConnectorConfigFormItemType } from '@logto/connector-kit';
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'aws-ses-mail',
|
||||
target: 'aws-ses',
|
||||
platform: null,
|
||||
name: {
|
||||
en: 'AWS Direct Mail',
|
||||
'zh-CN': 'AWS邮件推送',
|
||||
'tr-TR': 'AWS Direct Mail',
|
||||
ko: 'AWS 다이렉트 메일',
|
||||
},
|
||||
logo: './logo.svg',
|
||||
logoDark: './logo-dark.svg',
|
||||
description: {
|
||||
en: 'Amazon SES is a cloud email service provider that can integrate into any application for bulk email sending.',
|
||||
'zh-CN':
|
||||
'Amazon SES 是云电子邮件发送服务提供商,它可以集成到任何应用程序中,用于批量发送电子邮件。',
|
||||
'tr-TR':
|
||||
'Amazon SES, toplu e-posta dağıtımı için herhangi bir uygulamaya entegre edilebilen bir bulut e-posta dağıtım hizmeti sağlayıcısıdır.',
|
||||
ko: 'Amazon SES는 모든 애플리케이션에 통합하여 대량으로 이메일을 전송할 수 있는 클라우드 이메일 서비스 공급자입니다.',
|
||||
},
|
||||
readme: './README.md',
|
||||
formItems: [
|
||||
{
|
||||
key: 'accessKeyId',
|
||||
label: 'Access Key ID',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: true,
|
||||
placeholder: '<access-key-id>',
|
||||
},
|
||||
{
|
||||
key: 'accessKeySecret',
|
||||
label: 'Access Key Secret',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: true,
|
||||
placeholder: '<access-key-secret>',
|
||||
},
|
||||
{
|
||||
key: 'region',
|
||||
label: 'Region',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: true,
|
||||
placeholder: '<region>',
|
||||
},
|
||||
{
|
||||
key: 'emailAddress',
|
||||
label: 'Email Address',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: false,
|
||||
placeholder: '<email-address>',
|
||||
},
|
||||
{
|
||||
key: 'emailAddressIdentityArn',
|
||||
label: 'Email Address Identity ARN',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: false,
|
||||
placeholder: '<email-address-identity-arn>',
|
||||
},
|
||||
{
|
||||
key: 'feedbackForwardingEmailAddress',
|
||||
label: 'Feedback Forwarding Email Address',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: false,
|
||||
placeholder: '<feedback-forwarding-email-address>',
|
||||
},
|
||||
{
|
||||
key: 'feedbackForwardingEmailAddressIdentityArn',
|
||||
label: 'Feedback Forwarding Email Address Identity ARN',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: false,
|
||||
placeholder: '<feedback-forwarding-email-address-identity-arn>',
|
||||
},
|
||||
{
|
||||
key: 'configurationSetName',
|
||||
label: 'Configuration Set Name',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: false,
|
||||
placeholder: '<configuration-set-name>',
|
||||
},
|
||||
{
|
||||
key: 'templates',
|
||||
label: 'Templates',
|
||||
type: ConnectorConfigFormItemType.Json,
|
||||
required: true,
|
||||
defaultValue: [
|
||||
{
|
||||
usageType: 'SignIn',
|
||||
subject: '<sign-in-template-subject>',
|
||||
content:
|
||||
'Your Logto sign-in verification code is {{code}}. The code will remain active for 10 minutes.',
|
||||
},
|
||||
{
|
||||
usageType: 'Register',
|
||||
subject: '<register-template-subject>',
|
||||
content:
|
||||
'Your Logto sign-up verification code is {{code}}. The code will remain active for 10 minutes.',
|
||||
},
|
||||
{
|
||||
usageType: 'ForgotPassword',
|
||||
subject: '<forgot-password-template-subject>',
|
||||
content:
|
||||
'Your Logto password change verification code is {{code}}. The code will remain active for 10 minutes.',
|
||||
},
|
||||
{
|
||||
usageType: 'Generic',
|
||||
subject: '<generic-template-subject>',
|
||||
content:
|
||||
'Your Logto verification code is {{code}}. The code will remain active for 10 minutes.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
67
packages/connectors/connector-aws-ses/src/index.test.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { SESv2Client } from '@aws-sdk/client-sesv2';
|
||||
import { VerificationCodeType } from '@logto/connector-kit';
|
||||
|
||||
import createConnector from './index.js';
|
||||
import { mockedConfig } from './mock.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const getConfig = jest.fn().mockResolvedValue(mockedConfig);
|
||||
|
||||
jest.spyOn(SESv2Client.prototype, 'send').mockResolvedValue({
|
||||
MessageId: 'mocked-message-id',
|
||||
$metadata: {
|
||||
httpStatusCode: 200,
|
||||
},
|
||||
} as never);
|
||||
|
||||
describe('sendMessage()', () => {
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call SendMail() and replace code in content', async () => {
|
||||
const connector = await createConnector({ getConfig });
|
||||
const toMail = 'to@email.com';
|
||||
const { emailAddress } = mockedConfig;
|
||||
await connector.sendMessage({
|
||||
to: toMail,
|
||||
type: VerificationCodeType.SignIn,
|
||||
payload: { code: '1234' },
|
||||
});
|
||||
const toExpected = [toMail];
|
||||
expect(SESv2Client.prototype.send).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
input: {
|
||||
FromEmailAddress: emailAddress,
|
||||
Destination: { ToAddresses: toExpected },
|
||||
Content: {
|
||||
Simple: {
|
||||
Subject: { Data: 'subject', Charset: 'utf8' },
|
||||
Body: {
|
||||
Html: {
|
||||
Data: 'Your code is 1234, 1234 is your code',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
FeedbackForwardingEmailAddress: undefined,
|
||||
FeedbackForwardingEmailAddressIdentityArn: undefined,
|
||||
FromEmailAddressIdentityArn: undefined,
|
||||
ConfigurationSetName: undefined,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if template is missing', async () => {
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(
|
||||
connector.sendMessage({
|
||||
to: 'to@email.com',
|
||||
type: VerificationCodeType.Test,
|
||||
payload: { code: '1234' },
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
69
packages/connectors/connector-aws-ses/src/index.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
import type { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2';
|
||||
import { SESv2ServiceException } from '@aws-sdk/client-sesv2';
|
||||
import type {
|
||||
CreateConnector,
|
||||
EmailConnector,
|
||||
GetConnectorConfig,
|
||||
SendMessageFunction,
|
||||
} from '@logto/connector-kit';
|
||||
import {
|
||||
ConnectorError,
|
||||
ConnectorErrorCodes,
|
||||
ConnectorType,
|
||||
validateConfig,
|
||||
} from '@logto/connector-kit';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
|
||||
import { defaultMetadata } from './constant.js';
|
||||
import type { AwsSesConfig } from './types.js';
|
||||
import { awsSesConfigGuard } from './types.js';
|
||||
import { makeClient, makeCommand, makeEmailContent } from './utils.js';
|
||||
|
||||
const sendMessage =
|
||||
(getConfig: GetConnectorConfig): SendMessageFunction =>
|
||||
async (data, inputConfig) => {
|
||||
const { to, type, payload } = data;
|
||||
const config = inputConfig ?? (await getConfig(defaultMetadata.id));
|
||||
validateConfig<AwsSesConfig>(config, awsSesConfigGuard);
|
||||
const { accessKeyId, accessKeySecret, region, templates } = config;
|
||||
const template = templates.find((template) => template.usageType === type);
|
||||
|
||||
assert(
|
||||
template,
|
||||
new ConnectorError(
|
||||
ConnectorErrorCodes.TemplateNotFound,
|
||||
`Cannot find template for type: ${type}`
|
||||
)
|
||||
);
|
||||
|
||||
const client: SESv2Client = makeClient(accessKeyId, accessKeySecret, region);
|
||||
const emailContent = makeEmailContent(template, payload);
|
||||
const command: SendEmailCommand = makeCommand(config, emailContent, to);
|
||||
|
||||
try {
|
||||
const response = await client.send(command);
|
||||
|
||||
if (response.$metadata.httpStatusCode !== 200) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, { response });
|
||||
}
|
||||
|
||||
return response.MessageId;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof SESv2ServiceException) {
|
||||
const { message } = error;
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const createAwsSesConnector: CreateConnector<EmailConnector> = async ({ getConfig }) => {
|
||||
return {
|
||||
metadata: defaultMetadata,
|
||||
type: ConnectorType.Email,
|
||||
configGuard: awsSesConfigGuard,
|
||||
sendMessage: sendMessage(getConfig),
|
||||
};
|
||||
};
|
||||
|
||||
export default createAwsSesConnector;
|
28
packages/connectors/connector-aws-ses/src/mock.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
export const mockedConfig = {
|
||||
accessKeyId: 'accessKeyId',
|
||||
accessKeySecret: 'accessKeySecret+cltHAJ',
|
||||
region: 'region',
|
||||
emailAddress: 'fromEmail',
|
||||
templates: [
|
||||
{
|
||||
usageType: 'SignIn',
|
||||
content: 'Your code is {{code}}, {{code}} is your code',
|
||||
subject: 'subject',
|
||||
},
|
||||
{
|
||||
usageType: 'Register',
|
||||
content: 'Your code is {{code}}, {{code}} is your code',
|
||||
subject: 'subject',
|
||||
},
|
||||
{
|
||||
usageType: 'ForgotPassword',
|
||||
content: 'Your code is {{code}}, {{code}} is your code',
|
||||
subject: 'subject',
|
||||
},
|
||||
{
|
||||
usageType: 'Generic',
|
||||
content: 'Your code is {{code}}, {{code}} is your code',
|
||||
subject: 'subject',
|
||||
},
|
||||
],
|
||||
};
|
44
packages/connectors/connector-aws-ses/src/types.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* UsageType here is used to specify the use case of the template, can be either
|
||||
* 'Register', 'SignIn', 'ForgotPassword', 'Generic'.
|
||||
*/
|
||||
const requiredTemplateUsageTypes = ['Register', 'SignIn', 'ForgotPassword', 'Generic'];
|
||||
const templateGuard = z.object({
|
||||
usageType: z.string(),
|
||||
subject: z.string(),
|
||||
content: z.string(), // With variable {{code}}, support HTML
|
||||
});
|
||||
|
||||
export type Template = z.infer<typeof templateGuard>;
|
||||
|
||||
export const awsSesConfigGuard = z.object({
|
||||
accessKeyId: z.string(),
|
||||
accessKeySecret: z.string(),
|
||||
region: z.string(),
|
||||
emailAddress: z.string().optional(),
|
||||
emailAddressIdentityArn: z.string().optional(),
|
||||
templates: z.array(templateGuard).refine(
|
||||
(templates) =>
|
||||
requiredTemplateUsageTypes.every((requiredType) =>
|
||||
templates.map((template) => template.usageType).includes(requiredType)
|
||||
),
|
||||
(templates) => ({
|
||||
message: `Template with UsageType (${requiredTemplateUsageTypes
|
||||
.filter(
|
||||
(requiredType) => !templates.map((template) => template.usageType).includes(requiredType)
|
||||
)
|
||||
.join(', ')}) should be provided!`,
|
||||
})
|
||||
),
|
||||
feedbackForwardingEmailAddress: z.string().optional(),
|
||||
feedbackForwardingEmailAddressIdentityArn: z.string().optional(),
|
||||
configurationSetName: z.string().optional(),
|
||||
});
|
||||
|
||||
export type AwsSesConfig = z.infer<typeof awsSesConfigGuard>;
|
||||
|
||||
export type Payload = {
|
||||
code: string | number;
|
||||
};
|
50
packages/connectors/connector-aws-ses/src/utils.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import type { EmailContent } from '@aws-sdk/client-sesv2';
|
||||
import { SendEmailCommand, SESv2Client } from '@aws-sdk/client-sesv2';
|
||||
import type { AwsCredentialIdentity } from '@aws-sdk/types';
|
||||
|
||||
import type { AwsSesConfig, Template, Payload } from './types.js';
|
||||
|
||||
export const makeClient = (
|
||||
accessKeyId: string,
|
||||
secretAccessKey: string,
|
||||
region: string
|
||||
): SESv2Client => {
|
||||
const credentials: AwsCredentialIdentity = {
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
};
|
||||
|
||||
return new SESv2Client({ credentials, region });
|
||||
};
|
||||
|
||||
export const makeEmailContent = (template: Template, payload: Payload): EmailContent => {
|
||||
return {
|
||||
Simple: {
|
||||
Subject: { Data: template.subject, Charset: 'utf8' },
|
||||
Body: {
|
||||
Html: {
|
||||
Data:
|
||||
typeof payload.code === 'string'
|
||||
? template.content.replace(/{{code}}/g, payload.code)
|
||||
: template.content,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const makeCommand = (
|
||||
config: AwsSesConfig,
|
||||
emailContent: EmailContent,
|
||||
to: string
|
||||
): SendEmailCommand => {
|
||||
return new SendEmailCommand({
|
||||
Destination: { ToAddresses: [to] },
|
||||
Content: emailContent,
|
||||
FromEmailAddress: config.emailAddress,
|
||||
FromEmailAddressIdentityArn: config.emailAddressIdentityArn,
|
||||
FeedbackForwardingEmailAddress: config.feedbackForwardingEmailAddress,
|
||||
FeedbackForwardingEmailAddressIdentityArn: config.feedbackForwardingEmailAddressIdentityArn,
|
||||
ConfigurationSetName: config.configurationSetName,
|
||||
});
|
||||
};
|
110
packages/connectors/connector-azuread/CHANGELOG.md
Normal file
|
@ -0,0 +1,110 @@
|
|||
# Change Log
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 4ed07d2: bump connector-kit to v1.0.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 269d701: The console connector configuration page has been changed from JSON format to form view.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9ff0638: update connector-kit version
|
||||
- 7b0bf69: Bump version to upgrade connector kit
|
||||
- 4ec0889: bump connector-kit version
|
||||
- d8b9dea: 1. Update `@logto/connector-kit` from `1.0.0-beta.32` to `1.0.0-beta.33`.
|
||||
- 0de1e1a: Update Azure AD connector's name, description, logo (using Microsoft logo instead of AAD logo) and README.
|
||||
- d183d6d: Upgrade connector-kit
|
||||
|
||||
## 1.0.0-beta.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4ec0889: bump connector-kit version
|
||||
|
||||
## 1.0.0-beta.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 0de1e1a: Update Azure AD connector's name, description, logo (using Microsoft logo instead of AAD logo) and README.
|
||||
|
||||
## 1.0.0-beta.20
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 269d701: The console connector configuration page has been changed from JSON format to form view.
|
||||
|
||||
## 1.0.0-beta.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7b0bf69: Bump version to upgrade connector kit
|
||||
|
||||
## 1.0.0-beta.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9ff0638: update connector-kit version
|
||||
|
||||
## 1.0.0-beta.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d8b9dea: 1. Update `@logto/connector-kit` from `1.0.0-beta.32` to `1.0.0-beta.33`.
|
||||
|
||||
## 1.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d183d6d: Upgrade connector-kit
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.0-beta.12](https://github.com/logto-io/connectors/compare/v1.0.0-beta.11...v1.0.0-beta.12) (2022-11-29)
|
||||
|
||||
### Features
|
||||
|
||||
- add mock standard email connector ([#35](https://github.com/logto-io/connectors/issues/35)) ([479114e](https://github.com/logto-io/connectors/commit/479114e847fb4b11c6fbd697a36b7f5eb56305ed))
|
||||
|
||||
## [1.0.0-beta.11](https://github.com/logto-io/connectors/compare/v1.0.0-beta.10...v1.0.0-beta.11) (2022-11-06)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-azuread
|
||||
|
||||
## [1.0.0-beta.10](https://github.com/logto-io/connectors/compare/v1.0.0-beta.9...v1.0.0-beta.10) (2022-09-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **connector-azuread:** officeLocation attribute bug fix ([#19](https://github.com/logto-io/connectors/issues/19)) ([09241b1](https://github.com/logto-io/connectors/commit/09241b1b75e11bdce134d3d3c788f977eb6f4c32))
|
||||
|
||||
## 1.0.0-beta.9 (2022-09-18)
|
||||
|
||||
### Features
|
||||
|
||||
- add connectors ([#2](https://github.com/logto-io/connectors/issues/2)) ([2fbb578](https://github.com/logto-io/connectors/commit/2fbb57815406bace113617a6304eafcfc5db2d61))
|
||||
|
||||
## [1.0.0-beta.8](https://github.com/logto-io/logto/compare/v1.0.0-beta.6...v1.0.0-beta.8) (2022-09-01)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-azuread
|
||||
|
||||
## [1.0.0-beta.6](https://github.com/logto-io/logto/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-08-30)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-azuread
|
||||
|
||||
## [1.0.0-beta.5](https://github.com/logto-io/logto/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2022-08-19)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-azuread
|
||||
|
||||
## [1.0.0-beta.4](https://github.com/logto-io/logto/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-08-11)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-azuread
|
||||
|
||||
## [1.0.0-beta.3](https://github.com/logto-io/logto/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-08-01)
|
||||
|
||||
### Features
|
||||
|
||||
- **connector:** azure active directory connector added ([#1662](https://github.com/logto-io/logto/issues/1662)) ([875a828](https://github.com/logto-io/logto/commit/875a82883161b79b11873bcfce2856e7b84502b4))
|
||||
- **phrases:** tr language ([#1707](https://github.com/logto-io/logto/issues/1707)) ([411a8c2](https://github.com/logto-io/logto/commit/411a8c2fa2bfb16c4fef5f0a55c3c1dc5ead1124))
|
37
packages/connectors/connector-azuread/README.md
Normal file
|
@ -0,0 +1,37 @@
|
|||
# Microsoft Azure AD connector
|
||||
|
||||
The Microsoft Azure AD connector provides a succinct way for your application to use Azure’s OAuth 2.0 authentication system.
|
||||
|
||||
**Table of contents**
|
||||
- [Microsoft Azure AD connector](#microsoft-azure-ad-connector)
|
||||
- [Set up Microsoft Azure AD in the Azure Portal](#set-up-microsoft-azure-ad-in-the-azure-portal)
|
||||
- [Configure your client secret](#configure-your-client-secret)
|
||||
- [Config types](#config-types)
|
||||
- [References](#references)
|
||||
|
||||
## Set up Microsoft Azure AD in the Azure Portal
|
||||
|
||||
- Visit the [Azure Portal](https://portal.azure.com/#home) and sign in with your Azure account. You need to have an active subscription to access Microsoft Azure AD.
|
||||
- Click the **Azure Active Directory** from the services they offer, and click the **App Registrations** from the left menu.
|
||||
- Click **New Registration** at the top and enter a description, select your **access type** and add your **Redirect URI**, which redirect the user to the application after logging in. In our case, this will be `${your_logto_origin}/callback/${connector_id}`. e.g. `https://logto.dev/callback/${connector_id}`. You need to select Web as Platform. The `connector_id` can be found on the top bar of the Logto Admin Console connector details page.
|
||||
- If you select **Sign in users of a specific organization only** for access type then you need to enter **TenantID**.
|
||||
- If you select **Sign in users with work and school accounts or personal Microsoft accounts** for access type then you need to enter **common**.
|
||||
- If you select **Sign in users with work and school accounts** for access type then you need to enter **organizations**.
|
||||
- If you select **Sign in users with personal Microsoft accounts (MSA) only** for access type then you need to enter **consumers**.
|
||||
|
||||
## Configure your client secret
|
||||
- In your newly created project, click the **Certificates & Secrets** to get a client secret, and click the **New client secret** from the top.
|
||||
- Enter a description and an expiration.
|
||||
- This will only show your client secret once. Save the **value** to a secure location.
|
||||
|
||||
### Config types
|
||||
|
||||
| Name | Type |
|
||||
| ------------- | ------ |
|
||||
| clientId | string |
|
||||
| clientSecret | string |
|
||||
| tenantId | string |
|
||||
| cloudInstance | string |
|
||||
|
||||
## References
|
||||
* [Web app that signs in users](https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-web-app-sign-user-overview?tabs=nodejs)
|
1
packages/connectors/connector-azuread/logo.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 23 23"><path d="M0 0h23v23H0z"/><path fill="#f35325" d="M1 1h10v10H1z"/><path fill="#81bc06" d="M12 1h10v10H12z"/><path fill="#05a6f0" d="M1 12h10v10H1z"/><path fill="#ffba08" d="M12 12h10v10H12z"/></svg>
|
After Width: | Height: | Size: 293 B |
10
packages/connectors/connector-azuread/package.extend.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package",
|
||||
"name": "@logto/connector-azuread",
|
||||
"version": "1.0.0",
|
||||
"description": "Microsoft Azure AD connector implementation.",
|
||||
"author": "Mobilist Inc. <info@mobilist.com.tr>",
|
||||
"dependencies": {
|
||||
"@azure/msal-node": "^1.12.0"
|
||||
}
|
||||
}
|
58
packages/connectors/connector-azuread/src/constant.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import type { ConnectorMetadata } from '@logto/connector-kit';
|
||||
import { ConnectorPlatform, ConnectorConfigFormItemType } from '@logto/connector-kit';
|
||||
|
||||
export const graphAPIEndpoint = 'https://graph.microsoft.com/v1.0/me';
|
||||
export const scopes = ['User.Read'];
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'azuread-universal',
|
||||
target: 'azuread',
|
||||
platform: ConnectorPlatform.Universal,
|
||||
name: {
|
||||
en: 'Microsoft',
|
||||
'zh-CN': 'Microsoft',
|
||||
'tr-TR': 'Microsoft',
|
||||
ko: 'Microsoft',
|
||||
},
|
||||
logo: './logo.svg',
|
||||
logoDark: null,
|
||||
description: {
|
||||
en: 'Microsoft Azure Active Directory is a leading AD provider.',
|
||||
'zh-CN': 'Microsoft Azure Active Directory 是领先的 AD 服务提供商。',
|
||||
'tr-TR': 'Microsoft Azure Active Directory en büyük AD servisidir.', // UNTRANSLATED
|
||||
ko: 'Microsoft Azure Active Directory is the biggest AD provider.', // UNTRANSLATED
|
||||
},
|
||||
readme: './README.md',
|
||||
formItems: [
|
||||
{
|
||||
key: 'clientId',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: true,
|
||||
label: 'Client ID',
|
||||
placeholder: '<client-id>',
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: true,
|
||||
label: 'Client Secret',
|
||||
placeholder: '<client-secret>',
|
||||
},
|
||||
{
|
||||
key: 'cloudInstance',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: true,
|
||||
label: 'Cloud Instance',
|
||||
placeholder: '<cloud-instance>',
|
||||
},
|
||||
{
|
||||
key: 'tenantId',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: true,
|
||||
label: 'Tenant ID',
|
||||
placeholder: '<tenant-id>',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const defaultTimeout = 5000;
|
13
packages/connectors/connector-azuread/src/index.test.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import type { GetConnectorConfig } from '@logto/connector-kit';
|
||||
|
||||
import createConnector from './index.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const getConnectorConfig = jest.fn() as GetConnectorConfig;
|
||||
|
||||
describe('Azure AD connector', () => {
|
||||
it('init without exploding', () => {
|
||||
expect(async () => createConnector({ getConfig: getConnectorConfig })).not.toThrow();
|
||||
});
|
||||
});
|
169
packages/connectors/connector-azuread/src/index.ts
Normal file
|
@ -0,0 +1,169 @@
|
|||
import path from 'path';
|
||||
|
||||
import type { AuthorizationCodeRequest, AuthorizationUrlRequest } from '@azure/msal-node';
|
||||
import { ConfidentialClientApplication, CryptoProvider } from '@azure/msal-node';
|
||||
import type {
|
||||
GetAuthorizationUri,
|
||||
GetUserInfo,
|
||||
GetConnectorConfig,
|
||||
CreateConnector,
|
||||
SocialConnector,
|
||||
} from '@logto/connector-kit';
|
||||
import {
|
||||
ConnectorError,
|
||||
ConnectorErrorCodes,
|
||||
validateConfig,
|
||||
ConnectorType,
|
||||
parseJson,
|
||||
} from '@logto/connector-kit';
|
||||
import { assert, conditional } from '@silverhand/essentials';
|
||||
import { got, HTTPError } from 'got';
|
||||
|
||||
import { scopes, defaultMetadata, defaultTimeout, graphAPIEndpoint } from './constant.js';
|
||||
import type { AzureADConfig } from './types.js';
|
||||
import {
|
||||
azureADConfigGuard,
|
||||
accessTokenResponseGuard,
|
||||
userInfoResponseGuard,
|
||||
authResponseGuard,
|
||||
} from './types.js';
|
||||
|
||||
// eslint-disable-next-line @silverhand/fp/no-let
|
||||
let authCodeRequest: AuthorizationCodeRequest;
|
||||
|
||||
// This `cryptoProvider` seems not used.
|
||||
// Temporarily keep this as this is a refactor, which should not change the logics.
|
||||
const cryptoProvider = new CryptoProvider();
|
||||
|
||||
const getAuthorizationUri =
|
||||
(getConfig: GetConnectorConfig): GetAuthorizationUri =>
|
||||
async ({ state, redirectUri }) => {
|
||||
const config = await getConfig(defaultMetadata.id);
|
||||
|
||||
validateConfig<AzureADConfig>(config, azureADConfigGuard);
|
||||
const { clientId, clientSecret, cloudInstance, tenantId } = config;
|
||||
|
||||
const defaultAuthCodeUrlParameters: AuthorizationUrlRequest = {
|
||||
scopes,
|
||||
state,
|
||||
redirectUri,
|
||||
};
|
||||
|
||||
const clientApplication = new ConfidentialClientApplication({
|
||||
auth: {
|
||||
clientId,
|
||||
clientSecret,
|
||||
authority: new URL(path.join(cloudInstance, tenantId)).toString(),
|
||||
},
|
||||
});
|
||||
|
||||
const authCodeUrlParameters = {
|
||||
...defaultAuthCodeUrlParameters,
|
||||
};
|
||||
|
||||
const authCodeUrl = await clientApplication.getAuthCodeUrl(authCodeUrlParameters);
|
||||
|
||||
return authCodeUrl;
|
||||
};
|
||||
|
||||
const getAccessToken = async (config: AzureADConfig, code: string, redirectUri: string) => {
|
||||
const codeRequest: AuthorizationCodeRequest = {
|
||||
...authCodeRequest,
|
||||
redirectUri,
|
||||
scopes: ['User.Read'],
|
||||
code,
|
||||
};
|
||||
|
||||
const { clientId, clientSecret, cloudInstance, tenantId } = config;
|
||||
|
||||
const clientApplication = new ConfidentialClientApplication({
|
||||
auth: {
|
||||
clientId,
|
||||
clientSecret,
|
||||
authority: new URL(path.join(cloudInstance, tenantId)).toString(),
|
||||
},
|
||||
});
|
||||
|
||||
const authResult = await clientApplication.acquireTokenByCode(codeRequest);
|
||||
|
||||
const result = accessTokenResponseGuard.safeParse(authResult);
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error);
|
||||
}
|
||||
|
||||
const { accessToken } = result.data;
|
||||
|
||||
assert(accessToken, new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid));
|
||||
|
||||
return { accessToken };
|
||||
};
|
||||
|
||||
const getUserInfo =
|
||||
(getConfig: GetConnectorConfig): GetUserInfo =>
|
||||
async (data) => {
|
||||
const { code, redirectUri } = await authorizationCallbackHandler(data);
|
||||
|
||||
// Temporarily keep this as this is a refactor, which should not change the logics.
|
||||
const config = await getConfig(defaultMetadata.id);
|
||||
validateConfig<AzureADConfig>(config, azureADConfigGuard);
|
||||
|
||||
const { accessToken } = await getAccessToken(config, code, redirectUri);
|
||||
|
||||
try {
|
||||
const httpResponse = await got.get(graphAPIEndpoint, {
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
timeout: { request: defaultTimeout },
|
||||
});
|
||||
|
||||
const result = userInfoResponseGuard.safeParse(parseJson(httpResponse.body));
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error);
|
||||
}
|
||||
|
||||
const { id, mail, displayName } = result.data;
|
||||
|
||||
return {
|
||||
id,
|
||||
email: conditional(mail),
|
||||
name: conditional(displayName),
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof HTTPError) {
|
||||
const { statusCode, body: rawBody } = error.response;
|
||||
|
||||
if (statusCode === 401) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
|
||||
}
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(rawBody));
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const authorizationCallbackHandler = async (parameterObject: unknown) => {
|
||||
const result = authResponseGuard.safeParse(parameterObject);
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(parameterObject));
|
||||
}
|
||||
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const createAzureAdConnector: CreateConnector<SocialConnector> = async ({ getConfig }) => {
|
||||
return {
|
||||
metadata: defaultMetadata,
|
||||
type: ConnectorType.Social,
|
||||
configGuard: azureADConfigGuard,
|
||||
getAuthorizationUri: getAuthorizationUri(getConfig),
|
||||
getUserInfo: getUserInfo(getConfig),
|
||||
};
|
||||
};
|
||||
|
||||
export default createAzureAdConnector;
|
39
packages/connectors/connector-azuread/src/types.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const azureADConfigGuard = z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
cloudInstance: z.string(),
|
||||
tenantId: z.string(),
|
||||
});
|
||||
|
||||
export type AzureADConfig = z.infer<typeof azureADConfigGuard>;
|
||||
|
||||
export const accessTokenResponseGuard = z.object({
|
||||
accessToken: z.string(),
|
||||
scopes: z.array(z.string()),
|
||||
tokenType: z.string(),
|
||||
});
|
||||
|
||||
export type AccessTokenResponse = z.infer<typeof accessTokenResponseGuard>;
|
||||
|
||||
export const userInfoResponseGuard = z.object({
|
||||
id: z.string(),
|
||||
displayName: z.string().nullish(),
|
||||
givenName: z.string().nullish(),
|
||||
surname: z.string().nullish(),
|
||||
userPrincipalName: z.string().nullish(),
|
||||
jobTitle: z.string().nullish(),
|
||||
mail: z.string().nullish(),
|
||||
mobilePhone: z.string().nullish(),
|
||||
officeLocation: z.string().nullish(),
|
||||
preferredLanguage: z.string().nullish(),
|
||||
businessPhones: z.array(z.string()).nullish(),
|
||||
});
|
||||
|
||||
export type UserInfoResponse = z.infer<typeof userInfoResponseGuard>;
|
||||
|
||||
export const authResponseGuard = z.object({
|
||||
code: z.string(),
|
||||
redirectUri: z.string(),
|
||||
});
|
89
packages/connectors/connector-discord/CHANGELOG.md
Normal file
|
@ -0,0 +1,89 @@
|
|||
# Change Log
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 4ed07d2: bump connector-kit to v1.0.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 269d701: The console connector configuration page has been changed from JSON format to form view.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9ff0638: update connector-kit version
|
||||
- 7b0bf69: Bump version to upgrade connector kit
|
||||
- 4ec0889: bump connector-kit version
|
||||
- d8b9dea: 1. Update `@logto/connector-kit` from `1.0.0-beta.32` to `1.0.0-beta.33`.
|
||||
- d183d6d: Upgrade connector-kit
|
||||
- 2068973: fix discord user info endpoint parser
|
||||
|
||||
## 1.0.0-beta.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4ec0889: bump connector-kit version
|
||||
|
||||
## 1.0.0-beta.21
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 269d701: The console connector configuration page has been changed from JSON format to form view.
|
||||
|
||||
## 1.0.0-beta.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7b0bf69: Bump version to upgrade connector kit
|
||||
|
||||
## 1.0.0-beta.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2068973: fix discord user info endpoint parser
|
||||
|
||||
## 1.0.0-beta.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9ff0638: update connector-kit version
|
||||
|
||||
## 1.0.0-beta.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d8b9dea: 1. Update `@logto/connector-kit` from `1.0.0-beta.32` to `1.0.0-beta.33`.
|
||||
|
||||
## 1.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d183d6d: Upgrade connector-kit
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.0-beta.12](https://github.com/logto-io/connectors/compare/v1.0.0-beta.11...v1.0.0-beta.12) (2022-11-29)
|
||||
|
||||
### Features
|
||||
|
||||
- add mock standard email connector ([#35](https://github.com/logto-io/connectors/issues/35)) ([479114e](https://github.com/logto-io/connectors/commit/479114e847fb4b11c6fbd697a36b7f5eb56305ed))
|
||||
|
||||
## [1.0.0-beta.11](https://github.com/logto-io/connectors/compare/v1.0.0-beta.10...v1.0.0-beta.11) (2022-11-06)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-discord
|
||||
|
||||
## [1.0.0-beta.10](https://github.com/logto-io/connectors/compare/v1.0.0-beta.9...v1.0.0-beta.10) (2022-09-27)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-discord
|
||||
|
||||
## 1.0.0-beta.9 (2022-09-18)
|
||||
|
||||
### Features
|
||||
|
||||
- add discord connector ([c34bac6](https://github.com/logto-io/connectors/commit/c34bac62225ee50e9a7ffe320e8044b0bcb6b335))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add verified field to nock response ([dca3fb3](https://github.com/logto-io/connectors/commit/dca3fb3be19ad47b14f5fda648410c0735a0c169))
|
36
packages/connectors/connector-discord/README.md
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Discord OAuth2 Connector
|
||||
|
||||
The Discord connector provides a way for your application to use Discord as an authorization system.
|
||||
|
||||
**Table of contents**
|
||||
- [Discord OAuth2 Connector](#discord-oauth2-connector)
|
||||
- [Register a developer application](#register-a-developer-application)
|
||||
- [Configure Logto](#configure-logto)
|
||||
- [Config types](#config-types)
|
||||
- [clientId](#clientid)
|
||||
- [clientSecret](#clientsecret)
|
||||
|
||||
## Register a developer application
|
||||
- Visit [Discord Developer Portal](https://discord.com/developers/applications) and sign in with your Discord account.
|
||||
- Click the **New Application** button to create an application, choose a name for it (Ex: LogtoAuth), tick the box and click **Create**.
|
||||
- Go to **OAuth2** page and click **Reset Secret**
|
||||
- Take note of the **CLIENT ID** and **CLIENT SECRET** fields
|
||||
- Add the valid redirects (Ex: **`http://auth.mycompany.io/callback/${connector_id}`**). The `connector_id` can be found on the top bar of the Logto Admin Console connector details page.
|
||||
|
||||
|
||||
## Configure Logto
|
||||
|
||||
### Config types
|
||||
|
||||
| Name | Type |
|
||||
|--------------|---------|
|
||||
| clientId | string |
|
||||
| clientSecret | string |
|
||||
|
||||
#### clientId
|
||||
`clientId` is the `CLIENT ID` field we saved earlier.
|
||||
(You can find it in the Oauth2 Page in Discord Developer Portal.)
|
||||
|
||||
#### clientSecret
|
||||
`clientSecret` is the `CLIENT SECRET` we saved earlier.
|
||||
(If you've lost it you need to click **Reset Secret**)
|
4
packages/connectors/connector-discord/logo.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 4.8C0 2.14903 2.14903 0 4.8 0H19.2C21.851 0 24 2.14903 24 4.8V19.2C24 21.851 21.851 24 19.2 24H4.8C2.14903 24 0 21.851 0 19.2V4.8Z" fill="#5865F2"/>
|
||||
<path d="M17.8219 7.06149C16.7511 6.57017 15.6029 6.20819 14.4023 6.00087C14.3804 5.99687 14.3586 6.00687 14.3473 6.02687C14.1997 6.28952 14.0361 6.63217 13.9215 6.90149C12.6303 6.70817 11.3456 6.70817 10.0808 6.90149C9.96623 6.62618 9.79671 6.28952 9.64838 6.02687C9.63711 6.00754 9.61527 5.99754 9.59341 6.00087C8.39351 6.20753 7.24527 6.56951 6.17383 7.06149C6.16455 7.06549 6.1566 7.07216 6.15133 7.08082C3.97335 10.3347 3.37671 13.5085 3.6694 16.6431C3.67073 16.6584 3.67933 16.6731 3.69125 16.6824C5.12822 17.7377 6.52017 18.3783 7.88627 18.803C7.90813 18.8096 7.9313 18.8016 7.94521 18.7836C8.26836 18.3423 8.55643 17.877 8.80341 17.3877C8.81798 17.359 8.80407 17.325 8.77428 17.3137C8.31737 17.1404 7.8823 16.9291 7.46379 16.6891C7.43068 16.6697 7.42803 16.6224 7.45849 16.5997C7.54656 16.5337 7.63465 16.4651 7.71874 16.3957C7.73396 16.3831 7.75516 16.3804 7.77305 16.3884C10.5225 17.6437 13.4991 17.6437 16.216 16.3884C16.2339 16.3797 16.2551 16.3824 16.271 16.3951C16.3551 16.4644 16.4432 16.5337 16.5319 16.5997C16.5624 16.6224 16.5604 16.6697 16.5273 16.6891C16.1088 16.9337 15.6737 17.1404 15.2161 17.313C15.1863 17.3244 15.1731 17.359 15.1877 17.3877C15.44 17.8763 15.728 18.3417 16.0452 18.783C16.0585 18.8016 16.0823 18.8096 16.1041 18.803C17.4769 18.3783 18.8688 17.7377 20.3058 16.6824C20.3184 16.6731 20.3263 16.6591 20.3276 16.6437C20.6779 13.0199 19.7409 9.87203 17.8437 7.08148C17.8391 7.07216 17.8312 7.06549 17.8219 7.06149ZM9.21399 14.7345C8.38622 14.7345 7.70417 13.9745 7.70417 13.0412C7.70417 12.1079 8.373 11.348 9.21399 11.348C10.0616 11.348 10.737 12.1146 10.7238 13.0412C10.7238 13.9745 10.055 14.7345 9.21399 14.7345ZM14.7963 14.7345C13.9686 14.7345 13.2865 13.9745 13.2865 13.0412C13.2865 12.1079 13.9553 11.348 14.7963 11.348C15.6439 11.348 16.3194 12.1146 16.3061 13.0412C16.3061 13.9745 15.6439 14.7345 14.7963 14.7345Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package",
|
||||
"name": "@logto/connector-discord",
|
||||
"version": "1.0.0",
|
||||
"description": "Discord connector implementation.",
|
||||
"author": "ZR3SYSTEMS. <https://github.com/FlurryNight>"
|
||||
}
|
58
packages/connectors/connector-discord/src/constant.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import type { ConnectorMetadata } from '@logto/connector-kit';
|
||||
import { ConnectorConfigFormItemType, ConnectorPlatform } from '@logto/connector-kit';
|
||||
|
||||
/**
|
||||
* Base authorization URL.
|
||||
* https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-urls
|
||||
*/
|
||||
export const authorizationEndpoint = 'https://discord.com/oauth2/authorize';
|
||||
|
||||
/**
|
||||
* Discord exposes different versions of the API, You should specify which version to use by including it in your requests.
|
||||
* https://discord.com/developers/docs/reference#api-reference
|
||||
*/
|
||||
export const accessTokenEndpoint = 'https://discord.com/api/v10/oauth2/token';
|
||||
export const userInfoEndpoint = 'https://discord.com/api/v10/users/@me';
|
||||
|
||||
/**
|
||||
* OAuth2 Scopes
|
||||
* https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes
|
||||
*/
|
||||
export const scope = 'identify email';
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'discord-universal',
|
||||
target: 'discord',
|
||||
platform: ConnectorPlatform.Universal,
|
||||
name: {
|
||||
en: 'Discord',
|
||||
},
|
||||
logo: './logo.svg',
|
||||
logoDark: null,
|
||||
description: {
|
||||
en: 'Discord is the easiest way to talk over voice, video, and text.',
|
||||
'pt-PT': 'Discord é a forma mais fácil de comunicar por voz, vídeo e texto.',
|
||||
'zh-CN': 'Discord 是一款专为社群设计的免费网络实时通话软件与数字发行平台。',
|
||||
'tr-TR': 'Discord, sesli, görüntülü ve metin üzerinden konuşmanın en kolay yoludur.',
|
||||
ko: 'Discord는 음성, 비디오 및 텍스트로 대화하는 가장 쉬운 방법입니다.',
|
||||
},
|
||||
readme: './README.md',
|
||||
formItems: [
|
||||
{
|
||||
key: 'clientId',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: true,
|
||||
label: 'Client ID',
|
||||
placeholder: '<client-id>',
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: true,
|
||||
label: 'Client Secret',
|
||||
placeholder: '<client-secret>',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const defaultTimeout = 5000;
|
127
packages/connectors/connector-discord/src/index.test.ts
Normal file
|
@ -0,0 +1,127 @@
|
|||
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit';
|
||||
import nock from 'nock';
|
||||
|
||||
import { accessTokenEndpoint, authorizationEndpoint, userInfoEndpoint } from './constant.js';
|
||||
import createConnector, { getAccessToken } from './index.js';
|
||||
import { mockedConfig } from './mock.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const getConfig = jest.fn().mockResolvedValue(mockedConfig);
|
||||
|
||||
describe('Discord connector', () => {
|
||||
describe('getAuthorizationUri', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should get a valid authorizationUri with redirectUri and state', async () => {
|
||||
const connector = await createConnector({ getConfig });
|
||||
const authorizationUri = await connector.getAuthorizationUri(
|
||||
{
|
||||
state: 'some_state',
|
||||
redirectUri: 'http://localhost:3000/callback',
|
||||
connectorId: 'some_connector_id',
|
||||
connectorFactoryId: 'some_connector_factory_id',
|
||||
jti: 'some_jti',
|
||||
headers: {},
|
||||
},
|
||||
jest.fn()
|
||||
);
|
||||
expect(authorizationUri).toEqual(
|
||||
`${authorizationEndpoint}?client_id=%3Cclient-id%3E&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&response_type=code&scope=identify+email&state=some_state`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccessToken', () => {
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should get an accessToken by exchanging with code', async () => {
|
||||
nock(accessTokenEndpoint).post('').reply(200, {
|
||||
access_token: 'access_token',
|
||||
scope: 'scope',
|
||||
token_type: 'token_type',
|
||||
expires_in: 3600,
|
||||
});
|
||||
|
||||
const { accessToken } = await getAccessToken(mockedConfig, {
|
||||
code: 'code',
|
||||
redirectUri: 'dummyRedirectUri',
|
||||
});
|
||||
expect(accessToken).toEqual('access_token');
|
||||
});
|
||||
|
||||
it('throws SocialAuthCodeInvalid error if accessToken not found in response', async () => {
|
||||
nock(accessTokenEndpoint).post('').reply(200, {
|
||||
access_token: '',
|
||||
scope: 'scope',
|
||||
token_type: 'token_type',
|
||||
expires_in: 3600,
|
||||
});
|
||||
|
||||
await expect(
|
||||
getAccessToken(mockedConfig, { code: 'code', redirectUri: 'dummyRedirectUri' })
|
||||
).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserInfo', () => {
|
||||
beforeEach(() => {
|
||||
nock(accessTokenEndpoint).post('').reply(200, {
|
||||
access_token: 'access_token',
|
||||
scope: 'scope',
|
||||
token_type: 'token_type',
|
||||
expires_in: 3600,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should get valid SocialUserInfo', async () => {
|
||||
nock(userInfoEndpoint).get('').reply(200, {
|
||||
id: '1234567890',
|
||||
username: 'Whumpus',
|
||||
avatar: 'avatar_id',
|
||||
email: 'whumpus@discord.com',
|
||||
verified: true,
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
const socialUserInfo = await connector.getUserInfo(
|
||||
{
|
||||
code: 'code',
|
||||
redirectUri: 'dummyRedirectUri',
|
||||
},
|
||||
jest.fn()
|
||||
);
|
||||
expect(socialUserInfo).toMatchObject({
|
||||
id: '1234567890',
|
||||
name: 'Whumpus',
|
||||
avatar: 'https://cdn.discordapp.com/avatars/1234567890/avatar_id',
|
||||
email: 'whumpus@discord.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('throws SocialAccessTokenInvalid error if remote response code is 401', async () => {
|
||||
nock(userInfoEndpoint).get('').reply(401);
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(
|
||||
connector.getUserInfo({ code: 'code', redirectUri: 'dummyRedirectUri' }, jest.fn())
|
||||
).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid));
|
||||
});
|
||||
|
||||
it('throws unrecognized error', async () => {
|
||||
nock(userInfoEndpoint).get('').reply(500);
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(
|
||||
connector.getUserInfo({ code: 'code', redirectUri: 'dummyRedirectUri' }, jest.fn())
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
163
packages/connectors/connector-discord/src/index.ts
Normal file
|
@ -0,0 +1,163 @@
|
|||
/**
|
||||
* Discord OAuth2 Connector
|
||||
* https://discord.com/developers/docs/topics/oauth2
|
||||
*/
|
||||
|
||||
import type {
|
||||
GetConnectorConfig,
|
||||
GetAuthorizationUri,
|
||||
GetUserInfo,
|
||||
CreateConnector,
|
||||
SocialConnector,
|
||||
} from '@logto/connector-kit';
|
||||
import {
|
||||
socialUserInfoGuard,
|
||||
validateConfig,
|
||||
ConnectorError,
|
||||
ConnectorErrorCodes,
|
||||
ConnectorType,
|
||||
parseJson,
|
||||
} from '@logto/connector-kit';
|
||||
import { assert, conditional } from '@silverhand/essentials';
|
||||
import { got, HTTPError } from 'got';
|
||||
|
||||
import {
|
||||
defaultMetadata,
|
||||
scope,
|
||||
authorizationEndpoint,
|
||||
accessTokenEndpoint,
|
||||
defaultTimeout,
|
||||
userInfoEndpoint,
|
||||
} from './constant.js';
|
||||
import type { DiscordConfig } from './types.js';
|
||||
import {
|
||||
discordConfigGuard,
|
||||
authResponseGuard,
|
||||
accessTokenResponseGuard,
|
||||
userInfoResponseGuard,
|
||||
} from './types.js';
|
||||
|
||||
const getAuthorizationUri =
|
||||
(getConfig: GetConnectorConfig): GetAuthorizationUri =>
|
||||
async ({ state, redirectUri }) => {
|
||||
const config = await getConfig(defaultMetadata.id);
|
||||
validateConfig<DiscordConfig>(config, discordConfigGuard);
|
||||
|
||||
const queryParameters = new URLSearchParams({
|
||||
client_id: config.clientId,
|
||||
redirect_uri: redirectUri,
|
||||
response_type: 'code',
|
||||
scope,
|
||||
state,
|
||||
});
|
||||
|
||||
return `${authorizationEndpoint}?${queryParameters.toString()}`;
|
||||
};
|
||||
|
||||
export const getAccessToken = async (
|
||||
config: DiscordConfig,
|
||||
codeObject: { code: string; redirectUri: string }
|
||||
) => {
|
||||
const { code, redirectUri } = codeObject;
|
||||
|
||||
const { clientId: client_id, clientSecret: client_secret } = config;
|
||||
|
||||
const httpResponse = await got.post(accessTokenEndpoint, {
|
||||
form: {
|
||||
client_id,
|
||||
client_secret,
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
redirect_uri: redirectUri,
|
||||
},
|
||||
timeout: { request: defaultTimeout },
|
||||
});
|
||||
|
||||
const result = accessTokenResponseGuard.safeParse(parseJson(httpResponse.body));
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error);
|
||||
}
|
||||
|
||||
const { access_token: accessToken } = result.data;
|
||||
|
||||
assert(accessToken, new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid));
|
||||
|
||||
return { accessToken };
|
||||
};
|
||||
|
||||
const getUserInfo =
|
||||
(getConfig: GetConnectorConfig): GetUserInfo =>
|
||||
// eslint-disable-next-line complexity
|
||||
async (data) => {
|
||||
const { code, redirectUri } = await authorizationCallbackHandler(data);
|
||||
const config = await getConfig(defaultMetadata.id);
|
||||
validateConfig<DiscordConfig>(config, discordConfigGuard);
|
||||
const { accessToken } = await getAccessToken(config, { code, redirectUri });
|
||||
|
||||
try {
|
||||
const httpResponse = await got.get(userInfoEndpoint, {
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
timeout: { request: defaultTimeout },
|
||||
});
|
||||
|
||||
const result = userInfoResponseGuard.safeParse(parseJson(httpResponse.body));
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error);
|
||||
}
|
||||
|
||||
const { id, username: name, avatar, email, verified } = result.data;
|
||||
|
||||
const rawUserInfo = {
|
||||
id,
|
||||
name,
|
||||
avatar: conditional(avatar && `https://cdn.discordapp.com/avatars/${id}/${avatar}`),
|
||||
email: conditional(verified && email),
|
||||
};
|
||||
|
||||
const userInfoResult = socialUserInfoGuard.safeParse(rawUserInfo);
|
||||
|
||||
if (!userInfoResult.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, userInfoResult.error);
|
||||
}
|
||||
|
||||
return userInfoResult.data;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof HTTPError) {
|
||||
const { statusCode, body: rawBody } = error.response;
|
||||
|
||||
if (statusCode === 401) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
|
||||
}
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(rawBody));
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const authorizationCallbackHandler = async (parameterObject: unknown) => {
|
||||
const result = authResponseGuard.safeParse(parameterObject);
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(parameterObject));
|
||||
}
|
||||
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const createDiscordConnector: CreateConnector<SocialConnector> = async ({ getConfig }) => {
|
||||
return {
|
||||
metadata: defaultMetadata,
|
||||
type: ConnectorType.Social,
|
||||
configGuard: discordConfigGuard,
|
||||
getAuthorizationUri: getAuthorizationUri(getConfig),
|
||||
getUserInfo: getUserInfo(getConfig),
|
||||
};
|
||||
};
|
||||
|
||||
export default createDiscordConnector;
|
4
packages/connectors/connector-discord/src/mock.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export const mockedConfig = {
|
||||
clientId: '<client-id>',
|
||||
clientSecret: '<client-secret>',
|
||||
};
|
43
packages/connectors/connector-discord/src/types.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import type { Nullable, Optional } from '@silverhand/essentials';
|
||||
import { z } from 'zod';
|
||||
|
||||
const nullishToUndefined = <T = unknown>(input: Nullable<T>): Optional<T> => {
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
return input;
|
||||
};
|
||||
|
||||
export const discordConfigGuard = z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
});
|
||||
|
||||
export type DiscordConfig = z.infer<typeof discordConfigGuard>;
|
||||
|
||||
export const accessTokenResponseGuard = z.object({
|
||||
access_token: z.string(),
|
||||
token_type: z.string(),
|
||||
expires_in: z.number(),
|
||||
scope: z.string(),
|
||||
});
|
||||
|
||||
export type AccessTokenResponse = z.infer<typeof accessTokenResponseGuard>;
|
||||
|
||||
export const userInfoResponseGuard = z.object({
|
||||
id: z.string(),
|
||||
username: z.string().nullish().transform(nullishToUndefined),
|
||||
avatar: z.string().nullish().transform(nullishToUndefined),
|
||||
email: z.string().nullish().transform(nullishToUndefined),
|
||||
verified: z.boolean().nullish().transform(nullishToUndefined),
|
||||
});
|
||||
|
||||
export type UserInfoResponse = z.infer<typeof userInfoResponseGuard>;
|
||||
|
||||
export const authorizationCallbackErrorGuard = z.object({
|
||||
error: z.string(),
|
||||
error_description: z.string(),
|
||||
});
|
||||
|
||||
export const authResponseGuard = z.object({ code: z.string(), redirectUri: z.string() });
|
181
packages/connectors/connector-facebook/CHANGELOG.md
Normal file
|
@ -0,0 +1,181 @@
|
|||
# Change Log
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 4ed07d2: bump connector-kit to v1.0.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 269d701: The console connector configuration page has been changed from JSON format to form view.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9ff0638: update connector-kit version
|
||||
- 7b0bf69: Bump version to upgrade connector kit
|
||||
- 4ec0889: bump connector-kit version
|
||||
- d8b9dea: 1. Update `@logto/connector-kit` from `1.0.0-beta.32` to `1.0.0-beta.33`.
|
||||
- d183d6d: Upgrade connector-kit
|
||||
|
||||
## 1.0.0-beta.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4ec0889: bump connector-kit version
|
||||
|
||||
## 1.0.0-beta.20
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 269d701: The console connector configuration page has been changed from JSON format to form view.
|
||||
|
||||
## 1.0.0-beta.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7b0bf69: Bump version to upgrade connector kit
|
||||
|
||||
## 1.0.0-beta.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9ff0638: update connector-kit version
|
||||
|
||||
## 1.0.0-beta.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d8b9dea: 1. Update `@logto/connector-kit` from `1.0.0-beta.32` to `1.0.0-beta.33`.
|
||||
|
||||
## 1.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d183d6d: Upgrade connector-kit
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.0-beta.12](https://github.com/logto-io/connectors/compare/v1.0.0-beta.11...v1.0.0-beta.12) (2022-11-29)
|
||||
|
||||
### Features
|
||||
|
||||
- add mock standard email connector ([#35](https://github.com/logto-io/connectors/issues/35)) ([479114e](https://github.com/logto-io/connectors/commit/479114e847fb4b11c6fbd697a36b7f5eb56305ed))
|
||||
|
||||
## [1.0.0-beta.11](https://github.com/logto-io/connectors/compare/v1.0.0-beta.10...v1.0.0-beta.11) (2022-11-06)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-facebook
|
||||
|
||||
## [1.0.0-beta.10](https://github.com/logto-io/connectors/compare/v1.0.0-beta.9...v1.0.0-beta.10) (2022-09-27)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-facebook
|
||||
|
||||
## 1.0.0-beta.9 (2022-09-18)
|
||||
|
||||
### Features
|
||||
|
||||
- add connectors ([#2](https://github.com/logto-io/connectors/issues/2)) ([2fbb578](https://github.com/logto-io/connectors/commit/2fbb57815406bace113617a6304eafcfc5db2d61))
|
||||
|
||||
## [1.0.0-beta.8](https://github.com/logto-io/logto/compare/v1.0.0-beta.6...v1.0.0-beta.8) (2022-09-01)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-facebook
|
||||
|
||||
## [1.0.0-beta.6](https://github.com/logto-io/logto/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-08-30)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-facebook
|
||||
|
||||
## [1.0.0-beta.5](https://github.com/logto-io/logto/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2022-08-19)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-facebook
|
||||
|
||||
## [1.0.0-beta.4](https://github.com/logto-io/logto/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-08-11)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-facebook
|
||||
|
||||
## [1.0.0-beta.3](https://github.com/logto-io/logto/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-08-01)
|
||||
|
||||
### Features
|
||||
|
||||
- **phrases:** tr language ([#1707](https://github.com/logto-io/logto/issues/1707)) ([411a8c2](https://github.com/logto-io/logto/commit/411a8c2fa2bfb16c4fef5f0a55c3c1dc5ead1124))
|
||||
|
||||
## [1.0.0-beta.2](https://github.com/logto-io/logto/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-07-25)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-facebook
|
||||
|
||||
## [1.0.0-beta.1](https://github.com/logto-io/logto/compare/v1.0.0-beta.0...v1.0.0-beta.1) (2022-07-19)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-facebook
|
||||
|
||||
## [1.0.0-beta.0](https://github.com/logto-io/logto/compare/v1.0.0-alpha.4...v1.0.0-beta.0) (2022-07-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **connector:** fix connector getConfig and validateConfig type ([#1530](https://github.com/logto-io/logto/issues/1530)) ([88a54aa](https://github.com/logto-io/logto/commit/88a54aaa9ebce419c149a33150a4927296cb705b))
|
||||
- **connector:** refactor ConnectorInstance as class ([#1541](https://github.com/logto-io/logto/issues/1541)) ([6b9ad58](https://github.com/logto-io/logto/commit/6b9ad580ae86fbcc100a100aab1d834090e682a3))
|
||||
|
||||
## [1.0.0-alpha.4](https://github.com/logto-io/logto/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) (2022-07-08)
|
||||
|
||||
### Features
|
||||
|
||||
- **connector:** connector error handler, throw errmsg on general errors ([#1458](https://github.com/logto-io/logto/issues/1458)) ([7da1de3](https://github.com/logto-io/logto/commit/7da1de33e97de4aeeec9f9b6cea59d1bf90ba623))
|
||||
|
||||
## [1.0.0-alpha.3](https://github.com/logto-io/logto/compare/v1.0.0-alpha.2...v1.0.0-alpha.3) (2022-07-07)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-facebook
|
||||
|
||||
## [1.0.0-alpha.2](https://github.com/logto-io/logto/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2022-07-07)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-facebook
|
||||
|
||||
## [1.0.0-alpha.1](https://github.com/logto-io/logto/compare/v1.0.0-alpha.0...v1.0.0-alpha.1) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-facebook
|
||||
|
||||
## [1.0.0-alpha.0](https://github.com/logto-io/logto/compare/v0.1.2-alpha.5...v1.0.0-alpha.0) (2022-07-04)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-facebook
|
||||
|
||||
### [0.1.2-alpha.5](https://github.com/logto-io/logto/compare/v0.1.2-alpha.4...v0.1.2-alpha.5) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-facebook
|
||||
|
||||
### [0.1.2-alpha.4](https://github.com/logto-io/logto/compare/v0.1.2-alpha.3...v0.1.2-alpha.4) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-facebook
|
||||
|
||||
### [0.1.2-alpha.3](https://github.com/logto-io/logto/compare/v0.1.2-alpha.2...v0.1.2-alpha.3) (2022-07-03)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-facebook
|
||||
|
||||
### [0.1.2-alpha.2](https://github.com/logto-io/logto/compare/v0.1.2-alpha.1...v0.1.2-alpha.2) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-facebook
|
||||
|
||||
### [0.1.2-alpha.1](https://github.com/logto-io/logto/compare/v0.1.2-alpha.0...v0.1.2-alpha.1) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-facebook
|
||||
|
||||
### [0.1.2-alpha.0](https://github.com/logto-io/logto/compare/v0.1.1-alpha.0...v0.1.2-alpha.0) (2022-07-02)
|
||||
|
||||
**Note:** Version bump only for package @logto/connector-facebook
|
||||
|
||||
### [0.1.1-alpha.0](https://github.com/logto-io/logto/compare/v0.1.0-internal...v0.1.1-alpha.0) (2022-07-01)
|
||||
|
||||
### Features
|
||||
|
||||
- **connectors:** add logo for connectors ([#914](https://github.com/logto-io/logto/issues/914)) ([a3a7c52](https://github.com/logto-io/logto/commit/a3a7c52a91dba3603617a68e5ce47e0017081a91))
|
||||
- **connectors:** handle authorization callback parameters in each connector respectively ([#1166](https://github.com/logto-io/logto/issues/1166)) ([097aade](https://github.com/logto-io/logto/commit/097aade2e2e1b1ea1531bcb4c1cca8d24961a9b9))
|
||||
- **console:** connector logo and platform icon ([#892](https://github.com/logto-io/logto/issues/892)) ([97e6bdd](https://github.com/logto-io/logto/commit/97e6bdd8aacdf12dcf99a984d7b5bcd2f61f1530))
|
||||
- **core,connectors:** update Aliyun logo and add logo_dark to Apple, Github ([#1194](https://github.com/logto-io/logto/issues/1194)) ([98f8083](https://github.com/logto-io/logto/commit/98f808320b1c79c51f8bd6f49e35ca44363ea560))
|
||||
- **core:** serve connector logo ([#931](https://github.com/logto-io/logto/issues/931)) ([5b44b71](https://github.com/logto-io/logto/commit/5b44b7194ed4f98c6c2e77aae828a39b477b6010))
|
||||
- **core:** update connector db schema ([#732](https://github.com/logto-io/logto/issues/732)) ([8e1533a](https://github.com/logto-io/logto/commit/8e1533a70267d459feea4e5174296b17bef84d48))
|
||||
- **core:** wrap aliyun short message service connector ([#670](https://github.com/logto-io/logto/issues/670)) ([a06d3ee](https://github.com/logto-io/logto/commit/a06d3ee73ccc59f6aaef1dab4f45d6c118aab40d))
|
||||
- **core:** wrap facebook connector ([#672](https://github.com/logto-io/logto/issues/672)) ([cbd6cfa](https://github.com/logto-io/logto/commit/cbd6cfae8af7faee19a62869552acf2c6ca54125))
|
||||
- **native-connectors:** pass random state to native connector sdk ([#922](https://github.com/logto-io/logto/issues/922)) ([9679620](https://github.com/logto-io/logto/commit/96796203dd4247d7ecdee044f13f3d57f04ca461))
|
||||
- remove target, platform from connector schema and add id to metadata ([#930](https://github.com/logto-io/logto/issues/930)) ([054b0f7](https://github.com/logto-io/logto/commit/054b0f7b6a6dfed66540042ea69b0721126fe695))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- `lint:report` script ([#730](https://github.com/logto-io/logto/issues/730)) ([3b17324](https://github.com/logto-io/logto/commit/3b17324d189b2fe47985d0bee8b37b4ef1dbdd2b))
|
||||
- connectors platform ([#925](https://github.com/logto-io/logto/issues/925)) ([16ec018](https://github.com/logto-io/logto/commit/16ec018b711baeec28a22a7780370044c230bd24))
|
78
packages/connectors/connector-facebook/README.md
Normal file
|
@ -0,0 +1,78 @@
|
|||
# Facebook connector
|
||||
|
||||
The official Logto connector for Facebook social sign-in.
|
||||
|
||||
**Table of contents**
|
||||
|
||||
- [Facebook connector](#facebook-connector)
|
||||
- [Get started](#get-started)
|
||||
- [Register a Facebook developer account](#register-a-facebook-developer-account)
|
||||
- [Set up a Facebook app](#set-up-a-facebook-app)
|
||||
- [Compose the connector JSON](#compose-the-connector-json)
|
||||
- [Test sign-in with Facebook's test users](#test-sign-in-with-facebooks-test-users)
|
||||
- [Publish Facebook sign-in settings](#publish-facebook-sign-in-settings)
|
||||
- [Config types](#config-types)
|
||||
- [References](#references)
|
||||
## Get started
|
||||
|
||||
The Facebook connector provides a concise way for your application to use Facebook's OAuth 2.0 authentication system.
|
||||
|
||||
### Register a Facebook developer account
|
||||
|
||||
[Register as a Facebook Developer](https://developers.facebook.com/docs/development/register/) if you don't already have one
|
||||
|
||||
### Set up a Facebook app
|
||||
|
||||
1. Visit the [Apps](https://developers.facebook.com/apps) page.
|
||||
2. Click your existing app or [create a new one](https://developers.facebook.com/docs/development/create-an-app) if needed.
|
||||
- The selected [app type](https://developers.facebook.com/docs/development/create-an-app/app-dashboard/app-types) is up to you, but it should have the product _Facebook Login_.
|
||||
3. On the app dashboard page, scroll to the "Add a product" section and click the "Set up" button on the "Facebook Login" card.
|
||||
4. Skip the Facebook Login Quickstart page, and click the sidebar -> "Products" -> "Facebook Login" -> "Settings".
|
||||
5. In the Facebook Login Settings page, fill `${your_logto_origin}/callback/${connector_id}` in the "Valid OAuth Redirect URIs" field. The `connector_id` can be found on the top bar of the Logto Admin Console connector details page. E.g.:
|
||||
- `https://logto.dev/callback/${connector_id}` for production
|
||||
- `https://localhost:3001/callback/${connector_id}` for testing in the local environment
|
||||
6. Click the "Save changes" button at the bottom right corner.
|
||||
|
||||
## Compose the connector JSON
|
||||
|
||||
1. In the Facebook app dashboard page, click the sidebar -> "Settings" -> "Basic".
|
||||
2. You will see the "App ID" and "App secret" on the panel.
|
||||
3. Click the "Show" button following the App secret input box to copy its content.
|
||||
4. Fill out the Logto connector settings:
|
||||
- Fill out the `clientId` field with the string from _App ID_.
|
||||
- Fill out the `clientSecret` field with the string from _App secret_.
|
||||
|
||||
## Test sign-in with Facebook's test users
|
||||
|
||||
You can use the accounts of the test, developer, and admin users to test sign-in with the related app under both development and live [app modes](https://developers.facebook.com/docs/development/build-and-test/app-modes).
|
||||
|
||||
You can also [take the app live](#take-the-facebook-app-live) directly so that any Facebook user can sign in with the app.
|
||||
|
||||
- In the app dashboard page, click the sidebar -> "Roles" -> "Test Users".
|
||||
- Click the "Create test users" button to create a testing user.
|
||||
- Click the "Options" button of the existing test user, and you will see more operations, e.g., "Change name and password".
|
||||
|
||||
## Publish Facebook sign-in settings
|
||||
|
||||
Usually, only the test, admin, and developer users can sign in with the related app under [development mode](https://developers.facebook.com/docs/development/build-and-test/app-modes#development-mode).
|
||||
|
||||
To enable normal Facebook users sign-in with the app in the production environment, you maybe need to switch your Facebook app to _[live mode](https://developers.facebook.com/docs/development/build-and-test/app-modes#live-mode)_, depending on the app type.
|
||||
E.g., the pure _business type_ app doesn't have the "live" switch button, but it won't block your use.
|
||||
|
||||
1. In the Facebook app dashboard page, click the sidebar -> "Settings" -> "Basic".
|
||||
2. Fill out the "Privacy Policy URL" and "User data deletion" fields on the panel if required.
|
||||
3. Click the "Save changes" button at the bottom right corner.
|
||||
4. Click the "Live" switch button on the app top bar.
|
||||
|
||||
## Config types
|
||||
|
||||
| Name | Type |
|
||||
|--------------|--------|
|
||||
| clientId | string |
|
||||
| clientSecret | string |
|
||||
|
||||
## References
|
||||
|
||||
- [Facebook Login - Documentation - Facebook for Developers](https://developers.facebook.com/docs/facebook-login/)
|
||||
- [Manually Build a Login Flow](https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow/)
|
||||
- [Permissions Guide](https://developers.facebook.com/docs/facebook-login/guides/permissions)
|
11
packages/connectors/connector-facebook/logo.svg
Normal file
|
@ -0,0 +1,11 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_865_12306)">
|
||||
<path d="M24 12C24 5.3719 18.6281 0 12 0C5.3719 0 0 5.3719 0 12C0 17.9907 4.3875 22.9547 10.125 23.8547V15.4688H7.0781V12H10.125V9.35625C10.125 6.3492 11.9156 4.6875 14.6578 4.6875C15.9703 4.6875 17.3438 4.92188 17.3438 4.92188V7.875H15.8297C14.3391 7.875 13.875 8.8008 13.875 9.75V12H17.2031L16.6711 15.4688H13.875V23.8547C19.6125 22.9547 24 17.9907 24 12Z" fill="#1877F2"/>
|
||||
<path d="M16.6711 15.4688L17.2031 12H13.875V9.75C13.875 8.8008 14.3391 7.875 15.8297 7.875H17.3438V4.92188C17.3438 4.92188 15.9703 4.6875 14.6578 4.6875C11.9157 4.6875 10.125 6.3492 10.125 9.35625V12H7.07812V15.4688H10.125V23.8547C10.7367 23.9508 11.3625 24 12 24C12.6375 24 13.2633 23.9508 13.875 23.8547V15.4688H16.6711Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_865_12306">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 964 B |
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package",
|
||||
"name": "@logto/connector-facebook",
|
||||
"version": "1.0.0",
|
||||
"description": "Facebook web connector implementation.",
|
||||
"author": "Silverhand Inc. <contact@silverhand.io>"
|
||||
}
|
55
packages/connectors/connector-facebook/src/constant.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import type { ConnectorMetadata } from '@logto/connector-kit';
|
||||
import { ConnectorPlatform, ConnectorConfigFormItemType } from '@logto/connector-kit';
|
||||
|
||||
/**
|
||||
* Note: If you do not include a version number we will default to the oldest available version, so it's recommended to include the version number in your requests.
|
||||
* https://developers.facebook.com/docs/graph-api/overview#versions
|
||||
*/
|
||||
export const authorizationEndpoint = 'https://www.facebook.com/v13.0/dialog/oauth';
|
||||
export const accessTokenEndpoint = 'https://graph.facebook.com/v13.0/oauth/access_token';
|
||||
/**
|
||||
* Note: The /me node is a special endpoint that translates to the object ID of the person or Page whose access token is currently being used to make the API calls.
|
||||
* https://developers.facebook.com/docs/graph-api/overview#me
|
||||
* https://developers.facebook.com/docs/graph-api/reference/user#Reading
|
||||
*/
|
||||
export const userInfoEndpoint = 'https://graph.facebook.com/v13.0/me';
|
||||
export const scope = 'email,public_profile';
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'facebook-universal',
|
||||
target: 'facebook',
|
||||
platform: ConnectorPlatform.Universal,
|
||||
name: {
|
||||
en: 'Facebook',
|
||||
'zh-CN': 'Facebook',
|
||||
'tr-TR': 'Facebook',
|
||||
ko: 'Facebook',
|
||||
},
|
||||
logo: './logo.svg',
|
||||
logoDark: null,
|
||||
description: {
|
||||
en: 'Facebook is a worldwide social media platform with billions of users.',
|
||||
'zh-CN': 'Facebook 是有数十亿用户的社交平台。',
|
||||
'tr-TR': 'Facebook, en aktif kullanıcılara sahip dünya çapında bir sosyal medya platformudur.', // UNTRANSLATED
|
||||
ko: '페이스북은 가장 활동적인 사용자를 가진 세계적인 소셜 미디어 플랫폼입니다.', // UNTRANSLATED
|
||||
},
|
||||
readme: './README.md',
|
||||
formItems: [
|
||||
{
|
||||
key: 'clientId',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: true,
|
||||
label: 'Client ID',
|
||||
placeholder: '<client-id>',
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
type: ConnectorConfigFormItemType.Text,
|
||||
required: true,
|
||||
label: 'Client Secret',
|
||||
placeholder: '<client-secret>',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const defaultTimeout = 5000;
|
217
packages/connectors/connector-facebook/src/index.test.ts
Normal file
|
@ -0,0 +1,217 @@
|
|||
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit';
|
||||
import nock from 'nock';
|
||||
|
||||
import { accessTokenEndpoint, authorizationEndpoint, userInfoEndpoint } from './constant.js';
|
||||
import createConnector, { getAccessToken } from './index.js';
|
||||
import { clientId, clientSecret, code, dummyRedirectUri, fields, mockedConfig } from './mock.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const getConfig = jest.fn().mockResolvedValue(mockedConfig);
|
||||
|
||||
describe('Facebook connector', () => {
|
||||
describe('getAuthorizationUri', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should get a valid authorizationUri with redirectUri and state', async () => {
|
||||
const redirectUri = 'http://localhost:3000/callback';
|
||||
const state = 'some_state';
|
||||
const connector = await createConnector({ getConfig });
|
||||
const authorizationUri = await connector.getAuthorizationUri(
|
||||
{
|
||||
state,
|
||||
redirectUri,
|
||||
connectorId: 'some_connector_id',
|
||||
connectorFactoryId: 'some_connector_factory_id',
|
||||
jti: 'some_jti',
|
||||
headers: {},
|
||||
},
|
||||
jest.fn()
|
||||
);
|
||||
|
||||
const encodedRedirectUri = encodeURIComponent(redirectUri);
|
||||
expect(authorizationUri).toEqual(
|
||||
`${authorizationEndpoint}?client_id=${clientId}&redirect_uri=${encodedRedirectUri}&response_type=code&state=${state}&scope=email%2Cpublic_profile`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccessToken', () => {
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should get an accessToken by exchanging with code', async () => {
|
||||
nock(accessTokenEndpoint)
|
||||
.get('')
|
||||
.query({
|
||||
code,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
redirect_uri: dummyRedirectUri,
|
||||
})
|
||||
.reply(200, {
|
||||
access_token: 'access_token',
|
||||
scope: 'scope',
|
||||
token_type: 'token_type',
|
||||
expires_in: 3600,
|
||||
});
|
||||
|
||||
const { accessToken } = await getAccessToken(mockedConfig, {
|
||||
code,
|
||||
redirectUri: dummyRedirectUri,
|
||||
});
|
||||
expect(accessToken).toEqual('access_token');
|
||||
});
|
||||
|
||||
it('throws SocialAuthCodeInvalid error if accessToken not found in response', async () => {
|
||||
nock(accessTokenEndpoint)
|
||||
.get('')
|
||||
.query({
|
||||
code,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
redirect_uri: dummyRedirectUri,
|
||||
})
|
||||
.reply(200, {
|
||||
access_token: '',
|
||||
scope: 'scope',
|
||||
token_type: 'token_type',
|
||||
expires_in: 3600,
|
||||
});
|
||||
|
||||
await expect(
|
||||
getAccessToken(mockedConfig, { code, redirectUri: dummyRedirectUri })
|
||||
).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserInfo', () => {
|
||||
beforeEach(() => {
|
||||
nock(accessTokenEndpoint)
|
||||
.get('')
|
||||
.query({
|
||||
code,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
redirect_uri: dummyRedirectUri,
|
||||
})
|
||||
.reply(200, {
|
||||
access_token: 'access_token',
|
||||
scope: 'scope',
|
||||
token_type: 'token_type',
|
||||
expires_in: 3600,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should get valid SocialUserInfo', async () => {
|
||||
const avatar = 'https://github.com/images/error/octocat_happy.gif';
|
||||
nock(userInfoEndpoint)
|
||||
.get('')
|
||||
.query({ fields })
|
||||
.reply(200, {
|
||||
id: '1234567890',
|
||||
name: 'monalisa octocat',
|
||||
email: 'octocat@facebook.com',
|
||||
picture: { data: { url: avatar } },
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
const socialUserInfo = await connector.getUserInfo(
|
||||
{
|
||||
code,
|
||||
redirectUri: dummyRedirectUri,
|
||||
},
|
||||
jest.fn()
|
||||
);
|
||||
expect(socialUserInfo).toMatchObject({
|
||||
id: '1234567890',
|
||||
avatar,
|
||||
name: 'monalisa octocat',
|
||||
email: 'octocat@facebook.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('throws SocialAccessTokenInvalid error if remote response code is 401', async () => {
|
||||
nock(userInfoEndpoint).get('').query({ fields }).reply(400);
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(
|
||||
connector.getUserInfo({ code, redirectUri: dummyRedirectUri }, jest.fn())
|
||||
).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid));
|
||||
});
|
||||
|
||||
it('throws AuthorizationFailed error if error is access_denied', async () => {
|
||||
const avatar = 'https://github.com/images/error/octocat_happy.gif';
|
||||
nock(userInfoEndpoint)
|
||||
.get('')
|
||||
.query({ fields })
|
||||
.reply(200, {
|
||||
id: '1234567890',
|
||||
name: 'monalisa octocat',
|
||||
email: 'octocat@facebook.com',
|
||||
picture: { data: { url: avatar } },
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(
|
||||
connector.getUserInfo(
|
||||
{
|
||||
error: 'access_denied',
|
||||
error_code: 200,
|
||||
error_description: 'Permissions error.',
|
||||
error_reason: 'user_denied',
|
||||
},
|
||||
jest.fn()
|
||||
)
|
||||
).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.AuthorizationFailed, 'Permissions error.')
|
||||
);
|
||||
});
|
||||
|
||||
it('throws General error if error is not access_denied', async () => {
|
||||
const avatar = 'https://github.com/images/error/octocat_happy.gif';
|
||||
nock(userInfoEndpoint)
|
||||
.get('')
|
||||
.query({ fields })
|
||||
.reply(200, {
|
||||
id: '1234567890',
|
||||
name: 'monalisa octocat',
|
||||
email: 'octocat@facebook.com',
|
||||
picture: { data: { url: avatar } },
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(
|
||||
connector.getUserInfo(
|
||||
{
|
||||
error: 'general_error',
|
||||
error_code: 200,
|
||||
error_description: 'General error encountered.',
|
||||
error_reason: 'user_denied',
|
||||
},
|
||||
jest.fn()
|
||||
)
|
||||
).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.General, {
|
||||
error: 'general_error',
|
||||
error_code: 200,
|
||||
errorDescription: 'General error encountered.',
|
||||
error_reason: 'user_denied',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('throws unrecognized error', async () => {
|
||||
nock(userInfoEndpoint).get('').reply(500);
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(
|
||||
connector.getUserInfo({ code, redirectUri: dummyRedirectUri }, jest.fn())
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
174
packages/connectors/connector-facebook/src/index.ts
Normal file
|
@ -0,0 +1,174 @@
|
|||
/**
|
||||
* Reference: Manually Build a Login Flow
|
||||
* https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow
|
||||
*/
|
||||
|
||||
import type {
|
||||
CreateConnector,
|
||||
SocialConnector,
|
||||
GetAuthorizationUri,
|
||||
GetUserInfo,
|
||||
GetConnectorConfig,
|
||||
} from '@logto/connector-kit';
|
||||
import {
|
||||
ConnectorError,
|
||||
ConnectorErrorCodes,
|
||||
validateConfig,
|
||||
ConnectorType,
|
||||
parseJson,
|
||||
} from '@logto/connector-kit';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import { got, HTTPError } from 'got';
|
||||
|
||||
import {
|
||||
accessTokenEndpoint,
|
||||
authorizationEndpoint,
|
||||
scope,
|
||||
userInfoEndpoint,
|
||||
defaultMetadata,
|
||||
defaultTimeout,
|
||||
} from './constant.js';
|
||||
import type { FacebookConfig } from './types.js';
|
||||
import {
|
||||
authorizationCallbackErrorGuard,
|
||||
facebookConfigGuard,
|
||||
accessTokenResponseGuard,
|
||||
userInfoResponseGuard,
|
||||
authResponseGuard,
|
||||
} from './types.js';
|
||||
|
||||
const getAuthorizationUri =
|
||||
(getConfig: GetConnectorConfig): GetAuthorizationUri =>
|
||||
async ({ state, redirectUri }) => {
|
||||
const config = await getConfig(defaultMetadata.id);
|
||||
validateConfig<FacebookConfig>(config, facebookConfigGuard);
|
||||
|
||||
const queryParameters = new URLSearchParams({
|
||||
client_id: config.clientId,
|
||||
redirect_uri: redirectUri,
|
||||
response_type: 'code',
|
||||
state,
|
||||
scope, // Only support fixed scope for v1.
|
||||
});
|
||||
|
||||
return `${authorizationEndpoint}?${queryParameters.toString()}`;
|
||||
};
|
||||
|
||||
export const getAccessToken = async (
|
||||
config: FacebookConfig,
|
||||
codeObject: { code: string; redirectUri: string }
|
||||
) => {
|
||||
const { code, redirectUri } = codeObject;
|
||||
validateConfig<FacebookConfig>(config, facebookConfigGuard);
|
||||
|
||||
const { clientId: client_id, clientSecret: client_secret } = config;
|
||||
|
||||
const httpResponse = await got.get(accessTokenEndpoint, {
|
||||
searchParams: {
|
||||
code,
|
||||
client_id,
|
||||
client_secret,
|
||||
redirect_uri: redirectUri,
|
||||
},
|
||||
timeout: { request: defaultTimeout },
|
||||
});
|
||||
|
||||
const result = accessTokenResponseGuard.safeParse(parseJson(httpResponse.body));
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error);
|
||||
}
|
||||
|
||||
const { access_token: accessToken } = result.data;
|
||||
|
||||
assert(accessToken, new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid));
|
||||
|
||||
return { accessToken };
|
||||
};
|
||||
|
||||
const getUserInfo =
|
||||
(getConfig: GetConnectorConfig): GetUserInfo =>
|
||||
async (data) => {
|
||||
const { code, redirectUri } = await authorizationCallbackHandler(data);
|
||||
const config = await getConfig(defaultMetadata.id);
|
||||
validateConfig<FacebookConfig>(config, facebookConfigGuard);
|
||||
const { accessToken } = await getAccessToken(config, { code, redirectUri });
|
||||
|
||||
try {
|
||||
const httpResponse = await got.get(userInfoEndpoint, {
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
searchParams: {
|
||||
fields: 'id,name,email,picture',
|
||||
},
|
||||
timeout: { request: defaultTimeout },
|
||||
});
|
||||
|
||||
const result = userInfoResponseGuard.safeParse(parseJson(httpResponse.body));
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error);
|
||||
}
|
||||
|
||||
const { id, email, name, picture } = result.data;
|
||||
|
||||
return {
|
||||
id,
|
||||
avatar: picture?.data.url,
|
||||
email,
|
||||
name,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof HTTPError) {
|
||||
const { statusCode, body: rawBody } = error.response;
|
||||
|
||||
if (statusCode === 400) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
|
||||
}
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(rawBody));
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const authorizationCallbackHandler = async (parameterObject: unknown) => {
|
||||
const result = authResponseGuard.safeParse(parameterObject);
|
||||
|
||||
if (result.success) {
|
||||
return result.data;
|
||||
}
|
||||
|
||||
const parsedError = authorizationCallbackErrorGuard.safeParse(parameterObject);
|
||||
|
||||
if (!parsedError.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, JSON.stringify(parameterObject));
|
||||
}
|
||||
|
||||
const { error, error_code, error_description, error_reason } = parsedError.data;
|
||||
|
||||
if (error === 'access_denied') {
|
||||
throw new ConnectorError(ConnectorErrorCodes.AuthorizationFailed, error_description);
|
||||
}
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, {
|
||||
error,
|
||||
error_code,
|
||||
errorDescription: error_description,
|
||||
error_reason,
|
||||
});
|
||||
};
|
||||
|
||||
const createFacebookConnector: CreateConnector<SocialConnector> = async ({ getConfig }) => {
|
||||
return {
|
||||
metadata: defaultMetadata,
|
||||
type: ConnectorType.Social,
|
||||
configGuard: facebookConfigGuard,
|
||||
getAuthorizationUri: getAuthorizationUri(getConfig),
|
||||
getUserInfo: getUserInfo(getConfig),
|
||||
};
|
||||
};
|
||||
|
||||
export default createFacebookConnector;
|
7
packages/connectors/connector-facebook/src/mock.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export const clientId = 'client_id_value';
|
||||
export const clientSecret = 'client_secret_value';
|
||||
export const code = 'code';
|
||||
export const dummyRedirectUri = 'dummyRedirectUri';
|
||||
export const fields = 'id,name,email,picture';
|
||||
|
||||
export const mockedConfig = { clientId, clientSecret };
|