From 15bb27eefa862d5e94638d8824fc1d17c04d7bb8 Mon Sep 17 00:00:00 2001 From: CounterFire2023 <136581895+CounterFire2023@users.noreply.github.com> Date: Wed, 23 Aug 2023 19:07:08 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=80=9A=E7=94=A8=E7=9A=84?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/apple.controller.ts | 27 +++------------ src/controllers/facebook.controller.ts | 39 +++++----------------- src/controllers/google.controller.ts | 43 +++--------------------- src/controllers/login.controller.ts | 43 ++++++++++++++++++++++++ src/controllers/main.controllers.ts | 6 ++-- src/controllers/tiktok.controller.ts | 26 +++++---------- src/plats/IPlat.ts | 3 ++ src/plats/PlatApple.ts | 32 ++++++++++++++++++ src/plats/PlatFacebook.ts | 39 ++++++++++++++++++++++ src/plats/PlatGoogle.ts | 46 ++++++++++++++++++++++++++ src/plats/PlatTikTok.ts | 28 ++++++++++++++++ src/providers/facebook.provider.ts | 22 ++++++------ src/service/game.svr.ts | 14 ++++++++ 13 files changed, 243 insertions(+), 125 deletions(-) create mode 100644 src/controllers/login.controller.ts create mode 100644 src/plats/IPlat.ts create mode 100644 src/plats/PlatApple.ts create mode 100644 src/plats/PlatFacebook.ts create mode 100644 src/plats/PlatGoogle.ts create mode 100644 src/plats/PlatTikTok.ts diff --git a/src/controllers/apple.controller.ts b/src/controllers/apple.controller.ts index 3f52e27..0d3b37f 100644 --- a/src/controllers/apple.controller.ts +++ b/src/controllers/apple.controller.ts @@ -1,15 +1,11 @@ import BaseController, { ROLE_ANON } from 'common/base.controller' import { role, router } from 'decorators/router' -import verifyAppleToken from 'verify-apple-id-token' import { Account, PlatEnum } from 'modules/Account' -import axios from 'axios' import logger from 'logger/logger' -var https = require('follow-redirects').https - -const CLIENT_ID_DEBUG = 'com.jc.tebg' -const CLIENT_ID_RELEASE = 'com.cege.games.release' -const CLIEND_ID_ANDROID = 'wallet.cebggame.com' +import { PlatApple } from 'plats/PlatApple' +import { IPlat } from 'plats/IPlat' +const plat: IPlat = new PlatApple() class AppleController extends BaseController { @role(ROLE_ANON) @router('post /apple/login-notify') @@ -21,23 +17,8 @@ class AppleController extends BaseController { @role(ROLE_ANON) @router('post /wallet/login/apple') async checkGoogleJwt(req, res) { - const { token } = req.params logger.db('login', req) - const payload = await verifyAppleToken({ - idToken: token, - clientId: [CLIENT_ID_DEBUG, CLIENT_ID_RELEASE, CLIEND_ID_ANDROID], - }) - const openId = payload.sub - let data: any = {} - if (payload.email) data.email = payload.email - 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 - const { api_platform } = req.headers - if (api_platform) { - data.platform = api_platform - } + const { openId, data } = await plat.verifyToken(req) let user = await Account.insertOrUpdate({ plat: PlatEnum.APPLE, openId }, data) const ztoken = await res.jwtSign({ id: user.id, diff --git a/src/controllers/facebook.controller.ts b/src/controllers/facebook.controller.ts index 8c10225..2206443 100644 --- a/src/controllers/facebook.controller.ts +++ b/src/controllers/facebook.controller.ts @@ -3,8 +3,10 @@ import { ZError } from 'common/ZError' import { role, router } from 'decorators/router' import logger from 'logger/logger' import { Account, PlatEnum } from 'modules/Account' -import { FACEBOOK_APP_ID, fetchUserInfo, verifyFbUserAccessToken } from 'providers/facebook.provider' +import { IPlat } from 'plats/IPlat' +import { PlatFacebook } from 'plats/PlatFacebook' +const plat: IPlat = new PlatFacebook() class FacebookController extends BaseController { @role(ROLE_ANON) @router('post /wallet/login/facebook') @@ -14,41 +16,16 @@ class FacebookController extends BaseController { if (!code) { throw new ZError(10, 'params mismatch') } - const result = await verifyFbUserAccessToken(code) - if (!!result.error) { - throw new ZError(10, `${result.error?.message} (${result.error?.code})`) - } - const { data } = result - if (!data) { - throw new ZError(11, 'no data from facebook') - } - if (data.app_id !== FACEBOOK_APP_ID) { - throw new ZError(12, 'app id mismatch') - } - if (!data.is_valid) { - throw new ZError(13, 'access_token not valid') - } - const infoRes = await fetchUserInfo(code) - if (!!infoRes.error) { - throw new ZError(13, `${infoRes.error?.message} (${infoRes.error.code})`) - } - const openId = infoRes.id || data.user_id - let user: any = {} - let now = Date.now() / 1000 - user.accessToken = code - user.accessTokenExpire = result.data['expires_at'] - user.scope = data['scopes'] - if (infoRes['name']) user.nickname = infoRes['name'] - if (infoRes['email']) user.email = infoRes['email'] + const { openId, data } = await plat.verifyToken(req) const { api_platform } = req.headers if (api_platform) { - user.platform = api_platform + data.platform = api_platform } - let account = await Account.insertOrUpdate({ plat: PlatEnum.FACEBOOK, openId }, user) + let account = await Account.insertOrUpdate({ plat: PlatEnum.FACEBOOK, openId }, data) const ztoken = await res.jwtSign({ id: account.id, - openid: user.openId, - version: user.accountVersion || 0, + openid: account.openId, + version: account.accountVersion || 0, plat: PlatEnum.FACEBOOK, }) return { token: ztoken } diff --git a/src/controllers/google.controller.ts b/src/controllers/google.controller.ts index 6175f85..ad70244 100644 --- a/src/controllers/google.controller.ts +++ b/src/controllers/google.controller.ts @@ -1,54 +1,19 @@ import BaseController, { ROLE_ANON } from 'common/base.controller' -import { ZError } from 'common/ZError' import { role, router } from 'decorators/router' -import { OAuth2Client } from 'google-auth-library' import logger from 'logger/logger' import { Account, PlatEnum } from 'modules/Account' -import { customAlphabet } from 'nanoid' +import { IPlat } from 'plats/IPlat' +import { PlatGoogle } from 'plats/PlatGoogle' -const nanoid = customAlphabet('1234567890abcdef', 10) -const GOOGLE_OAUTH_ISS = 'https://accounts.google.com' -const GOOGLE_OAUTH_ISS1 = 'accounts.google.com' -const IOS_TEST = '53206975661-0d6q9pqljn84n9l63gm0to1ulap9cbk4.apps.googleusercontent.com' +const plat: IPlat = new PlatGoogle() class GoogleController extends BaseController { @role(ROLE_ANON) @router('post /wallet/login/google') async checkGoogleJwt(req, res) { - const { token } = req.params logger.db('login', req) - const CLIENT_ID = process.env.GOOGLE_OAUTH_CLIENT - const CLIENT_ID2 = process.env.GOOGLE_OAUTH_CLIENT2 - const CLIENT_ID_IOS = process.env.GOOGLE_OAUTH_CLIENT_IOS - const client = new OAuth2Client(CLIENT_ID) - const ticket = await client.verifyIdToken({ - idToken: token, - 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') - } - const openId = payload.sub - 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 - } - if (payload.locale) data.locale = payload.locale - if (payload.name) data.nickname = payload.name - if (payload.picture) data.avatar = payload.picture + const { openId, data } = await plat.verifyToken(req) const { api_platform } = req.headers if (api_platform) { data.platform = api_platform diff --git a/src/controllers/login.controller.ts b/src/controllers/login.controller.ts new file mode 100644 index 0000000..aecaa97 --- /dev/null +++ b/src/controllers/login.controller.ts @@ -0,0 +1,43 @@ +import { ZError } from 'common/ZError' +import BaseController, { ROLE_ANON } from 'common/base.controller' +import { role, router } from 'decorators/router' +import logger from 'logger/logger' +import { Account, PlatEnum } from 'modules/Account' +import { IPlat } from 'plats/IPlat' +import { PlatApple } from 'plats/PlatApple' +import { PlatFacebook } from 'plats/PlatFacebook' +import { PlatGoogle } from 'plats/PlatGoogle' +import { PlatTikTok } from 'plats/PlatTikTok' + +const plats: Map = new Map([ + [PlatEnum.GOOGLE, new PlatGoogle()], + [PlatEnum.APPLE, new PlatApple()], + [PlatEnum.FACEBOOK, new PlatFacebook()], + [PlatEnum.TIKTOK, new PlatTikTok()], +]) + +class LoginController extends BaseController { + @role(ROLE_ANON) + @router('post /wallet/login/general') + async generalLogin(req, res) { + const { channel, account } = req.params + logger.db('login', req) + const plat = plats.get(channel) + if (!plat) { + throw new ZError(10, 'plat not found') + } + 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 ztoken = await res.jwtSign({ + id: user.id, + openid: user.openId, + version: user.accountVersion || 0, + plat: user.plat, + }) + return { token: ztoken } + } +} diff --git a/src/controllers/main.controllers.ts b/src/controllers/main.controllers.ts index 4a8778b..916100c 100644 --- a/src/controllers/main.controllers.ts +++ b/src/controllers/main.controllers.ts @@ -1,8 +1,6 @@ -import BaseController, { ROLE_ANON } from 'common/base.controller' -import { ZError } from 'common/ZError' -import { role, router } from 'decorators/router' +import BaseController from 'common/base.controller' +import { router } from 'decorators/router' import logger from 'logger/logger' -import { Account } from 'modules/Account' class MainController extends BaseController { @router('post /wallet/account/reset') diff --git a/src/controllers/tiktok.controller.ts b/src/controllers/tiktok.controller.ts index 8c74f3e..ce4d936 100644 --- a/src/controllers/tiktok.controller.ts +++ b/src/controllers/tiktok.controller.ts @@ -3,36 +3,28 @@ import { ZError } from 'common/ZError' import { role, router } from 'decorators/router' import logger from 'logger/logger' import { Account, PlatEnum } from 'modules/Account' +import { IPlat } from 'plats/IPlat' +import { PlatTikTok } from 'plats/PlatTikTok' import { fetchAccessToken, refreshAccessToken } from 'service/tiktok.svr' // 在tiktok的过期时间中, 减少一个小时 const EXPIRE_REDUCE_SECOND = 3600 + +const plat: IPlat = new PlatTikTok() class TiktokController extends BaseController { @role(ROLE_ANON) @router('post /wallet/login/tiktok') async checkTiktokCode(req, res) { - let { code } = req.params logger.db('login', req) - let result = await fetchAccessToken(code) - if (!(result.message === 'success' && result.data?.error_code === 0)) { - throw new ZError(10, `${result.message}: ${result.data?.description} (${result.data?.error_code})`) - } - const openId = result.data['open_id'] - let user: any = {} - let now = Date.now() / 1000 - user.accessToken = result.data['access_token'] - user.refreshToken = result.data['refresh_token'] - user.accessTokenExpire = now + result.data['expires_in'] - EXPIRE_REDUCE_SECOND - user.refreshTokenExpire = now + result.data['refresh_expires_in'] - EXPIRE_REDUCE_SECOND - user.scope = result.data['scope'] + const { openId, data } = await plat.verifyToken(req) const { api_platform } = req.headers if (api_platform) { - user.platform = api_platform + data.platform = api_platform } - let account = await Account.insertOrUpdate({ plat: PlatEnum.TIKTOK, openId }, user) + let account = await Account.insertOrUpdate({ plat: PlatEnum.TIKTOK, openId }, data) const ztoken = await res.jwtSign({ id: account.id, - openid: user.openId, - version: user.accountVersion || 0, + openid: account.openId, + version: account.accountVersion || 0, plat: PlatEnum.TIKTOK, }) return { token: ztoken } diff --git a/src/plats/IPlat.ts b/src/plats/IPlat.ts new file mode 100644 index 0000000..11a0095 --- /dev/null +++ b/src/plats/IPlat.ts @@ -0,0 +1,3 @@ +export interface IPlat { + verifyToken(req: any): Promise +} diff --git a/src/plats/PlatApple.ts b/src/plats/PlatApple.ts new file mode 100644 index 0000000..2836d9b --- /dev/null +++ b/src/plats/PlatApple.ts @@ -0,0 +1,32 @@ +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' +const CLIEND_ID_ANDROID = 'wallet.cebggame.com' + +export class PlatApple implements IPlat { + async verifyToken(req: any): Promise { + let { code, token } = req.params + code = code || token + const payload = await verifyAppleToken({ + idToken: code, + clientId: [CLIENT_ID_DEBUG, CLIENT_ID_RELEASE, CLIEND_ID_ANDROID], + }) + const openId = payload.sub + let data: any = {} + if (payload.email) data.email = payload.email + 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 + const { api_platform } = req.headers + if (api_platform) { + data.platform = api_platform + } + + return { openId, data } + } +} diff --git a/src/plats/PlatFacebook.ts b/src/plats/PlatFacebook.ts new file mode 100644 index 0000000..92f5bee --- /dev/null +++ b/src/plats/PlatFacebook.ts @@ -0,0 +1,39 @@ +import { IPlat } from './IPlat' + +import { ZError } from 'common/ZError' +import { FACEBOOK_APP_ID, fetchUserInfo, verifyFbUserAccessToken } from 'providers/facebook.provider' + +export class PlatFacebook implements IPlat { + async verifyToken(req: any): Promise { + let { code, token } = req.params + code = code || token + const result = await verifyFbUserAccessToken(code) + if (!!result.error) { + throw new ZError(10, `${result.error?.message} (${result.error?.code})`) + } + const { data } = result + if (!data) { + throw new ZError(11, 'no data from facebook') + } + if (data.app_id !== FACEBOOK_APP_ID) { + throw new ZError(12, 'app id mismatch') + } + if (!data.is_valid) { + throw new ZError(13, 'access_token not valid') + } + const infoRes = await fetchUserInfo(code) + if (!!infoRes.error) { + throw new ZError(13, `${infoRes.error?.message} (${infoRes.error.code})`) + } + const openId = infoRes.id || data.user_id + + let user: any = {} + user.accessToken = code + user.accessTokenExpire = result.data['expires_at'] + user.scope = data['scopes'] + if (infoRes['name']) user.nickname = infoRes['name'] + if (infoRes['email']) user.email = infoRes['email'] + + return { openId, data: user } + } +} diff --git a/src/plats/PlatGoogle.ts b/src/plats/PlatGoogle.ts new file mode 100644 index 0000000..b04adf1 --- /dev/null +++ b/src/plats/PlatGoogle.ts @@ -0,0 +1,46 @@ +import { OAuth2Client } from 'google-auth-library' +import { IPlat } from './IPlat' +import { ZError } from 'common/ZError' + +const GOOGLE_OAUTH_ISS = 'https://accounts.google.com' +const GOOGLE_OAUTH_ISS1 = 'accounts.google.com' +const IOS_TEST = '53206975661-0d6q9pqljn84n9l63gm0to1ulap9cbk4.apps.googleusercontent.com' +const CLIENT_ID = process.env.GOOGLE_OAUTH_CLIENT +const CLIENT_ID2 = process.env.GOOGLE_OAUTH_CLIENT2 +const CLIENT_ID_IOS = process.env.GOOGLE_OAUTH_CLIENT_IOS + +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 + } + 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/plats/PlatTikTok.ts b/src/plats/PlatTikTok.ts new file mode 100644 index 0000000..0cf462c --- /dev/null +++ b/src/plats/PlatTikTok.ts @@ -0,0 +1,28 @@ +import { fetchAccessToken } from 'service/tiktok.svr' +import { IPlat } from './IPlat' + +import { ZError } from 'common/ZError' + +// 在tiktok的过期时间中, 减少一个小时 +const EXPIRE_REDUCE_SECOND = 3600 + +export class PlatTikTok implements IPlat { + async verifyToken(req: any): Promise { + let { code, token } = req.params + code = code || token + let result = await fetchAccessToken(code) + if (!(result.message === 'success' && result.data?.error_code === 0)) { + throw new ZError(10, `${result.message}: ${result.data?.description} (${result.data?.error_code})`) + } + const openId = result.data['open_id'] + let user: any = {} + let now = Date.now() / 1000 + user.accessToken = result.data['access_token'] + user.refreshToken = result.data['refresh_token'] + user.accessTokenExpire = now + result.data['expires_in'] - EXPIRE_REDUCE_SECOND + user.refreshTokenExpire = now + result.data['refresh_expires_in'] - EXPIRE_REDUCE_SECOND + user.scope = result.data['scope'] + + return { openId, data: user } + } +} diff --git a/src/providers/facebook.provider.ts b/src/providers/facebook.provider.ts index 259ca1b..6b17559 100644 --- a/src/providers/facebook.provider.ts +++ b/src/providers/facebook.provider.ts @@ -1,19 +1,19 @@ -import {NetClient} from "net/NetClient"; +import { NetClient } from 'net/NetClient' const FACEBOOK_API_HOST = 'https://graph.facebook.com' -export const FACEBOOK_APP_ID = '1204701000119770'; -const FACEBOOK_APP_SECRET = '5a1deba64b30c7326f497fc52691207f'; +export const FACEBOOK_APP_ID = '1204701000119770' +const FACEBOOK_APP_SECRET = '5a1deba64b30c7326f497fc52691207f' export async function getAppAccessToken() { - const url = `${FACEBOOK_API_HOST}/oauth/access_token?client_id=${FACEBOOK_APP_ID}&clent_secret=${FACEBOOK_APP_SECRET}&grant_type=client_credentials`; - return new NetClient().httpGet(url); -} -export async function verifyFbUserAccessToken(accessToken: string){ - const url = `${FACEBOOK_API_HOST}/debug_token?input_token=${accessToken}&access_token=GG|${FACEBOOK_APP_ID}|${FACEBOOK_APP_SECRET}`; - return new NetClient().httpGet(url); + const url = `${FACEBOOK_API_HOST}/oauth/access_token?client_id=${FACEBOOK_APP_ID}&clent_secret=${FACEBOOK_APP_SECRET}&grant_type=client_credentials` + return new NetClient().httpGet(url) +} +export async function verifyFbUserAccessToken(accessToken: string) { + const url = `${FACEBOOK_API_HOST}/debug_token?input_token=${accessToken}&access_token=GG|${FACEBOOK_APP_ID}|${FACEBOOK_APP_SECRET}` + return new NetClient().httpGet(url) } export async function fetchUserInfo(accessToken: string) { - const url = `${FACEBOOK_API_HOST}/me?fields=["email","id", "name"]&access_token=${accessToken}`; - return new NetClient().httpGet(url); + const url = `${FACEBOOK_API_HOST}/me?fields=["email","id", "name"]&access_token=${accessToken}` + return new NetClient().httpGet(url) } diff --git a/src/service/game.svr.ts b/src/service/game.svr.ts index b8b6f34..070a2e2 100644 --- a/src/service/game.svr.ts +++ b/src/service/game.svr.ts @@ -26,3 +26,17 @@ export async function reportPayResult(data: DocumentType) { } return axios(reqConfig) } +/** + * TODO::向游戏服务查询guest账号和平台账号能否绑定 + */ +export async function checkReleation() { + let url = `${process.env.GAME_CHECK_RELATION_URL}` + let reqConfig: any = { + method: 'get', + url, + headers: { + 'Content-Type': 'application/json', + }, + } + return axios(reqConfig) +}