diff --git a/src/routes-simple/handle-request.ts b/src/routes-simple/handle-request.ts index a79f37c..3defce4 100644 --- a/src/routes-simple/handle-request.ts +++ b/src/routes-simple/handle-request.ts @@ -184,6 +184,11 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR // router自己管理 return; } + // if (req.url === '/MP_verify_NGWvli5lGpEkByyt.txt') { + // res.writeHead(200, { 'Content-Type': 'text/plain' }); + // res.end('NGWvli5lGpEkByyt'); + // return; + // } if (req.url && simpleAppsPrefixs.some(prefix => req.url!.startsWith(prefix))) { // 简单应用路由处理 // 设置跨域 diff --git a/src/routes/user/index.ts b/src/routes/user/index.ts index d7718af..33ca6ea 100644 --- a/src/routes/user/index.ts +++ b/src/routes/user/index.ts @@ -14,3 +14,5 @@ import './org-user/list.ts'; import './admin/user.ts'; import './secret-key/list.ts'; + +import './wx-login.ts' diff --git a/src/routes/user/modules/wx-services.ts b/src/routes/user/modules/wx-services.ts new file mode 100644 index 0000000..c30d18a --- /dev/null +++ b/src/routes/user/modules/wx-services.ts @@ -0,0 +1,187 @@ +import { WxTokenResponse, fetchToken, getUserInfo } from './wx.ts'; +import { useContextKey } from '@kevisual/use-config/context'; +import { UserModel } from '@kevisual/code-center-module'; +import { Buffer } from 'buffer'; +import { CustomError } from '@kevisual/router'; +import { customAlphabet } from 'nanoid'; +const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10); +const User = useContextKey('UserModel'); +export class WxServices { + wxToken: WxTokenResponse; + // 创建一个webToken,用户登录 + webToken: string; + accessToken: string; + refreshToken: string; + isNew: boolean; + // @ts-ignore + user: User; + constructor() { + // + } + async checkUserExist(username: string) { + const user = await User.findOne({ + where: { + username, + }, + }); + return !!user; + } + async randomUsername() { + const a = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10); + const b = customAlphabet('1234567890', 10); + const random = '@' + a(4) + '' + b(4); + const user = await this.checkUserExist(random); + if (user) { + return this.randomUsername(); + } + return random; + } + /** + * 获取openid + * @param code + * @returns + */ + async getOpenid(code: string, type: 'mp' | 'open' = 'open') { + const token = await fetchToken(code, type); + console.log('login token', token); + return { + openid: token.openid, + scope: token.scope, + unionid: token.unionid, + }; + } + async login(code: string, type: 'mp' | 'open' = 'open') { + const token = await fetchToken(code, type); + console.log('login token', token); + if (!token.unionid) { + throw new CustomError(400, 'code is invalid, wxdata can not be found'); + } + this.wxToken = token; + const unionid = token.unionid; + let user = await User.findOne({ + where: { + data: { + wxUnionId: unionid, + }, + }, + }); + // @ts-ignore + if (type === 'open' && user && user.data.wxOpenid !== token.openid) { + user.data = { + ...user.data, + // @ts-ignore + wxOpenid: token.openid, + }; + user = await user.update({ data: user.data }); + console.log('mp-user login openid update=============', token.openid, token.unionid); + // @ts-ignore + } else if (type === 'mp' && user && user.data.wxmpOpenid !== token.openid) { + user.data = { + ...user.data, + // @ts-ignore + wxmpOpenid: token.openid, + }; + user = await user.update({ data: user.data }); + } + if (!user) { + const username = await this.randomUsername(); + user = await User.createUser(username, nanoid(10)); + let data = { + ...user.data, + wxUnionId: unionid, + }; + user.data = data; + if ((type = 'mp')) { + // @ts-ignore + data.wxmpOpenid = token.openid; + } else { + // @ts-ignore + data.wxOpenid = token.openid; + } + this.user = await user.save({ fields: ['data'] }); + + this.getUserInfo(); + this.isNew = true; + } + this.user = user; + const tokenInfo = await user.createToken(null, 'plugin', { + wx: { + openid: token.openid, + unionid: unionid, + type, + }, + }); + this.webToken = tokenInfo.accessToken; + + this.accessToken = tokenInfo.accessToken; + this.refreshToken = tokenInfo.refreshToken; + this.user = user; + return { + accessToken: this.accessToken, + refreshToken: this.refreshToken, + isNew: this.isNew, + }; + } + + async checkHasUser() {} + async getUserInfo() { + try { + if (!this.wxToken) { + throw new CustomError(400, 'wxToken is not set'); + } + const userInfo = await getUserInfo(this.wxToken.access_token, this.wxToken.openid); + const { nickname, headimgurl } = userInfo; + this.user.nickname = nickname; + try { + const downloadImgUrl = await this.downloadImg(headimgurl); + this.user.avatar = downloadImgUrl; + } catch (error) { + console.error('Error downloading or converting image:', error); + } + const data = { + ...this.user.data, + wxUserInfo: userInfo, + }; + this.user.data = data; + await this.user.save({ fields: ['data', 'nickname', 'avatar'] }); + } catch (error) { + console.error('Error getting user info:', error); + } + } + /** + * 转成base64 + * @param url + */ + async downloadImg(url: string): Promise { + try { + return await downloadImag(url); + } catch (error) { + console.error('Error downloading or converting image:', error); + return ''; + } + } +} + +// https://thirdwx.qlogo.cn/mmopen/vi_32/WvOPpbDwUKEVJvSst8Z91Y68m7CsBeecMqRGlqey5HejByePD89boYGaVCM8vESsYmokk1jABUDsK08IrfI6JEkibZkDIC2zsb96DGBTEF7E/132 +export const downloadImag = async (url: string) => { + try { + const res = await fetch(url); + if (!res.ok) { + throw new Error(`HTTP error! Status: ${res.status}`); + } + const arrayBufferToBase64 = (buffer: ArrayBuffer): string => { + const bytes = new Uint8Array(buffer); + let binary = ''; + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]); + } + return Buffer.from(binary, 'binary').toString('base64'); + }; + + const buffer = await res.arrayBuffer(); + return `data:image/jpeg;base64,${arrayBufferToBase64(buffer)}`; + } catch (error) { + console.error('Error downloading or converting image:', error); + throw error; + } +}; diff --git a/src/routes/user/modules/wx.ts b/src/routes/user/modules/wx.ts new file mode 100644 index 0000000..0106dfe --- /dev/null +++ b/src/routes/user/modules/wx.ts @@ -0,0 +1,125 @@ +import { CustomError } from '@kevisual/router'; +import { useConfig } from '@kevisual/use-config'; + +export const config = useConfig() +const wxOpen = { + appId: config.WX_OPEN_APP_ID, + appSecret: config.WX_OPEN_APP_SECRET, +} +const wx = { + appId: config.WX_MP_APP_ID, + appSecret: config.WX_MP_APP_SECRET, +} +console.log('wx config', wx, wxOpen); +export type WxTokenResponse = { + access_token: string; + expires_in: number; + refresh_token: string; + openid: string; + scope: string; + unionid: string; +}; + +export type WxToken = { + access_token: string; + expires_in: number; + refresh_token: string; + openid: string; + scope: string; + unionid: string; +}; + +/** + * 根据code获取token + * @param code + * @returns + */ +export const fetchToken = async (code: string, type: 'open' | 'mp' = 'open'): Promise => { + let appId = wxOpen.appId; + let appSecret = wxOpen.appSecret; + if (type === 'mp') { + appId = wx.appId; + appSecret = wx.appSecret; + } + if (!appId || !appSecret) { + throw new CustomError(500, 'appId or appSecret is not set'); + } + console.log('fetchToken===', appId, appSecret, code); + const wxUrl = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appId}&secret=${appSecret}&code=${code}&grant_type=authorization_code`; + const res = await fetch(wxUrl); + const data = await res.json(); + console.log('query token', data); + return data; +}; + +type UserInfo = { + openid: string; + nickname: string; + sex: number; + language: string; + province: string; + country: string; + headimgurl: string; + privilege: string[]; + unionid: string; +}; + +/** + * 获取用户信息 + * @param token + * @param openid + * @returns + */ +export const getUserInfo = async (token: string, openid: string): Promise => { + const phoneUrl = `https://api.weixin.qq.com/sns/userinfo?access_token=${token}&openid=${openid}`; + + const res = await fetch(phoneUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + const data = await res.json(); + console.log(data); + return data; +}; + +// getUserInfo(token.access_token, token.openid) + +type AuthRes = { + errcode: number; + errmsg: string; +}; +/** + * errorcode 0: 正常 + * @param token + * @param openid + * @returns + */ +export const getAuth = async (token: string, openid: string): Promise => { + const authUrl = `https://api.weixin.qq.com/sns/auth?access_token=${token}&openid=${openid}`; + + const res = await fetch(authUrl); + const data = await res.json(); + // console.log(data) + return data; +}; + +// getAuth(token.access_token, token.openid) + +type RefreshToken = { + access_token: string; + expires_in: number; + refresh_token: string; + openid: string; + scope: string; +}; +export const refreshToken = async (refreshToken: string): Promise => { + const { appId } = config.wx; + const refreshUrl = `https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=${appId}&grant_type=refresh_token&refresh_token=${refreshToken}`; + const res = await fetch(refreshUrl); + const data = await res.json(); + return data; +}; + +// refreshToken(token.refresh_token) diff --git a/src/routes/user/wx-login.ts b/src/routes/user/wx-login.ts new file mode 100644 index 0000000..d04c26a --- /dev/null +++ b/src/routes/user/wx-login.ts @@ -0,0 +1,91 @@ +import { WxServices } from "./modules/wx-services.ts"; +import { app, redis } from "@/app.ts"; +app + .route({ + path: 'wx', + key: 'checkLogin', + }) + .define(async (ctx) => { + const state = ctx.query.state; + if (!state) { + ctx.throw(400, 'state is required'); + return; + } + const token = await redis.get(`wx:mp:login:${state}`); + if (!token) { + ctx.throw(400, 'Invalid state'); + return; + } + try { + ctx.body = JSON.parse(token); + } catch (error) { + ctx.throw(500, 'Invalid token get'); + } + }) + .addTo(app); + +app + .route({ + path: 'wx', + key: 'mplogin', + }) + .define(async (ctx) => { + const state = ctx.query.state; + const code = ctx.query.code; + try { + const wx = new WxServices(); + const token = await wx.login(code, 'mp'); + await redis.set(`wx:mp:login:${state}`, JSON.stringify(token), 'EX', 10000); // 30秒过期 + ctx.body = { + token, + }; + } catch (error) { + console.error(error); + ctx.throw(500, 'Invalid code'); + } + }) + .addTo(app); + +app + .route({ + path: 'wx', + key: 'mp-get-openid', + isDebug: true, + }) + .define(async (ctx) => { + const code = ctx.query.code; + if (!code) { + ctx.throw(400, 'code is required'); + return; + } + const wx = new WxServices(); + const mpInfo = await wx.getOpenid(code, 'mp'); + ctx.body = mpInfo; + }) + .addTo(app); + +app + .route({ + path: 'wx', + key: 'open-login', + isDebug: true, + }) + .define(async (ctx) => { + const code = ctx.query.code; + const wx = new WxServices(); + if (!code) { + ctx.throw(400, 'code is required'); + return; + } + try { + const token = await wx.login(code); + ctx.body = token; + if (!token.accessToken) { + ctx.throw(500, 'Invalid code'); + } + } catch (error) { + console.error(error); + ctx.throw(500, 'Invalid code'); + } + }) + .addTo(app);