增加通用的登录接口

This commit is contained in:
CounterFire2023 2023-08-23 19:07:08 +08:00
parent 099f1a8135
commit 15bb27eefa
13 changed files with 243 additions and 125 deletions

View File

@ -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,

View File

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

View File

@ -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

View File

@ -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<PlatEnum, IPlat> = 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 }
}
}

View File

@ -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')

View File

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

3
src/plats/IPlat.ts Normal file
View File

@ -0,0 +1,3 @@
export interface IPlat {
verifyToken(req: any): Promise<any>
}

32
src/plats/PlatApple.ts Normal file
View File

@ -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<any> {
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 }
}
}

39
src/plats/PlatFacebook.ts Normal file
View File

@ -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<any> {
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 }
}
}

46
src/plats/PlatGoogle.ts Normal file
View File

@ -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<any> {
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 }
}
}

28
src/plats/PlatTikTok.ts Normal file
View File

@ -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<any> {
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 }
}
}

View File

@ -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)
}

View File

@ -26,3 +26,17 @@ export async function reportPayResult(data: DocumentType<PayRecordClass>) {
}
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)
}