@kevisual/kv-login 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,188 @@
1
+ import { query } from './query.ts';
2
+ import { createMessage } from '../pages/kv-message.ts';
3
+ import { WX_MP_APP_ID } from '../pages/kv-login.ts';
4
+ export const message = createMessage();
5
+ type LoginOpts = {
6
+ loginMethod: 'password' | 'phone' | 'wechat' | 'wechat-mp',
7
+ data: any,
8
+ el: HTMLElement
9
+ }
10
+ export const redirectHome = () => {
11
+ console.log('重定向到首页')
12
+ const href = window.location.href;
13
+ const url = new URL(href);
14
+ const redirect = url.searchParams.get('redirect');
15
+ if (redirect) {
16
+ const href = decodeURIComponent(redirect);
17
+ window.open(href, '_self');
18
+ } else {
19
+ window.open('/root/home', '_self');
20
+ }
21
+ }
22
+ export const loginHandle = async (opts: LoginOpts) => {
23
+ const { loginMethod, data, el } = opts
24
+ switch (loginMethod) {
25
+ case 'password':
26
+ await loginByPassword(data)
27
+ break
28
+ case 'phone':
29
+ await loginByPhone(data)
30
+ break
31
+ case 'wechat-mp':
32
+ await loginByWeChatMp(data)
33
+ break
34
+ case 'wechat':
35
+ await loginByWeChat(data)
36
+ break
37
+ default:
38
+ console.warn('未知的登录方式:', loginMethod)
39
+ }
40
+ }
41
+
42
+ const loginByPassword = async (data: { username: string, password: string }) => {
43
+ console.log('使用用户名密码登录:', data)
44
+ let needLogin = true; // 这里可以根据实际情况决定是否需要登录, 只能判断密码登录和手机号登录
45
+
46
+ const isLogin = await query.checkLocalToken()
47
+ if (isLogin) {
48
+ const loginUser = await query.checkLocalUser()
49
+ if (loginUser?.username === data?.username) {
50
+ const res = await query.getMe()
51
+ if (res.code === 200) {
52
+ needLogin = false
53
+ console.log('已登录,跳过登录步骤')
54
+ message.success('已登录')
55
+ }
56
+ }
57
+ }
58
+ if (!needLogin) {
59
+ redirectHome()
60
+ return;
61
+ }
62
+ const res = await query.login({
63
+ username: data.username,
64
+ password: data.password
65
+ })
66
+ if (res.code === 200) {
67
+ console.log('登录成功')
68
+ message.success('登录成功')
69
+ redirectHome()
70
+ } else {
71
+ message.error(`登录失败: ${res.message}`)
72
+ }
73
+ }
74
+
75
+ const loginByPhone = async (data: { phone: string, code: string }) => {
76
+ console.log('使用手机号登录:', data)
77
+ }
78
+
79
+ const loginByWeChat = async (data: { wechatCode: string }) => {
80
+ console.log('使用微信登录:', data)
81
+ }
82
+ const loginByWeChatMp = async (data: { wechatMpCode: string }) => {
83
+ console.log('使用微信公众号登录:', data)
84
+ }
85
+
86
+ const clearCode = () => {
87
+ const url = new URL(window.location.href);
88
+ // 清理 URL 中的 code 参数
89
+ url.searchParams.delete('code');
90
+ url.searchParams.delete('state');
91
+ window.history.replaceState({}, document.title, url.toString());
92
+ }
93
+ export const checkWechat = async () => {
94
+ const url = new URL(window.location.href);
95
+ const code = url.searchParams.get('code');
96
+ const state = url.searchParams.get('state');
97
+ if (state?.includes?.('-')) {
98
+ // 公众号登录流程,不在这里处理
99
+ return;
100
+ }
101
+ if (!code) {
102
+ return;
103
+ }
104
+ const res = await query.loginByWechat({ code });
105
+ if (res.code === 200) {
106
+ message.success('登录成功');
107
+ redirectHome();
108
+ } else {
109
+ message.error(res.message || '登录失败');
110
+ clearCode();
111
+ }
112
+ };
113
+
114
+ export const checkMpWechat = async () => {
115
+ const url = new URL(window.location.href);
116
+ const originState = url.searchParams.get('state');
117
+ const [mpLogin, state] = originState ? originState.split('-') : [null, null];
118
+ console.log('检查微信公众号登录流程:', mpLogin, state, originState);
119
+ if (mpLogin === '1') {
120
+ // 手机端扫描的时候访问的链接,跳转到微信公众号授权页面
121
+ checkMpWechatInWx()
122
+ } else if (mpLogin === '2') {
123
+ const code = url.searchParams.get('code');
124
+ // 推送登录成功状态到扫码端
125
+ const res2 = await query.post({
126
+ path: 'wx',
127
+ key: 'mplogin',
128
+ state,
129
+ code
130
+ })
131
+ if (res2.code === 200) {
132
+ message.success('登录成功');
133
+ } else {
134
+ message.error(res2.message || '登录失败');
135
+ }
136
+ closePage();
137
+ }
138
+ }
139
+ const isWechat = () => {
140
+ const ua = navigator.userAgent.toLowerCase();
141
+ return /micromessenger/i.test(ua);
142
+ };
143
+
144
+ const closePage = (time = 2000) => {
145
+ if (!isWechat()) {
146
+ setTimeout(() => {
147
+ window.close();
148
+ }, time);
149
+ return;
150
+ }
151
+ // @ts-ignore
152
+ if (window.WeixinJSBridge) {
153
+ setTimeout(() => {
154
+ // @ts-ignore
155
+ window.WeixinJSBridge.call('closeWindow');
156
+ }, time);
157
+ } else {
158
+ setTimeout(() => {
159
+ window.close();
160
+ }, time);
161
+ }
162
+ };
163
+ const checkMpWechatInWx = async () => {
164
+ const wxAuthUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect`
165
+ const appid = WX_MP_APP_ID;
166
+ const url = new URL(window.location.href);
167
+ const originState = url.searchParams.get('state');
168
+ let [mpLogin, state] = originState ? originState.split('-') : [null, null];
169
+
170
+ const redirectURL = new URL(url.pathname, url.origin);
171
+ state = '2-' + state; // 标记为第二步登录
172
+ const redirect_uri = encodeURIComponent(redirectURL.toString())
173
+ document.body.innerHTML = `<p>正在准备跳转到微信公众号授权页面...</p>`;
174
+ const scope = `snsapi_userinfo`
175
+ if (!state) {
176
+ alert('Invalid state. Please try again later.');
177
+ closePage();
178
+ return;
179
+ }
180
+ const link = wxAuthUrl.replace('APPID', appid).replace('REDIRECT_URI', redirect_uri).replace('SCOPE', scope).replace('STATE', state);
181
+ setTimeout(() => {
182
+ window.location.href = link;
183
+ }, 100);
184
+ }
185
+
186
+ setTimeout(() => {
187
+ checkMpWechat();
188
+ }, 100);
@@ -0,0 +1,10 @@
1
+ import { Query } from '@kevisual/query'
2
+ import { QueryLoginBrowser } from '@kevisual/query-login';
3
+
4
+
5
+ export const queryBase = new Query()
6
+
7
+ export const query = new QueryLoginBrowser({
8
+ query: queryBase,
9
+ })
10
+
@@ -0,0 +1,21 @@
1
+ // <script src="https://turing.captcha.qcloud.com/TCaptcha.js"></script>
2
+
3
+ export const dynimicLoadTcapTcha = async (): Promise<boolean> => {
4
+ return new Promise((resolve, reject) => {
5
+ const script = document.createElement('script')
6
+ script.type = 'text/javascript'
7
+ script.id = 'tencent-captcha'
8
+ if (document.getElementById('tencent-captcha')) {
9
+ resolve(true)
10
+ return
11
+ }
12
+ script.src = 'https://turing.captcha.qcloud.com/TCaptcha.js'
13
+ script.onload = () => {
14
+ resolve(true)
15
+ }
16
+ script.onerror = (error) => {
17
+ reject(error)
18
+ }
19
+ document.body.appendChild(script)
20
+ })
21
+ }
@@ -0,0 +1,70 @@
1
+ // 定义回调函数
2
+ export function callback(res: any) {
3
+ // 第一个参数传入回调结果,结果如下:
4
+ // ret Int 验证结果,0:验证成功。2:用户主动关闭验证码。
5
+ // ticket String 验证成功的票据,当且仅当 ret = 0 时 ticket 有值。
6
+ // CaptchaAppId String 验证码应用ID。
7
+ // bizState Any 自定义透传参数。
8
+ // randstr String 本次验证的随机串,后续票据校验时需传递该参数。
9
+ console.log('callback:', res);
10
+ // res(用户主动关闭验证码)= {ret: 2, ticket: null}
11
+ // res(验证成功) = {ret: 0, ticket: "String", randstr: "String"}
12
+ // res(请求验证码发生错误,验证码自动返回terror_前缀的容灾票据) = {ret: 0, ticket: "String", randstr: "String", errorCode: Number, errorMessage: "String"}
13
+ // 此处代码仅为验证结果的展示示例,真实业务接入,建议基于ticket和errorCode情况做不同的业务处理
14
+ if (res.ret === 0) {
15
+ // 复制结果至剪切板
16
+ var str = '【randstr】->【' + res.randstr + '】 【ticket】->【' + res.ticket + '】';
17
+ var ipt = document.createElement('input');
18
+ ipt.value = str;
19
+ document.body.appendChild(ipt);
20
+ ipt.select();
21
+ document.body.removeChild(ipt);
22
+ alert('1. 返回结果(randstr、ticket)已复制到剪切板,ctrl+v 查看。 2. 打开浏览器控制台,查看完整返回结果。');
23
+ }
24
+ }
25
+ export type TencentCaptcha = {
26
+ actionDuration?: number;
27
+ appid?: string;
28
+ bizState?: any;
29
+ randstr?: string;
30
+ ret: number;
31
+ sid?: string;
32
+ ticket?: string;
33
+ errorCode?: number;
34
+ errorMessage?: string;
35
+ verifyDuration?: number;
36
+ };
37
+ // 定义验证码触发事件
38
+ export const checkCaptcha = (captchaAppId: string): Promise<TencentCaptcha> => {
39
+ return new Promise((resolve, reject) => {
40
+ const callback = (res: TencentCaptcha) => {
41
+ console.log('callback:', res);
42
+ if (res.ret === 0) {
43
+ resolve(res);
44
+ } else {
45
+ reject(res);
46
+ }
47
+ };
48
+ const appid = captchaAppId;
49
+ try {
50
+ // 生成一个验证码对象
51
+ // CaptchaAppId:登录验证码控制台,从【验证管理】页面进行查看。如果未创建过验证,请先新建验证。注意:不可使用客户端类型为小程序的CaptchaAppId,会导致数据统计错误。
52
+ //callback:定义的回调函数
53
+ // @ts-ignore
54
+ var captcha = new TencentCaptcha(appid, callback, {});
55
+ // 调用方法,显示验证码
56
+ captcha.show();
57
+ } catch (error) {
58
+ // 加载异常,调用验证码js加载错误处理函数
59
+ var ticket = 'terror_1001_' + appid + '_' + Math.floor(new Date().getTime() / 1000);
60
+ // 生成容灾票据或自行做其它处理
61
+ callback({
62
+ ret: 0,
63
+ randstr: '@' + Math.random().toString(36).substring(2),
64
+ ticket: ticket,
65
+ errorCode: 1001,
66
+ errorMessage: 'jsload_error',
67
+ });
68
+ }
69
+ });
70
+ };
@@ -0,0 +1,61 @@
1
+ type WxLoginConfig = {
2
+ redirect_uri?: string;
3
+ appid?: string;
4
+ scope?: string;
5
+ state?: string;
6
+ style?: string;
7
+ };
8
+ export const createLogin = async (config?: WxLoginConfig) => {
9
+ let redirect_uri = config?.redirect_uri;
10
+ const { appid } = config || {};
11
+ if (!redirect_uri) {
12
+ redirect_uri = window.location.href;
13
+ }
14
+ const url = new URL(redirect_uri); // remove code and state params
15
+ url.searchParams.delete('code');
16
+ url.searchParams.delete('state');
17
+ redirect_uri = url.toString();
18
+
19
+ console.log('redirect_uri', redirect_uri);
20
+ if (!appid) {
21
+ console.error('appid is not cant be empty');
22
+ return;
23
+ }
24
+ // @ts-ignore
25
+ const obj = new WxLogin({
26
+ self_redirect: false,
27
+ id: 'weixinLogin', // 需要显示的容器id
28
+ appid: appid, // 微信开放平台appid wx*******
29
+ scope: 'snsapi_login', // 网页默认即可 snsapi_userinfo
30
+ redirect_uri: encodeURIComponent(redirect_uri), // 授权成功后回调的url
31
+ state: Math.ceil(Math.random() * 1000), // 可设置为简单的随机数加session用来校验
32
+ stylelite: true, // 是否使用简洁模式
33
+ // https://juejin.cn/post/6982473580063752223
34
+ href: "data:text/css;base64,LmltcG93ZXJCb3ggLnFyY29kZSB7d2lkdGg6IDIwMHB4O30NCi5pbXBvd2VyQm94IC50aXRsZSB7ZGlzcGxheTogbm9uZTt9DQouaW1wb3dlckJveCAuaW5mbyB7d2lkdGg6IDIwMHB4O30NCi5zdGF0dXNfaWNvbiB7ZGlzcGxheTogbm9uZX0NCi5pbXBvd2VyQm94IC5zdGF0dXMge3RleHQtYWxpZ246IGNlbnRlcjt9"
35
+ });
36
+ const login = document.querySelector('#weixinLogin')
37
+ if (login) {
38
+ // login 下的 iframe 样式调整
39
+ const iframe = login.querySelector('iframe');
40
+ if (iframe) {
41
+ // iframe.style.width = '200px';
42
+ iframe.style.height = '300px';
43
+ }
44
+ }
45
+ return obj;
46
+ };
47
+ export const wxId = 'weixinLogin';
48
+ export function setWxerwma(config?: WxLoginConfig) {
49
+ const s = document.createElement('script');
50
+ s.type = 'text/javascript';
51
+ s.src = '//res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js';
52
+ s.id = 'weixinLogin-js';
53
+ if (document.getElementById('weixinLogin-js')) {
54
+ createLogin(config);
55
+ return;
56
+ }
57
+ const wxElement = document.body.appendChild(s);
58
+ wxElement.onload = function () {
59
+ createLogin(config);
60
+ };
61
+ }
@@ -0,0 +1,57 @@
1
+ import QRCode, { QRCodeToDataURLOptions } from 'qrcode';
2
+ import { redirectHome } from '../login-handle.ts';
3
+ import { query } from '../query.ts';
4
+ export const useCreateLoginQRCode = (el?: HTMLCanvasElement) => {
5
+ var opts: QRCodeToDataURLOptions = {
6
+ errorCorrectionLevel: 'H',
7
+ type: 'image/jpeg',
8
+ margin: 1,
9
+ width: 300,
10
+ };
11
+ let timer: any = null;
12
+ const createQrcode = async (state: string) => {
13
+ const url = new URL(window.location.href);
14
+ const loginUrl = new URL(url.pathname, url.origin);
15
+ loginUrl.searchParams.set('state', '1-' + state);
16
+ console.log('生成登录二维码链接:', loginUrl.toString());
17
+ var img = el || document.getElementById('qrcode')! as HTMLCanvasElement;
18
+ const res = await QRCode.toDataURL(img!, loginUrl.toString(), opts);
19
+ };
20
+ const checkLogin = async (state: string) => {
21
+ const res = await fetch(`/api/router?path=wx&key=checkLogin&state=${state}`).then((res) => res.json());
22
+ if (res.code === 200) {
23
+ console.log(res);
24
+ const token = res.data;
25
+ if (token) {
26
+ localStorage.setItem('token', token.accessToken);
27
+ await query.setLoginToken(token);
28
+ }
29
+ clear();
30
+ setTimeout(() => {
31
+ redirectHome();
32
+ }, 1000);
33
+ } else {
34
+ timer = setTimeout(() => {
35
+ checkLogin(state);
36
+ console.log('继续检测登录状态');
37
+ }, 2000);
38
+ }
39
+ };
40
+ // 随机生成一个state
41
+ const state = Math.random().toString(36).substring(2, 15);
42
+ createQrcode(state);
43
+ checkLogin(state);
44
+ const timer2 = setInterval(() => {
45
+ const state = Math.random().toString(36).substring(2, 15);
46
+ clearTimeout(timer); // 清除定时器
47
+ createQrcode(state); // 90秒后更新二维码
48
+ checkLogin(state);
49
+ console.log('更新二维码');
50
+ }, 90000);
51
+ const clear = () => {
52
+ clearTimeout(timer);
53
+ clearInterval(timer2);
54
+ console.log('停止检测登录状态');
55
+ }
56
+ return { createQrcode, clear };
57
+ };