github-actions[bot]
commited on
Commit
·
6c6d16c
1
Parent(s):
9657167
Update from GitHub Actions
Browse files- functions/api/mail/auth.ts +12 -191
- functions/api/mail/batch.ts +118 -0
- functions/api/mail/status.ts +69 -0
- functions/utils/authService.ts +204 -0
- functions/utils/browser.ts +52 -0
- functions/utils/emailVerification.ts +1 -0
- index.ts +9 -2
- src/services/mailApi.ts +25 -0
- src/views/MailView.vue +65 -1
functions/api/mail/auth.ts
CHANGED
@@ -1,14 +1,11 @@
|
|
1 |
import { authApiToken, authMiddleware } from "../../utils/auth.js";
|
2 |
import { addCorsHeaders } from "../../utils/cors.js";
|
3 |
-
import {
|
4 |
-
import { getVerificationCode } from '../../utils/emailVerification.js';
|
5 |
-
|
6 |
|
7 |
export const onRequest = async (context: RouteContext): Promise<Response> => {
|
8 |
const request = context.request;
|
9 |
const env: Env = context.env;
|
10 |
|
11 |
-
// 验证权限
|
12 |
const authResponse = await authMiddleware(request, env);
|
13 |
const apiResponse = await authApiToken(request, env);
|
14 |
if (authResponse && apiResponse) {
|
@@ -21,196 +18,20 @@ export const onRequest = async (context: RouteContext): Promise<Response> => {
|
|
21 |
throw new Error("Email is required");
|
22 |
}
|
23 |
|
24 |
-
|
25 |
-
const
|
26 |
-
const accounts: any[] = accountsStr ? JSON.parse(accountsStr) : [];
|
27 |
-
const account = accounts.find(a => a.email === email);
|
28 |
-
if (!account) {
|
29 |
-
throw new Error("Account not found");
|
30 |
-
}
|
31 |
-
const clientId = env.ENTRA_CLIENT_ID;
|
32 |
-
const clientSecret = env.ENTRA_CLIENT_SECRET;
|
33 |
-
const redirectUri = env.AUTH_REDIRECT_URI;
|
34 |
-
let browser;
|
35 |
-
let context;
|
36 |
-
try {
|
37 |
-
browser = await chromium.launch({
|
38 |
-
headless: process.env.NODE_ENV === 'development' ? false : true,
|
39 |
-
args: [
|
40 |
-
'--no-sandbox',
|
41 |
-
'--disable-setuid-sandbox',
|
42 |
-
'--disable-dev-shm-usage',
|
43 |
-
'--disable-blink-features=AutomationControlled', // 禁用自动化特征
|
44 |
-
'--disable-infobars',
|
45 |
-
'--window-size=1920,1080'
|
46 |
-
],
|
47 |
-
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH, // 使用系统 Chromium
|
48 |
-
});
|
49 |
-
context = await browser.newContext();
|
50 |
-
const page = await context.newPage();
|
51 |
-
|
52 |
-
const authUrl = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?` +
|
53 |
-
`client_id=${clientId}` +
|
54 |
-
`&response_type=code` +
|
55 |
-
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
|
56 |
-
`&response_mode=query` +
|
57 |
-
`&scope=offline_access%20IMAP.AccessAsUser.All%20User.Read%20Mail.ReadWrite.Shared%20Mail.Send%20Mail.Read` +
|
58 |
-
`&prompt=consent` + //强制显示权限确认窗口
|
59 |
-
`&state=${email}`;
|
60 |
-
|
61 |
-
|
62 |
-
console.log(authUrl)
|
63 |
-
|
64 |
-
//开始认证
|
65 |
-
await page.goto(authUrl);
|
66 |
-
await page.fill('input[type="email"]', account.email);
|
67 |
-
await page.click('input[type="submit"]');
|
68 |
-
|
69 |
-
try {
|
70 |
-
await page.waitForSelector('#idA_PWD_SwitchToPassword', { timeout: 3000 });
|
71 |
-
await page.click('#idA_PWD_SwitchToPassword');
|
72 |
-
} catch (error) {
|
73 |
-
console.log(`没有切换到密码登录,继续执行: ${error}`);
|
74 |
-
}
|
75 |
-
|
76 |
-
|
77 |
-
//输入密码
|
78 |
-
await page.waitForURL("https://login.live.com/**")
|
79 |
-
await page.fill('input[type="password"]', account.password);
|
80 |
-
await page.click('button[type="submit"]');
|
81 |
-
|
82 |
-
|
83 |
-
try {
|
84 |
-
await page.waitForURL("https://account.live.com/recover/**", { timeout: 5000 })
|
85 |
-
await page.click('#iLandingViewAction');
|
86 |
-
} catch (error) {
|
87 |
-
// 如果超时或页面未出现,直接继续执行
|
88 |
-
console.log("验证确定 page not found or timeout, skipping...");
|
89 |
-
}
|
90 |
-
|
91 |
-
for (let i = 0; i < 2; i++) {
|
92 |
-
try {
|
93 |
-
await page.waitForURL('https://account.live.com/identity/**', { timeout: 5000 });
|
94 |
-
const proofEmail = account.proofEmail;
|
95 |
-
if (!proofEmail) {
|
96 |
-
throw new Error("No proof email provided");
|
97 |
-
}
|
98 |
-
const timestamp = Math.floor(Date.now() / 1000);
|
99 |
-
try {
|
100 |
-
await page.waitForSelector('#iProof0', { timeout: 3000 });
|
101 |
-
await page.click('#iProof0')
|
102 |
-
} catch (error) {
|
103 |
-
console.log(`没有#iProof0,继续执行: ${error}`);
|
104 |
-
}
|
105 |
-
await page.fill("#iProofEmail", proofEmail)
|
106 |
-
await page.click('input[type="submit"]')
|
107 |
-
|
108 |
-
//处理email验证
|
109 |
-
const proof = [
|
110 |
-
{
|
111 |
-
"suffix": "godgodgame.com",
|
112 |
-
"apiUrl": "https://seedmail.igiven.com/api/latest-email",
|
113 |
-
"token": env.PROOF_GODGODGAME_TOKEN
|
114 |
-
},
|
115 |
-
{
|
116 |
-
"suffix": "igiven.com",
|
117 |
-
"apiUrl": "https://mail.igiven.com/api/latest-email",
|
118 |
-
"token": env.PROOF_IGIVEN_TOKEN
|
119 |
-
}
|
120 |
-
]
|
121 |
-
|
122 |
-
const suffix = proofEmail.substring(proofEmail.indexOf('@') + 1);
|
123 |
-
const proofConfig = proof.find(p => p.suffix === suffix)!;
|
124 |
-
console.log(suffix, proofConfig)
|
125 |
-
const verificationCode = await getVerificationCode(proofConfig.apiUrl, proofConfig.token!, account.proofEmail, timestamp);
|
126 |
-
console.log(`verificationCode:${verificationCode}`)
|
127 |
-
await page.fill('input[type="tel"]', verificationCode);
|
128 |
-
await page.click('input[type="submit"]');
|
129 |
-
} catch (error) {
|
130 |
-
console.log(account.email, `没有多重验证,继续执行: ${error}`);
|
131 |
-
}
|
132 |
-
}
|
133 |
-
|
134 |
|
135 |
-
|
136 |
-
|
137 |
-
return url.href.startsWith("https://login.live.com");
|
138 |
-
})
|
139 |
-
await page.click('button[type="submit"]#acceptButton'); // 同意按钮
|
140 |
-
|
141 |
-
//同意授权
|
142 |
-
try {
|
143 |
-
// 等待同意授权页面,如果超时5秒则跳过
|
144 |
-
await page.waitForURL("https://account.live.com/Consent/**", { timeout: 10000 })
|
145 |
-
await page.click('button[type="submit"][data-testid="appConsentPrimaryButton"]');
|
146 |
-
} catch (error) {
|
147 |
-
// 如果超时或页面未出现,直接继续执行
|
148 |
-
console.log("Consent page not found or timeout, skipping...");
|
149 |
-
}
|
150 |
-
await page.waitForURL((url) => {
|
151 |
-
return url.href.startsWith(redirectUri);
|
152 |
-
})
|
153 |
-
} finally {
|
154 |
-
if (context) await context.close();
|
155 |
-
if (browser) await browser.close();
|
156 |
}
|
157 |
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
while (!code && retries < maxRetries) {
|
165 |
-
const codeKey = `code_${email}`;
|
166 |
-
code = await env.KV.get(codeKey);
|
167 |
-
|
168 |
-
if (!code) {
|
169 |
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
170 |
-
retries++;
|
171 |
}
|
172 |
-
|
173 |
-
|
174 |
-
if (!code) {
|
175 |
-
throw new Error("Authorization timeout");
|
176 |
-
}
|
177 |
-
|
178 |
-
// 使用授权码获取刷新令牌
|
179 |
-
const tokenResponse = await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
|
180 |
-
method: 'POST',
|
181 |
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
182 |
-
body: new URLSearchParams({
|
183 |
-
client_id: clientId,
|
184 |
-
client_secret: clientSecret,
|
185 |
-
code: code,
|
186 |
-
redirect_uri: redirectUri,
|
187 |
-
grant_type: 'authorization_code'
|
188 |
-
})
|
189 |
-
});
|
190 |
-
|
191 |
-
const tokenData: any = await tokenResponse.json();
|
192 |
-
if (!tokenData.refresh_token) {
|
193 |
-
throw new Error("Failed to get refresh token");
|
194 |
-
}
|
195 |
-
if (!tokenData.expires_in) {
|
196 |
-
throw new Error("Missing expires_in in token response");
|
197 |
-
}
|
198 |
-
|
199 |
-
// 存储刷新令牌和时间戳
|
200 |
-
const tokenInfo = {
|
201 |
-
...tokenData,
|
202 |
-
timestamp: Date.now()
|
203 |
-
};
|
204 |
-
console.log(tokenInfo);
|
205 |
-
await env.KV.put(`refresh_token_${email}`, JSON.stringify(tokenInfo));
|
206 |
-
|
207 |
-
// 删除临时授权码
|
208 |
-
await env.KV.delete(`code_${email}`);
|
209 |
-
|
210 |
-
return new Response(JSON.stringify({ success: true }), {
|
211 |
-
status: 200,
|
212 |
-
headers: { 'Content-Type': 'application/json' }
|
213 |
-
});
|
214 |
|
215 |
} catch (error: any) {
|
216 |
return new Response(
|
@@ -218,4 +39,4 @@ export const onRequest = async (context: RouteContext): Promise<Response> => {
|
|
218 |
{ status: 500 }
|
219 |
);
|
220 |
}
|
221 |
-
};
|
|
|
1 |
import { authApiToken, authMiddleware } from "../../utils/auth.js";
|
2 |
import { addCorsHeaders } from "../../utils/cors.js";
|
3 |
+
import { AuthService } from "../../utils/authService.js";
|
|
|
|
|
4 |
|
5 |
export const onRequest = async (context: RouteContext): Promise<Response> => {
|
6 |
const request = context.request;
|
7 |
const env: Env = context.env;
|
8 |
|
|
|
9 |
const authResponse = await authMiddleware(request, env);
|
10 |
const apiResponse = await authApiToken(request, env);
|
11 |
if (authResponse && apiResponse) {
|
|
|
18 |
throw new Error("Email is required");
|
19 |
}
|
20 |
|
21 |
+
const authService = new AuthService(env);
|
22 |
+
const result = await authService.authenticateEmail(email);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
|
24 |
+
if (!result.success) {
|
25 |
+
throw new Error(result.error);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
}
|
27 |
|
28 |
+
return new Response(
|
29 |
+
JSON.stringify({ success: true }),
|
30 |
+
{
|
31 |
+
status: 200,
|
32 |
+
headers: { 'Content-Type': 'application/json' }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
}
|
34 |
+
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
35 |
|
36 |
} catch (error: any) {
|
37 |
return new Response(
|
|
|
39 |
{ status: 500 }
|
40 |
);
|
41 |
}
|
42 |
+
};
|
functions/api/mail/batch.ts
ADDED
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { authApiToken, authMiddleware } from "../../utils/auth.js";
|
2 |
+
import { addCorsHeaders } from "../../utils/cors.js";
|
3 |
+
import { AuthService } from "../../utils/authService.js";
|
4 |
+
|
5 |
+
// 并发处理函数
|
6 |
+
async function processBatch<T>(
|
7 |
+
items: T[],
|
8 |
+
processor: (item: T) => Promise<any>,
|
9 |
+
concurrency: number = 2
|
10 |
+
): Promise<any[]> {
|
11 |
+
// 创建一个队列来存储所有的 promise
|
12 |
+
const queue: Promise<any>[] = [];
|
13 |
+
const results: any[] = new Array(items.length);
|
14 |
+
let nextIndex = 0;
|
15 |
+
|
16 |
+
// 创建指定数量的worker
|
17 |
+
const workers = new Array(concurrency).fill(null).map(async () => {
|
18 |
+
while (nextIndex < items.length) {
|
19 |
+
const currentIndex = nextIndex++;
|
20 |
+
try {
|
21 |
+
const result = await processor(items[currentIndex]);
|
22 |
+
results[currentIndex] = result;
|
23 |
+
} catch (error) {
|
24 |
+
results[currentIndex] = {
|
25 |
+
error: error instanceof Error ? error.message : 'Unknown error'
|
26 |
+
};
|
27 |
+
}
|
28 |
+
}
|
29 |
+
});
|
30 |
+
|
31 |
+
// 等待所有worker完成
|
32 |
+
await Promise.all(workers);
|
33 |
+
return results;
|
34 |
+
}
|
35 |
+
|
36 |
+
// 检查认证状态和时间戳
|
37 |
+
async function checkAuthStatus(email: string, env: Env): Promise<{ needsAuth: boolean, reason?: string }> {
|
38 |
+
const tokenInfoStr = await env.KV.get(`refresh_token_${email}`);
|
39 |
+
if (!tokenInfoStr) {
|
40 |
+
return { needsAuth: true, reason: "未认证" };
|
41 |
+
}
|
42 |
+
|
43 |
+
const tokenInfo = JSON.parse(tokenInfoStr);
|
44 |
+
const threeMonths = 90 * 24 * 60 * 60 * 1000; // 90天转换为毫秒
|
45 |
+
const isExpired = Date.now() - tokenInfo.timestamp > threeMonths;
|
46 |
+
|
47 |
+
if (isExpired) {
|
48 |
+
return { needsAuth: true, reason: "认证已过期" };
|
49 |
+
}
|
50 |
+
|
51 |
+
return { needsAuth: false };
|
52 |
+
}
|
53 |
+
|
54 |
+
export const onRequest = async (context: RouteContext): Promise<Response> => {
|
55 |
+
const request = context.request;
|
56 |
+
const env: Env = context.env;
|
57 |
+
|
58 |
+
const authResponse = await authMiddleware(request, env);
|
59 |
+
const apiResponse = await authApiToken(request, env);
|
60 |
+
if (authResponse && apiResponse) {
|
61 |
+
return addCorsHeaders(authResponse);
|
62 |
+
}
|
63 |
+
|
64 |
+
try {
|
65 |
+
const { emails } = await request.json() as { emails: string[] };
|
66 |
+
if (!emails || !Array.isArray(emails)) {
|
67 |
+
throw new Error("Emails array is required");
|
68 |
+
}
|
69 |
+
|
70 |
+
const authService = new AuthService(env);
|
71 |
+
const results = await processBatch(
|
72 |
+
emails,
|
73 |
+
async (email) => {
|
74 |
+
try {
|
75 |
+
// 检查认证状态
|
76 |
+
const authStatus = await checkAuthStatus(email, env);
|
77 |
+
if (!authStatus.needsAuth) {
|
78 |
+
return {
|
79 |
+
email,
|
80 |
+
success: true,
|
81 |
+
message: "已认证且在有效期内"
|
82 |
+
};
|
83 |
+
}
|
84 |
+
|
85 |
+
// 需要重新认证
|
86 |
+
const result = await authService.authenticateEmail(email);
|
87 |
+
return {
|
88 |
+
email,
|
89 |
+
success: result.success,
|
90 |
+
message: authStatus.reason,
|
91 |
+
error: result.error
|
92 |
+
};
|
93 |
+
} catch (error: any) {
|
94 |
+
return {
|
95 |
+
email,
|
96 |
+
success: false,
|
97 |
+
error: error.message || 'Failed to process'
|
98 |
+
};
|
99 |
+
}
|
100 |
+
},
|
101 |
+
2
|
102 |
+
);
|
103 |
+
|
104 |
+
return new Response(
|
105 |
+
JSON.stringify(results),
|
106 |
+
{
|
107 |
+
status: 200,
|
108 |
+
headers: { 'Content-Type': 'application/json' }
|
109 |
+
}
|
110 |
+
);
|
111 |
+
|
112 |
+
} catch (error: any) {
|
113 |
+
return new Response(
|
114 |
+
JSON.stringify({ error: error.message }),
|
115 |
+
{ status: 500 }
|
116 |
+
);
|
117 |
+
}
|
118 |
+
};
|
functions/api/mail/status.ts
ADDED
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { authApiToken, authMiddleware } from "../../utils/auth.js";
|
2 |
+
import { addCorsHeaders } from "../../utils/cors.js";
|
3 |
+
|
4 |
+
export const onRequest = async (context: RouteContext): Promise<Response> => {
|
5 |
+
const request = context.request;
|
6 |
+
const env: Env = context.env;
|
7 |
+
|
8 |
+
// 验证权限
|
9 |
+
const authResponse = await authMiddleware(request, env);
|
10 |
+
const apiResponse = await authApiToken(request, env);
|
11 |
+
if (authResponse && apiResponse) {
|
12 |
+
return addCorsHeaders(authResponse);
|
13 |
+
}
|
14 |
+
|
15 |
+
try {
|
16 |
+
const { email } = await request.json() as any;
|
17 |
+
if (!email) {
|
18 |
+
throw new Error("Email is required");
|
19 |
+
}
|
20 |
+
|
21 |
+
// 获取刷新令牌信息
|
22 |
+
const tokenInfoStr = await env.KV.get(`refresh_token_${email}`);
|
23 |
+
if (!tokenInfoStr) {
|
24 |
+
return new Response(
|
25 |
+
JSON.stringify({
|
26 |
+
authenticated: false,
|
27 |
+
message: "未认证"
|
28 |
+
}),
|
29 |
+
{
|
30 |
+
status: 200,
|
31 |
+
headers: {
|
32 |
+
'Content-Type': 'application/json',
|
33 |
+
'Access-Control-Allow-Origin': '*'
|
34 |
+
}
|
35 |
+
}
|
36 |
+
);
|
37 |
+
}
|
38 |
+
|
39 |
+
const tokenInfo = JSON.parse(tokenInfoStr);
|
40 |
+
|
41 |
+
|
42 |
+
return new Response(
|
43 |
+
JSON.stringify({
|
44 |
+
authenticated: true,
|
45 |
+
message: "已认证",
|
46 |
+
timestamp: tokenInfo.timestamp
|
47 |
+
}),
|
48 |
+
{
|
49 |
+
status: 200,
|
50 |
+
headers: {
|
51 |
+
'Content-Type': 'application/json',
|
52 |
+
'Access-Control-Allow-Origin': '*'
|
53 |
+
}
|
54 |
+
}
|
55 |
+
);
|
56 |
+
|
57 |
+
} catch (error: any) {
|
58 |
+
return new Response(
|
59 |
+
JSON.stringify({ error: error.message }),
|
60 |
+
{
|
61 |
+
status: 500,
|
62 |
+
headers: {
|
63 |
+
'Content-Type': 'application/json',
|
64 |
+
'Access-Control-Allow-Origin': '*'
|
65 |
+
}
|
66 |
+
}
|
67 |
+
);
|
68 |
+
}
|
69 |
+
};
|
functions/utils/authService.ts
ADDED
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Page } from 'playwright';
|
2 |
+
import BrowserManager from './browser';
|
3 |
+
import { getVerificationCode } from './emailVerification.js';
|
4 |
+
|
5 |
+
interface Account {
|
6 |
+
email: string;
|
7 |
+
password: string;
|
8 |
+
proofEmail: string;
|
9 |
+
}
|
10 |
+
|
11 |
+
|
12 |
+
export class AuthService {
|
13 |
+
constructor(private env: Env) { }
|
14 |
+
|
15 |
+
async authenticateEmail(email: string): Promise<{ success: boolean; error?: string }> {
|
16 |
+
try {
|
17 |
+
const accountsStr = await this.env.KV.get("accounts");
|
18 |
+
const accounts: Account[] = accountsStr ? JSON.parse(accountsStr) : [];
|
19 |
+
const account = accounts.find(a => a.email === email);
|
20 |
+
|
21 |
+
if (!account) {
|
22 |
+
throw new Error("Account not found");
|
23 |
+
}
|
24 |
+
|
25 |
+
await this.performAuthentication(account);
|
26 |
+
await this.handleAuthorizationCallback(email);
|
27 |
+
|
28 |
+
return { success: true };
|
29 |
+
} catch (error: any) {
|
30 |
+
return {
|
31 |
+
success: false,
|
32 |
+
error: error.message
|
33 |
+
};
|
34 |
+
}
|
35 |
+
}
|
36 |
+
|
37 |
+
private async performAuthentication(account: Account) {
|
38 |
+
const clientId = this.env.ENTRA_CLIENT_ID;
|
39 |
+
const redirectUri = this.env.AUTH_REDIRECT_URI;
|
40 |
+
let browser;
|
41 |
+
let context;
|
42 |
+
|
43 |
+
try {
|
44 |
+
browser = await BrowserManager.getInstance();
|
45 |
+
context = await browser.newContext();
|
46 |
+
const page = await context.newPage();
|
47 |
+
|
48 |
+
const authUrl = this.buildAuthUrl(clientId, redirectUri, account.email);
|
49 |
+
await this.handleLoginProcess(page, account, authUrl);
|
50 |
+
await this.handleMultiFactorAuth(page, account);
|
51 |
+
await this.handleConsent(page, redirectUri);
|
52 |
+
} finally {
|
53 |
+
if (context) await context.close();
|
54 |
+
}
|
55 |
+
}
|
56 |
+
|
57 |
+
private buildAuthUrl(clientId: string, redirectUri: string, email: string): string {
|
58 |
+
return `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?` +
|
59 |
+
`client_id=${clientId}` +
|
60 |
+
`&response_type=code` +
|
61 |
+
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
|
62 |
+
`&response_mode=query` +
|
63 |
+
`&scope=offline_access%20IMAP.AccessAsUser.All%20User.Read%20Mail.ReadWrite.Shared%20Mail.Send%20Mail.Read` +
|
64 |
+
`&prompt=consent` +
|
65 |
+
`&state=${email}`;
|
66 |
+
}
|
67 |
+
|
68 |
+
private async handleLoginProcess(page: Page, account: Account, authUrl: string) {
|
69 |
+
await page.goto(authUrl);
|
70 |
+
await page.fill('input[type="email"]', account.email);
|
71 |
+
await page.click('input[type="submit"]');
|
72 |
+
|
73 |
+
try {
|
74 |
+
await page.waitForSelector('#idA_PWD_SwitchToPassword', { timeout: 3000 });
|
75 |
+
await page.click('#idA_PWD_SwitchToPassword');
|
76 |
+
} catch (error) {
|
77 |
+
console.log(`没有切换到密码登录,继续执行: ${error}`);
|
78 |
+
}
|
79 |
+
|
80 |
+
await page.waitForURL("https://login.live.com/**");
|
81 |
+
await page.fill('input[type="password"]', account.password);
|
82 |
+
await page.fill('input[type="password"]', account.password);
|
83 |
+
await page.click('button[type="submit"]');
|
84 |
+
|
85 |
+
try {
|
86 |
+
await page.waitForURL("https://account.live.com/recover/**", { timeout: 5000 });
|
87 |
+
await page.click('#iLandingViewAction');
|
88 |
+
} catch (error) {
|
89 |
+
console.log("验证确定 page not found or timeout, skipping...");
|
90 |
+
}
|
91 |
+
}
|
92 |
+
|
93 |
+
private async handleMultiFactorAuth(page: Page, account: Account) {
|
94 |
+
for (let i = 0; i < 2; i++) {
|
95 |
+
try {
|
96 |
+
await page.waitForURL('https://account.live.com/identity/**', { timeout: 5000 });
|
97 |
+
const proofEmail = account.proofEmail;
|
98 |
+
if (!proofEmail) {
|
99 |
+
throw new Error("No proof email provided");
|
100 |
+
}
|
101 |
+
|
102 |
+
const timestamp = Math.floor(Date.now() / 1000);
|
103 |
+
try {
|
104 |
+
await page.waitForSelector('#iProof0', { timeout: 3000 });
|
105 |
+
await page.click('#iProof0');
|
106 |
+
} catch (error) {
|
107 |
+
console.log(`没有#iProof0,继续执行: ${error}`);
|
108 |
+
}
|
109 |
+
|
110 |
+
await page.fill("#iProofEmail", proofEmail);
|
111 |
+
await page.click('input[type="submit"]');
|
112 |
+
|
113 |
+
const proof = [
|
114 |
+
{
|
115 |
+
"suffix": "godgodgame.com",
|
116 |
+
"apiUrl": "https://seedmail.igiven.com/api/latest-email",
|
117 |
+
"token": this.env.PROOF_GODGODGAME_TOKEN
|
118 |
+
},
|
119 |
+
{
|
120 |
+
"suffix": "igiven.com",
|
121 |
+
"apiUrl": "https://mail.igiven.com/api/latest-email",
|
122 |
+
"token": this.env.PROOF_IGIVEN_TOKEN
|
123 |
+
}
|
124 |
+
];
|
125 |
+
|
126 |
+
const suffix = proofEmail.substring(proofEmail.indexOf('@') + 1);
|
127 |
+
const proofConfig = proof.find(p => p.suffix === suffix)!;
|
128 |
+
const verificationCode = await getVerificationCode(
|
129 |
+
proofConfig.apiUrl,
|
130 |
+
proofConfig.token!,
|
131 |
+
proofEmail,
|
132 |
+
timestamp
|
133 |
+
);
|
134 |
+
|
135 |
+
await page.fill('input[type="tel"]', verificationCode);
|
136 |
+
await page.click('input[type="submit"]');
|
137 |
+
} catch (error) {
|
138 |
+
console.log(account.email, `没有多重验证,继续执行: ${error}`);
|
139 |
+
}
|
140 |
+
}
|
141 |
+
}
|
142 |
+
|
143 |
+
private async handleConsent(page: Page, redirectUri: string) {
|
144 |
+
await page.waitForURL((url: any) => url.href.startsWith("https://login.live.com"));
|
145 |
+
await page.click('button[type="submit"]#acceptButton');
|
146 |
+
|
147 |
+
try {
|
148 |
+
await page.waitForURL("https://account.live.com/Consent/**", { timeout: 10000 });
|
149 |
+
await page.click('button[type="submit"][data-testid="appConsentPrimaryButton"]');
|
150 |
+
} catch (error) {
|
151 |
+
console.log("Consent page not found or timeout, skipping...");
|
152 |
+
}
|
153 |
+
|
154 |
+
await page.waitForURL((url: any) => url.href.startsWith(redirectUri));
|
155 |
+
}
|
156 |
+
|
157 |
+
private async handleAuthorizationCallback(email: string) {
|
158 |
+
let code = null;
|
159 |
+
const maxRetries = 30;
|
160 |
+
let retries = 0;
|
161 |
+
|
162 |
+
while (!code && retries < maxRetries) {
|
163 |
+
const codeKey = `code_${email}`;
|
164 |
+
code = await this.env.KV.get(codeKey);
|
165 |
+
|
166 |
+
if (!code) {
|
167 |
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
168 |
+
retries++;
|
169 |
+
}
|
170 |
+
}
|
171 |
+
|
172 |
+
if (!code) {
|
173 |
+
throw new Error("Authorization timeout");
|
174 |
+
}
|
175 |
+
|
176 |
+
const tokenResponse = await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
|
177 |
+
method: 'POST',
|
178 |
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
179 |
+
body: new URLSearchParams({
|
180 |
+
client_id: this.env.ENTRA_CLIENT_ID,
|
181 |
+
client_secret: this.env.ENTRA_CLIENT_SECRET,
|
182 |
+
code: code,
|
183 |
+
redirect_uri: this.env.AUTH_REDIRECT_URI,
|
184 |
+
grant_type: 'authorization_code'
|
185 |
+
})
|
186 |
+
});
|
187 |
+
|
188 |
+
const tokenData: any = await tokenResponse.json();
|
189 |
+
if (!tokenData.refresh_token) {
|
190 |
+
throw new Error("Failed to get refresh token");
|
191 |
+
}
|
192 |
+
if (!tokenData.expires_in) {
|
193 |
+
throw new Error("Missing expires_in in token response");
|
194 |
+
}
|
195 |
+
|
196 |
+
const tokenInfo = {
|
197 |
+
...tokenData,
|
198 |
+
timestamp: Date.now()
|
199 |
+
};
|
200 |
+
|
201 |
+
await this.env.KV.put(`refresh_token_${email}`, JSON.stringify(tokenInfo));
|
202 |
+
await this.env.KV.delete(`code_${email}`);
|
203 |
+
}
|
204 |
+
}
|
functions/utils/browser.ts
ADDED
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Browser, chromium } from 'playwright';
|
2 |
+
|
3 |
+
class BrowserManager {
|
4 |
+
private static instance: Browser | null = null;
|
5 |
+
private static isInitializing = false;
|
6 |
+
private static initPromise: Promise<Browser> | null = null;
|
7 |
+
|
8 |
+
static async getInstance(): Promise<Browser> {
|
9 |
+
if (this.instance && this.instance.isConnected()) {
|
10 |
+
return this.instance;
|
11 |
+
}
|
12 |
+
|
13 |
+
if (this.isInitializing) {
|
14 |
+
return this.initPromise!;
|
15 |
+
}
|
16 |
+
|
17 |
+
this.isInitializing = true;
|
18 |
+
this.initPromise = this.initializeBrowser();
|
19 |
+
|
20 |
+
try {
|
21 |
+
this.instance = await this.initPromise;
|
22 |
+
return this.instance;
|
23 |
+
} finally {
|
24 |
+
this.isInitializing = false;
|
25 |
+
this.initPromise = null;
|
26 |
+
}
|
27 |
+
}
|
28 |
+
|
29 |
+
private static async initializeBrowser(): Promise<Browser> {
|
30 |
+
return chromium.launch({
|
31 |
+
headless: process.env.NODE_ENV === 'development' ? false : true,
|
32 |
+
args: [
|
33 |
+
'--no-sandbox',
|
34 |
+
'--disable-setuid-sandbox',
|
35 |
+
'--disable-dev-shm-usage',
|
36 |
+
'--disable-blink-features=AutomationControlled',
|
37 |
+
'--disable-infobars',
|
38 |
+
'--window-size=1920,1080'
|
39 |
+
],
|
40 |
+
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
|
41 |
+
});
|
42 |
+
}
|
43 |
+
|
44 |
+
static async closeBrowser() {
|
45 |
+
if (this.instance) {
|
46 |
+
await this.instance.close();
|
47 |
+
this.instance = null;
|
48 |
+
}
|
49 |
+
}
|
50 |
+
}
|
51 |
+
|
52 |
+
export default BrowserManager;
|
functions/utils/emailVerification.ts
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
export async function getVerificationCode(proofApi: string, apiKey: String, proofEmail: string, timestamp: number): Promise<string> {
|
2 |
const maxRetries = 30;
|
|
|
3 |
console.log(`开始获取验证码${proofApi},${apiKey},${timestamp},${new Date(timestamp * 1000)}`);
|
4 |
for (let i = 0; i < maxRetries; i++) {
|
5 |
try {
|
|
|
1 |
export async function getVerificationCode(proofApi: string, apiKey: String, proofEmail: string, timestamp: number): Promise<string> {
|
2 |
const maxRetries = 30;
|
3 |
+
timestamp = timestamp - 5;
|
4 |
console.log(`开始获取验证码${proofApi},${apiKey},${timestamp},${new Date(timestamp * 1000)}`);
|
5 |
for (let i = 0; i < maxRetries; i++) {
|
6 |
try {
|
index.ts
CHANGED
@@ -22,7 +22,8 @@ import { onRequest as handleMailCallback } from './functions/api/mail/callback.j
|
|
22 |
import { onRequest as handleMailAll } from './functions/api/mail/all.js'
|
23 |
import { onRequest as handleMailNew } from './functions/api/mail/new.js'
|
24 |
import { onRequest as handleMailSend } from './functions/api/mail/send.js'
|
25 |
-
|
|
|
26 |
dotenv.config({ path: ['.env', '.env.local'], override: true });
|
27 |
const isDev = process.env.NODE_ENV === 'development'
|
28 |
|
@@ -55,7 +56,7 @@ var kv: KVNamespace = {
|
|
55 |
put: async (key: string, value: string) => {
|
56 |
await storage.setItem(key, value);
|
57 |
},
|
58 |
-
delete:async(key:string)=>{
|
59 |
await storage.removeItem(key);
|
60 |
}
|
61 |
};
|
@@ -147,6 +148,12 @@ app.all('/api/*', async (c) => {
|
|
147 |
case '/api/mail/send':
|
148 |
response = await handleMailSend(context);
|
149 |
break;
|
|
|
|
|
|
|
|
|
|
|
|
|
150 |
default:
|
151 |
return c.json({ error: 'Route not found' }, 404);
|
152 |
}
|
|
|
22 |
import { onRequest as handleMailAll } from './functions/api/mail/all.js'
|
23 |
import { onRequest as handleMailNew } from './functions/api/mail/new.js'
|
24 |
import { onRequest as handleMailSend } from './functions/api/mail/send.js'
|
25 |
+
import { onRequest as handleBatch } from './functions/api/mail/batch.js'
|
26 |
+
import { onRequest as handleMailStatus } from './functions/api/mail/status.js'
|
27 |
dotenv.config({ path: ['.env', '.env.local'], override: true });
|
28 |
const isDev = process.env.NODE_ENV === 'development'
|
29 |
|
|
|
56 |
put: async (key: string, value: string) => {
|
57 |
await storage.setItem(key, value);
|
58 |
},
|
59 |
+
delete: async (key: string) => {
|
60 |
await storage.removeItem(key);
|
61 |
}
|
62 |
};
|
|
|
148 |
case '/api/mail/send':
|
149 |
response = await handleMailSend(context);
|
150 |
break;
|
151 |
+
case '/api/mail/status':
|
152 |
+
response = await handleMailStatus(context);
|
153 |
+
break;
|
154 |
+
case '/api/mail/batch':
|
155 |
+
response = await handleBatch(context);
|
156 |
+
break;
|
157 |
default:
|
158 |
return c.json({ error: 'Route not found' }, 404);
|
159 |
}
|
src/services/mailApi.ts
CHANGED
@@ -1,6 +1,31 @@
|
|
1 |
import { API_BASE_URL, getHeaders, handleResponse } from './util';
|
2 |
|
3 |
export const mailApi = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
async refreshAuth(email: string) {
|
5 |
const response = await fetch(
|
6 |
`${API_BASE_URL}/api/mail/auth`,
|
|
|
1 |
import { API_BASE_URL, getHeaders, handleResponse } from './util';
|
2 |
|
3 |
export const mailApi = {
|
4 |
+
async getAuthStatus(email: string) {
|
5 |
+
const response = await fetch(
|
6 |
+
`${API_BASE_URL}/api/mail/status`,
|
7 |
+
{
|
8 |
+
headers: getHeaders(),
|
9 |
+
method: 'POST',
|
10 |
+
body: JSON.stringify({ email })
|
11 |
+
}
|
12 |
+
);
|
13 |
+
return handleResponse(response);
|
14 |
+
},
|
15 |
+
|
16 |
+
// Add new batch refresh methods
|
17 |
+
async refreshAuthBatch(emails: string[]) {
|
18 |
+
const response = await fetch(
|
19 |
+
`${API_BASE_URL}/api/mail/batch`,
|
20 |
+
{
|
21 |
+
headers: getHeaders(),
|
22 |
+
method: 'POST',
|
23 |
+
body: JSON.stringify({ emails })
|
24 |
+
}
|
25 |
+
);
|
26 |
+
return handleResponse(response);
|
27 |
+
},
|
28 |
+
|
29 |
async refreshAuth(email: string) {
|
30 |
const response = await fetch(
|
31 |
`${API_BASE_URL}/api/mail/auth`,
|
src/views/MailView.vue
CHANGED
@@ -28,9 +28,9 @@ const columns = [
|
|
28 |
}
|
29 |
];
|
30 |
|
31 |
-
|
32 |
// 添加下拉菜单选项函数
|
33 |
const actionOptions = (row: Account) => [
|
|
|
34 |
{ content: '刷新认证', value: 'refresh', onClick: () => handleRefreshAuth(row) },
|
35 |
{ content: '最新邮件', value: 'latest', onClick: () => handleLatestMails(row) },
|
36 |
{ content: '所有邮件', value: 'all', onClick: () => handleAllMails(row) },
|
@@ -196,6 +196,67 @@ onMounted(() => {
|
|
196 |
fetchData();
|
197 |
});
|
198 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
199 |
|
200 |
</script>
|
201 |
|
@@ -203,6 +264,8 @@ onMounted(() => {
|
|
203 |
<div class="w-full h-full flex flex-col p-2 md:p-5 gap-2 md:gap-5">
|
204 |
<div class="flex flex-col md:flex-row md:items-center md:justify-between">
|
205 |
<div class="flex flex-col md:flex-row md:items-center gap-4">
|
|
|
|
|
206 |
<t-input v-model="userSearch" class="w-full md:w-40" placeholder="搜索用户" clearable />
|
207 |
</div>
|
208 |
</div>
|
@@ -213,6 +276,7 @@ onMounted(() => {
|
|
213 |
<template #action="{ row }">
|
214 |
<!-- 桌面端显示 -->
|
215 |
<div class="hidden md:flex flex-wrap gap-2">
|
|
|
216 |
<t-button size="small" variant="outline" @click="handleRefreshAuth(row)">刷新认证</t-button>
|
217 |
<t-button size="small" variant="outline" @click="handleLatestMails(row)">最新邮件</t-button>
|
218 |
<t-button size="small" variant="outline" @click="handleAllMails(row)">所有邮件</t-button>
|
|
|
28 |
}
|
29 |
];
|
30 |
|
|
|
31 |
// 添加下拉菜单选项函数
|
32 |
const actionOptions = (row: Account) => [
|
33 |
+
{ content: '查看认证', value: 'check', onClick: () => handleCheckAuth(row) },
|
34 |
{ content: '刷新认证', value: 'refresh', onClick: () => handleRefreshAuth(row) },
|
35 |
{ content: '最新邮件', value: 'latest', onClick: () => handleLatestMails(row) },
|
36 |
{ content: '所有邮件', value: 'all', onClick: () => handleAllMails(row) },
|
|
|
196 |
fetchData();
|
197 |
});
|
198 |
|
199 |
+
// Add new methods for batch refresh
|
200 |
+
const handleRefreshAll = async () => {
|
201 |
+
try {
|
202 |
+
loading.value = true;
|
203 |
+
const emails = allData.value.map(account => account.email);
|
204 |
+
const result = await mailApi.refreshAuthBatch(emails);
|
205 |
+
|
206 |
+
// Count successes and failures
|
207 |
+
const successes = result.filter((r:any) => r.success).length;
|
208 |
+
const failures = result.length - successes;
|
209 |
+
|
210 |
+
if (failures > 0) {
|
211 |
+
MessagePlugin.warning(`刷新完成: ${successes}个成功, ${failures}个失败`);
|
212 |
+
} else {
|
213 |
+
MessagePlugin.success('所有账号刷新成功');
|
214 |
+
}
|
215 |
+
} catch (error: any) {
|
216 |
+
MessagePlugin.error(`批量刷新失败: ${error.message}`);
|
217 |
+
} finally {
|
218 |
+
loading.value = false;
|
219 |
+
}
|
220 |
+
};
|
221 |
+
|
222 |
+
const handleRefreshPage = async () => {
|
223 |
+
try {
|
224 |
+
loading.value = true;
|
225 |
+
const emails = processedData.value.map(account => account.email);
|
226 |
+
const result = await mailApi.refreshAuthBatch(emails);
|
227 |
+
|
228 |
+
const successes = result.filter((r:any) => r.success).length;
|
229 |
+
const failures = result.length - successes;
|
230 |
+
|
231 |
+
if (failures > 0) {
|
232 |
+
MessagePlugin.warning(`刷新完成: ${successes}个成功, ${failures}个失败`);
|
233 |
+
} else {
|
234 |
+
MessagePlugin.success('当前页账号刷新成功');
|
235 |
+
}
|
236 |
+
} catch (error: any) {
|
237 |
+
MessagePlugin.error(`批量刷新失败: ${error.message}`);
|
238 |
+
} finally {
|
239 |
+
loading.value = false;
|
240 |
+
}
|
241 |
+
};
|
242 |
+
|
243 |
+
// 添加查看认证状态处理函数
|
244 |
+
const handleCheckAuth = async (row: Account) => {
|
245 |
+
try {
|
246 |
+
loading.value = true;
|
247 |
+
const result = await mailApi.getAuthStatus(row.email);
|
248 |
+
if (result.authenticated) {
|
249 |
+
MessagePlugin.success(`认证状态: ${result.message}\n认证时间: ${new Date(result.timestamp).toLocaleString()}`);
|
250 |
+
} else {
|
251 |
+
MessagePlugin.warning(`认证状态: ${result.message}`);
|
252 |
+
}
|
253 |
+
} catch (error: any) {
|
254 |
+
MessagePlugin.error(`检查认证失败: ${error.message}`);
|
255 |
+
} finally {
|
256 |
+
loading.value = false;
|
257 |
+
}
|
258 |
+
};
|
259 |
+
|
260 |
|
261 |
</script>
|
262 |
|
|
|
264 |
<div class="w-full h-full flex flex-col p-2 md:p-5 gap-2 md:gap-5">
|
265 |
<div class="flex flex-col md:flex-row md:items-center md:justify-between">
|
266 |
<div class="flex flex-col md:flex-row md:items-center gap-4">
|
267 |
+
<t-button @click="handleRefreshAll">刷新全部</t-button>
|
268 |
+
<t-button @click="handleRefreshPage">刷新本页</t-button>
|
269 |
<t-input v-model="userSearch" class="w-full md:w-40" placeholder="搜索用户" clearable />
|
270 |
</div>
|
271 |
</div>
|
|
|
276 |
<template #action="{ row }">
|
277 |
<!-- 桌面端显示 -->
|
278 |
<div class="hidden md:flex flex-wrap gap-2">
|
279 |
+
<t-button size="small" variant="outline" @click="handleCheckAuth(row)">查看认证</t-button>
|
280 |
<t-button size="small" variant="outline" @click="handleRefreshAuth(row)">刷新认证</t-button>
|
281 |
<t-button size="small" variant="outline" @click="handleLatestMails(row)">最新邮件</t-button>
|
282 |
<t-button size="small" variant="outline" @click="handleAllMails(row)">所有邮件</t-button>
|