소스 검색

feat: noah 签名

max 5 달 전
부모
커밋
b754bf2bb4
1개의 변경된 파일152개의 추가작업 그리고 0개의 파일을 삭제
  1. 152 0
      src/modules/payment/adapter/noah.adapter.ts

+ 152 - 0
src/modules/payment/adapter/noah.adapter.ts

@@ -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;
+    }
+  }
+}