|
@@ -0,0 +1,152 @@
|
|
|
+import { Provide, Inject, Config, ALL } from '@midwayjs/decorator';
|
|
|
+
|
|
|
+import { Repository } from 'typeorm';
|
|
|
+import { InjectEntityModel } from '@midwayjs/typeorm';
|
|
|
+import { PaymentChannelEntity } from '../entity/channel';
|
|
|
+import axios from 'axios';
|
|
|
+import { CustomerEntity } from '../entity/customer';
|
|
|
+import { ILogger } from '@midwayjs/core';
|
|
|
+
|
|
|
+import * as crypto from 'crypto';
|
|
|
+import * as jwt from 'jsonwebtoken';
|
|
|
+
|
|
|
+@Provide()
|
|
|
+export class NoahPayAdapter {
|
|
|
+ @InjectEntityModel(PaymentChannelEntity)
|
|
|
+ channelEntity: Repository<PaymentChannelEntity>;
|
|
|
+ @InjectEntityModel(CustomerEntity)
|
|
|
+ customerEntity: Repository<CustomerEntity>;
|
|
|
+ @Config(ALL)
|
|
|
+ globalConfig;
|
|
|
+ @Inject()
|
|
|
+ ctx;
|
|
|
+
|
|
|
+ @Inject()
|
|
|
+ logger: ILogger;
|
|
|
+
|
|
|
+ private config: {
|
|
|
+ apiKey: string;
|
|
|
+ apiSecret: string;
|
|
|
+ apiUrl: string;
|
|
|
+ chainApiKey: string;
|
|
|
+ chainApiSecret: string;
|
|
|
+ chainApiUrl: string;
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化渠道配置
|
|
|
+ */
|
|
|
+ private async initConfig() {
|
|
|
+ if (!this.config) {
|
|
|
+ const channel = await this.channelEntity.findOne({
|
|
|
+ where: { code: 'NOAHPAY', isEnabled: true },
|
|
|
+ });
|
|
|
+ if (!channel) {
|
|
|
+ throw new Error('FusionPay channel not found or disabled');
|
|
|
+ }
|
|
|
+ this.config = {
|
|
|
+ apiKey: channel.apiKey,
|
|
|
+ apiSecret: channel.apiSecret,
|
|
|
+ apiUrl: channel.apiUrl,
|
|
|
+ chainApiKey: channel.chainApiKey,
|
|
|
+ chainApiSecret: channel.chainApiSecret,
|
|
|
+ chainApiUrl: channel.chainApiUrl,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Creates a JWT token for authenticating API requests.
|
|
|
+ * https://docs.noah.com/api-concepts/signing
|
|
|
+ *
|
|
|
+ * @param opts - Options for JWT creation.
|
|
|
+ * @param opts.body - A buffer made from the body of the request. Important to use the exact same body buffer in the request.
|
|
|
+ * @param opts.method - The HTTP method of the request, e.g., GET, POST, PUT, DELETE.
|
|
|
+ * @param opts.path - The path of the request, e.g., /api/v1/customers.
|
|
|
+ * @param opts.privateKey - The private key used to sign the JWT, in PEM format.
|
|
|
+ * @param opts.queryParams - The query parameters of the request.
|
|
|
+ * @returns A signed JWT token as a string.
|
|
|
+ */
|
|
|
+ async createJwt(opts: {
|
|
|
+ body: Buffer | undefined;
|
|
|
+ method: string;
|
|
|
+ path: string;
|
|
|
+ privateKey: string;
|
|
|
+ queryParams: object | undefined;
|
|
|
+ }): Promise<string> {
|
|
|
+ const { body, method, path, privateKey, queryParams } = opts;
|
|
|
+ let bodyHash;
|
|
|
+
|
|
|
+ if (body) {
|
|
|
+ bodyHash = crypto.createHash('sha256').update(body).digest('hex');
|
|
|
+ }
|
|
|
+
|
|
|
+ const payload = {
|
|
|
+ bodyHash,
|
|
|
+ method,
|
|
|
+ path,
|
|
|
+ queryParams,
|
|
|
+ };
|
|
|
+
|
|
|
+ // ES384 is recommended but the algorithm can also be ES256
|
|
|
+ const token = jwt.sign(payload, privateKey, {
|
|
|
+ algorithm: 'ES384',
|
|
|
+ audience: this.config.apiUrl,
|
|
|
+ // use a short expiry time, less than 15m
|
|
|
+ expiresIn: '5m',
|
|
|
+ });
|
|
|
+ return token;
|
|
|
+ }
|
|
|
+ async getAccessToken(method: string, path: string, data?: any) {
|
|
|
+ if (method === 'GET') {
|
|
|
+ const signature = await this.createJwt({
|
|
|
+ body: undefined,
|
|
|
+ method: 'GET',
|
|
|
+ path,
|
|
|
+ privateKey: this.config.apiSecret,
|
|
|
+ queryParams: data,
|
|
|
+ });
|
|
|
+ return signature;
|
|
|
+ }
|
|
|
+ const body = Buffer.from(JSON.stringify(data));
|
|
|
+ const signature = await this.createJwt({
|
|
|
+ body,
|
|
|
+ method: 'POST',
|
|
|
+ path,
|
|
|
+ privateKey: this.config.apiSecret,
|
|
|
+ queryParams: undefined,
|
|
|
+ });
|
|
|
+ return signature;
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 发送请求到 NoahPay API
|
|
|
+ */
|
|
|
+ async request(method: string, endpoint: string, data?: any) {
|
|
|
+ await this.initConfig();
|
|
|
+ let url = this.config.apiUrl;
|
|
|
+ try {
|
|
|
+ url = `${url}${endpoint.replace('/api/open/v4', '')}`;
|
|
|
+ const signature = this.getAccessToken(method, url, data);
|
|
|
+ const axiosParams: any = {
|
|
|
+ method,
|
|
|
+ url,
|
|
|
+ data: method !== 'GET' ? data : undefined,
|
|
|
+ params: method === 'GET' ? data : undefined,
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ 'Api-Signature': signature,
|
|
|
+ },
|
|
|
+ };
|
|
|
+ this.logger.info('向noahPay发送请求', axiosParams);
|
|
|
+ const response = await axios(axiosParams);
|
|
|
+ return response.data.data;
|
|
|
+ } catch (error) {
|
|
|
+ if (axios.isAxiosError(error) && error.response) {
|
|
|
+ this.ctx.status = error.response.status; // 服务器错误
|
|
|
+ this.logger.info('向noahPay发送请求失败了', error.response);
|
|
|
+ return error.response.data;
|
|
|
+ }
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|