diff --git a/.env.development b/.env.development index 03f30ab..860c27a 100644 --- a/.env.development +++ b/.env.development @@ -45,4 +45,7 @@ HASH_SALT='iG4Rpsa)6U31$H#^T85$^^3' GAME_PAY_CB_URL=https://game2006api-test.kingsome.cn/webapp/index.php?c=Shop&a=buyGoodsDirect # client登录时,验证用户数据的private key -WALLET_CLIENT_SK='38d9baa24aaea6f87a1caa51f588b0c9578368a1cb00b1639eb9f450b6cada00' \ No newline at end of file +WALLET_CLIENT_SK='38d9baa24aaea6f87a1caa51f588b0c9578368a1cb00b1639eb9f450b6cada00' + +# 检查guest能否绑定平台账号 +GAME_CHECK_RELATION_URL='https://game2006api-test.kingsome.cn/webapp/index.php?c=AccountVerify&a=canBind' \ No newline at end of file diff --git a/src/controllers/apple.controller.ts b/src/controllers/apple.controller.ts index 0d3b37f..bbe6390 100644 --- a/src/controllers/apple.controller.ts +++ b/src/controllers/apple.controller.ts @@ -1,9 +1,10 @@ import BaseController, { ROLE_ANON } from 'common/base.controller' import { role, router } from 'decorators/router' -import { Account, PlatEnum } from 'modules/Account' +import { Account } from 'modules/Account' import logger from 'logger/logger' import { PlatApple } from 'plats/PlatApple' import { IPlat } from 'plats/IPlat' +import { PlatEnum } from 'enums/PlatEnum' const plat: IPlat = new PlatApple() class AppleController extends BaseController { @@ -16,9 +17,13 @@ class AppleController extends BaseController { @role(ROLE_ANON) @router('post /wallet/login/apple') - async checkGoogleJwt(req, res) { + async checkAppleJwt(req, res) { logger.db('login', req) const { openId, data } = await plat.verifyToken(req) + const { api_platform } = req.headers + if (api_platform) { + data.platform = api_platform + } let user = await Account.insertOrUpdate({ plat: PlatEnum.APPLE, openId }, data) const ztoken = await res.jwtSign({ id: user.id, diff --git a/src/controllers/client.controller.ts b/src/controllers/client.controller.ts index bd2dd4a..d0a32ef 100644 --- a/src/controllers/client.controller.ts +++ b/src/controllers/client.controller.ts @@ -1,41 +1,29 @@ import BaseController, { ROLE_ANON } from 'common/base.controller' import { ZError } from 'common/ZError' import { role, router } from 'decorators/router' +import { PlatEnum } from 'enums/PlatEnum' import logger from 'logger/logger' -import { Account, PlatEnum } from 'modules/Account' -import * as wasm from 'rustwallet' -import { isUUID } from 'utils/string.util' +import { Account } from 'modules/Account' +import { IPlat } from 'plats/IPlat' +import { PlatClient } from 'plats/PlatClient' -const CLIENT_SUFFIX = '_clientid' - -function checkClientId(clientId: string) { - if (!clientId) { - return false - } - if (!clientId.endsWith(CLIENT_SUFFIX)) { - return false - } - const id = clientId.slice(0, clientId.length - CLIENT_SUFFIX.length) - return isUUID(id) -} +const plat: IPlat = new PlatClient() class ClientController extends BaseController { @role(ROLE_ANON) @router('post /wallet/login/client') async clientLogin(req, res) { const { code } = req.params - const { api_platform } = req.headers logger.db('login', req) if (!code) { throw new ZError(11, 'param missing') } - const sk = process.env.WALLET_CLIENT_SK - let codeDecrypto = wasm.rdecrypt(sk, code) - if (!checkClientId(codeDecrypto)) { - throw new ZError(12, 'param invalid') + const { openId, data } = await plat.verifyToken(req) + const { api_platform } = req.headers + if (api_platform) { + data.platform = api_platform } - const openId = codeDecrypto.slice(0, codeDecrypto.length - CLIENT_SUFFIX.length) logger.info('clientLogin', openId) - let user = await Account.insertOrUpdate({ plat: PlatEnum.CLIENT, openId }, { platform: api_platform }) + let user = await Account.insertOrUpdate({ plat: PlatEnum.CLIENT, openId }, data) const ztoken = await res.jwtSign({ id: user.id, openid: user.openId, diff --git a/src/controllers/facebook.controller.ts b/src/controllers/facebook.controller.ts index 2206443..a1ad719 100644 --- a/src/controllers/facebook.controller.ts +++ b/src/controllers/facebook.controller.ts @@ -1,8 +1,9 @@ import BaseController, { ROLE_ANON } from 'common/base.controller' import { ZError } from 'common/ZError' import { role, router } from 'decorators/router' +import { PlatEnum } from 'enums/PlatEnum' import logger from 'logger/logger' -import { Account, PlatEnum } from 'modules/Account' +import { Account } from 'modules/Account' import { IPlat } from 'plats/IPlat' import { PlatFacebook } from 'plats/PlatFacebook' diff --git a/src/controllers/google.controller.ts b/src/controllers/google.controller.ts index ad70244..bdadc8b 100644 --- a/src/controllers/google.controller.ts +++ b/src/controllers/google.controller.ts @@ -1,8 +1,9 @@ import BaseController, { ROLE_ANON } from 'common/base.controller' import { role, router } from 'decorators/router' +import { PlatEnum } from 'enums/PlatEnum' import logger from 'logger/logger' -import { Account, PlatEnum } from 'modules/Account' +import { Account } from 'modules/Account' import { IPlat } from 'plats/IPlat' import { PlatGoogle } from 'plats/PlatGoogle' diff --git a/src/controllers/login.controller.ts b/src/controllers/login.controller.ts index aecaa97..e9fe3c3 100644 --- a/src/controllers/login.controller.ts +++ b/src/controllers/login.controller.ts @@ -1,42 +1,120 @@ import { ZError } from 'common/ZError' import BaseController, { ROLE_ANON } from 'common/base.controller' import { role, router } from 'decorators/router' +import { PlatEnum } from 'enums/PlatEnum' import logger from 'logger/logger' -import { Account, PlatEnum } from 'modules/Account' +import { Account } from 'modules/Account' +import { UnionAccount } from 'modules/UnionAccount' +import { Wallet } from 'modules/Wallet' import { IPlat } from 'plats/IPlat' import { PlatApple } from 'plats/PlatApple' +import { PlatClient } from 'plats/PlatClient' import { PlatFacebook } from 'plats/PlatFacebook' import { PlatGoogle } from 'plats/PlatGoogle' import { PlatTikTok } from 'plats/PlatTikTok' +import { checkReleation } from 'service/game.svr' const plats: Map = new Map([ [PlatEnum.GOOGLE, new PlatGoogle()], [PlatEnum.APPLE, new PlatApple()], [PlatEnum.FACEBOOK, new PlatFacebook()], [PlatEnum.TIKTOK, new PlatTikTok()], + [PlatEnum.CLIENT, new PlatClient()], ]) +// 如果客户端有传入account, 则说明该次登录是绑定账号 +// 首先查找该账号是否已经绑定了其他账号 +const parseBindAccount = async (account: string, channel: PlatEnum, user: any) => { + const uid = user.id + let unionAccount + const filterData: any = {} + filterData[`plats.${channel}`] = uid + if (account) { + // TODO:: check from game svr, verify account and check if plat account could bind + let checkResult: any = await checkReleation(account, channel, user.openId) + console.log(checkResult) + if (checkResult.errcode) { + throw new ZError(30, checkResult.errmsg) + } + unionAccount = await UnionAccount.findOne({ gameAccount: account }) + if (unionAccount) { + let platInfo = unionAccount.plats.get(channel + '') + // 如果已经绑定, 且绑定的相同平台下不同的账号, 则抛出异常 + // 如果未绑定, 那么查找平台账号是否已经绑定了其他账号 + if (platInfo && platInfo !== uid) { + throw new ZError(21, 'account already bind') + } else if (!platInfo) { + // 检查pid是否已经绑定了其他账号 + let unionAccount2 = await UnionAccount.findOne(filterData) + // 如果记录不存在, 那么将平台账号绑定至当前unionAccount + if (!unionAccount2) { + // 如果当前unionAccount已经设置了钱包账号, 且钱包账号不是当前账号, 那么检查当前账号是否开启了钱包 + // 如果开启了钱包, 那么就不允许绑定 + if (unionAccount.walletAccount && unionAccount.walletAccount !== uid) { + const wallet = await Wallet.findByAccount(uid) + if (wallet && wallet.address) { + throw new ZError(23, 'plat account already had wallet') + } + } + unionAccount.plats.set(channel + '', uid) + unionAccount.markModified('plats') + } else if (unionAccount2.gameAccount && unionAccount2.gameAccount === account) { + // 这种情况不用处理, 理论上是不可能出现的 + } else { + throw new ZError(22, 'plat account already bind') + } + } + } else { + unionAccount = await UnionAccount.insertOrUpdate(filterData, {}) + if (unionAccount.gameAccount && unionAccount.gameAccount !== account) { + throw new ZError(22, 'plat account already bind') + } + unionAccount.gameAccount = account + } + await unionAccount.save() + } else { + unionAccount = await UnionAccount.findOne(filterData) + } + let walletUser + // 如果统一账号存在, 但钱包账号不存在,那么就把当前绑定账号的信息写入钱包账号 + if (unionAccount && !unionAccount.walletAccount) { + unionAccount.walletAccount = uid + walletUser = user + await unionAccount.save() + } else if (unionAccount && unionAccount.walletAccount) { + walletUser = await Account.findById(unionAccount.walletAccount) + } else { + walletUser = user + } + return { unionAccount, walletUser } +} class LoginController extends BaseController { @role(ROLE_ANON) @router('post /wallet/login/general') async generalLogin(req, res) { - const { channel, account } = req.params + const { code, channel, account } = req.params logger.db('login', req) + if (!code) { + throw new ZError(10, 'code not found') + } const plat = plats.get(channel) if (!plat) { - throw new ZError(10, 'plat not found') + throw new ZError(11, 'plat not support') } const { openId, data } = await plat.verifyToken(req) const { api_platform } = req.headers if (api_platform) { data.platform = api_platform } - let user = await Account.insertOrUpdate({ plat: channel, openId }, data) + const user = await Account.insertOrUpdate({ plat: channel, openId }, data) + const { unionAccount, walletUser } = await parseBindAccount(account, channel, user) const ztoken = await res.jwtSign({ - id: user.id, - openid: user.openId, - version: user.accountVersion || 0, - plat: user.plat, + id: walletUser.id, + uid: unionAccount?.id || '', + gid: unionAccount?.gameAccount || '', + openid: walletUser.openId, + version: walletUser.accountVersion || 0, + plat: walletUser.plat, }) return { token: ztoken } } diff --git a/src/controllers/mail.controller.ts b/src/controllers/mail.controller.ts index 554f1cb..b1a475e 100644 --- a/src/controllers/mail.controller.ts +++ b/src/controllers/mail.controller.ts @@ -1,8 +1,9 @@ import BaseController, { ROLE_ANON } from 'common/base.controller' import { ZError } from 'common/ZError' import { role, router } from 'decorators/router' +import { PlatEnum } from 'enums/PlatEnum' import logger from 'logger/logger' -import { Account, PlatEnum } from 'modules/Account' +import { Account } from 'modules/Account' import { CodeRecord, CodeStatus, CodeType, DEFAULT_CODE, DEFAULT_EXPIRE_TIME } from 'modules/CodeRecord' import { DEFAULT_REGIST_HTML, diff --git a/src/controllers/tiktok.controller.ts b/src/controllers/tiktok.controller.ts index ce4d936..6782b09 100644 --- a/src/controllers/tiktok.controller.ts +++ b/src/controllers/tiktok.controller.ts @@ -1,8 +1,9 @@ import BaseController, { ROLE_ANON } from 'common/base.controller' import { ZError } from 'common/ZError' import { role, router } from 'decorators/router' +import { PlatEnum } from 'enums/PlatEnum' import logger from 'logger/logger' -import { Account, PlatEnum } from 'modules/Account' +import { Account } from 'modules/Account' import { IPlat } from 'plats/IPlat' import { PlatTikTok } from 'plats/PlatTikTok' import { fetchAccessToken, refreshAccessToken } from 'service/tiktok.svr' diff --git a/src/enums/PlatEnum.ts b/src/enums/PlatEnum.ts new file mode 100644 index 0000000..680a80f --- /dev/null +++ b/src/enums/PlatEnum.ts @@ -0,0 +1,11 @@ +export enum PlatEnum { + GOOGLE = 0, + APPLE = 1, + TIKTOK = 2, + FACEBOOK = 3, + TWITTER = 4, + TELEGRAM = 5, + EMAIL = 6, + DISCORD = 7, + CLIENT = 10, +} diff --git a/src/modules/Account.ts b/src/modules/Account.ts index 7ee65b4..9ee02ea 100644 --- a/src/modules/Account.ts +++ b/src/modules/Account.ts @@ -3,18 +3,7 @@ import { dbconn } from 'decorators/dbconn' import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses' import { BaseModule } from './Base' import { genRandomString, sha512 } from 'utils/security.util' - -export enum PlatEnum { - GOOGLE = 0, - APPLE = 1, - TIKTOK = 2, - FACEBOOK = 3, - TWITTER = 4, - TELEGRAM = 5, - EMAIL = 6, - DISCORD = 7, - CLIENT = 10, -} +import { PlatEnum } from 'enums/PlatEnum' /** * 生成密码的salt和hash diff --git a/src/modules/UnionAccount.ts b/src/modules/UnionAccount.ts new file mode 100644 index 0000000..083f455 --- /dev/null +++ b/src/modules/UnionAccount.ts @@ -0,0 +1,35 @@ +import { getModelForClass, index, modelOptions, mongoose, prop, ReturnModelType, Severity } from '@typegoose/typegoose' +import { dbconn } from 'decorators/dbconn' +import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses' +import { BaseModule } from './Base' +import { PlatEnum } from 'enums/PlatEnum' +/** + * 账号绑定表 + */ +interface UnionAccountClass extends Base, TimeStamps {} +@dbconn() +@index({ gameAccount: 1 }, { unique: true, partialFilterExpression: { gameAccount: { $exists: true } } }) +@index({ walletAccount: 1 }, { unique: true, partialFilterExpression: { walletAccount: { $exists: true } } }) +@modelOptions({ + schemaOptions: { collection: 'union_account', timestamps: true }, + options: { allowMixed: Severity.ALLOW }, +}) +class UnionAccountClass extends BaseModule { + // 生成钱包所用账号 + @prop() + public walletAccount?: string + // 绑定的guest账号 + @prop() + public gameAccount?: string + + @prop({ type: String }) + public plats: Map + + public static async findByPlat(this: ReturnModelType, channel: PlatEnum, uid: string) { + const filterData: any = {} + filterData[`plats.${channel}`] = uid + return this.findOne(filterData).exec() + } +} + +export const UnionAccount = getModelForClass(UnionAccountClass, { existingConnection: UnionAccountClass.db }) diff --git a/src/modules/Wallet.ts b/src/modules/Wallet.ts index 1a49568..2758b0b 100644 --- a/src/modules/Wallet.ts +++ b/src/modules/Wallet.ts @@ -1,4 +1,4 @@ -import { getModelForClass, index, modelOptions, mongoose, prop, Severity } from '@typegoose/typegoose' +import { getModelForClass, index, modelOptions, mongoose, prop, ReturnModelType, Severity } from '@typegoose/typegoose' import { dbconn } from 'decorators/dbconn' import { BaseModule } from './Base' @@ -28,6 +28,10 @@ class WalletClass extends BaseModule { @prop({ required: true, default: true }) public nweRecord: boolean + public static async findByAccount(this: ReturnModelType, account: string) { + return this.findOne({ account }).exec() + } + public toJson() { return { key: this.key, diff --git a/src/plats/PlatApple.ts b/src/plats/PlatApple.ts index 2836d9b..46b4d30 100644 --- a/src/plats/PlatApple.ts +++ b/src/plats/PlatApple.ts @@ -1,7 +1,5 @@ -import { OAuth2Client } from 'google-auth-library' import { IPlat } from './IPlat' import verifyAppleToken from 'verify-apple-id-token' -import { ZError } from 'common/ZError' const CLIENT_ID_DEBUG = 'com.jc.tebg' const CLIENT_ID_RELEASE = 'com.cege.games.release' diff --git a/src/plats/PlatClient.ts b/src/plats/PlatClient.ts new file mode 100644 index 0000000..3ea7296 --- /dev/null +++ b/src/plats/PlatClient.ts @@ -0,0 +1,31 @@ +import { IPlat } from './IPlat' +import verifyAppleToken from 'verify-apple-id-token' +import { ZError } from 'common/ZError' +import { isUUID } from 'utils/string.util' +import * as wasm from 'rustwallet' + +const CLIENT_SUFFIX = '_clientid' + +function checkClientId(clientId: string) { + if (!clientId) { + return false + } + if (!clientId.endsWith(CLIENT_SUFFIX)) { + return false + } + const id = clientId.slice(0, clientId.length - CLIENT_SUFFIX.length) + return isUUID(id) +} +export class PlatClient implements IPlat { + async verifyToken(req: any): Promise { + let { code, token } = req.params + code = code || token + const sk = process.env.WALLET_CLIENT_SK + let codeDecrypto = wasm.rdecrypt(sk, code) + if (!checkClientId(codeDecrypto)) { + throw new ZError(12, 'param invalid') + } + const openId = codeDecrypto.slice(0, codeDecrypto.length - CLIENT_SUFFIX.length) + return { openId, data: {} } + } +} diff --git a/src/plats/PlatGoogle.ts b/src/plats/PlatGoogle.ts index b04adf1..8108629 100644 --- a/src/plats/PlatGoogle.ts +++ b/src/plats/PlatGoogle.ts @@ -1,6 +1,7 @@ import { OAuth2Client } from 'google-auth-library' import { IPlat } from './IPlat' import { ZError } from 'common/ZError' +import logger from 'logger/logger' const GOOGLE_OAUTH_ISS = 'https://accounts.google.com' const GOOGLE_OAUTH_ISS1 = 'accounts.google.com' @@ -13,34 +14,55 @@ export class PlatGoogle implements IPlat { async verifyToken(req: any): Promise { let { code, token } = req.params code = code || token - const client = new OAuth2Client(CLIENT_ID) - const ticket = await client.verifyIdToken({ - idToken: code, - audience: [CLIENT_ID, CLIENT_ID2, CLIENT_ID_IOS, IOS_TEST], // Specify the CLIENT_ID of the app that accesses the backend - // Or, if multiple clients access the backend: - //[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3] - }) - const payload = ticket.getPayload() - if (!(payload.iss === GOOGLE_OAUTH_ISS || payload.iss === GOOGLE_OAUTH_ISS1)) { - throw new ZError(10, 'id token error') - } - if ( - payload.aud !== CLIENT_ID && - payload.aud !== CLIENT_ID2 && - payload.aud !== CLIENT_ID_IOS && - payload.aud !== IOS_TEST - ) { - throw new ZError(11, 'client id mismatch') - } let data: any = {} - if (payload.email) data.email = payload.email - if (process.env.NODE_ENV !== 'development') { - if (payload.email_verified !== undefined) data.emailVerified = payload.email_verified + let openId + const client = new OAuth2Client(CLIENT_ID) + try { + const ticket = await client.verifyIdToken({ + idToken: code, + audience: [CLIENT_ID, CLIENT_ID2, CLIENT_ID_IOS, IOS_TEST], // Specify the CLIENT_ID of the app that accesses the backend + // Or, if multiple clients access the backend: + //[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3] + }) + const payload = ticket.getPayload() + if (!(payload.iss === GOOGLE_OAUTH_ISS || payload.iss === GOOGLE_OAUTH_ISS1)) { + throw new ZError(10, 'id token error') + } + if ( + payload.aud !== CLIENT_ID && + payload.aud !== CLIENT_ID2 && + payload.aud !== CLIENT_ID_IOS && + payload.aud !== IOS_TEST + ) { + throw new ZError(11, 'client id mismatch') + } + + if (payload.email) data.email = payload.email + if (process.env.NODE_ENV !== 'development') { + if (payload.email_verified !== undefined) data.emailVerified = payload.email_verified + } + if (payload.locale) data.locale = payload.locale + if (payload.name) data.nickname = payload.name + if (payload.picture) data.avatar = payload.picture + openId = payload.sub + } catch (err) { + logger.log('error parse google id token', err) + try { + let info: any = await client.getTokenInfo(code) + console.log(info) + if (info.email) data.email = info.email + if (info.aud !== CLIENT_ID2) { + throw new ZError(11, 'client id mismatch') + } + if (process.env.NODE_ENV !== 'development') { + if (info.email_verified !== undefined) data.emailVerified = info.email_verified + } + if (info.name) data.nickname = info.name + openId = info.sub + } catch (e2) { + logger.log('error parse google access token', e2) + } } - if (payload.locale) data.locale = payload.locale - if (payload.name) data.nickname = payload.name - if (payload.picture) data.avatar = payload.picture - const openId = payload.sub return { openId, data } } } diff --git a/src/service/game.svr.ts b/src/service/game.svr.ts index 070a2e2..9c55ea0 100644 --- a/src/service/game.svr.ts +++ b/src/service/game.svr.ts @@ -2,6 +2,7 @@ import axios from 'axios' import { PayRecordClass } from 'modules/PayRecord' import { DocumentType } from '@typegoose/typegoose' import { hmacsha256 } from 'utils/security.util' +import { PlatEnum } from 'enums/PlatEnum' export async function reportPayResult(data: DocumentType) { let repData = { @@ -27,10 +28,10 @@ export async function reportPayResult(data: DocumentType) { return axios(reqConfig) } /** - * TODO::向游戏服务查询guest账号和平台账号能否绑定 + * 向游戏服务查询guest账号和平台账号能否绑定 */ -export async function checkReleation() { - let url = `${process.env.GAME_CHECK_RELATION_URL}` +export async function checkReleation(gid: string, plat: PlatEnum, openId: string) { + let url = `${process.env.GAME_CHECK_RELATION_URL}&guest_account=${gid}&target_plat=${plat}&target_account=${openId}` let reqConfig: any = { method: 'get', url, @@ -38,5 +39,6 @@ export async function checkReleation() { 'Content-Type': 'application/json', }, } - return axios(reqConfig) + let res = await axios(reqConfig) + return res.data }