完善登录相关逻辑

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 { ZError } from "common/ZError";
import { role, router } from "decorators/router";
import logger from "logger/logger";
import {
DiscordSvr,
exchangeDiscrodCodeForToken,
userInfo,
} from "services/discord.svr";
import BaseController, { ROLE_ANON } from 'common/base.controller'
import { ZError } from 'common/ZError'
import { role, router } from 'decorators/router'
import logger from 'logger/logger'
import { AuthRecord } from 'modules/AuthRecord'
import { DiscordSvr, exchangeDiscrodCodeForToken, userInfo } from 'services/discord.svr'
class DiscordController extends BaseController {
@role(ROLE_ANON)
@router("get /discord/redirect_uri")
@router('get /discord/redirect_uri')
async discordCallback(req, res) {
let { code } = req.params;
logger.info("discord redirect: ", req.params);
let access_token = "";
if (code) {
access_token = await exchangeDiscrodCodeForToken(code);
let uinfo = await userInfo(access_token);
console.log(uinfo);
return res.view("/templates/discord_redirect.ejs");
let { code, state } = req.params
if (code && state) {
const stateArr = state.split('|')
const address = stateArr[0]
const record = await AuthRecord.insertOrUpdate(
{ address, platform: 7 },
{ address, platform: 7, $inc: { version: 1 } },
)
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 {
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 { ZError } from "common/ZError";
import { role, router } from "decorators/router";
import logger from "logger/logger";
import BaseController, { ROLE_ANON } from 'common/base.controller'
import { ZError } from 'common/ZError'
import { role, router } from 'decorators/router'
import logger from 'logger/logger'
import { AuthRecord } from 'modules/AuthRecord'
import { DiscordSvr } from 'services/discord.svr'
class MainController extends BaseController {
/**
* Refresh token
*/
@role(ROLE_ANON)
@router("post /open/api/v3/merchant/getToken")
@router('get /user/status/:address')
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 { role, router } from 'decorators/router'
import logger from 'logger/logger'
import { AuthRecord } from 'modules/AuthRecord'
import { exchangeTwitterCodeForToken, getTwitterUserInfo } from 'services/twitter.svr'
class TwitterController extends BaseController {
@role(ROLE_ANON)
@router('get /twitter/redirect_uri')
async discordCallback(req, res) {
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')
}
}

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