From 252368e51efeea3ca14ce6279ecdbfc16699d70b Mon Sep 17 00:00:00 2001 From: CounterFire2023 <136581895+CounterFire2023@users.noreply.github.com> Date: Fri, 27 Sep 2024 16:36:35 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=BA=94=E7=94=A8=E5=86=85?= =?UTF-8?q?=E5=85=85=E5=80=BC=EF=BC=8C=E8=AE=B0=E5=BD=95=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E4=B8=8D=E5=86=8D=E4=BD=9C=E4=B8=BA=E8=B4=AD?= =?UTF-8?q?=E4=B9=B0=E8=80=85=EF=BC=8C=20=E5=A2=9E=E5=8A=A0passport=20toke?= =?UTF-8?q?n=E7=9A=84=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/inapp.md | 49 +++++++++++++ package.json | 1 + packages/zutils | 2 +- src/common/ReadOnlyCache.ts | 14 ++++ src/controllers/applepay.controller.ts | 17 +++-- src/controllers/googlepay.controller.ts | 18 +++-- src/modules/AppleInApp.ts | 6 +- src/modules/GoogleInApp.ts | 6 +- src/plugins/apiauth.ts | 26 ++++--- src/utils/jwt.utils.ts | 91 +++++++++++++++++++++++++ start.json | 29 +++++++- yarn.lock | 38 ++++++++++- 12 files changed, 269 insertions(+), 28 deletions(-) create mode 100644 docs/inapp.md create mode 100644 src/common/ReadOnlyCache.ts create mode 100644 src/utils/jwt.utils.ts diff --git a/docs/inapp.md b/docs/inapp.md new file mode 100644 index 0000000..fddc112 --- /dev/null +++ b/docs/inapp.md @@ -0,0 +1,49 @@ +# + +## 说明 + +``` + +1. 如无特殊说明, 所有接口返回 json, 顶级结构如下, 接口 Response 的数据结构说明只包含 data 部分 + +```JSON +{ + "errcode": 0, //0:成功 100: 所有未定义的错误, 其他根据errmsg判断 + "errmsg": "", //错误描述, 一般在code=0时, 该字段为空 + "data": {}, // 数据 +} +``` + +## 接口列表 + +### 1. 验证订单信息 + +1. Method: POST +2. URI: google: /native_pay/google/verify + apple: /native_pay/apple/verify +3. HOST: 测试: https://pay.cebggame.com/v2 + 生产: https://pay.cebggame.com/v0 + +> header 参数 + + +> url param 参数 + +| 字段 | 说明 | +| --------- | -------------------- | +| list | 通过queryPurchase或buyProduct获取的数据中的data对象 | + +5. Response: JSON + +```js +[ + { + productId: 'google或apple后台获取的productId', + gameOrderId: '游戏订单号', + orderId: 'google或apple返回的orderId', + status: '订单状态', + } +] +``` + +status: 0:默认状态, 未支付 3: 已支付, 未确认 9: 已成功支付和确认 diff --git a/package.json b/package.json index 25da4e4..675e2ad 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "fast-rbac": "^2.0.1", "fastify": "^4.8.1", "fastify-plugin": "^4.2.1", + "get-jwks": "^9.0.2", "google-auth-library": "^8.5.2", "googleapis": "^120.0.0", "jose": "^4.14.4", diff --git a/packages/zutils b/packages/zutils index b982e43..cf4af92 160000 --- a/packages/zutils +++ b/packages/zutils @@ -1 +1 @@ -Subproject commit b982e43cd717e09acafba79f40c63e2572dbef96 +Subproject commit cf4af92b819c06a6263a7b06075154b1db5cff49 diff --git a/src/common/ReadOnlyCache.ts b/src/common/ReadOnlyCache.ts new file mode 100644 index 0000000..8220fbf --- /dev/null +++ b/src/common/ReadOnlyCache.ts @@ -0,0 +1,14 @@ +import { singleton } from 'zutils' + +@singleton +export class ReadOnlyCache { + map: Map = new Map() + + public getData(key: string) { + return this.map.get(key) + } + + public setData(key: string, value: any) { + this.map.set(key, value) + } +} diff --git a/src/controllers/applepay.controller.ts b/src/controllers/applepay.controller.ts index bdab793..8ec1ec4 100644 --- a/src/controllers/applepay.controller.ts +++ b/src/controllers/applepay.controller.ts @@ -7,10 +7,18 @@ import { BaseController, router, ZError, role, ROLE_ANON } from 'zutils' class ApplePayController extends BaseController { @router('post /pay/apple/verify') + @router('post /native_pay/apple/verify') async verifyApplePay(req, res) { logger.db('applepay_verify', req) - const user = req.user - const { list } = req.params + const { openId } = req.user + let { list } = req.params + if (typeof list === 'string') { + try { + list = JSON.parse(list) + } catch (err) { + throw new ZError(10, 'purchase data is empty') + } + } if (!list || !list.length) { throw new ZError(10, 'purchase data is empty') } @@ -23,15 +31,16 @@ class ApplePayController extends BaseController { if (!tx) { continue } - await InAppRecord.addRecord(user.id, tx, 'apple') + await InAppRecord.addRecord(openId, tx, 'apple') logger.info('getTransactionInfo:: ', tx) if (tx.transactionReason === 'PURCHASE' && tx.inAppOwnershipType === 'PURCHASED') { status = ApplePayStatus.SUCCESS } if (status === ApplePayStatus.SUCCESS) { let record = await AppleInApp.insertOrUpdate( - { account: user.id, outOrderId: tx.transactionId }, + { outOrderId: tx.transactionId }, { + account: openId, productId: tx.productId, gameOrderId: tx.appAccountToken, country: tx.storefront, diff --git a/src/controllers/googlepay.controller.ts b/src/controllers/googlepay.controller.ts index 668d2ee..5ad0163 100644 --- a/src/controllers/googlepay.controller.ts +++ b/src/controllers/googlepay.controller.ts @@ -7,10 +7,19 @@ import { BaseController, router, ZError } from 'zutils' class GooglePayController extends BaseController { @router('post /pay/google/verify') + @router('post /native_pay/google/verify') async verifyGooglePay(req, res) { logger.db('googlepay_verify', req) - const user = req.user - const { list } = req.params + const { openId } = req.user + let { list } = req.params + // check if list is string + if (typeof list === 'string') { + try { + list = JSON.parse(list) + } catch (err) { + throw new ZError(10, 'purchase data is empty') + } + } if (!list || !list.length) { throw new ZError(10, 'purchase data is empty') } @@ -23,7 +32,7 @@ class GooglePayController extends BaseController { throw new ZError(20, 'fetch purchase data failed') } let info = infoRes.data - await InAppRecord.addRecord(user.id, info, 'google') + await InAppRecord.addRecord(openId, info, 'google') let status = GooglePayStatus.PENDING if (info.purchaseState === 0 && info.consumptionState === 0) { status = GooglePayStatus.PURCHASED @@ -31,8 +40,9 @@ class GooglePayController extends BaseController { if (status === GooglePayStatus.PURCHASED) { try { let record = await GoogleInApp.insertOrUpdate( - { account: user.id, outOrderId: info.orderId }, + { outOrderId: info.orderId }, { + account: openId, purchaseToken: sub.token, productId: sub.id, gameOrderId: info.obfuscatedExternalAccountId, diff --git a/src/modules/AppleInApp.ts b/src/modules/AppleInApp.ts index 13461f8..bfecadc 100644 --- a/src/modules/AppleInApp.ts +++ b/src/modules/AppleInApp.ts @@ -18,16 +18,16 @@ export enum ApplePayStatus { @dbconn('pay') @index({ gameOrderId: 1 }, { unique: true, partialFilterExpression: { gameOrderId: { $exists: true } } }) -@index({ account: 1, outOrderId: 1 }, { unique: true }) +@index({ outOrderId: 1 }, { unique: true }) @index({ account: 1 }, { unique: false }) @modelOptions({ - schemaOptions: { collection: 'apple_inapp_record', timestamps: true }, + schemaOptions: { collection: 'apple_inapp_record_202410', timestamps: true }, options: { allowMixed: Severity.ALLOW }, }) export class AppleInAppClass extends BaseModule { // 用户账号 @prop({ required: true }) - public account: string + public account: string // 这里只表示谁来消耗这个订单, 不代表购买者 // 渠道返回的订单id // transactionId @prop() diff --git a/src/modules/GoogleInApp.ts b/src/modules/GoogleInApp.ts index a70fd75..18e3b53 100644 --- a/src/modules/GoogleInApp.ts +++ b/src/modules/GoogleInApp.ts @@ -18,16 +18,16 @@ export enum GooglePayStatus { @dbconn('pay') @index({ gameOrderId: 1 }, { unique: true, partialFilterExpression: { gameOrderId: { $exists: true } } }) -@index({ account: 1, outOrderId: 1 }, { unique: true }) +@index({ outOrderId: 1 }, { unique: true }) @index({ account: 1 }, { unique: false }) @modelOptions({ - schemaOptions: { collection: 'google_inapp_record', timestamps: true }, + schemaOptions: { collection: 'google_inapp_record_202410', timestamps: true }, options: { allowMixed: Severity.ALLOW }, }) export class GoogleInAppClass extends BaseModule { // 用户账号 @prop({ required: true }) - public account: string + public account: string // 这里只表示谁来消耗这个订单, 不代表购买者 // 渠道返回的订单id @prop() public outOrderId: string diff --git a/src/plugins/apiauth.ts b/src/plugins/apiauth.ts index 13676db..240250d 100644 --- a/src/plugins/apiauth.ts +++ b/src/plugins/apiauth.ts @@ -1,6 +1,7 @@ import { FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify' import fastifyPlugin from 'fastify-plugin' import { Account } from 'modules/Account' +import { verifyCombinedToken } from 'utils/jwt.utils' declare module 'fastify' { interface FastifyRequest { @@ -33,16 +34,23 @@ const apiAuthPlugin: FastifyPluginAsync = async function (fastif if (!request.token) { return reply.send({ errcode: 11, errmsg: 'need login' }) } - //@ts-ignore - const data = this.jwt.verify(request.token) - if (!data || !data.id) { - return reply.send({ errcode: 10, errmsg: 'need login' }) + if (request.url.indexOf('native_pay/') >= 0) { + const data = await verifyCombinedToken(request.token) + data.openId = data.openId || data.openid || data.passport?.zkevm_eth_address || data.passport?.eth_key + request.user = data + } else { + //@ts-ignore + const data = this.jwt.verify(request.token) + if (!data || !data.id) { + return reply.send({ errcode: 10, errmsg: 'need login' }) + } + let account = await Account.findById(data.id) + if (!account) { + return reply.send({ errcode: 10, errmsg: 'need login' }) + } + request.user = account } - let account = await Account.findById(data.id) - if (!account) { - return reply.send({ errcode: 10, errmsg: 'need login' }) - } - request.user = account + } catch (err) { return reply.send({ errcode: 401, errmsg: 'need auth' }) } diff --git a/src/utils/jwt.utils.ts b/src/utils/jwt.utils.ts new file mode 100644 index 0000000..e3836b5 --- /dev/null +++ b/src/utils/jwt.utils.ts @@ -0,0 +1,91 @@ + +import { ReadOnlyCache } from 'common/ReadOnlyCache' +import { createSigner, createVerifier } from 'fast-jwt' + +import buildGetJwks from 'get-jwks' + +const domain = 'https://auth.immutable.com' + +const privateKey = `-----BEGIN PRIVATE KEY----- +${process.env.REFRESH_TOKEN_SECRET_PRIVATE} +-----END PRIVATE KEY----- +` +const publicKey = `-----BEGIN PUBLIC KEY----- +${process.env.REFRESH_TOKEN_SECRET_PUBLIC} +-----END PUBLIC KEY----- +` +const REFRESH_TOKEN_EXPIRES_IN = 30 * 24 * 60 * 60 * 1000 +const privateKeyWallet = `-----BEGIN PRIVATE KEY----- +${process.env.API_TOKEN_SECRET_PRIVATE} +-----END PRIVATE KEY----- +` +const publicKeyWallet = `-----BEGIN PUBLIC KEY----- +${process.env.API_TOKEN_SECRET_PUBLIC} +-----END PUBLIC KEY----- +` +const WALLET_TOKEN_EXPIRES_IN = 30 * 24 * 60 * 60 * 1000 + +const getJwks = buildGetJwks({ }) + +export const generateRefreshToken = (data: any) => { + const signSync = createSigner({ + algorithm: 'EdDSA', + expiresIn: REFRESH_TOKEN_EXPIRES_IN, + key: privateKey, + }) + return signSync(data) +} + +export const verifyRefreshToken = (token: string) => { + const verifier = createVerifier({ + algorithms: ['EdDSA'], + key: publicKey, + }) + return verifier(token) +} + +export const generateWalletToken = (data: any) => { + const signSync = createSigner({ + algorithm: 'EdDSA', + expiresIn: WALLET_TOKEN_EXPIRES_IN, + key: privateKeyWallet, + }) + return signSync(data) +} + +export const verifyWalletToken = (token: string) => { + const verifier = createVerifier({ + algorithms: ['EdDSA'], + key: publicKeyWallet, + }) + return verifier(token) +} + +export const verifyPassportToken = (token: string) => { + const verifyWithPromise = createVerifier({ + key: async function (header) { + let publicKey = new ReadOnlyCache().getData('passport_public_key') + if (!publicKey) { + publicKey = await getJwks.getPublicKey({ + kid: header.kid, + alg: header.alg, + domain, + }) + new ReadOnlyCache().setData('passport_public_key', publicKey) + } + return Promise.resolve(publicKey) + }, + }) + return verifyWithPromise(token) +} + +export const verifyCombinedToken = (token: string) => { + let tokenSuffix = token.split('.')[3] + tokenSuffix = tokenSuffix || 'passport' + token = token.replace(`.${tokenSuffix}`, '') + if (tokenSuffix === 'passport') { + return verifyPassportToken(token) + } else { + return verifyWalletToken(token) + } +} \ No newline at end of file diff --git a/start.json b/start.json index 24b11f3..1930999 100644 --- a/start.json +++ b/start.json @@ -1,10 +1,10 @@ { "apps": [ { - "name": "wallet-svr", + "name": "pay-svr-release", "script": "npm", "args": "run prod:api", - "cwd": "/data/apps/wallet-svr", + "cwd": "/home/kingsome/code/pay-svr", "max_memory_restart": "1024M", "log_date_format" : "YYYY-MM-DD HH:mm Z", "watch": true, @@ -15,13 +15,36 @@ "tasks" ], "instances": 1, - "exec_mode": "cluster", + "exec_mode": "fork", "env": { "NODE_ENV": "production" }, "env_production": { "NODE_ENV": "production" } + }, + { + "name": "pay-svr-test-v0", + "script": "npm", + "args": "run dev:api", + "cwd": "/home/kingsome/code/pay-svr", + "max_memory_restart": "1024M", + "log_date_format" : "YYYY-MM-DD HH:mm Z", + "watch": true, + "ignore_watch": [ + "node_modules", + "logs", + "fixtures", + "tasks" + ], + "instances": 1, + "exec_mode": "fork", + "env": { + "NODE_ENV": "development" + }, + "env_production": { + "NODE_ENV": "development" + } } ] } diff --git a/yarn.lock b/yarn.lock index a1592a7..cd9824d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1560,6 +1560,19 @@ elliptic@6.5.4, elliptic@^6.4.0, elliptic@^6.5.2, elliptic@^6.5.4: minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" +elliptic@^6.5.7: + version "6.5.7" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.7.tgz#8ec4da2cb2939926a1b9a73619d768207e647c8b" + integrity sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -2277,6 +2290,15 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2: has-symbols "^1.0.3" hasown "^2.0.0" +get-jwks@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/get-jwks/-/get-jwks-9.0.2.tgz#9364efb7a48b126a8df88e67757c413d6abf747d" + integrity sha512-zn2OvElozYtckpYJvgRWMOMhEkW8KgFp+lN0B7Q6SXPZg/CFfeiPoh73Wbhacj4fYXDJxkxbcwI9j+/cubpzSQ== + dependencies: + jwk-to-pem "^2.0.4" + lru-cache "^10.0.0" + node-fetch "^2.6.1" + get-stream@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" @@ -2904,6 +2926,15 @@ jwa@^2.0.0: ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" +jwk-to-pem@^2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/jwk-to-pem/-/jwk-to-pem-2.0.6.tgz#0810c03307e873d5c81faeb650408fa3ae91eb9c" + integrity sha512-zPC/5vjyR08TpknpTGW6Z3V3lDf9dU92oHbf0jJlG8tGOzslF9xk2UiO/seSx2llCUrNAe+AvmuGTICSXiYU7A== + dependencies: + asn1.js "^5.3.0" + elliptic "^6.5.7" + safe-buffer "^5.0.1" + jwks-rsa@^2.1.3: version "2.1.5" resolved "https://registry.npmmirror.com/jwks-rsa/-/jwks-rsa-2.1.5.tgz#bb7bf8c5767836bc273bf5b27870066aca39c1bb" @@ -3052,6 +3083,11 @@ lowercase-keys@^3.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-3.0.0.tgz#c5e7d442e37ead247ae9db117a9d0a467c89d4f2" integrity sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ== +lru-cache@^10.0.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -3388,7 +3424,7 @@ node-addon-api@^2.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== -node-fetch@^2.6.12: +node-fetch@2, node-fetch@^2.6.1, node-fetch@^2.6.12: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==