feat: add Docker deployment and Microsoft OAuth support

This commit is contained in:
2026-04-18 14:02:24 +08:00
parent 6704cfb418
commit e69c0dd226
53 changed files with 5139 additions and 4029 deletions

View File

@@ -1,109 +1,111 @@
const TOKEN_KEY = 'mengpost_token'
const EVT = 'mengpost-token'
/** 生产环境前后端分离时设 VITE_API_URL, 如 http://127.0.0.1:8787 */
const base = (import.meta.env.VITE_API_URL || '').replace(/\/$/, '')
export function apiUrl(path) {
if (!path.startsWith('/')) path = '/' + path
return base ? base + path : path
}
export const tokenStore = {
get: () => localStorage.getItem(TOKEN_KEY) || '',
set: (t) => {
localStorage.setItem(TOKEN_KEY, t)
window.dispatchEvent(new Event(EVT))
},
clear: () => {
localStorage.removeItem(TOKEN_KEY)
window.dispatchEvent(new Event(EVT))
},
subscribe: (fn) => {
window.addEventListener(EVT, fn)
return () => window.removeEventListener(EVT, fn)
},
}
function parseJsonSafe(txt) {
if (!txt || !txt.trim()) return null
try {
return JSON.parse(txt)
} catch {
return { error: txt.length > 120 ? txt.slice(0, 120) + '…' : txt }
}
}
async function request(method, path, body) {
const headers = { 'Content-Type': 'application/json' }
const t = tokenStore.get()
if (t) headers['X-Auth-Token'] = t
let res
try {
res = await fetch(apiUrl(path), {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
})
} catch (e) {
throw new Error(
e.message?.includes('Failed to fetch')
? '无法连接后端:请确认已启动 mengpost-backend (默认 8787),且本页通过 Vite 开发服务器打开或已配置 VITE_API_URL'
: e.message,
)
}
const txt = await res.text()
const data = parseJsonSafe(txt)
if (!res.ok) {
const msg = (data && data.error) || res.statusText || '请求失败'
throw new Error(msg)
}
return data
}
/** 列表接口在空 body / SW 缓存异常时可能得到 null统一成数组 */
function asArray(v) {
return Array.isArray(v) ? v : []
}
/** 校验 token 时不带 X-Auth-Token避免旧 token 干扰 */
export async function verifyToken(token) {
let res
try {
res = await fetch(apiUrl('/api/auth/verify'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
})
} catch (e) {
throw new Error(
e.message?.includes('Failed to fetch')
? '无法连接后端:请先运行 dev.bat / dev.sh 启动后端,或设置 VITE_API_URL'
: e.message,
)
}
const txt = await res.text()
const data = parseJsonSafe(txt)
if (!res.ok) {
if (data && data.ok === false) return { ok: false }
throw new Error((data && data.error) || res.statusText || '验证失败')
}
return data
}
export const api = {
listAccounts: () => request('GET', '/api/accounts').then(asArray),
createAccount: (a) => request('POST', '/api/accounts', a),
updateAccount: (id, a) => request('PUT', `/api/accounts/${id}`, a),
deleteAccount: (id) => request('DELETE', `/api/accounts/${id}`),
listMailboxes: (id) => request('GET', `/api/accounts/${id}/mailboxes`).then(asArray),
listMessages: (id, mailbox = 'INBOX', limit = 30) =>
request('GET', `/api/accounts/${id}/messages?mailbox=${encodeURIComponent(mailbox)}&limit=${limit}`).then(
asArray,
),
getMessage: (id, uid, mailbox = 'INBOX') =>
request('GET', `/api/accounts/${id}/messages/${uid}?mailbox=${encodeURIComponent(mailbox)}`),
send: (id, body) => request('POST', `/api/accounts/${id}/send`, body),
listSent: (id, limit = 50) =>
request('GET', `/api/accounts/${id}/sent?limit=${limit}`).then(asArray),
}
const TOKEN_KEY = 'mengpost_token'
const EVT = 'mengpost-token'
/** 生产环境前后端分离时设 VITE_API_URL, 如 http://127.0.0.1:8787 */
const base = (import.meta.env.VITE_API_URL || '').replace(/\/$/, '')
export function apiUrl(path) {
if (!path.startsWith('/')) path = '/' + path
return base ? base + path : path
}
export const tokenStore = {
get: () => localStorage.getItem(TOKEN_KEY) || '',
set: (t) => {
localStorage.setItem(TOKEN_KEY, t)
window.dispatchEvent(new Event(EVT))
},
clear: () => {
localStorage.removeItem(TOKEN_KEY)
window.dispatchEvent(new Event(EVT))
},
subscribe: (fn) => {
window.addEventListener(EVT, fn)
return () => window.removeEventListener(EVT, fn)
},
}
function parseJsonSafe(txt) {
if (!txt || !txt.trim()) return null
try {
return JSON.parse(txt)
} catch {
return { error: txt.length > 120 ? txt.slice(0, 120) + '…' : txt }
}
}
async function request(method, path, body) {
const headers = { 'Content-Type': 'application/json' }
const t = tokenStore.get()
if (t) headers['X-Auth-Token'] = t
let res
try {
res = await fetch(apiUrl(path), {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
})
} catch (e) {
throw new Error(
e.message?.includes('Failed to fetch')
? '无法连接后端:请确认已启动 mengpost-backend (默认 8787),且本页通过 Vite 开发服务器打开或已配置 VITE_API_URL'
: e.message,
)
}
const txt = await res.text()
const data = parseJsonSafe(txt)
if (!res.ok) {
const msg = (data && data.error) || res.statusText || '请求失败'
throw new Error(msg)
}
return data
}
/** 列表接口在空 body / SW 缓存异常时可能得到 null统一成数组 */
function asArray(v) {
return Array.isArray(v) ? v : []
}
/** 校验 token 时不带 X-Auth-Token避免旧 token 干扰 */
export async function verifyToken(token) {
let res
try {
res = await fetch(apiUrl('/api/auth/verify'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
})
} catch (e) {
throw new Error(
e.message?.includes('Failed to fetch')
? '无法连接后端:请先运行 dev.bat / dev.sh 启动后端,或设置 VITE_API_URL'
: e.message,
)
}
const txt = await res.text()
const data = parseJsonSafe(txt)
if (!res.ok) {
if (data && data.ok === false) return { ok: false }
throw new Error((data && data.error) || res.statusText || '验证失败')
}
return data
}
export const api = {
listAccounts: () => request('GET', '/api/accounts').then(asArray),
createAccount: (a) => request('POST', '/api/accounts', a),
updateAccount: (id, a) => request('PUT', `/api/accounts/${id}`, a),
deleteAccount: (id) => request('DELETE', `/api/accounts/${id}`),
listMailboxes: (id) => request('GET', `/api/accounts/${id}/mailboxes`).then(asArray),
listMessages: (id, mailbox = 'INBOX', limit = 30) =>
request('GET', `/api/accounts/${id}/messages?mailbox=${encodeURIComponent(mailbox)}&limit=${limit}`).then(
asArray,
),
getMessage: (id, uid, mailbox = 'INBOX') =>
request('GET', `/api/accounts/${id}/messages/${uid}?mailbox=${encodeURIComponent(mailbox)}`),
send: (id, body) => request('POST', `/api/accounts/${id}/send`, body),
listSent: (id, limit = 50) =>
request('GET', `/api/accounts/${id}/sent?limit=${limit}`).then(asArray),
microsoftOAuthStart: () => request('GET', '/api/oauth/microsoft/start'),
microsoftOAuthFinish: (state) => request('POST', '/api/oauth/microsoft/finish', { state }),
}