修改应用内充值,记录中的账号不再作为购买者, 增加passport token的验证
This commit is contained in:
parent
959740af19
commit
252368e51e
49
docs/inapp.md
Normal file
49
docs/inapp.md
Normal file
@ -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: 已成功支付和确认
|
@ -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",
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit b982e43cd717e09acafba79f40c63e2572dbef96
|
||||
Subproject commit cf4af92b819c06a6263a7b06075154b1db5cff49
|
14
src/common/ReadOnlyCache.ts
Normal file
14
src/common/ReadOnlyCache.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { singleton } from 'zutils'
|
||||
|
||||
@singleton
|
||||
export class ReadOnlyCache {
|
||||
map: Map<string, any> = new Map()
|
||||
|
||||
public getData(key: string) {
|
||||
return this.map.get(key)
|
||||
}
|
||||
|
||||
public setData(key: string, value: any) {
|
||||
this.map.set(key, value)
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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<ApiAuthOptions> = 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' })
|
||||
}
|
||||
|
91
src/utils/jwt.utils.ts
Normal file
91
src/utils/jwt.utils.ts
Normal file
@ -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)
|
||||
}
|
||||
}
|
29
start.json
29
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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
38
yarn.lock
38
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==
|
||||
|
Loading…
x
Reference in New Issue
Block a user