This commit is contained in:
2025-03-27 10:14:11 +08:00
parent 2ae2876aab
commit 865cd3fee3
16 changed files with 936 additions and 331 deletions

View File

@@ -1,5 +1,4 @@
import { app } from './app.ts';
import './demo-route.ts';
import './routes/wx/login.ts';
export { app };

18
app/src/modules/config.ts Normal file
View File

@@ -0,0 +1,18 @@
import dotenv from 'dotenv';
import path from 'path';
export const env = dotenv.config({
path: [path.resolve(process.cwd(), '.env'), path.resolve(process.cwd(), '.env.wxopen')],
});
console.log(env.parsed);
export const config = {
domain: env.parsed?.DOMAIN,
wx: {
appId: env.parsed?.WX_MP_APP_ID,
appSecret: env.parsed?.WX_MP_APP_SECRET,
},
wxOpen: {
appId: env.parsed?.WX_OPEN_APP_ID,
appSecret: env.parsed?.WX_OPEN_APP_SECRET,
},
};

View File

@@ -1,11 +1,5 @@
import { useConfig } from '@kevisual/use-config';
type WxConfig = {
appId: string;
appSecret: string;
};
const config = useConfig<{ wx: WxConfig }>();
import { CustomError } from '@kevisual/router';
import { config } from './config.ts';
export type WxTokenResponse = {
access_token: string;
@@ -25,12 +19,26 @@ export type WxToken = {
unionid: string;
};
/**
* 根据code获取token
* @param code
* @returns
*/
export const fetchToken = async (code: string): Promise<WxToken> => {
const { appId, appSecret } = config.wx;
let appId = config.wxOpen.appId;
let appSecret = config.wxOpen.appSecret;
if (!appId && !appSecret) {
appId = config.wx.appId;
appSecret = config.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(data)
console.log('query token', data);
return data;
};

View File

@@ -1,74 +0,0 @@
import { WxServices } from '@/routes/wx/services.ts'
import { simple } from './simple.ts'
export const createLoginHtml = (wxService: WxServices) => {
const redirectUrl = wxService.isNew ? '/user/info' : '/'
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wx Login</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
margin-top: 50px;
}
#loading {
font-size: 1.2rem;
color: #555;
}
</style>
</head>
<body>
<div>Login Success</div>
<div id="loading">Redirecting, please wait...</div>
<script>
(function() {
// Save the token to localStorage
localStorage.setItem('token', '${wxService.webToken}');
// Redirect after 2 seconds
setTimeout(() => {
window.location.href = '${redirectUrl}';
}, 2000);
})();
</script>
</body>
</html>
`
}
simple.get('/api/wx/login', async (req, res) => {
try {
const url = req.url
const query = new URLSearchParams(url.split('?')[1])
const code = query.get('code')
const state = query.get('state')
if (!code) {
res.end('code is required')
return
}
const wxService = new WxServices()
await wxService.login(code)
if (wxService.isNew) {
await wxService.getUserInfo()
}
res.setHeader('Content-Type', 'text/html')
res.end(createLoginHtml(wxService))
} catch (e) {
console.error(e)
res.end('error')
}
})
simple.get('/api/wx/on-ai/login', async (req, res) => {
const url = req.url
const query = new URLSearchParams(url.split('?')[1])
const code = query.get('code')
const state = query.get('state')
const onAIBaseUrl = 'https://note.on-ai.ai'
const newUrl = `${onAIBaseUrl}/api/wx/login?code=${code}&state=${state}`
res.setHeader('Content-Type', 'text/html')
res.end(`<script>window.location.href='${newUrl}'</script>`)
})

View File

@@ -1,6 +1,40 @@
import { app } from '@/app.ts';
import { useContextKey } from '@kevisual/use-config/context';
import { WxServices } from './services.ts';
import { config } from '@/modules/config.ts';
export const createCookie = async (token: any, ctx: any) => {
if (!config.domain) {
return;
}
//TODO, 获取访问的 hostname 如果访问的和 domain 的不一致也创建cookie
const browser = ctx.req.headers['user-agent'];
const isBrowser = browser.includes('Mozilla'); // 浏览器
if (isBrowser && ctx.res.cookie) {
// const reqDomain = ctx.req?.headers?.host;
// if (reqDomain !== config.domain) {
// const redis = await useContextKey('redis');
// if (!redis) {
// console.error('redis is not set');
// return;
// }
// const getCacheToken = await redis.get(`login:check:domain:${reqDomain}`);
// if (getCacheToken) {
// ctx.res.cookie('token', getCacheToken, {
// maxAge: 7 * 24 * 60 * 60 * 1000, // 过期时间, 设置7天
// domain: config.domain,
// sameSite: 'lax',
// httpOnly: true,
// });
// }
// }
ctx.res.cookie('token', token.accessToken || token?.token, {
maxAge: 7 * 24 * 60 * 60 * 1000, // 过期时间, 设置7天
domain: config.domain,
sameSite: 'lax',
httpOnly: true,
});
}
};
app
.route({
path: 'wx',
@@ -8,6 +42,10 @@ app
})
.define(async (ctx) => {
const state = ctx.query.state;
if (!state) {
ctx.throw(400, 'state is required');
return;
}
const redis = useContextKey('redis');
const token = await redis.get(`wx:mp:login:${state}`);
if (!token) {
@@ -42,3 +80,29 @@ app
}
})
.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);

View File

@@ -2,12 +2,14 @@ import { WxTokenResponse, fetchToken, getUserInfo } from '@/modules/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';
const User = useContextKey<typeof UserModel>('UserModel');
export class WxServices {
token: WxTokenResponse;
// 创建一个webToken用户登录
webToken: string;
accessToken: string;
refreshToken: string;
isNew: boolean;
// @ts-ignore
user: User;
@@ -16,9 +18,9 @@ export class WxServices {
}
async login(code: string) {
const token = await fetchToken(code);
this.token = token;
console.log('login token', token);
if (!token.unionid) {
throw new Error('unionid is required');
throw new CustomError(400, 'code is invalid, wxdata can not be found');
}
const unionid = token.unionid;
let user = await User.findOne({
@@ -32,14 +34,29 @@ export class WxServices {
user = await User.createUser(unionid, unionid.slice(0, 8));
user.data = {
...user.data,
// @ts-ignore
wxOpenid: token.openid,
wxUnionId: unionid,
};
this.isNew = true;
}
const tokenInfo = await user.createToken(null, 'plugin');
this.webToken = tokenInfo.token;
console.log('mp-user login=============', token.openid, token.unionid);
const tokenInfo = await user.createToken(null, 'plugin', {
wx: {
openid: token.openid,
unionid: unionid,
},
});
this.webToken = tokenInfo.accessToken;
this.accessToken = tokenInfo.accessToken;
this.refreshToken = tokenInfo.refreshToken;
this.user = user;
return this.webToken;
return {
accessToken: this.accessToken,
refreshToken: this.refreshToken,
isNew: this.isNew,
};
}
async checkHasUser() {}