完善登录相关逻辑

This commit is contained in:
zhl 2023-06-14 09:57:36 +08:00
parent dda8e192a1
commit fce1614233
7 changed files with 244 additions and 163 deletions

View File

@ -1,36 +1,38 @@
import BaseController, { ROLE_ANON } from "common/base.controller"; import BaseController, { ROLE_ANON } from 'common/base.controller'
import { ZError } from "common/ZError"; import { ZError } from 'common/ZError'
import { role, router } from "decorators/router"; import { role, router } from 'decorators/router'
import logger from "logger/logger"; import logger from 'logger/logger'
import { import { AuthRecord } from 'modules/AuthRecord'
DiscordSvr, import { DiscordSvr, exchangeDiscrodCodeForToken, userInfo } from 'services/discord.svr'
exchangeDiscrodCodeForToken,
userInfo,
} from "services/discord.svr";
class DiscordController extends BaseController { class DiscordController extends BaseController {
@role(ROLE_ANON) @role(ROLE_ANON)
@router("get /discord/redirect_uri") @router('get /discord/redirect_uri')
async discordCallback(req, res) { async discordCallback(req, res) {
let { code } = req.params; let { code, state } = req.params
logger.info("discord redirect: ", req.params); if (code && state) {
let access_token = ""; const stateArr = state.split('|')
if (code) { const address = stateArr[0]
access_token = await exchangeDiscrodCodeForToken(code); const record = await AuthRecord.insertOrUpdate(
let uinfo = await userInfo(access_token); { address, platform: 7 },
console.log(uinfo); { address, platform: 7, $inc: { version: 1 } },
return res.view("/templates/discord_redirect.ejs"); )
let tokenResponse = await exchangeDiscrodCodeForToken(code)
record.accessToken = tokenResponse.access_token
record.refreshToken = tokenResponse.refresh_token
record.scope = tokenResponse.scope
record.tokenType = tokenResponse.token_type
record.expiresIn = tokenResponse.expires_in + Date.now()
await record.save()
let uinfo = await userInfo(tokenResponse.access_token)
record.nickname = uinfo.username
record.username = uinfo.username
record.discriminator = uinfo.discriminator
record.openId = uinfo.id
await record.save()
return res.view('/templates/discord_redirect.ejs')
} else { } else {
return res.view("/templates/discord_redirect.ejs"); return res.view('/templates/discord_redirect.ejs')
} }
} }
@role(ROLE_ANON)
@router("get /discord/check_user_role")
async checkUserRole(req, res) {
// let { uid } = req.params;
let uid = "1034482894690861116";
let role = await new DiscordSvr().checkUserRole(uid);
return { role };
}
} }

View File

@ -1,15 +1,58 @@
import BaseController, { ROLE_ANON } from "common/base.controller"; import BaseController, { ROLE_ANON } from 'common/base.controller'
import { ZError } from "common/ZError"; import { ZError } from 'common/ZError'
import { role, router } from "decorators/router"; import { role, router } from 'decorators/router'
import logger from "logger/logger"; import logger from 'logger/logger'
import { AuthRecord } from 'modules/AuthRecord'
import { DiscordSvr } from 'services/discord.svr'
class MainController extends BaseController { class MainController extends BaseController {
/** /**
* Refresh token * Refresh token
*/ */
@role(ROLE_ANON) @role(ROLE_ANON)
@router("post /open/api/v3/merchant/getToken") @router('get /user/status/:address')
async getToken(req, res) { async getToken(req, res) {
return {}; let { address } = req.params
let records = await AuthRecord.find({ address })
let result: any = {
discord: {},
twitter: {},
}
for (const record of records) {
switch (record.platform) {
case 4:
result.twitter = {
id: record.openId,
username: record.username,
}
break
case 7:
result.discord = {
id: record.openId,
username: record.username,
discriminator: record.discriminator,
verified: record.condition,
}
}
}
return result
}
@role(ROLE_ANON)
@router('get /user/check_verify/:address')
async checkUserRole(req, res) {
let { address } = req.params
if (!address) {
throw new ZError(10, 'address is required')
}
let discordRecord = await AuthRecord.findByAddress(address, 7)
if (!discordRecord) {
throw new ZError(11, 'discord not found')
}
if (discordRecord.condition) {
return { verified: true }
}
let role = await new DiscordSvr().checkUserRole(discordRecord.openId)
return { verified: role }
} }
} }

View File

@ -2,12 +2,36 @@ import BaseController, { ROLE_ANON } from 'common/base.controller'
import { ZError } from 'common/ZError' import { ZError } from 'common/ZError'
import { role, router } from 'decorators/router' import { role, router } from 'decorators/router'
import logger from 'logger/logger' import logger from 'logger/logger'
import { AuthRecord } from 'modules/AuthRecord'
import { exchangeTwitterCodeForToken, getTwitterUserInfo } from 'services/twitter.svr'
class TwitterController extends BaseController { class TwitterController extends BaseController {
@role(ROLE_ANON) @role(ROLE_ANON)
@router('get /twitter/redirect_uri') @router('get /twitter/redirect_uri')
async discordCallback(req, res) { async discordCallback(req, res) {
logger.info('twitter redirect: ', req.params) logger.info('twitter redirect: ', req.params)
const { code, state } = req.params
if (code && state) {
const stateArr = state.split('|')
const address = stateArr[0]
const record = await AuthRecord.insertOrUpdate(
{ address, platform: 4 },
{ address, platform: 4, $inc: { version: 1 } },
)
const vcode = stateArr[1] || stateArr[0]
const tokenResponse = await exchangeTwitterCodeForToken(code, vcode)
record.accessToken = tokenResponse.access_token
record.refreshToken = tokenResponse.refresh_token
record.scope = tokenResponse.scope
record.tokenType = tokenResponse.token_type
record.expiresIn = tokenResponse.expires_in + Date.now()
await record.save()
const uinfo = await getTwitterUserInfo(tokenResponse.access_token)
record.nickname = uinfo.data.name
record.username = uinfo.data.username
record.openId = uinfo.data.id
await record.save()
}
return res.view('/templates/twitter_redirect.ejs') return res.view('/templates/twitter_redirect.ejs')
} }
} }

73
src/modules/AuthRecord.ts Normal file
View File

@ -0,0 +1,73 @@
import { getModelForClass, index, modelOptions, mongoose, prop, ReturnModelType, Severity } from '@typegoose/typegoose'
import { dbconn } from 'decorators/dbconn'
import { BaseModule } from './Base'
export enum PlatEnum {
GOOGLE = 0,
APPLE = 1,
TIKTOK = 2,
FACEBOOK = 3,
TWITTER = 4,
TELEGRAM = 5,
EMAIL = 6,
DISCORD = 7,
}
@dbconn()
@index({ address: 1, platform: 1 }, { unique: true })
@modelOptions({
schemaOptions: { collection: 'auth_record', timestamps: true },
options: { allowMixed: Severity.ALLOW },
})
export class AuthRecordClass extends BaseModule {
@prop({ required: true })
public address: string
@prop({ enum: PlatEnum, default: PlatEnum.DISCORD })
public platform: PlatEnum
@prop()
public nickname?: string
@prop()
public username?: string
// 对应discord上的discriminator
@prop()
public discriminator?: string
@prop()
public openId?: string
@prop()
public email?: string
@prop({ default: 0 })
public condition: number
@prop({ default: 0 })
public version: number
@prop()
public tokenType: string
@prop()
public accessToken: string
@prop()
public refreshToken: string
@prop()
public expiresIn: number
@prop()
public scope: string
@prop({ type: mongoose.Schema.Types.Mixed })
public outData: any
public static async findByAddress(
this: ReturnModelType<typeof AuthRecordClass>,
address: string,
platform: PlatEnum,
) {
return this.findOne({ address, platform }).exec()
}
}
export const AuthRecord = getModelForClass(AuthRecordClass, { existingConnection: AuthRecordClass.db })

View File

@ -1,96 +0,0 @@
import { getModelForClass, index, modelOptions, mongoose, prop, ReturnModelType, Severity } from '@typegoose/typegoose'
import { dbconn } from 'decorators/dbconn'
import { BaseModule } from './Base'
export enum PayType {
BUY = 1,
SELL = 2,
}
export enum PayStatus {
PENDING = 0,
TRANSFERING = 1,
TRANSFERED = 2, //只有国库模式才会有该状态
SUCCESS = 9,
TRANSFER_FAIL = 98, // 转账错误
FAIL = 99,
}
@dbconn()
@index({ merchantOrderNo: 1 }, { unique: true, partialFilterExpression: { outOrderId: { $exists: true } } })
@modelOptions({
schemaOptions: { collection: 'pay_record', timestamps: true },
options: { allowMixed: Severity.ALLOW },
})
export class PayRecordClass extends BaseModule {
@prop({ required: true, default: PayType.BUY })
public type: PayType
@prop()
public address: string
@prop()
public network?: string
@prop()
public crypto?: string
// 法币
@prop()
public fiat?: string
// 法币数量
@prop()
public fiatAmount?: string
@prop()
public processFee?: string
@prop()
public networkFee?: string
// 加密货币数量
@prop()
public cryptoAmount?: string
// 加密货币价格
@prop()
public cryptoPrice?: string
// 该笔交易渠道会给我们多少usdt
@prop()
public usdtAmount?: string
// 国家
@prop()
public country?: string
@prop({ required: true, default: PayStatus.PENDING })
public status: PayStatus
// 渠道返回的原始资料
@prop({ type: mongoose.Schema.Types.Mixed })
public outData: any
// 商户订单id
@prop()
public merchantOrderNo: string
@prop()
public email: string
@prop()
public callbackUrl: string
@prop()
public merchantName: string
// 交易的txHash
@prop()
public txHash?: string
@prop({ default: 0 })
public version: number
public static async findByRecordId(this: ReturnModelType<typeof PayRecordClass>, merchantOrderNo: string) {
return this.findOne({ merchantOrderNo }).exec()
}
}
export const PayRecord = getModelForClass(PayRecordClass, { existingConnection: PayRecordClass.db })

View File

@ -1,66 +1,65 @@
import { ZError } from "common/ZError"; import { ZError } from 'common/ZError'
import { singleton } from "decorators/singleton"; import { singleton } from 'decorators/singleton'
const { Client, Events, GatewayIntentBits } = require("discord.js"); const { Client, Events, GatewayIntentBits } = require('discord.js')
export async function exchangeDiscrodCodeForToken(code: string) { export async function exchangeDiscrodCodeForToken(code: string) {
const clientId = process.env.DISCORD_CLIENT_ID; const clientId = process.env.DISCORD_CLIENT_ID
const clientSecret = process.env.DISCORD_CLIENT_SECRET; const clientSecret = process.env.DISCORD_CLIENT_SECRET
const redirectUri = "http://localhost:3010/discord/redirect_uri"; const redirectUri = 'https://oauth-svr.cebggame.com/discord/redirect_uri'
const response = await fetch("https://discord.com/api/oauth2/token", { const response = await fetch('https://discord.com/api/oauth2/token', {
method: "POST", method: 'POST',
headers: { headers: {
"Content-Type": "application/x-www-form-urlencoded", 'Content-Type': 'application/x-www-form-urlencoded',
}, },
body: new URLSearchParams({ body: new URLSearchParams({
grant_type: "authorization_code", grant_type: 'authorization_code',
client_id: clientId, client_id: clientId,
client_secret: clientSecret, client_secret: clientSecret,
redirect_uri: redirectUri, redirect_uri: redirectUri,
code, code,
scope: "identify email", scope: 'identify email',
}), }),
}); })
const data = await response.json(); const data = await response.json()
console.log(data); return data
return data.access_token;
} }
export async function userInfo(token: string) { export async function userInfo(token: string) {
const response = await fetch("https://discord.com/api/users/@me", { const response = await fetch('https://discord.com/api/users/@me', {
headers: { headers: {
authorization: `Bearer ${token}`, authorization: `Bearer ${token}`,
}, },
}); })
const data = await response.json(); const data = await response.json()
console.log(data); console.log(data)
return data; return data
} }
@singleton @singleton
export class DiscordSvr { export class DiscordSvr {
private client: any; private client: any
private guild: any; private guild: any
public async init() { public async init() {
console.log("DiscordSvr init"); console.log('DiscordSvr init')
this.client = new Client({ intents: [GatewayIntentBits.Guilds] }); this.client = new Client({ intents: [GatewayIntentBits.Guilds] })
this.client.once(Events.ClientReady, async (c) => { this.client.once(Events.ClientReady, async c => {
console.log(`Ready! Logged in as ${c.user.tag}`); console.log(`Ready! Logged in as ${c.user.tag}`)
this.guild = await this.client.guilds.fetch(process.env.DISCROD_GUILD_ID); this.guild = await this.client.guilds.fetch(process.env.DISCROD_GUILD_ID)
}); })
this.client.login(process.env.DISCORD_BOT_TOKEN); this.client.login(process.env.DISCORD_BOT_TOKEN)
} }
public async checkUserRole(uid: string) { public async checkUserRole(uid: string) {
if (!this.guild) { if (!this.guild) {
throw new ZError(10, "DiscordSvr not init"); throw new ZError(10, 'DiscordSvr not init')
} }
const member = await this.guild.members.fetch(uid); const member = await this.guild.members.fetch(uid)
if (!member) { if (!member) {
return false; return false
} }
return member.roles.cache.has(process.env.DISCORD_ROLE_ID); return member.roles.cache.has(process.env.DISCORD_ROLE_ID)
} }
} }

View File

@ -0,0 +1,36 @@
const consumerKey = process.env.TWITTER_CLIENT_ID
const consumerSecret = process.env.TWITTER_CLIENT_SECRET
export async function exchangeTwitterCodeForToken(code: string, vcode: string) {
const url = 'https://api.twitter.com/2/oauth2/token'
const credentials = Buffer.from(`${consumerKey}:${consumerSecret}`).toString('base64')
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${credentials}`,
},
body: new URLSearchParams({
code,
grant_type: 'authorization_code',
// client_id: process.env.TWITTER_CLIENT_ID,
redirect_uri: 'https://oauth-svr.cebggame.com/twitter/redirect_uri',
code_verifier: vcode,
}),
})
const data = await response.json()
return data
}
export async function getTwitterUserInfo(accessToken: string) {
const url = 'https://api.twitter.com/2/users/me'
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
const data = await response.json()
return data
}