Initial commit: WpywMail 桌面客户端:Node 零依赖本地服务 + React 19 / shadcn-ui 界面,支持收发信、注册、找回密码、会话与账号管理
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 邮件服务器「账号接口」客户端(对接服务端 v2.2.0 的账号体系)。
|
||||
*
|
||||
* 走的是服务器上**只放账号类接口**的公网 HTTPS 入口(默认 https://mail.example.com:9443):
|
||||
* 注册、邮箱验证码、找回密码、登录换 token、改密码、会话与资料。
|
||||
* 邮件读写(读信/发件/队列)**不经过这里**,仍然走 IMAP 993 / SMTP 587 ——
|
||||
* 那个入口对邮件与管理接口一律返回 404(可在服务器上用 /api/messages 验证)。
|
||||
*
|
||||
* 零依赖:用 Node 内置的全局 fetch(Node 18+)。
|
||||
*/
|
||||
|
||||
const DEFAULT_API_BASE = 'https://mail.example.com:9443';
|
||||
|
||||
/** 服务器给的错误信息本身就是中文说明,直接透传给 UI;只补上网络层失败的可读提示。 */
|
||||
async function call(base, path, options = {}) {
|
||||
const { method = 'GET', body, token, timeoutMs = 20000 } = options;
|
||||
const root = String(base || DEFAULT_API_BASE).replace(/\/+$/, '');
|
||||
const url = root + path;
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const headers = { 'Content-Type': 'application/json; charset=utf-8' };
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
const text = await res.text();
|
||||
let data = null;
|
||||
try {
|
||||
data = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
data = { error: text };
|
||||
}
|
||||
if (!res.ok) {
|
||||
const err = new Error((data && data.error) || `账号接口返回 HTTP ${res.status}`);
|
||||
err.status = res.status;
|
||||
err.data = data;
|
||||
throw err;
|
||||
}
|
||||
return data;
|
||||
} catch (err) {
|
||||
if (err && err.status) throw err;
|
||||
if (err && err.name === 'AbortError') throw new Error(`账号接口超时:${url}`);
|
||||
throw new Error(`连不上账号接口 ${url}:${err && err.message}`);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
function createAccountApi(base) {
|
||||
const apiBase = String(base || DEFAULT_API_BASE).replace(/\/+$/, '');
|
||||
return {
|
||||
apiBase,
|
||||
policy: () => call(apiBase, '/api/auth/policy'),
|
||||
register: (body) => call(apiBase, '/api/register', { method: 'POST', body }),
|
||||
verifyRegistration: (body) => call(apiBase, '/api/register/verify', { method: 'POST', body }),
|
||||
resendCode: (body) => call(apiBase, '/api/register/resend', { method: 'POST', body }),
|
||||
forgot: (email) => call(apiBase, '/api/auth/forgot', { method: 'POST', body: { email } }),
|
||||
reset: (body) => call(apiBase, '/api/auth/reset', { method: 'POST', body }),
|
||||
login: (email, password) => call(apiBase, '/api/login', { method: 'POST', body: { email, password } }),
|
||||
logout: (token) => call(apiBase, '/api/logout', { method: 'POST', body: {}, token }),
|
||||
changePassword: (token, body) => call(apiBase, '/api/account/password', { method: 'POST', body, token }),
|
||||
profile: (token) => call(apiBase, '/api/me', { token }),
|
||||
updateProfile: (token, displayName) =>
|
||||
call(apiBase, '/api/account/profile', { method: 'PATCH', body: { displayName }, token }),
|
||||
sessions: (token) => call(apiBase, '/api/account/sessions', { token }),
|
||||
revokeSessions: (token, body) => call(apiBase, '/api/account/sessions/revoke', { method: 'POST', body, token }),
|
||||
audit: (token, limit = 30) => call(apiBase, `/api/account/audit?limit=${limit}`, { token }),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { DEFAULT_API_BASE, createAccountApi };
|
||||
Reference in New Issue
Block a user