新增权限域名限制和链接背景上传

This commit is contained in:
eoao
2025-07-26 23:11:00 +08:00
parent 0e86b93e0c
commit 12833cd901
36 changed files with 584 additions and 354 deletions

View File

@@ -161,9 +161,9 @@ After the update, run `https://your-project-domain/api/init/your-jwt-secret` to
## Support ## Support
**Buy me a cup of milk tea** <a href="https://support.skymail.ink">
<img width="170px" src="./doc/images/support.png" alt="">
<a href="https://afdian.com/a/eoao_"><img width="150" src="https://pic1.afdiancdn.com/static/img/welcome/button-sponsorme.png" alt=""></a> </a><br><br>
**Special Sponsors** **Special Sponsors**

View File

@@ -182,9 +182,10 @@ jwt_secret = "" #登录身份令牌的密钥,随便填一串字符串
## 赞助 ## 赞助
**请我喝一杯奶茶**
<a href="https://afdian.com/a/eoao_"><img width="150" src="https://pic1.afdiancdn.com/static/img/welcome/button-sponsorme.png" alt=""></a> <a href="https://support.skymail.ink" >
<img width="170px" src="./doc/images/support.png" alt="">
</a><br><br>
**特别赞助商** **特别赞助商**

BIN
doc/images/support.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -22,7 +22,6 @@
"pinia": "^3.0.2", "pinia": "^3.0.2",
"pinia-plugin-persistedstate": "^4.2.0", "pinia-plugin-persistedstate": "^4.2.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-cropper": "^1.1.4",
"vue-i18n": "^11.1.10", "vue-i18n": "^11.1.10",
"vue-router": "^4.5.0" "vue-router": "^4.5.0"
}, },
@@ -3842,12 +3841,6 @@
} }
} }
}, },
"node_modules/vue-cropper": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/vue-cropper/-/vue-cropper-1.1.4.tgz",
"integrity": "sha512-5m98vBsCEI9rbS4JxELxXidtAui3qNyTHLHg67Qbn7g8cg+E6LcnC+hh3SM/p94x6mFh6KRxT1ttnta+wCYqWA==",
"license": "ISC"
},
"node_modules/vue-i18n": { "node_modules/vue-i18n": {
"version": "11.1.10", "version": "11.1.10",
"resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-11.1.10.tgz", "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-11.1.10.tgz",

View File

@@ -24,7 +24,6 @@
"pinia": "^3.0.2", "pinia": "^3.0.2",
"pinia-plugin-persistedstate": "^4.2.0", "pinia-plugin-persistedstate": "^4.2.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-cropper": "^1.1.4",
"vue-i18n": "^11.1.10", "vue-i18n": "^11.1.10",
"vue-router": "^4.5.0" "vue-router": "^4.5.0"
}, },

View File

@@ -7,7 +7,7 @@
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import {useSettingStore} from "@/store/setting.js"; import {useSettingStore} from "@/store/setting.js";
const settingStore = useSettingStore() const settingStore = useSettingStore()
import zhCn from 'element-plus/es/locale/lang/zh-cn'; import zhCn from 'element-plus/es/locale/lang/zh-cn';;
const { locale } = useI18n() const { locale } = useI18n()
locale.value = settingStore.lang locale.value = settingStore.lang
</script> </script>

View File

@@ -96,7 +96,6 @@ const en = {
expand: 'Expand', expand: 'Expand',
collapse: 'Collapse', collapse: 'Collapse',
daily: 'Daily', daily: 'Daily',
emailBlock: '*Intercept all incoming emails to example.com',
searchRegKeyDesc: 'Enter invite code to search', searchRegKeyDesc: 'Enter invite code to search',
remainingUses: 'Remaining uses', remainingUses: 'Remaining uses',
exhausted: 'Exhausted', exhausted: 'Exhausted',
@@ -168,11 +167,11 @@ const en = {
addOsDomain: 'Add domain', addOsDomain: 'Add domain',
domainDesc: 'Domain', domainDesc: 'Domain',
addTurnstileSecret: 'Add turnstile secret', addTurnstileSecret: 'Add turnstile secret',
backgroundCropping: 'Background cropping', backgroundTitle: 'Change background',
tgBotDesc: 'Forward received emails to a Telegram bot', tgBotDesc: 'Forward received emails to a Telegram bot',
tgBotToken: 'Bot token', tgBotToken: 'Bot token',
toBotTokenDesc: 'Multiple user chat_ids, separated by commas', toBotTokenDesc: 'Multiple user chat_ids, separated by commas',
otherEmailDec: 'emails can be forwarded to external email, but must be verified via cloudflare.', otherEmailDesc: 'emails can be forwarded to external email, but must be verified via cloudflare.',
otherEmailInputDesc: 'Separate multiple email addresses with commas.', otherEmailInputDesc: 'Separate multiple email addresses with commas.',
forwardingRulesDesc: 'Rule-based forwarding only forwards emails received by the specified address.', forwardingRulesDesc: 'Rule-based forwarding only forwards emails received by the specified address.',
ruleEmailsInputDesc: 'Separate multiple email addresses with commas.', ruleEmailsInputDesc: 'Separate multiple email addresses with commas.',
@@ -246,7 +245,17 @@ const en = {
totalUserAccount: '{msg}', totalUserAccount: '{msg}',
sendBanned: 'Banned', sendBanned: 'Banned',
wrote: 'wrote', wrote: 'wrote',
support: 'Support' support: 'Support',
supportDesc: 'Buy me tea',
featDesc: 'Feature Description',
emailInterception: 'Email Interception',
emailInterceptionDesc: '*Intercept emails by blocking entire domain using example.com to prevent users from receiving emails from certain websites.',
availableDomains: 'Available domains',
availableDomainsDesc: 'Restrict users to email domains specified. Domains not on the approved list will be blocked from registration, adding email addresses, and sending/receiving emails. If left blank, all domains will be allowed by default.',
backgroundUrlDesc: 'Image URL',
localUpload: ' Local upload',
imageLink: 'Image URL',
backgroundWarning: 'Image file size affects website load speed.'
} }
export default en export default en

View File

@@ -96,7 +96,6 @@ const zh = {
expand: '展开', expand: '展开',
collapse: '收起', collapse: '收起',
daily: '每天', daily: '每天',
emailBlock: '输入邮箱拦截收件, 拦截所有前缀 *example.com',
searchRegKeyDesc: '输入注册码搜索', searchRegKeyDesc: '输入注册码搜索',
remainingUses: '剩余次数', remainingUses: '剩余次数',
exhausted: '已用尽', exhausted: '已用尽',
@@ -165,14 +164,14 @@ const zh = {
community: '交流', community: '交流',
changeTitle: '修改标题', changeTitle: '修改标题',
addResendTokenDesc: '输入内容添加,不填则删除', addResendTokenDesc: '输入内容添加,不填则删除',
addOsDomain: '添加访对象存储问域名', addOsDomain: '添加访问域名',
domainDesc: '域名', domainDesc: '域名',
addTurnstileSecret: '添加 Turnstile 密钥', addTurnstileSecret: '添加 Turnstile 密钥',
backgroundCropping: '背景截图', backgroundTitle: '设置背景',
tgBotDesc: '可以将接收的邮件转发到Tg机器人', tgBotDesc: '可以将接收的邮件转发到Tg机器人',
tgBotToken: '机器人 token', tgBotToken: '机器人 token',
toBotTokenDesc: '用户 chat_id 多个用,分开', toBotTokenDesc: '用户 chat_id 多个用,分开',
otherEmailDec: '可以将邮件转到其他服务商邮箱需要在cloudflare验证邮箱', otherEmailDesc: '可以将邮件转到其他服务商邮箱需要在cloudflare验证邮箱',
otherEmailInputDesc: '多个邮箱用, 分开', otherEmailInputDesc: '多个邮箱用, 分开',
forwardingRulesDesc: '规则转发只会转发设置邮箱所接收的邮件', forwardingRulesDesc: '规则转发只会转发设置邮箱所接收的邮件',
ruleEmailsInputDesc: '多个邮箱用, 分开', ruleEmailsInputDesc: '多个邮箱用, 分开',
@@ -246,6 +245,16 @@ const zh = {
totalUserAccount: '{msg} 个', totalUserAccount: '{msg} 个',
sendBanned: '已禁用', sendBanned: '已禁用',
wrote: '来信', wrote: '来信',
support: '赞助' support: '赞助',
supportDesc: '请我喝杯奶茶',
featDesc: '功能说明',
emailInterception: '邮件拦截',
emailInterceptionDesc: '拦截邮件, 要拦截整个域名输入 *example.com, 可用于禁止用户接收某些网站的邮件',
availableDomains: '可用域名',
availableDomainsDesc: '限制用户只能使用指定的域名邮箱,不在配置名单内的域名会被禁止使用注册添加邮箱,接收发送邮件等功能,留空默认允许可用所有域名',
backgroundUrlDesc: '在线图片链接',
localUpload: '本地上传',
imageLink: '图片链接',
backgroundWarning: '图片文件大小会影响网站加载速度',
} }
export default zh export default zh

View File

@@ -61,8 +61,8 @@
</el-menu-item> </el-menu-item>
<el-menu-item @click="router.push({name: 'sys-setting'})" index="sys-setting" v-perm="'setting:query'" <el-menu-item @click="router.push({name: 'sys-setting'})" index="sys-setting" v-perm="'setting:query'"
:class="route.meta.name === 'sys-setting' ? 'choose-item' : ''"> :class="route.meta.name === 'sys-setting' ? 'choose-item' : ''">
<Icon icon="eos-icons:system-ok-outlined" width="18" height="18" /> <Icon icon="eos-icons:system-ok-outlined" width="18" height="18" style="margin-left: 2px" />
<span class="menu-name" style="margin-left: 24px">{{$t('SystemSettings')}}</span> <span class="menu-name" style="margin-left: 22px">{{$t('SystemSettings')}}</span>
</el-menu-item> </el-menu-item>
</el-menu> </el-menu>
</div> </div>

View File

@@ -38,7 +38,6 @@ const handleResize = () => {
} }
onMounted(() => { onMounted(() => {
uiStore.writerRef = writerRef uiStore.writerRef = writerRef
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)

View File

@@ -2,8 +2,6 @@ import {createApp} from 'vue';
import App from './App.vue'; import App from './App.vue';
import router from './router'; import router from './router';
import './style.css'; import './style.css';
import VueCropper from 'vue-cropper';
import 'vue-cropper/dist/index.css'
import { init } from '@/init/init.js'; import { init } from '@/init/init.js';
import { createPinia } from 'pinia'; import { createPinia } from 'pinia';
import piniaPersistedState from 'pinia-plugin-persistedstate'; import piniaPersistedState from 'pinia-plugin-persistedstate';
@@ -12,7 +10,7 @@ const pinia = createPinia().use(piniaPersistedState)
import i18n from "@/i18n/index.js"; import i18n from "@/i18n/index.js";
const app = createApp(App).use(pinia) const app = createApp(App).use(pinia)
await init() await init()
app.use(router).use(VueCropper).use(i18n).directive('perm',perm) app.use(router).use(i18n).directive('perm',perm)
app.config.devtools = true; app.config.devtools = true;
app.mount('#app'); app.mount('#app');

View File

@@ -1,5 +1,24 @@
import {useSettingStore} from "@/store/setting.js"; import {useSettingStore} from "@/store/setting.js";
export function cvtR2Url(key) { export function cvtR2Url(key) {
const settingStore = useSettingStore();
return settingStore.settings.r2Domain + '/' + key if (!key) {
return + 'https://' + ''
}
if (key.startsWith('https://')) {
return key
}
const { settings } = useSettingStore();
let domain = settings.r2Domain
if (!domain.startsWith('http')) {
return 'https://' + domain + '/' + key
}
if (domain.endsWith("/")) {
domain = domain.slice(0, -1);
}
return domain + '/' + key
} }

View File

@@ -40,11 +40,11 @@ export function fromNow(date) {
if (diffSeconds < 60) return `Just now`; if (diffSeconds < 60) return `Just now`;
if (diffMinutes < 60) return `${diffMinutes} min ago`; if (diffMinutes < 60) return `${diffMinutes} min ago`;
if (diffHours < 2) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`; if (diffHours < 2) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
return d.format('HH:mm'); return d.format('hh:mm A');
} }
if (now.subtract(1, 'day').isSame(d, 'day')) { if (now.subtract(1, 'day').isSame(d, 'day')) {
return d.format('hh:mm A'); return d.format('MMM D');
} }
return d.year() === now.year() return d.year() === now.year()

View File

@@ -14,13 +14,18 @@ export function formatBytes(bytes) {
return `${size} ${units[i]}`; return `${size} ${units[i]}`;
} }
export function fileToBase64(file) { export function fileToBase64(file, type = false) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
reader.readAsDataURL(file); reader.readAsDataURL(file);
reader.onload = () => { reader.onload = () => {
const base64 = reader.result.split(',')[1]; if (type) {
resolve(base64); const base64 = reader.result;
resolve(base64);
} else {
const base64 = reader.result.split(',')[1];
resolve(base64);
}
}; };
reader.onerror = reject; reader.onerror = reject;
}); });
@@ -50,4 +55,4 @@ export function compressImage(file, config = {}) {
}, },
}); });
}); });
} }

View File

@@ -47,15 +47,52 @@
</el-table> </el-table>
</div> </div>
</el-scrollbar> </el-scrollbar>
<el-dialog top="5vh" class="dialog" v-model="roleFormShow" :title="dialogType.title" @closed="resetForm"> <el-dialog top="5vh" class="dialog" v-model="roleFormShow" @closed="resetForm">
<template #header>
<span style="font-size: 18px">{{dialogType.title}}</span>
<el-popover
width ="340"
:title="t('featDesc')"
placement="bottom"
>
<template #reference>
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</template>
<div style="font-weight: bold;margin-bottom: 2px;">{{t('emailInterception')}}</div>
<div>{{t('emailInterceptionDesc')}}</div>
<div style="font-weight: bold;margin-top: 10px;margin-bottom: 2px;">{{t('availableDomains')}}</div>
<div>
{{t('availableDomainsDesc')}}
</div>
</el-popover>
</template>
<div class="dialog-box"> <div class="dialog-box">
<el-input class="dialog-input" v-model="form.name" type="text" :maxlength="12" :placeholder="$t('roleName')" autocomplete="off" /> <el-input class="dialog-input" v-model="form.name" type="text" :maxlength="12" :placeholder="$t('roleName')" autocomplete="off" />
<el-input class="dialog-input" v-model="form.description" :maxlength="30" type="text" :placeholder="$t('description')" autocomplete="off" /> <el-input class="dialog-input" v-model="form.description" :maxlength="30" type="text" :placeholder="$t('description')" autocomplete="off" />
<el-input-tag class="dialog-input-tag" tag-type="warning" :class="form.banEmail.length === 0 ? 'dialog-input' : '' " v-model="form.banEmail" @add-tag="banEmailAddTag" type="text" :placeholder="$t('emailBlock')" autocomplete="off" /> <el-input-tag class="dialog-input-tag" tag-type="warning" :class="form.banEmail.length === 0 ? 'dialog-input' : '' " v-model="form.banEmail" @add-tag="banEmailAddTag" type="text" :placeholder="$t('emailInterception')" autocomplete="off" />
<el-radio-group class="dialog-radio" v-model="form.banEmailType" v-if="form.banEmail.length > 0"> <el-radio-group class="dialog-radio" v-model="form.banEmailType" v-if="form.banEmail.length > 0">
<el-radio :label="$t('removeAll')" :value="0" /> <el-radio :label="$t('removeAll')" :value="0" />
<el-radio :label="$t('removeBody')" :value="1" /> <el-radio :label="$t('removeBody')" :value="1" />
</el-radio-group> </el-radio-group>
<el-select
class="dialog-input"
v-model="form.availDomain"
multiple
filterable
allow-create
default-first-option
:reserve-keyword="false"
tag-type="success"
:placeholder="$t('availableDomains')"
@change="availDomainChange"
>
<el-option
v-for="item in domainOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<div class="dialog-input"> <div class="dialog-input">
<el-input-number :placeholder="$t('order')" :min="0" :max="9999" v-model.number="form.sort" controls-position="right" autocomplete="off" /> <el-input-number :placeholder="$t('order')" :min="0" :max="9999" v-model.number="form.sort" controls-position="right" autocomplete="off" />
</div> </div>
@@ -108,6 +145,7 @@ import {roleAdd, roleDelete, rolePermTree, roleRoleList, roleSet, roleSetDef} fr
import loading from '@/components/loading/index.vue'; import loading from '@/components/loading/index.vue';
import {useRoleStore} from "@/store/role.js"; import {useRoleStore} from "@/store/role.js";
import {useUserStore} from "@/store/user.js"; import {useUserStore} from "@/store/user.js";
import {useSettingStore} from "@/store/setting.js";
import {isEmail} from "@/utils/verify-utils.js"; import {isEmail} from "@/utils/verify-utils.js";
import {useI18n} from "vue-i18n"; import {useI18n} from "vue-i18n";
@@ -115,6 +153,7 @@ defineOptions({
name: 'role' name: 'role'
}) })
const { domainList } = useSettingStore();
const { t, locale } = useI18n(); const { t, locale } = useI18n();
const userStore = useUserStore(); const userStore = useUserStore();
const roleStore = useRoleStore(); const roleStore = useRoleStore();
@@ -144,8 +183,11 @@ const form = reactive({
accountCount: 0, accountCount: 0,
sort: 0, sort: 0,
isDefault: 0, isDefault: 0,
availDomain: []
}) })
let domainOptions = []
const expand = ref(false) const expand = ref(false)
let chooseRole = {} let chooseRole = {}
@@ -156,6 +198,18 @@ rolePermTree().then(tree => {
treeList.push(...tree) treeList.push(...tree)
}) })
domainOptions = domainList.map(domain => ({label: domain,value: domain}))
function availDomainChange() {
const index = form.availDomain.findIndex(domain => {
return !domainOptions.map(option => option.value).includes(domain)
})
if (index > -1) {
form.availDomain.splice(index,1)
}
}
function banEmailAddTag(val) { function banEmailAddTag(val) {
const emails = Array.from(new Set( const emails = Array.from(new Set(
val.split(/[,]/).map(item => item.trim()).filter(item => item) val.split(/[,]/).map(item => item.trim()).filter(item => item)
@@ -270,6 +324,7 @@ function resetForm() {
form.accountCount = 0 form.accountCount = 0
form.banEmail = [] form.banEmail = []
form.banEmailType = 0 form.banEmailType = 0
form.availDomain = []
tree.value.setCheckedKeys([]) tree.value.setCheckedKeys([])
} }
@@ -285,6 +340,7 @@ function openRoleSet(role) {
form.sendCount = role.sendCount form.sendCount = role.sendCount
form.accountCount = role.accountCount form.accountCount = role.accountCount
form.banEmail = role.banEmail form.banEmail = role.banEmail
form.availDomain = role.availDomain
nextTick(() => { nextTick(() => {
tree.value.setCheckedKeys(role.permIds) tree.value.setCheckedKeys(role.permIds)
}) })
@@ -394,7 +450,7 @@ window.onresize = () => {
.warning { .warning {
position: relative; position: relative;
left: 5px; left: 5px;
top: 5px; top: 2px;
color: gray; color: gray;
cursor: pointer; cursor: pointer;
} }

View File

@@ -73,9 +73,8 @@ import {accountSetName} from "@/request/account.js";
import {useAccountStore} from "@/store/account.js"; import {useAccountStore} from "@/store/account.js";
import {useI18n} from "vue-i18n"; import {useI18n} from "vue-i18n";
import {useSettingStore} from "@/store/setting.js"; import {useSettingStore} from "@/store/setting.js";
import dayjs from "dayjs";
const { t, locale } = useI18n() const { t } = useI18n()
const settingStore = useSettingStore() const settingStore = useSettingStore()
const accountStore = useAccountStore() const accountStore = useAccountStore()
const userStore = useUserStore(); const userStore = useUserStore();

View File

@@ -101,13 +101,13 @@
fit="cover" fit="cover"
> >
<template #error> <template #error>
<div class="error-image" @click="openCut"> <div class="error-image" @click="openSetBackground">
<Icon icon="ph:image" width="24" height="24"/> <Icon icon="ph:image" width="24" height="24"/>
</div> </div>
</template> </template>
</el-image> </el-image>
<div class="background-btn"> <div class="background-btn">
<el-button class="opt-button" size="small" type="primary" @click="openCut"> <el-button class="opt-button" size="small" type="primary" @click="openSetBackground">
<Icon icon="lsicon:edit-outline" width="16" height="16"/> <Icon icon="lsicon:edit-outline" width="16" height="16"/>
</el-button> </el-button>
<el-button class="opt-button" size="small" type="primary" @click="delBackground"> <el-button class="opt-button" size="small" type="primary" @click="delBackground">
@@ -286,10 +286,10 @@
</div> </div>
<div class="concerning-item"> <div class="concerning-item">
<span>{{$t('support')}} : </span> <span>{{$t('support')}} : </span>
<el-button @click="jump('https://afdian.com/a/eoao_')" > <el-button @click="jump('https://support.skymail.ink')" >
Afdian {{t('supportDesc')}}
<template #icon> <template #icon>
<Icon color="#8261DB" icon="simple-icons:afdian" width="24" height="24" /> <Icon color="#79D6B5" icon="simple-icons:buymeacoffee" width="20" height="20" />
</template> </template>
</el-button> </el-button>
</div> </div>
@@ -299,7 +299,7 @@
</div> </div>
<!-- Dialogs remain the same --> <!-- Dialogs remain the same -->
<el-dialog v-model="editTitleShow" :title="$t('changeTitle')" width="340" @closed="editTitle = ''"> <el-dialog v-model="editTitleShow" :title="$t('changeTitle')" width="340" >
<form> <form>
<el-input type="text" :placeholder="$t('websiteTitle')" v-model="editTitle"/> <el-input type="text" :placeholder="$t('websiteTitle')" v-model="editTitle"/>
<el-button type="primary" :loading="settingLoading" @click="saveTitle">{{$t('save')}}</el-button> <el-button type="primary" :loading="settingLoading" @click="saveTitle">{{$t('save')}}</el-button>
@@ -319,7 +319,7 @@
<el-button type="primary" :loading="settingLoading" @click="saveResendToken">{{$t('save')}}</el-button> <el-button type="primary" :loading="settingLoading" @click="saveResendToken">{{$t('save')}}</el-button>
</form> </form>
</el-dialog> </el-dialog>
<el-dialog v-model="r2DomainShow" :title="$t('addOsDomain')" width="340" @closed="r2DomainInput = ''"> <el-dialog v-model="r2DomainShow" :title="$t('addOsDomain')" width="340" @closed="r2DomainInput = setting.r2Domain">
<form> <form>
<el-input type="text" :placeholder="$t('domainDesc')" v-model="r2DomainInput"/> <el-input type="text" :placeholder="$t('domainDesc')" v-model="r2DomainInput"/>
<el-button type="primary" :loading="settingLoading" @click="saveR2domain">{{$t('save')}}</el-button> <el-button type="primary" :loading="settingLoading" @click="saveR2domain">{{$t('save')}}</el-button>
@@ -334,24 +334,37 @@
</form> </form>
</el-dialog> </el-dialog>
<el-dialog <el-dialog
v-model="cutShow" v-model="showSetBackground"
:title="$t('backgroundCropping')"
class="cut-dialog" class="cut-dialog"
@closed="closedSetBackground"
> >
<div class="cropper"> <template #header>
<vueCropper <span style="font-size: 18px">
ref="cropper" {{$t('backgroundTitle')}}
:img="cutImage" <el-tooltip>
:fixedNumber="[16,9]" <template #content>
outputType="jpeg" <span>{{$t('backgroundWarning')}}</span>
:fixed="true" </template>
:centerBox="true" <Icon class="title-icon warning" icon="fe:warning" width="18" height="18"/>
:full="true" </el-tooltip>
:autoCrop="true" </span>
:outputSize="0.92" </template>
></vueCropper> <el-input :placeholder="$t('backgroundUrlDesc')" v-model="backgroundUrl" v-if="!localUpShow" class="background-url" />
</div> <el-image
v-if="localUpShow"
:preview-src-list="[backgroundImage]"
show-progress
class="cropper"
fit="cover"
:src="backgroundImage"
></el-image>
<div class="cut-button"> <div class="cut-button">
<el-button type="primary" link @click="openCut" v-if="!localUpShow">
{{$t('localUpload')}}
</el-button>
<el-button type="primary" link @click="localUpShow = false" v-if="localUpShow">
{{$t('imageLink')}}
</el-button>
<el-button type="primary" :loading="settingLoading" @click="saveBackground">{{$t('save')}}</el-button> <el-button type="primary" :loading="settingLoading" @click="saveBackground">{{$t('save')}}</el-button>
</div> </div>
</el-dialog> </el-dialog>
@@ -387,7 +400,7 @@
<template #header> <template #header>
<div class="forward-head"> <div class="forward-head">
<span class="forward-set-title">{{$t('otherEmail')}}</span> <span class="forward-set-title">{{$t('otherEmail')}}</span>
<el-tooltip effect="dark" :content="$t('otherEmailDec')"> <el-tooltip effect="dark" :content="$t('otherEmailDesc')">
<Icon class="warning" icon="fe:warning" width="18" height="18"/> <Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip> </el-tooltip>
</div> </div>
@@ -454,6 +467,7 @@ import {debounce} from 'lodash-es'
import {isEmail} from "@/utils/verify-utils.js"; import {isEmail} from "@/utils/verify-utils.js";
import loading from "@/components/loading/index.vue"; import loading from "@/components/loading/index.vue";
import {getTextWidth} from "@/utils/text.js"; import {getTextWidth} from "@/utils/text.js";
import { fileToBase64 } from "@/utils/file-utils.js"
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
defineOptions({ defineOptions({
@@ -462,9 +476,8 @@ defineOptions({
const { t, locale } = useI18n(); const { t, locale } = useI18n();
const firstLoading = ref(true) const firstLoading = ref(true)
const cropper = ref() const backgroundImage = ref('')
const cutImage = ref('') const localUpShow = ref(false)
const cutShow = ref(false)
const accountStore = useAccountStore(); const accountStore = useAccountStore();
const userStore = useUserStore(); const userStore = useUserStore();
const editTitleShow = ref(false) const editTitleShow = ref(false)
@@ -481,6 +494,9 @@ const editTitle = ref('')
const settingLoading = ref(false) const settingLoading = ref(false)
const r2DomainInput = ref('') const r2DomainInput = ref('')
const loginOpacity = ref(0) const loginOpacity = ref(0)
const backgroundUrl = ref('')
let backgroundFile = {}
const showSetBackground = ref(false)
let backup = '{}' let backup = '{}'
const resendTokenForm = reactive({ const resendTokenForm = reactive({
domain: '', domain: '',
@@ -517,6 +533,17 @@ const tokenColumnWidth = ref(0)
const ruleType = ref(0) const ruleType = ref(0)
const ruleEmail = ref([]) const ruleEmail = ref([])
settingQuery().then(settingData => {
setting.value = settingData
resendTokenForm.domain = setting.value.domainList[0]
loginOpacity.value = setting.value.loginOpacity
firstLoading.value = false
backgroundUrl.value = setting.value.background?.startsWith('http') ? setting.value.background : ''
editTitle.value = setting.value.title
r2DomainInput.value = setting.value.r2Domain
})
const resendList = computed(() => { const resendList = computed(() => {
let list = Object.keys(setting.value.resendTokens).map(key => { let list = Object.keys(setting.value.resendTokens).map(key => {
@@ -548,13 +575,11 @@ const compareByLengthAndUpperCase = (a, b, key) => {
}; };
function closedSetBackground() {
settingQuery().then(settingData => { backgroundImage.value = ''
setting.value = settingData localUpShow.value = false
resendTokenForm.domain = setting.value.domainList[0] backgroundUrl.value = setting.value.background?.startsWith('http') ? setting.value.background : ''
loginOpacity.value = setting.value.loginOpacity }
firstLoading.value = false
})
function openTgSetting() { function openTgSetting() {
tgBotStatus.value = setting.value.tgBotStatus tgBotStatus.value = setting.value.tgBotStatus
@@ -697,6 +722,7 @@ function delBackground() {
cancelButtonText: t('cancel'), cancelButtonText: t('cancel'),
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
backgroundUrl.value = ''
setting.value.background = null setting.value.background = null
editSetting({background: null}) editSetting({background: null})
}) })
@@ -709,21 +735,43 @@ function saveTurnstileKey() {
editSetting(settingForm) editSetting(settingForm)
} }
function saveBackground() { async function saveBackground() {
settingLoading.value = true
cropper.value.getCropData(data => { let image = ''
setBackground(data).then(key => {
setting.value.background = key if (localUpShow.value) {
cutShow.value = false image = await fileToBase64(backgroundFile,true);
} else {
if (!backgroundUrl.value.startsWith('http')) {
ElMessage({ ElMessage({
message: t('changSuccessMsg'), message: '图片地址不正确',
type: "success", type: "error",
plain: true plain: true
}) })
}).finally(() => { return
settingLoading.value = false }
image = backgroundUrl.value
}
settingLoading.value = true
setBackground(image).then(key => {
setting.value.background = key
showSetBackground.value = false
ElMessage({
message: t('changSuccessMsg'),
type: "success",
plain: true
}) })
localUpShow.value = false
backgroundImage.value = ''
}).finally(() => {
settingLoading.value = false
}) })
}
function openSetBackground() {
showSetBackground.value = true
} }
function openCut() { function openCut() {
@@ -732,14 +780,15 @@ function openCut() {
doc.setAttribute('accept', 'image/*') doc.setAttribute('accept', 'image/*')
doc.click() doc.click()
doc.onchange = async (e) => { doc.onchange = async (e) => {
cutImage.value = URL.createObjectURL(e.target.files[0]) backgroundFile = e.target.files[0]
cutShow.value = true backgroundImage.value = URL.createObjectURL(e.target.files[0])
localUpShow.value = true
} }
} }
function saveR2domain() { function saveR2domain() {
const settingForm = {r2Domain: r2DomainInput.value} const settingForm = {r2Domain: r2DomainInput.value}
editSetting(settingForm) editSetting(settingForm, true)
} }
function openResendForm() { function openResendForm() {
@@ -824,7 +873,7 @@ function editSetting(settingForm, refreshStatus = true) {
thirdEmailShow.value = false thirdEmailShow.value = false
forwardRulesShow.value = false forwardRulesShow.value = false
}).catch((e) => { }).catch((e) => {
console.log(e) console.error(e)
loginOpacity.value = setting.value.loginOpacity loginOpacity.value = setting.value.loginOpacity
setting.value = {...setting.value, ...JSON.parse(backup)} setting.value = {...setting.value, ...JSON.parse(backup)}
}).finally(() => { }).finally(() => {
@@ -892,6 +941,8 @@ function editSetting(settingForm, refreshStatus = true) {
flex-direction: column; flex-direction: column;
} }
.settings-card { .settings-card {
background-color: #fff; background-color: #fff;
border-radius: 8px; border-radius: 8px;
@@ -939,6 +990,14 @@ function editSetting(settingForm, refreshStatus = true) {
} }
} }
.title-icon.warning {
position: relative;
top: 2px;
color: gray;
cursor: pointer;
margin-left: 2px;
}
.warning { .warning {
margin-left: 5px; margin-left: 5px;
color: gray; color: gray;
@@ -959,6 +1018,11 @@ function editSetting(settingForm, refreshStatus = true) {
justify-content: space-between; justify-content: space-between;
} }
.background-url {
width: min(calc(100vw - 70px), 500px);
}
:deep(.el-dialog) { :deep(.el-dialog) {
width: 400px !important; width: 400px !important;
@media (max-width: 440px) { @media (max-width: 440px) {
@@ -1027,8 +1091,7 @@ function editSetting(settingForm, refreshStatus = true) {
padding-top: 15px; padding-top: 15px;
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: end; justify-content: space-between;
.el-button { .el-button {
width: fit-content; width: fit-content;
} }

View File

@@ -644,7 +644,6 @@ function setType() {
function resetUserForm() { function resetUserForm() {
userForm.password = null userForm.password = null
userForm.userId = 0 userForm.userId = 0
userForm.type = 0
} }
function search() { function search() {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -6,8 +6,8 @@
<title></title> <title></title>
<link rel="icon" href="/assets/favicon-C5dAZutX.svg" type="image/svg+xml"> <link rel="icon" href="/assets/favicon-C5dAZutX.svg" type="image/svg+xml">
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<script type="module" crossorigin src="/assets/index-mj4MUxg3.js"></script> <script type="module" crossorigin src="/assets/index-DzqHrwam.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C5vjKzw7.css"> <link rel="stylesheet" crossorigin href="/assets/index-CTj27R8v.css">
</head> </head>
<body> <body>
<div id="loading-first"> <div id="loading-first">

View File

@@ -8,11 +8,12 @@ import fileUtils from '../utils/file-utils';
import { emailConst, isDel, roleConst, settingConst } from '../const/entity-const'; import { emailConst, isDel, roleConst, settingConst } from '../const/entity-const';
import emailUtils from '../utils/email-utils'; import emailUtils from '../utils/email-utils';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc' import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone' import timezone from 'dayjs/plugin/timezone';
import roleService from '../service/role-service'; import roleService from '../service/role-service';
dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(utc);
dayjs.extend(timezone);
export async function email(message, env, ctx) { export async function email(message, env, ctx) {
@@ -50,25 +51,29 @@ export async function email(message, env, ctx) {
if (account && account.email !== env.admin) { if (account && account.email !== env.admin) {
let { banEmail, banEmailType } = await roleService.selectByUserId({ env: env}, account.userId); let { banEmail, banEmailType, availDomain } = await roleService.selectByUserId({ env: env }, account.userId);
banEmail = banEmail.split(",").filter(item => item !== "") if(!roleService.hasAvailDomainPerm(availDomain, message.to)) {
return;
}
banEmail = banEmail.split(',').filter(item => item !== '');
for (const item of banEmail) { for (const item of banEmail) {
if (item.startsWith('*@')) { if (item.startsWith('*@')) {
const banDomain = emailUtils.getDomain(item.toLowerCase()) const banDomain = emailUtils.getDomain(item.toLowerCase());
const receiveDomain = emailUtils.getDomain(email.from.address.toLowerCase()) const receiveDomain = emailUtils.getDomain(email.from.address.toLowerCase());
if (banDomain === receiveDomain) { if (banDomain === receiveDomain) {
if (banEmailType === roleConst.banEmailType.ALL) return if (banEmailType === roleConst.banEmailType.ALL) return;
if (banEmailType === roleConst.banEmailType.CONTENT) { if (banEmailType === roleConst.banEmailType.CONTENT) {
email.html = 'messageRemoved' email.html = 'The content has been deleted';
email.text = 'messageRemoved' email.text = 'The content has been deleted';
email.attachments = [] email.attachments = [];
} }
} }
@@ -77,12 +82,12 @@ export async function email(message, env, ctx) {
if (item.toLowerCase() === email.from.address.toLowerCase()) { if (item.toLowerCase() === email.from.address.toLowerCase()) {
if (banEmailType === roleConst.banEmailType.ALL) return if (banEmailType === roleConst.banEmailType.ALL) return;
if (banEmailType === roleConst.banEmailType.CONTENT) { if (banEmailType === roleConst.banEmailType.CONTENT) {
email.html = 'messageRemoved' email.html = 'The content has been deleted';
email.text = 'messageRemoved' email.text = 'The content has been deleted';
email.attachments = [] email.attachments = [];
} }
} }

View File

@@ -7,6 +7,7 @@ export const role = sqliteTable('role', {
description: text('description'), description: text('description'),
banEmail: text('ban_email').notNull().default(''), banEmail: text('ban_email').notNull().default(''),
banEmailType: integer('ban_email_type').notNull().default(0), banEmailType: integer('ban_email_type').notNull().default(0),
availDomain: text('avail_domain').default(''),
sort: integer('sort'), sort: integer('sort'),
isDefault: integer('is_default').default(0), isDefault: integer('is_default').default(0),
createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`).notNull(), createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`).notNull(),

View File

@@ -54,6 +54,11 @@ const en = {
unauthorized: 'Unauthorized', unauthorized: 'Unauthorized',
bannedSend: 'You are banned from sending emails', bannedSend: 'You are banned from sending emails',
initSuccess: 'Successfully initialized', initSuccess: 'Successfully initialized',
noDomainPermAdd: "No permission to add this domain email",
noDomainPermReg: "No permission to register this domain email",
noDomainPermRegKey: "Registration code not valid for this domain",
noDomainPermSend: "No permission to send from this domain email",
JWTMismatch: 'JWT secret mismatch',
perms: { perms: {
"邮件": "Email", "邮件": "Email",
"邮件发送": "Send email", "邮件发送": "Send email",

View File

@@ -46,14 +46,19 @@ const zh = {
delDefRole: '默认身份不能删除', delDefRole: '默认身份不能删除',
notJsonDomain: '环境变量domain必须是JSON类型', notJsonDomain: '环境变量domain必须是JSON类型',
noOsUpBack: 'r2对象存储未配置不能上传背景', noOsUpBack: 'r2对象存储未配置不能上传背景',
noOsDomainUpBack: 'r2域名未配置不上传背景', noOsDomainUpBack: 'r2域名未配置不上传背景',
starNotExistEmail: '星标的邮件不存在', starNotExistEmail: '星标的邮件不存在',
emptyBotToken: '验证token不能为空', emptyBotToken: '验证token不能为空',
botVerifyFail: '人机验证失败,请重试', botVerifyFail: '人机验证失败,请重试',
authExpired: '身份认证失效,请重新登录', authExpired: '身份认证失效,请重新登录',
unauthorized: '权限不足', unauthorized: '权限不足',
bannedSend: '你已被禁止发送邮件', bannedSend: '你已被禁止发送邮件',
initSuccess: 'Successfully initialized', initSuccess: '初始化成功',
noDomainPermAdd: '你没有权限添加该域名邮箱',
noDomainPermReg: '你没有权限注册该域名邮箱',
noDomainPermRegKey: '你的注册码没有权限注册该域名邮箱',
noDomainPermSend: '你没有权限使用该域名邮箱发送邮件',
JWTMismatch: 'jwt_secret 不匹配',
perms: { perms: {
"邮件": "邮件", "邮件": "邮件",
"邮件发送": "邮件发送", "邮件发送": "邮件发送",

View File

@@ -8,7 +8,7 @@ const init = {
const secret = c.req.param('secret'); const secret = c.req.param('secret');
if (secret !== c.env.jwt_secret) { if (secret !== c.env.jwt_secret) {
return c.text(t('initSuccess')); return c.text('jwt_secret 不匹配');
} }
await this.intDB(c); await this.intDB(c);
@@ -19,12 +19,17 @@ const init = {
await this.v1_4DB(c); await this.v1_4DB(c);
await this.v1_5DB(c); await this.v1_5DB(c);
await settingService.refresh(c); await settingService.refresh(c);
return c.text('初始化成功'); return c.text(t('initSuccess'));
}, },
async v1_5DB(c) { async v1_5DB(c) {
await c.env.db.prepare(`UPDATE perm SET perm_key = 'sys-email:list' WHERE perm_key = 'all-email:list'`).run(); await c.env.db.prepare(`UPDATE perm SET perm_key = 'sys-email:list' WHERE perm_key = 'all-email:list'`).run();
await c.env.db.prepare(`UPDATE perm SET perm_key = 'sys-email:delete' WHERE perm_key = 'all-email:delete'`).run(); await c.env.db.prepare(`UPDATE perm SET perm_key = 'sys-email:delete' WHERE perm_key = 'all-email:delete'`).run();
try {
await c.env.db.prepare(`ALTER TABLE role ADD COLUMN avail_domain TEXT NOT NULL DEFAULT ''`).run();
} catch (e) {
console.warn(`跳过字段添加,原因:${e.message}`);
}
}, },
async v1_4DB(c) { async v1_4DB(c) {

View File

@@ -126,7 +126,6 @@ app.use('*', async (c, next) => {
const userPaths = permKeyToPaths(permKeys); const userPaths = permKeyToPaths(permKeys);
const userPermIndex = userPaths.findIndex(item => { const userPermIndex = userPaths.findIndex(item => {
console.log(item)
return path.startsWith(item); return path.startsWith(item);
}); });

View File

@@ -48,9 +48,17 @@ const accountService = {
const userRow = await userService.selectById(c, userId); const userRow = await userService.selectById(c, userId);
const roleRow = await roleService.selectById(c, userRow.type); const roleRow = await roleService.selectById(c, userRow.type);
if (roleRow.accountCount && userRow.email !== c.env.admin) { if (userRow.email !== c.env.admin) {
const userAccountCount = await accountService.countUserAccount(c, userId)
if(userAccountCount >= roleRow.accountCount) throw new BizError(t('accountLimit'), 403); if (roleRow.accountCount > 0) {
const userAccountCount = await accountService.countUserAccount(c, userId)
if(userAccountCount >= roleRow.accountCount) throw new BizError(t('accountLimit'), 403);
}
if(!roleService.hasAvailDomainPerm(roleRow.availDomain, email)) {
throw new BizError(t('noDomainPermAdd'),403)
}
} }
if (await settingService.isAddEmailVerify(c)) { if (await settingService.isAddEmailVerify(c)) {

View File

@@ -159,6 +159,7 @@ const emailService = {
} }
if (attDataList.length > 0 && !r2Domain) { if (attDataList.length > 0 && !r2Domain) {
throw new BizError(t('noOsDomainSendPic')); throw new BizError(t('noOsDomainSendPic'));
} }
@@ -186,6 +187,18 @@ const emailService = {
throw new BizError(t('senderAccountNotExist')); throw new BizError(t('senderAccountNotExist'));
} }
if (accountRow.userId !== userId) {
throw new BizError(t('sendEmailNotCurUser'));
}
if (c.env.admin !== userRow.email) {
if(!roleService.hasAvailDomainPerm(roleRow.availDomain, accountRow.email)) {
throw new BizError(t('noDomainPermSend'),403)
}
}
const domain = emailUtils.getDomain(accountRow.email); const domain = emailUtils.getDomain(accountRow.email);
const resendToken = resendTokens[domain]; const resendToken = resendTokens[domain];
@@ -194,10 +207,6 @@ const emailService = {
} }
if (accountRow.userId !== userId) {
throw new BizError(t('sendEmailNotCurUser'));
}
if (!name) { if (!name) {
name = emailUtils.getName(accountRow.email); name = emailUtils.getName(accountRow.email);
} }

View File

@@ -25,7 +25,11 @@ const loginService = {
const { email, password, token, code } = params; const { email, password, token, code } = params;
const {regKey, register} = await settingService.query(c) const {regKey, register, registerVerify} = await settingService.query(c)
if (registerVerify === settingConst.registerVerify.OPEN) {
await turnstileService.verify(c,token)
}
if (register === settingConst.register.CLOSE) { if (register === settingConst.register.CLOSE) {
throw new BizError(t('regDisabled')); throw new BizError(t('regDisabled'));
@@ -76,10 +80,6 @@ const loginService = {
throw new BizError(t('isRegAccount')); throw new BizError(t('isRegAccount'));
} }
if (await settingService.isRegisterVerify(c)) {
await turnstileService.verify(c,token)
}
const { salt, hash } = await saltHashUtils.hashPassword(password); const { salt, hash } = await saltHashUtils.hashPassword(password);
let defType = null let defType = null
@@ -89,6 +89,21 @@ const loginService = {
defType = roleRow.roleId defType = roleRow.roleId
} }
const roleRow = await roleService.selectById(c, type || defType);
if(!roleService.hasAvailDomainPerm(roleRow.availDomain, email)) {
if (type) {
throw new BizError(t('noDomainPermRegKey'),403)
}
if (defType) {
throw new BizError(t('noDomainPermReg'),403)
}
}
const userId = await userService.insert(c, { email, regKeyId,password: hash, salt, type: type || defType }); const userId = await userService.insert(c, { email, regKeyId,password: hash, salt, type: type || defType });
await userService.updateUserInfo(c, userId, true); await userService.updateUserInfo(c, userId, true);

View File

@@ -9,12 +9,13 @@ import userService from './user-service';
import user from '../entity/user'; import user from '../entity/user';
import verifyUtils from '../utils/verify-utils'; import verifyUtils from '../utils/verify-utils';
import { t } from '../i18n/i18n.js'; import { t } from '../i18n/i18n.js';
import emailUtils from '../utils/email-utils';
const roleService = { const roleService = {
async add(c, params, userId) { async add(c, params, userId) {
let { name, permIds, banEmail } = params; let { name, permIds, banEmail, availDomain } = params;
if (!name) { if (!name) {
throw new BizError(t('emptyRoleName')); throw new BizError(t('emptyRoleName'));
@@ -32,9 +33,11 @@ const roleService = {
throw new BizError(t('notEmail')); throw new BizError(t('notEmail'));
} }
banEmail = banEmail.join(',') banEmail = banEmail.join(',');
roleRow = await orm(c).insert(role).values({...params, banEmail, userId}).returning().get(); availDomain = availDomain.join(',');
roleRow = await orm(c).insert(role).values({...params, banEmail, availDomain, userId}).returning().get();
if (permIds.length === 0) { if (permIds.length === 0) {
return; return;
@@ -55,7 +58,8 @@ const roleService = {
.where(eq(perm.type, permConst.type.BUTTON)).all(); .where(eq(perm.type, permConst.type.BUTTON)).all();
roleList.forEach(role => { roleList.forEach(role => {
role.banEmail = role.banEmail.split(",").filter(item => item !== "") role.banEmail = role.banEmail.split(",").filter(item => item !== "");
role.availDomain = role.availDomain.split(",").filter(item => item !== "");
role.permIds = permList.filter(perm => perm.roleId === role.roleId).map(perm => perm.permId); role.permIds = permList.filter(perm => perm.roleId === role.roleId).map(perm => perm.permId);
}); });
@@ -64,7 +68,7 @@ const roleService = {
async setRole(c, params) { async setRole(c, params) {
let { name, permIds, roleId, banEmail } = params; let { name, permIds, roleId, banEmail, availDomain } = params;
if (!name) { if (!name) {
throw new BizError(t('emptyRoleName')); throw new BizError(t('emptyRoleName'));
@@ -80,7 +84,9 @@ const roleService = {
banEmail = banEmail.join(',') banEmail = banEmail.join(',')
await orm(c).update(role).set({...params, banEmail}).where(eq(role.roleId, roleId)).run(); availDomain = availDomain.join(',')
await orm(c).update(role).set({...params, banEmail, availDomain}).where(eq(role.roleId, roleId)).run();
await orm(c).delete(rolePerm).where(eq(rolePerm.roleId, roleId)).run(); await orm(c).delete(rolePerm).where(eq(rolePerm.roleId, roleId)).run();
if (permIds.length > 0) { if (permIds.length > 0) {
@@ -150,6 +156,24 @@ const roleService = {
selectByUserId(c, userId) { selectByUserId(c, userId) {
return orm(c).select(role).from(user).leftJoin(role, eq(role.roleId, user.type)).where(eq(user.userId, userId)).get(); return orm(c).select(role).from(user).leftJoin(role, eq(role.roleId, user.type)).where(eq(user.userId, userId)).get();
},
hasAvailDomainPerm(availDomain, email) {
availDomain = availDomain.split(',').filter(item => item !== '');
if (availDomain.length === 0) {
return true
}
const availIndex = availDomain.findIndex(item => {
const domain = emailUtils.getDomain(email.toLowerCase());
const availDomainItem = emailUtils.getDomain(item.toLowerCase());
console.log(domain,availDomainItem)
return domain === availDomainItem
})
return availIndex > -1
} }
}; };

View File

@@ -69,30 +69,41 @@ const settingService = {
const settingRow = await this.query(c); const settingRow = await this.query(c);
let { background } = params
if (!background.startsWith('http')) {
if (!c.env.r2) {
throw new BizError(t('noOsUpBack'));
}
if (!settingRow.r2Domain) {
throw new BizError(t('noOsDomainUpBack'));
}
const file = fileUtils.base64ToFile(background)
const arrayBuffer = await file.arrayBuffer();
background = constant.BACKGROUND_PREFIX + await fileUtils.getBuffHash(arrayBuffer) + fileUtils.getExtFileName(file.name);
await r2Service.putObj(c, background, arrayBuffer, {
contentType: file.type
});
if (!c.env.r2) {
throw new BizError(t('noOsUpBack'));
} }
if (!settingRow.r2Domain) {
throw new BizError(t('noOsDomainUpBack'));
}
const { background } = params;
const file = fileUtils.base64ToFile(background);
const arrayBuffer = await file.arrayBuffer();
const key = constant.BACKGROUND_PREFIX + await fileUtils.getBuffHash(arrayBuffer) + fileUtils.getExtFileName(file.name);
await r2Service.putObj(c, key, file, {
contentType: file.type
});
if (settingRow.background) { if (settingRow.background) {
await r2Service.delete(c, settingRow.background); try {
await r2Service.delete(c, settingRow.background);
} catch (e) {
console.error(e)
}
} }
await orm(c).update(setting).set({ background: key }).run(); await orm(c).update(setting).set({ background }).run();
await this.refresh(c); await this.refresh(c);
return key; return background;
}, },
async physicsDeleteAll(c) { async physicsDeleteAll(c) {

View File

@@ -27,7 +27,7 @@ const fileUtils = {
* @returns {File} File 对象 * @returns {File} File 对象
*/ */
base64ToFile(base64Data, customFilename) { base64ToFile(base64Data, customFilename) {
const match = base64Data.match(/^data:(image|video)\/([a-zA-Z0-9.+-]+);base64,/); const match = base64Data.match(/^data:(image|jpeg|video)\/([a-zA-Z0-9.+-]+);base64,/);
if (!match) { if (!match) {
throw new Error('Invalid base64 data format'); throw new Error('Invalid base64 data format');
} }
@@ -35,7 +35,7 @@ const fileUtils = {
const type = match[1]; // image 或 video const type = match[1]; // image 或 video
const ext = match[2]; // jpg, png, mp4 等 const ext = match[2]; // jpg, png, mp4 等
const mimeType = `${type}/${ext}`; const mimeType = `${type}/${ext}`;
const cleanBase64 = base64Data.replace(/^data:(image|video)\/[a-zA-Z0-9.+-]+;base64,/, ''); const cleanBase64 = base64Data.replace(/^data:(image|jpeg|video)\/[a-zA-Z0-9.+-]+;base64,/, '');
const byteCharacters = atob(cleanBase64); const byteCharacters = atob(cleanBase64);
const byteArrays = []; const byteArrays = [];

View File

@@ -32,7 +32,7 @@ run_worker_first = true
crons = ["0 16 * * *"] crons = ["0 16 * * *"]
[vars] [vars]
orm_log = true orm_log = false
domain = ["example.com", "example2.com"] domain = ["example.com", "example2.com", "example3.com", "example4.com"]
admin = "admin@example.com" admin = "admin@example.com"
jwt_secret = "b7f29a1d-18e2-4d3b-941f-f6b2c97c02fd" jwt_secret = "b7f29a1d-18e2-4d3b-941f-f6b2c97c02fd"