feat: admin search mailbox && fix generateName multi dot && user jwt exp in 30 days && UI globalTabplacement && useSideMargin (#214)

* fix: generateName multi dot && user jwt exp in 30 days

* feat: support admin search mailbox

* fix: DELETE mail bug(should be raw_mails)

* feat: UI add globalTabplacement

* feat: UI add useSideMargin option
This commit is contained in:
Dream Hunter
2024-05-09 18:43:09 +08:00
committed by GitHub
parent 1fa56dfe98
commit b7308587c6
18 changed files with 231 additions and 147 deletions
+5
View File
@@ -20,6 +20,11 @@
### function changs ### function changs
- 增加用户注册功能,可绑定邮箱地址,绑定后可自动获取邮箱JWT凭证 - 增加用户注册功能,可绑定邮箱地址,绑定后可自动获取邮箱JWT凭证
- 增加默认以文本显示邮件,文本和HTML邮箱显示方式切换按钮
- 修复 `BUG` 随机生成的邮箱名字不合法 #211
- `admin` 邮件页面支持邮件内容搜索 #210
- 修复删除地址时邮件未删除的BUG #213
- UI 增加全局标签页位置配置, 侧边距配置
## v0.3.3 ## v0.3.3
+5 -4
View File
@@ -8,10 +8,11 @@ import Header from './views/Header.vue';
import Footer from './views/Footer.vue'; import Footer from './views/Footer.vue';
const { localeCache, isDark, loading } = useGlobalState() const { localeCache, isDark, loading, useSideMargin } = useGlobalState()
const theme = computed(() => isDark.value ? darkTheme : null) const theme = computed(() => isDark.value ? darkTheme : null)
const localeConfig = computed(() => localeCache.value == 'zh' ? zhCN : null) const localeConfig = computed(() => localeCache.value == 'zh' ? zhCN : null)
const isMobile = useIsMobile() const isMobile = useIsMobile()
const showSideMargin = computed(() => !isMobile.value && !useSideMargin.value);
const { locale } = useI18n({ const { locale } = useI18n({
useScope: 'global', useScope: 'global',
@@ -39,8 +40,8 @@ onMounted(async () => {
<n-spin description="loading..." :show="loading"> <n-spin description="loading..." :show="loading">
<n-message-provider> <n-message-provider>
<n-grid x-gap="12" :cols="12"> <n-grid x-gap="12" :cols="12">
<n-gi v-if="!isMobile" span="1"></n-gi> <n-gi v-if="!showSideMargin" span="1"></n-gi>
<n-gi :span="isMobile ? 12 : 10"> <n-gi :span="showSideMargin ? 12 : 10">
<div class="main"> <div class="main">
<n-space vertical> <n-space vertical>
<n-layout style="min-height: 80vh;"> <n-layout style="min-height: 80vh;">
@@ -51,7 +52,7 @@ onMounted(async () => {
</n-space> </n-space>
</div> </div>
</n-gi> </n-gi>
<n-gi v-if="!isMobile" span="1"></n-gi> <n-gi v-if="!showSideMargin" span="1"></n-gi>
</n-grid> </n-grid>
<n-back-top /> <n-back-top />
</n-message-provider> </n-message-provider>
+1 -1
View File
@@ -10,7 +10,7 @@ const {
const instance = axios.create({ const instance = axios.create({
baseURL: API_BASE, baseURL: API_BASE,
timeout: 10000 timeout: 30000
}); });
const apiFetch = async (path, options = {}) => { const apiFetch = async (path, options = {}) => {
+4
View File
@@ -54,6 +54,8 @@ export const useGlobalState = createGlobalState(
const userJwt = useStorage('userJwt', ''); const userJwt = useStorage('userJwt', '');
const userTab = useStorage('userTab', 'user_settings'); const userTab = useStorage('userTab', 'user_settings');
const indexTab = useStorage('indexTab', 'mailbox'); const indexTab = useStorage('indexTab', 'mailbox');
const globalTabplacement = useStorage('globalTabplacement', 'top');
const useSideMargin = useStorage('useSideMargin', true);
const userOpenSettings = ref({ const userOpenSettings = ref({
enable: false, enable: false,
enableMailVerify: false, enableMailVerify: false,
@@ -91,6 +93,8 @@ export const useGlobalState = createGlobalState(
indexTab, indexTab,
userOpenSettings, userOpenSettings,
userSettings, userSettings,
globalTabplacement,
useSideMargin,
} }
}, },
) )
+13 -3
View File
@@ -15,9 +15,10 @@ import UserSettings from './admin/UserSettings.vue';
import Mails from './admin/Mails.vue'; import Mails from './admin/Mails.vue';
import MailsUnknow from './admin/MailsUnknow.vue'; import MailsUnknow from './admin/MailsUnknow.vue';
import Maintenance from './admin/Maintenance.vue'; import Maintenance from './admin/Maintenance.vue';
import Appearance from './common/Appearance.vue';
const { const {
localeCache, adminAuth, showAdminAuth, adminTab, loading localeCache, adminAuth, showAdminAuth, adminTab, loading, globalTabplacement
} = useGlobalState() } = useGlobalState()
const message = useMessage() const message = useMessage()
@@ -44,7 +45,9 @@ const { t } = useI18n({
unknow: 'Mails with unknow receiver', unknow: 'Mails with unknow receiver',
senderAccess: 'Sender Access Control', senderAccess: 'Sender Access Control',
sendBox: 'Send Box', sendBox: 'Send Box',
statistics: 'Statistics',
maintenance: 'Maintenance', maintenance: 'Maintenance',
appearance: 'Appearance',
ok: 'OK', ok: 'OK',
}, },
zh: { zh: {
@@ -59,7 +62,9 @@ const { t } = useI18n({
unknow: '无收件人邮件', unknow: '无收件人邮件',
senderAccess: '发件权限控制', senderAccess: '发件权限控制',
sendBox: '发件箱', sendBox: '发件箱',
statistics: '统计',
maintenance: '维护', maintenance: '维护',
appearance: '外观',
ok: '确定', ok: '确定',
} }
} }
@@ -85,8 +90,7 @@ onMounted(async () => {
</n-button> </n-button>
</template> </template>
</n-modal> </n-modal>
<Statistics /> <n-tabs type="card" v-model:value="adminTab" :placement="globalTabplacement">
<n-tabs type="card" v-model:value="adminTab">
<n-tab-pane name="account" :tab="t('account')"> <n-tab-pane name="account" :tab="t('account')">
<Account /> <Account />
</n-tab-pane> </n-tab-pane>
@@ -114,9 +118,15 @@ onMounted(async () => {
<n-tab-pane name="sendBox" :tab="t('sendBox')"> <n-tab-pane name="sendBox" :tab="t('sendBox')">
<SendBox /> <SendBox />
</n-tab-pane> </n-tab-pane>
<n-tab-pane name="statistics" :tab="t('statistics')">
<Statistics />
</n-tab-pane>
<n-tab-pane name="maintenance" :tab="t('maintenance')"> <n-tab-pane name="maintenance" :tab="t('maintenance')">
<Maintenance /> <Maintenance />
</n-tab-pane> </n-tab-pane>
<n-tab-pane name="appearance" :tab="t('appearance')">
<Appearance />
</n-tab-pane>
</n-tabs> </n-tabs>
</div> </div>
</template> </template>
+2 -2
View File
@@ -11,7 +11,7 @@ import SendBox from './index/SendBox.vue';
import SendMail from './index/SendMail.vue'; import SendMail from './index/SendMail.vue';
import AccountSettings from './index/AccountSettings.vue'; import AccountSettings from './index/AccountSettings.vue';
const { localeCache, settings, openSettings, indexTab } = useGlobalState() const { localeCache, settings, openSettings, indexTab, globalTabplacement } = useGlobalState()
const { t } = useI18n({ const { t } = useI18n({
locale: localeCache.value || 'zh', locale: localeCache.value || 'zh',
@@ -45,7 +45,7 @@ const deleteMail = async (curMailId) => {
<template> <template>
<div> <div>
<AddressBar /> <AddressBar />
<n-tabs v-if="settings.address" type="card" v-model:value="indexTab"> <n-tabs v-if="settings.address" type="card" v-model:value="indexTab" :placement="globalTabplacement">
<n-tab-pane name="mailbox" :tab="t('mailbox')"> <n-tab-pane name="mailbox" :tab="t('mailbox')">
<MailBox :showEMailTo="false" :showReply="true" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail" <MailBox :showEMailTo="false" :showReply="true" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
:fetchMailData="fetchMailData" :deleteMail="deleteMail" /> :fetchMailData="fetchMailData" :deleteMail="deleteMail" />
+2 -2
View File
@@ -9,7 +9,7 @@ import UserBar from './user/UserBar.vue';
import BindAddress from './user/BindAddress.vue'; import BindAddress from './user/BindAddress.vue';
const { const {
localeCache, userTab, userOpenSettings, userSettings localeCache, userTab, globalTabplacement, userSettings
} = useGlobalState() } = useGlobalState()
const { t } = useI18n({ const { t } = useI18n({
@@ -33,7 +33,7 @@ const { t } = useI18n({
<template> <template>
<div> <div>
<UserBar /> <UserBar />
<n-tabs v-if="userSettings.user_email" type="card" v-model:value="userTab"> <n-tabs v-if="userSettings.user_email" type="card" v-model:value="userTab" :placement="globalTabplacement">
<n-tab-pane name="address_management" :tab="t('address_management')"> <n-tab-pane name="address_management" :tab="t('address_management')">
<AddressMangement /> <AddressMangement />
</n-tab-pane> </n-tab-pane>
+14 -4
View File
@@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue'; import { ref, onMounted, watch } from 'vue';
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useGlobalState } from '../../store' import { useGlobalState } from '../../store'
@@ -16,19 +16,27 @@ const { t } = useI18n({
messages: { messages: {
en: { en: {
addressQueryTip: 'Leave blank to query all addresses', addressQueryTip: 'Leave blank to query all addresses',
keywordQueryTip: 'Leave blank to not query by keyword',
query: 'Query', query: 'Query',
}, },
zh: { zh: {
addressQueryTip: '留空查询所有地址', addressQueryTip: '留空查询所有地址',
keywordQueryTip: '留空不按关键字查询',
query: '查询', query: '查询',
} }
} }
}); });
const mailBoxKey = ref("") const mailBoxKey = ref("")
const mailKeyword = ref("")
const queryAddress = () => { watch([adminMailTabAddress, mailKeyword], () => {
mailBoxKey.value = adminMailTabAddress.value; adminMailTabAddress.value = adminMailTabAddress.value.trim();
mailKeyword.value = mailKeyword.value.trim();
});
const queryMail = () => {
mailBoxKey.value = Date.now();
} }
const fetchMailData = async (limit, offset) => { const fetchMailData = async (limit, offset) => {
@@ -37,6 +45,7 @@ const fetchMailData = async (limit, offset) => {
+ `?limit=${limit}` + `?limit=${limit}`
+ `&offset=${offset}` + `&offset=${offset}`
+ (adminMailTabAddress.value ? `&address=${adminMailTabAddress.value}` : '') + (adminMailTabAddress.value ? `&address=${adminMailTabAddress.value}` : '')
+ (mailKeyword.value ? `&keyword=${mailKeyword.value}` : '')
); );
} }
@@ -52,7 +61,8 @@ onMounted(async () => {
<div> <div>
<n-input-group> <n-input-group>
<n-input v-model:value="adminMailTabAddress" :placeholder="t('addressQueryTip')" /> <n-input v-model:value="adminMailTabAddress" :placeholder="t('addressQueryTip')" />
<n-button @click="queryAddress" type="primary" tertiary> <n-input v-model:value="mailKeyword" :placeholder="t('keywordQueryTip')" />
<n-button @click="queryMail" type="primary" tertiary>
{{ t('query') }} {{ t('query') }}
</n-button> </n-button>
</n-input-group> </n-input-group>
+9
View File
@@ -32,6 +32,7 @@ const { t } = useI18n({
autoCleanup: "Auto cleanup", autoCleanup: "Auto cleanup",
cleanupSuccess: "Cleanup success", cleanupSuccess: "Cleanup success",
save: "Save", save: "Save",
cronTip: "Enable cron cleanup, need to configure [crons] in worker, please refer to the document",
}, },
zh: { zh: {
tip: '请输入清理天数', tip: '请输入清理天数',
@@ -43,6 +44,7 @@ const { t } = useI18n({
cleanupSuccess: "清理成功", cleanupSuccess: "清理成功",
cleanupNow: "立即清理", cleanupNow: "立即清理",
save: "保存", save: "保存",
cronTip: "启用定时清理, 需在 worker 配置 [crons] 参数, 请参考文档",
} }
} }
}); });
@@ -93,6 +95,9 @@ onMounted(async () => {
<template> <template>
<div class="center"> <div class="center">
<n-card> <n-card>
<n-alert :show-icon="false">
<span>{{ t('cronTip') }}</span>
</n-alert>
<n-form :model="cleanupModel"> <n-form :model="cleanupModel">
<n-form-item-row :label="t('mailBoxLabel')"> <n-form-item-row :label="t('mailBoxLabel')">
<n-checkbox v-model:checked="cleanupModel.enableMailsAutoCleanup"> <n-checkbox v-model:checked="cleanupModel.enableMailsAutoCleanup">
@@ -162,6 +167,10 @@ onMounted(async () => {
justify-content: center; justify-content: center;
} }
.n-alert {
margin-bottom: 20px;
}
.item { .item {
display: flex; display: flex;
margin: 10px; margin: 10px;
+82
View File
@@ -0,0 +1,82 @@
<script setup>
import { useI18n } from 'vue-i18n'
import { useGlobalState } from '../../store'
const {
localeCache, mailboxSplitSize, useIframeShowMail, preferShowTextMail,
globalTabplacement, useSideMargin
} = useGlobalState()
const { t } = useI18n({
locale: localeCache.value || 'zh',
messages: {
en: {
mailboxSplitSize: 'Mailbox Split Size',
useIframeShowMail: 'Use iframe Show HTML Mail',
preferShowTextMail: 'Display text Mail by default',
useSideMargin: 'Turn on the side margins on the left and right sides of the page',
globalTabplacement: 'Global Tab Placement',
left: 'left',
top: 'top',
right: 'right',
bottom: 'bottom',
},
zh: {
mailboxSplitSize: '邮箱界面分栏大小',
preferShowTextMail: '默认以文本显示邮件',
useIframeShowMail: '使用iframe显示HTML邮件',
globalTabplacement: '全局选项卡位置',
useSideMargin: '开启页面左右两侧侧边距',
left: '左侧',
top: '顶部',
right: '右侧',
bottom: '底部',
}
}
});
</script>
<template>
<div class="center">
<n-card>
<n-form-item-row :label="t('mailboxSplitSize')">
<n-slider v-model:value="mailboxSplitSize" :min="0.25" :max="0.75" :step="0.01" :marks="{
0.25: '0.25',
0.5: '0.5',
0.75: '0.75'
}" />
</n-form-item-row>
<n-form-item-row :label="t('preferShowTextMail')">
<n-switch v-model:value="preferShowTextMail" :round="false" />
</n-form-item-row>
<n-form-item-row :label="t('useIframeShowMail')">
<n-switch v-model:value="useIframeShowMail" :round="false" />
</n-form-item-row>
<n-form-item-row :label="t('useSideMargin')">
<n-switch v-model:value="useSideMargin" :round="false" />
</n-form-item-row>
<n-form-item-row :label="t('globalTabplacement')">
<n-radio-group v-model:value="globalTabplacement">
<n-radio-button value="top" :label="t('top')" />
<n-radio-button value="left" :label="t('left')" />
<n-radio-button value="right" :label="t('right')" />
<n-radio-button value="bottom" :label="t('bottom')" />
</n-radio-group>
</n-form-item-row>
</n-card>
</div>
</template>
<style scoped>
.center {
display: flex;
justify-content: center;
}
.n-card {
max-width: 800px;
text-align: left;
}
</style>
+3 -23
View File
@@ -5,10 +5,10 @@ import { useRouter } from 'vue-router'
import { useGlobalState } from '../../store' import { useGlobalState } from '../../store'
import { api } from '../../api' import { api } from '../../api'
import Appearance from '../common/Appearance.vue'
const { const {
jwt, localeCache, settings, showAddressCredential, loading, jwt, localeCache, settings, showAddressCredential, loading
mailboxSplitSize, useIframeShowMail, preferShowTextMail
} = useGlobalState() } = useGlobalState()
const router = useRouter() const router = useRouter()
const message = useMessage() const message = useMessage()
@@ -19,9 +19,6 @@ const { t } = useI18n({
locale: localeCache.value || 'zh', locale: localeCache.value || 'zh',
messages: { messages: {
en: { en: {
mailboxSplitSize: 'Mailbox Split Size',
useIframeShowMail: 'Use iframe Show HTML Mail',
preferShowTextMail: 'Display text Mail by default',
logout: "Logout", logout: "Logout",
delteAccount: "Delete Account", delteAccount: "Delete Account",
showAddressCredential: 'Show Address Credential', showAddressCredential: 'Show Address Credential',
@@ -30,9 +27,6 @@ const { t } = useI18n({
delteAccountConfirm: "Are you sure to delete your account and all emails for this account?", delteAccountConfirm: "Are you sure to delete your account and all emails for this account?",
}, },
zh: { zh: {
mailboxSplitSize: '邮箱界面分栏大小',
preferShowTextMail: '默认以文本显示邮件',
useIframeShowMail: '使用iframe显示HTML邮件',
logout: '退出登录', logout: '退出登录',
delteAccount: "删除账户", delteAccount: "删除账户",
showAddressCredential: '查看邮箱地址凭证', showAddressCredential: '查看邮箱地址凭证',
@@ -66,21 +60,7 @@ const deleteAccount = async () => {
<template> <template>
<div class="center" v-if="settings.address"> <div class="center" v-if="settings.address">
<n-card> <n-card>
<n-card> <Appearance />
<n-form-item-row :label="t('mailboxSplitSize')">
<n-slider v-model:value="mailboxSplitSize" :min="0.25" :max="0.75" :step="0.01" :marks="{
0.25: '0.25',
0.5: '0.5',
0.75: '0.75'
}" />
</n-form-item-row>
<n-form-item-row :label="t('preferShowTextMail')">
<n-switch v-model:value="preferShowTextMail" :round="false" />
</n-form-item-row>
<n-form-item-row :label="t('useIframeShowMail')">
<n-switch v-model:value="useIframeShowMail" :round="false" />
</n-form-item-row>
</n-card>
<n-button @click="showAddressCredential = true" type="primary" secondary block strong> <n-button @click="showAddressCredential = true" type="primary" secondary block strong>
{{ t('showAddressCredential') }} {{ t('showAddressCredential') }}
</n-button> </n-button>
+3 -2
View File
@@ -83,9 +83,10 @@ const generateName = async () => {
try { try {
generateNameLoading.value = true; generateNameLoading.value = true;
const { faker } = await import('https://esm.sh/@faker-js/faker'); const { faker } = await import('https://esm.sh/@faker-js/faker');
emailName.value = faker.person emailName.value = faker.internet.email()
.fullName() .split('@')[0]
.replace(/\s+/g, '.') .replace(/\s+/g, '.')
.replace(/\.{2,}/g, '.')
.replace(/[^a-zA-Z0-9.]/g, '') .replace(/[^a-zA-Z0-9.]/g, '')
.toLowerCase(); .toLowerCase();
} catch (error) { } catch (error) {
+44 -100
View File
@@ -1,7 +1,7 @@
import { Hono } from 'hono' import { Hono } from 'hono'
import { Jwt } from 'hono/utils/jwt' import { Jwt } from 'hono/utils/jwt'
import { sendAdminInternalMail, getJsonSetting, saveSetting } from '../utils' import { sendAdminInternalMail, getJsonSetting, saveSetting } from '../utils'
import { newAddress } from '../common' import { newAddress, handleListQuery } from '../common'
import { CONSTANTS } from '../constants' import { CONSTANTS } from '../constants'
import cleanup_api from './cleanup_api' import cleanup_api from './cleanup_api'
import admin_user_api from './admin_user_api' import admin_user_api from './admin_user_api'
@@ -74,7 +74,7 @@ api.delete('/admin/delete_address/:id', async (c) => {
return c.text("Failed to delete address", 500) return c.text("Failed to delete address", 500)
} }
const { success: mailSuccess } = await c.env.DB.prepare( const { success: mailSuccess } = await c.env.DB.prepare(
`DELETE FROM mails WHERE address IN` `DELETE FROM raw_mails WHERE address IN`
+ ` (select name from address where id = ?) ` + ` (select name from address where id = ?) `
).bind(id).run(); ).bind(id).run();
if (!mailSuccess) { if (!mailSuccess) {
@@ -107,111 +107,58 @@ api.get('/admin/show_password/:id', async (c) => {
}) })
api.get('/admin/mails', async (c) => { api.get('/admin/mails', async (c) => {
const { address, limit, offset } = c.req.query(); const { address, limit, offset, keyword } = c.req.query();
if (!limit || limit < 0 || limit > 100) { if (address && keyword) {
return c.text("Invalid limit", 400) return await handleListQuery(c,
`SELECT * FROM raw_mails where address = ? and raw like ? `,
`SELECT count(*) as count FROM raw_mails where address = ? and raw like ? `,
[address, `%${keyword}%`], limit, offset
);
} else if (keyword) {
return await handleListQuery(c,
`SELECT * FROM raw_mails where raw like ? `,
`SELECT count(*) as count FROM raw_mails where raw like ? `,
[`%${keyword}%`], limit, offset
);
} else if (address) {
return await handleListQuery(c,
`SELECT * FROM raw_mails where address = ? `,
`SELECT count(*) as count FROM raw_mails where address = ? `,
[address], limit, offset
);
} else {
return await handleListQuery(c,
`SELECT * FROM raw_mails `,
`SELECT count(*) as count FROM raw_mails `,
[], limit, offset
);
} }
if (!offset || offset < 0) {
return c.text("Invalid offset", 400)
}
if (!address) {
const { results } = await c.env.DB.prepare(
`SELECT * FROM raw_mails order by id desc limit ? offset ?`
).bind(limit, offset).all();
let count = 0;
if (offset == 0) {
const { count: mailCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM raw_mails`
).first();
count = mailCount;
}
return c.json({
results: results,
count: count
})
}
const { results } = await c.env.DB.prepare(
`SELECT * FROM raw_mails where address = ? order by id desc limit ? offset ?`
).bind(address, limit, offset).all();
let count = 0;
if (offset == 0) {
const { count: mailCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM raw_mails where address = ? `
).bind(address).first();
count = mailCount;
}
return c.json({
results: results,
count: count
})
}); });
api.get('/admin/mails_unknow', async (c) => { api.get('/admin/mails_unknow', async (c) => {
const { limit, offset } = c.req.query(); const { limit, offset } = c.req.query();
if (!limit || limit < 0 || limit > 100) { return await handleListQuery(c,
return c.text("Invalid limit", 400) `SELECT * FROM raw_mails where address NOT IN (select name from address) `,
} `SELECT count(*) as count FROM raw_mails`
if (!offset || offset < 0) { + ` where address NOT IN (select name from address) `,
return c.text("Invalid offset", 400) [], limit, offset
} );
const { results } = await c.env.DB.prepare(`
SELECT * FROM raw_mails
where address NOT IN (select name from address)
order by id desc limit ? offset ? `
).bind(limit, offset).all();
let count = 0;
if (offset == 0) {
const { count: mailCount } = await c.env.DB.prepare(`
SELECT count(*) as count FROM raw_mails
where address NOT IN
(select name from address)`
).first();
count = mailCount;
}
return c.json({
results: results,
count: count
})
}); });
api.get('/admin/address_sender', async (c) => { api.get('/admin/address_sender', async (c) => {
const { address, limit, offset } = c.req.query(); const { address, limit, offset } = c.req.query();
if (!limit || limit < 0 || limit > 100) {
return c.text("Invalid limit", 400)
}
if (!offset || offset < 0) {
return c.text("Invalid offset", 400)
}
if (address) { if (address) {
const { results } = await c.env.DB.prepare( return await handleListQuery(c,
`SELECT * FROM address_sender where address = ? order by id desc limit ? offset ?` `SELECT * FROM address_sender where address = ? `,
).bind(address, limit, offset).all(); `SELECT count(*) as count FROM address_sender where address = ? `,
let count = 0; [address], limit, offset
if (offset == 0) { );
const { count: addressCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM address_sender where address = ?`
).bind(address).first();
count = addressCount;
}
return c.json({
results: results,
count: count
})
} }
const { results } = await c.env.DB.prepare( return await handleListQuery(c,
`SELECT * FROM address_sender order by id desc limit ? offset ? ` `SELECT * FROM address_sender `,
).bind(limit, offset).all(); `SELECT count(*) as count FROM address_sender `,
let count = 0; [], limit, offset
if (offset == 0) { );
const { count: addressCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM address_sender`
).first();
count = addressCount;
}
return c.json({
results: results,
count: count
})
}) })
api.post('/admin/address_sender', async (c) => { api.post('/admin/address_sender', async (c) => {
@@ -276,9 +223,6 @@ api.get('/admin/sendbox', async (c) => {
}) })
api.get('/admin/statistics', async (c) => { api.get('/admin/statistics', async (c) => {
const { count: mailCountV1 } = await c.env.DB.prepare(`
SELECT count(*) as count FROM mails`
).first();
const { count: mailCount } = await c.env.DB.prepare(` const { count: mailCount } = await c.env.DB.prepare(`
SELECT count(*) as count FROM raw_mails` SELECT count(*) as count FROM raw_mails`
).first(); ).first();
@@ -292,7 +236,7 @@ api.get('/admin/statistics', async (c) => {
SELECT count(*) as count FROM sendbox` SELECT count(*) as count FROM sendbox`
).first(); ).first();
return c.json({ return c.json({
mailCount: (mailCountV1 || 0) + (mailCount || 0), mailCount: mailCount,
userCount: addressCount, userCount: addressCount,
activeUserCount7days: activeUserCount7days, activeUserCount7days: activeUserCount7days,
sendMailCount: sendMailCount sendMailCount: sendMailCount
+29
View File
@@ -74,6 +74,7 @@ export const cleanup = async (c, cleanType, cleanDays) => {
case "address": case "address":
await c.env.DB.prepare(` await c.env.DB.prepare(`
DELETE FROM address WHERE updated_at < datetime('now', '-${cleanDays} day')` DELETE FROM address WHERE updated_at < datetime('now', '-${cleanDays} day')`
+ ` AND id NOT IN (SELECT address_id FROM users_address)`
).run(); ).run();
break; break;
case "sendbox": case "sendbox":
@@ -86,3 +87,31 @@ export const cleanup = async (c, cleanType, cleanDays) => {
} }
return true; return true;
} }
/**
*
* @param {*} c context
* @param {*} query @type {string} query
* @param {*} countQuery @type {string} countQuery
* @param {*} limit @type {number} limit
* @param {*} offset @type {number} offset
* @returns {Promise} Promise
*/
export const handleListQuery = async (
c, query, countQuery, params, limit, offset
) => {
if (!limit || limit < 0 || limit > 100) {
return c.text("Invalid limit", 400)
}
if (!offset || offset < 0) {
return c.text("Invalid offset", 400)
}
const resultsQuery = `${query} order by id desc limit ? offset ?`;
const { results } = await c.env.DB.prepare(resultsQuery).bind(
...params, limit, offset
).all();
const count = offset == 0 ? await c.env.DB.prepare(
countQuery
).bind(...params).first("count") : 0;
return c.json({ results, count });
}
+1 -1
View File
@@ -155,7 +155,7 @@ api.post('/api/send_mail', async (c) => {
api.post('/external/api/send_mail', async (c) => { api.post('/external/api/send_mail', async (c) => {
const { token } = await c.req.json(); const { token } = await c.req.json();
try { try {
const { address } = await Jwt.verify(token, c.env.JWT_SECRET); const { address } = await Jwt.verify(token, c.env.JWT_SECRET, "HS256");
if (!address) { if (!address) {
return c.text("No address", 400) return c.text("No address", 400)
} }
+1 -1
View File
@@ -162,7 +162,7 @@ api.delete('/api/delete_address', async (c) => {
return c.text("Failed to delete address", 500) return c.text("Failed to delete address", 500)
} }
const { success: mailSuccess } = await c.env.DB.prepare( const { success: mailSuccess } = await c.env.DB.prepare(
`DELETE FROM mails WHERE address = ? ` `DELETE FROM raw_mails WHERE address = ? `
).bind(address).run(); ).bind(address).run();
if (!mailSuccess) { if (!mailSuccess) {
return c.text("Failed to delete mails", 500) return c.text("Failed to delete mails", 500)
+4 -1
View File
@@ -132,7 +132,10 @@ export default {
// create jwt // create jwt
const jwt = await Jwt.sign({ const jwt = await Jwt.sign({
user_email: email, user_email: email,
user_id: user_id user_id: user_id,
// 30 days expire in seconds
exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
iat: Math.floor(Date.now() / 1000),
}, c.env.JWT_SECRET) }, c.env.JWT_SECRET)
return c.json({ return c.json({
jwt: jwt jwt: jwt
+9 -3
View File
@@ -52,7 +52,7 @@ app.use('/api/*', async (c, next) => {
await next(); await next();
return; return;
} }
return jwt({ secret: c.env.JWT_SECRET })(c, next); return jwt({ secret: c.env.JWT_SECRET, alg: "HS256" })(c, next);
}); });
// user_api auth // user_api auth
app.use('/user_api/*', async (c, next) => { app.use('/user_api/*', async (c, next) => {
@@ -67,7 +67,13 @@ app.use('/user_api/*', async (c, next) => {
} }
try { try {
const token = c.req.raw.headers.get("x-user-token"); const token = c.req.raw.headers.get("x-user-token");
const payload = await Jwt.verify(token, c.env.JWT_SECRET); const payload = await Jwt.verify(token, c.env.JWT_SECRET, "HS256");
// check expired
if (!payload.exp) return c.text("Invalid Token", 401);
// exp is in seconds
if (payload.exp < Math.floor(Date.now() / 1000)) {
return c.text("Token Expired", 401)
}
c.set("userPayload", payload); c.set("userPayload", payload);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@@ -76,7 +82,7 @@ app.use('/user_api/*', async (c, next) => {
if (c.req.path.startsWith('/user_api/bind_address') if (c.req.path.startsWith('/user_api/bind_address')
&& c.req.method === 'POST' && c.req.method === 'POST'
) { ) {
return jwt({ secret: c.env.JWT_SECRET })(c, next); return jwt({ secret: c.env.JWT_SECRET, alg: "HS256" })(c, next);
} }
await next(); await next();
}); });