新增权限域名限制和链接背景上传
This commit is contained in:
@@ -161,9 +161,9 @@ After the update, run `https://your-project-domain/api/init/your-jwt-secret` to
|
||||
|
||||
## Support
|
||||
|
||||
**Buy me a cup of milk tea**
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
**Special Sponsors**
|
||||
|
||||
@@ -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
BIN
doc/images/support.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
7
mail-vue/package-lock.json
generated
7
mail-vue/package-lock.json
generated
@@ -22,7 +22,6 @@
|
||||
"pinia": "^3.0.2",
|
||||
"pinia-plugin-persistedstate": "^4.2.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-cropper": "^1.1.4",
|
||||
"vue-i18n": "^11.1.10",
|
||||
"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": {
|
||||
"version": "11.1.10",
|
||||
"resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-11.1.10.tgz",
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
"pinia": "^3.0.2",
|
||||
"pinia-plugin-persistedstate": "^4.2.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-cropper": "^1.1.4",
|
||||
"vue-i18n": "^11.1.10",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {useSettingStore} from "@/store/setting.js";
|
||||
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()
|
||||
locale.value = settingStore.lang
|
||||
</script>
|
||||
@@ -96,7 +96,6 @@ const en = {
|
||||
expand: 'Expand',
|
||||
collapse: 'Collapse',
|
||||
daily: 'Daily',
|
||||
emailBlock: '*Intercept all incoming emails to @example.com',
|
||||
searchRegKeyDesc: 'Enter invite code to search',
|
||||
remainingUses: 'Remaining uses',
|
||||
exhausted: 'Exhausted',
|
||||
@@ -168,11 +167,11 @@ const en = {
|
||||
addOsDomain: 'Add domain',
|
||||
domainDesc: 'Domain',
|
||||
addTurnstileSecret: 'Add turnstile secret',
|
||||
backgroundCropping: 'Background cropping',
|
||||
backgroundTitle: 'Change background',
|
||||
tgBotDesc: 'Forward received emails to a Telegram bot',
|
||||
tgBotToken: 'Bot token',
|
||||
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.',
|
||||
forwardingRulesDesc: 'Rule-based forwarding only forwards emails received by the specified address.',
|
||||
ruleEmailsInputDesc: 'Separate multiple email addresses with commas.',
|
||||
@@ -246,7 +245,17 @@ const en = {
|
||||
totalUserAccount: '{msg}',
|
||||
sendBanned: 'Banned',
|
||||
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
|
||||
|
||||
@@ -96,7 +96,6 @@ const zh = {
|
||||
expand: '展开',
|
||||
collapse: '收起',
|
||||
daily: '每天',
|
||||
emailBlock: '输入邮箱拦截收件, 拦截所有前缀 *@example.com',
|
||||
searchRegKeyDesc: '输入注册码搜索',
|
||||
remainingUses: '剩余次数',
|
||||
exhausted: '已用尽',
|
||||
@@ -165,14 +164,14 @@ const zh = {
|
||||
community: '交流',
|
||||
changeTitle: '修改标题',
|
||||
addResendTokenDesc: '输入内容添加,不填则删除',
|
||||
addOsDomain: '添加访对象存储问域名',
|
||||
addOsDomain: '添加访问域名',
|
||||
domainDesc: '域名',
|
||||
addTurnstileSecret: '添加 Turnstile 密钥',
|
||||
backgroundCropping: '背景截图',
|
||||
backgroundTitle: '设置背景',
|
||||
tgBotDesc: '可以将接收的邮件转发到Tg机器人',
|
||||
tgBotToken: '机器人 token',
|
||||
toBotTokenDesc: '用户 chat_id 多个用,分开',
|
||||
otherEmailDec: '可以将邮件转到其他服务商邮箱,需要在cloudflare验证邮箱',
|
||||
otherEmailDesc: '可以将邮件转到其他服务商邮箱,需要在cloudflare验证邮箱',
|
||||
otherEmailInputDesc: '多个邮箱用, 分开',
|
||||
forwardingRulesDesc: '规则转发只会转发设置邮箱所接收的邮件',
|
||||
ruleEmailsInputDesc: '多个邮箱用, 分开',
|
||||
@@ -246,6 +245,16 @@ const zh = {
|
||||
totalUserAccount: '{msg} 个',
|
||||
sendBanned: '已禁用',
|
||||
wrote: '来信',
|
||||
support: '赞助'
|
||||
support: '赞助',
|
||||
supportDesc: '请我喝杯奶茶',
|
||||
featDesc: '功能说明',
|
||||
emailInterception: '邮件拦截',
|
||||
emailInterceptionDesc: '拦截邮件, 要拦截整个域名输入 *@example.com, 可用于禁止用户接收某些网站的邮件',
|
||||
availableDomains: '可用域名',
|
||||
availableDomainsDesc: '限制用户只能使用指定的域名邮箱,不在配置名单内的域名会被禁止使用注册添加邮箱,接收发送邮件等功能,留空默认允许可用所有域名',
|
||||
backgroundUrlDesc: '在线图片链接',
|
||||
localUpload: '本地上传',
|
||||
imageLink: '图片链接',
|
||||
backgroundWarning: '图片文件大小会影响网站加载速度',
|
||||
}
|
||||
export default zh
|
||||
@@ -61,8 +61,8 @@
|
||||
</el-menu-item>
|
||||
<el-menu-item @click="router.push({name: 'sys-setting'})" index="sys-setting" v-perm="'setting:query'"
|
||||
:class="route.meta.name === 'sys-setting' ? 'choose-item' : ''">
|
||||
<Icon icon="eos-icons:system-ok-outlined" width="18" height="18" />
|
||||
<span class="menu-name" style="margin-left: 24px">{{$t('SystemSettings')}}</span>
|
||||
<Icon icon="eos-icons:system-ok-outlined" width="18" height="18" style="margin-left: 2px" />
|
||||
<span class="menu-name" style="margin-left: 22px">{{$t('SystemSettings')}}</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,6 @@ const handleResize = () => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
uiStore.writerRef = writerRef
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
@@ -2,8 +2,6 @@ import {createApp} from 'vue';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
import './style.css';
|
||||
import VueCropper from 'vue-cropper';
|
||||
import 'vue-cropper/dist/index.css'
|
||||
import { init } from '@/init/init.js';
|
||||
import { createPinia } from 'pinia';
|
||||
import piniaPersistedState from 'pinia-plugin-persistedstate';
|
||||
@@ -12,7 +10,7 @@ const pinia = createPinia().use(piniaPersistedState)
|
||||
import i18n from "@/i18n/index.js";
|
||||
const app = createApp(App).use(pinia)
|
||||
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.mount('#app');
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
import {useSettingStore} from "@/store/setting.js";
|
||||
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
|
||||
}
|
||||
@@ -40,11 +40,11 @@ export function fromNow(date) {
|
||||
if (diffSeconds < 60) return `Just now`;
|
||||
if (diffMinutes < 60) return `${diffMinutes} min 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')) {
|
||||
return d.format('hh:mm A');
|
||||
return d.format('MMM D');
|
||||
}
|
||||
|
||||
return d.year() === now.year()
|
||||
|
||||
@@ -14,13 +14,18 @@ export function formatBytes(bytes) {
|
||||
return `${size} ${units[i]}`;
|
||||
}
|
||||
|
||||
export function fileToBase64(file) {
|
||||
export function fileToBase64(file, type = false) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => {
|
||||
const base64 = reader.result.split(',')[1];
|
||||
resolve(base64);
|
||||
if (type) {
|
||||
const base64 = reader.result;
|
||||
resolve(base64);
|
||||
} else {
|
||||
const base64 = reader.result.split(',')[1];
|
||||
resolve(base64);
|
||||
}
|
||||
};
|
||||
reader.onerror = reject;
|
||||
});
|
||||
@@ -50,4 +55,4 @@ export function compressImage(file, config = {}) {
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -47,15 +47,52 @@
|
||||
</el-table>
|
||||
</div>
|
||||
</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">
|
||||
<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-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 :label="$t('removeAll')" :value="0" />
|
||||
<el-radio :label="$t('removeBody')" :value="1" />
|
||||
</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">
|
||||
<el-input-number :placeholder="$t('order')" :min="0" :max="9999" v-model.number="form.sort" controls-position="right" autocomplete="off" />
|
||||
</div>
|
||||
@@ -108,6 +145,7 @@ import {roleAdd, roleDelete, rolePermTree, roleRoleList, roleSet, roleSetDef} fr
|
||||
import loading from '@/components/loading/index.vue';
|
||||
import {useRoleStore} from "@/store/role.js";
|
||||
import {useUserStore} from "@/store/user.js";
|
||||
import {useSettingStore} from "@/store/setting.js";
|
||||
import {isEmail} from "@/utils/verify-utils.js";
|
||||
import {useI18n} from "vue-i18n";
|
||||
|
||||
@@ -115,6 +153,7 @@ defineOptions({
|
||||
name: 'role'
|
||||
})
|
||||
|
||||
const { domainList } = useSettingStore();
|
||||
const { t, locale } = useI18n();
|
||||
const userStore = useUserStore();
|
||||
const roleStore = useRoleStore();
|
||||
@@ -144,8 +183,11 @@ const form = reactive({
|
||||
accountCount: 0,
|
||||
sort: 0,
|
||||
isDefault: 0,
|
||||
availDomain: []
|
||||
})
|
||||
|
||||
let domainOptions = []
|
||||
|
||||
const expand = ref(false)
|
||||
|
||||
let chooseRole = {}
|
||||
@@ -156,6 +198,18 @@ rolePermTree().then(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) {
|
||||
const emails = Array.from(new Set(
|
||||
val.split(/[,,]/).map(item => item.trim()).filter(item => item)
|
||||
@@ -270,6 +324,7 @@ function resetForm() {
|
||||
form.accountCount = 0
|
||||
form.banEmail = []
|
||||
form.banEmailType = 0
|
||||
form.availDomain = []
|
||||
tree.value.setCheckedKeys([])
|
||||
}
|
||||
|
||||
@@ -285,6 +340,7 @@ function openRoleSet(role) {
|
||||
form.sendCount = role.sendCount
|
||||
form.accountCount = role.accountCount
|
||||
form.banEmail = role.banEmail
|
||||
form.availDomain = role.availDomain
|
||||
nextTick(() => {
|
||||
tree.value.setCheckedKeys(role.permIds)
|
||||
})
|
||||
@@ -394,7 +450,7 @@ window.onresize = () => {
|
||||
.warning {
|
||||
position: relative;
|
||||
left: 5px;
|
||||
top: 5px;
|
||||
top: 2px;
|
||||
color: gray;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -73,9 +73,8 @@ import {accountSetName} from "@/request/account.js";
|
||||
import {useAccountStore} from "@/store/account.js";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {useSettingStore} from "@/store/setting.js";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const { t } = useI18n()
|
||||
const settingStore = useSettingStore()
|
||||
const accountStore = useAccountStore()
|
||||
const userStore = useUserStore();
|
||||
|
||||
@@ -101,13 +101,13 @@
|
||||
fit="cover"
|
||||
>
|
||||
<template #error>
|
||||
<div class="error-image" @click="openCut">
|
||||
<div class="error-image" @click="openSetBackground">
|
||||
<Icon icon="ph:image" width="24" height="24"/>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<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"/>
|
||||
</el-button>
|
||||
<el-button class="opt-button" size="small" type="primary" @click="delBackground">
|
||||
@@ -286,10 +286,10 @@
|
||||
</div>
|
||||
<div class="concerning-item">
|
||||
<span>{{$t('support')}} : </span>
|
||||
<el-button @click="jump('https://afdian.com/a/eoao_')" >
|
||||
Afdian
|
||||
<el-button @click="jump('https://support.skymail.ink')" >
|
||||
{{t('supportDesc')}}
|
||||
<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>
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -299,7 +299,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<el-input type="text" :placeholder="$t('websiteTitle')" v-model="editTitle"/>
|
||||
<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>
|
||||
</form>
|
||||
</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>
|
||||
<el-input type="text" :placeholder="$t('domainDesc')" v-model="r2DomainInput"/>
|
||||
<el-button type="primary" :loading="settingLoading" @click="saveR2domain">{{$t('save')}}</el-button>
|
||||
@@ -334,24 +334,37 @@
|
||||
</form>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
v-model="cutShow"
|
||||
:title="$t('backgroundCropping')"
|
||||
v-model="showSetBackground"
|
||||
class="cut-dialog"
|
||||
@closed="closedSetBackground"
|
||||
>
|
||||
<div class="cropper">
|
||||
<vueCropper
|
||||
ref="cropper"
|
||||
:img="cutImage"
|
||||
:fixedNumber="[16,9]"
|
||||
outputType="jpeg"
|
||||
:fixed="true"
|
||||
:centerBox="true"
|
||||
:full="true"
|
||||
:autoCrop="true"
|
||||
:outputSize="0.92"
|
||||
></vueCropper>
|
||||
</div>
|
||||
<template #header>
|
||||
<span style="font-size: 18px">
|
||||
{{$t('backgroundTitle')}}
|
||||
<el-tooltip>
|
||||
<template #content>
|
||||
<span>{{$t('backgroundWarning')}}</span>
|
||||
</template>
|
||||
<Icon class="title-icon warning" icon="fe:warning" width="18" height="18"/>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<el-input :placeholder="$t('backgroundUrlDesc')" v-model="backgroundUrl" v-if="!localUpShow" class="background-url" />
|
||||
<el-image
|
||||
v-if="localUpShow"
|
||||
:preview-src-list="[backgroundImage]"
|
||||
show-progress
|
||||
class="cropper"
|
||||
fit="cover"
|
||||
:src="backgroundImage"
|
||||
></el-image>
|
||||
<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>
|
||||
</div>
|
||||
</el-dialog>
|
||||
@@ -387,7 +400,7 @@
|
||||
<template #header>
|
||||
<div class="forward-head">
|
||||
<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"/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
@@ -454,6 +467,7 @@ import {debounce} from 'lodash-es'
|
||||
import {isEmail} from "@/utils/verify-utils.js";
|
||||
import loading from "@/components/loading/index.vue";
|
||||
import {getTextWidth} from "@/utils/text.js";
|
||||
import { fileToBase64 } from "@/utils/file-utils.js"
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
defineOptions({
|
||||
@@ -462,9 +476,8 @@ defineOptions({
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const firstLoading = ref(true)
|
||||
const cropper = ref()
|
||||
const cutImage = ref('')
|
||||
const cutShow = ref(false)
|
||||
const backgroundImage = ref('')
|
||||
const localUpShow = ref(false)
|
||||
const accountStore = useAccountStore();
|
||||
const userStore = useUserStore();
|
||||
const editTitleShow = ref(false)
|
||||
@@ -481,6 +494,9 @@ const editTitle = ref('')
|
||||
const settingLoading = ref(false)
|
||||
const r2DomainInput = ref('')
|
||||
const loginOpacity = ref(0)
|
||||
const backgroundUrl = ref('')
|
||||
let backgroundFile = {}
|
||||
const showSetBackground = ref(false)
|
||||
let backup = '{}'
|
||||
const resendTokenForm = reactive({
|
||||
domain: '',
|
||||
@@ -517,6 +533,17 @@ const tokenColumnWidth = ref(0)
|
||||
const ruleType = ref(0)
|
||||
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(() => {
|
||||
|
||||
let list = Object.keys(setting.value.resendTokens).map(key => {
|
||||
@@ -548,13 +575,11 @@ const compareByLengthAndUpperCase = (a, b, key) => {
|
||||
};
|
||||
|
||||
|
||||
|
||||
settingQuery().then(settingData => {
|
||||
setting.value = settingData
|
||||
resendTokenForm.domain = setting.value.domainList[0]
|
||||
loginOpacity.value = setting.value.loginOpacity
|
||||
firstLoading.value = false
|
||||
})
|
||||
function closedSetBackground() {
|
||||
backgroundImage.value = ''
|
||||
localUpShow.value = false
|
||||
backgroundUrl.value = setting.value.background?.startsWith('http') ? setting.value.background : ''
|
||||
}
|
||||
|
||||
function openTgSetting() {
|
||||
tgBotStatus.value = setting.value.tgBotStatus
|
||||
@@ -697,6 +722,7 @@ function delBackground() {
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
backgroundUrl.value = ''
|
||||
setting.value.background = null
|
||||
editSetting({background: null})
|
||||
})
|
||||
@@ -709,21 +735,43 @@ function saveTurnstileKey() {
|
||||
editSetting(settingForm)
|
||||
}
|
||||
|
||||
function saveBackground() {
|
||||
settingLoading.value = true
|
||||
cropper.value.getCropData(data => {
|
||||
setBackground(data).then(key => {
|
||||
setting.value.background = key
|
||||
cutShow.value = false
|
||||
async function saveBackground() {
|
||||
|
||||
let image = ''
|
||||
|
||||
if (localUpShow.value) {
|
||||
image = await fileToBase64(backgroundFile,true);
|
||||
} else {
|
||||
if (!backgroundUrl.value.startsWith('http')) {
|
||||
ElMessage({
|
||||
message: t('changSuccessMsg'),
|
||||
type: "success",
|
||||
message: '图片地址不正确',
|
||||
type: "error",
|
||||
plain: true
|
||||
})
|
||||
}).finally(() => {
|
||||
settingLoading.value = false
|
||||
return
|
||||
}
|
||||
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() {
|
||||
@@ -732,14 +780,15 @@ function openCut() {
|
||||
doc.setAttribute('accept', 'image/*')
|
||||
doc.click()
|
||||
doc.onchange = async (e) => {
|
||||
cutImage.value = URL.createObjectURL(e.target.files[0])
|
||||
cutShow.value = true
|
||||
backgroundFile = e.target.files[0]
|
||||
backgroundImage.value = URL.createObjectURL(e.target.files[0])
|
||||
localUpShow.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function saveR2domain() {
|
||||
const settingForm = {r2Domain: r2DomainInput.value}
|
||||
editSetting(settingForm)
|
||||
editSetting(settingForm, true)
|
||||
}
|
||||
|
||||
function openResendForm() {
|
||||
@@ -824,7 +873,7 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
thirdEmailShow.value = false
|
||||
forwardRulesShow.value = false
|
||||
}).catch((e) => {
|
||||
console.log(e)
|
||||
console.error(e)
|
||||
loginOpacity.value = setting.value.loginOpacity
|
||||
setting.value = {...setting.value, ...JSON.parse(backup)}
|
||||
}).finally(() => {
|
||||
@@ -892,6 +941,8 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.settings-card {
|
||||
background-color: #fff;
|
||||
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 {
|
||||
margin-left: 5px;
|
||||
color: gray;
|
||||
@@ -959,6 +1018,11 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.background-url {
|
||||
width: min(calc(100vw - 70px), 500px);
|
||||
}
|
||||
|
||||
|
||||
:deep(.el-dialog) {
|
||||
width: 400px !important;
|
||||
@media (max-width: 440px) {
|
||||
@@ -1027,8 +1091,7 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
padding-top: 15px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
|
||||
justify-content: space-between;
|
||||
.el-button {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
@@ -644,7 +644,6 @@ function setType() {
|
||||
function resetUserForm() {
|
||||
userForm.password = null
|
||||
userForm.userId = 0
|
||||
userForm.type = 0
|
||||
}
|
||||
|
||||
function search() {
|
||||
|
||||
1
mail-worker/dist/assets/index-C5vjKzw7.css
vendored
1
mail-worker/dist/assets/index-C5vjKzw7.css
vendored
File diff suppressed because one or more lines are too long
1
mail-worker/dist/assets/index-CTj27R8v.css
vendored
Normal file
1
mail-worker/dist/assets/index-CTj27R8v.css
vendored
Normal file
File diff suppressed because one or more lines are too long
193
mail-worker/dist/assets/index-DzqHrwam.js
vendored
Normal file
193
mail-worker/dist/assets/index-DzqHrwam.js
vendored
Normal file
File diff suppressed because one or more lines are too long
199
mail-worker/dist/assets/index-mj4MUxg3.js
vendored
199
mail-worker/dist/assets/index-mj4MUxg3.js
vendored
File diff suppressed because one or more lines are too long
4
mail-worker/dist/index.html
vendored
4
mail-worker/dist/index.html
vendored
@@ -6,8 +6,8 @@
|
||||
<title></title>
|
||||
<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 type="module" crossorigin src="/assets/index-mj4MUxg3.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-C5vjKzw7.css">
|
||||
<script type="module" crossorigin src="/assets/index-DzqHrwam.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CTj27R8v.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading-first">
|
||||
|
||||
@@ -8,11 +8,12 @@ import fileUtils from '../utils/file-utils';
|
||||
import { emailConst, isDel, roleConst, settingConst } from '../const/entity-const';
|
||||
import emailUtils from '../utils/email-utils';
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
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) {
|
||||
|
||||
@@ -50,25 +51,29 @@ export async function email(message, env, ctx) {
|
||||
|
||||
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) {
|
||||
|
||||
if (item.startsWith('*@')) {
|
||||
|
||||
const banDomain = emailUtils.getDomain(item.toLowerCase())
|
||||
const receiveDomain = emailUtils.getDomain(email.from.address.toLowerCase())
|
||||
const banDomain = emailUtils.getDomain(item.toLowerCase());
|
||||
const receiveDomain = emailUtils.getDomain(email.from.address.toLowerCase());
|
||||
|
||||
if (banDomain === receiveDomain) {
|
||||
|
||||
if (banEmailType === roleConst.banEmailType.ALL) return
|
||||
if (banEmailType === roleConst.banEmailType.ALL) return;
|
||||
|
||||
if (banEmailType === roleConst.banEmailType.CONTENT) {
|
||||
email.html = 'messageRemoved'
|
||||
email.text = 'messageRemoved'
|
||||
email.attachments = []
|
||||
email.html = 'The content has been deleted';
|
||||
email.text = 'The content has been deleted';
|
||||
email.attachments = [];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -77,12 +82,12 @@ export async function email(message, env, ctx) {
|
||||
|
||||
if (item.toLowerCase() === email.from.address.toLowerCase()) {
|
||||
|
||||
if (banEmailType === roleConst.banEmailType.ALL) return
|
||||
if (banEmailType === roleConst.banEmailType.ALL) return;
|
||||
|
||||
if (banEmailType === roleConst.banEmailType.CONTENT) {
|
||||
email.html = 'messageRemoved'
|
||||
email.text = 'messageRemoved'
|
||||
email.attachments = []
|
||||
email.html = 'The content has been deleted';
|
||||
email.text = 'The content has been deleted';
|
||||
email.attachments = [];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export const role = sqliteTable('role', {
|
||||
description: text('description'),
|
||||
banEmail: text('ban_email').notNull().default(''),
|
||||
banEmailType: integer('ban_email_type').notNull().default(0),
|
||||
availDomain: text('avail_domain').default(''),
|
||||
sort: integer('sort'),
|
||||
isDefault: integer('is_default').default(0),
|
||||
createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
|
||||
@@ -54,6 +54,11 @@ const en = {
|
||||
unauthorized: 'Unauthorized',
|
||||
bannedSend: 'You are banned from sending emails',
|
||||
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: {
|
||||
"邮件": "Email",
|
||||
"邮件发送": "Send email",
|
||||
|
||||
@@ -46,14 +46,19 @@ const zh = {
|
||||
delDefRole: '默认身份不能删除',
|
||||
notJsonDomain: '环境变量domain必须是JSON类型',
|
||||
noOsUpBack: 'r2对象存储未配置不能上传背景',
|
||||
noOsDomainUpBack: 'r2域名未配置不上传背景',
|
||||
noOsDomainUpBack: 'r2域名未配置不能上传背景',
|
||||
starNotExistEmail: '星标的邮件不存在',
|
||||
emptyBotToken: '验证token不能为空',
|
||||
botVerifyFail: '人机验证失败,请重试',
|
||||
authExpired: '身份认证失效,请重新登录',
|
||||
unauthorized: '权限不足',
|
||||
bannedSend: '你已被禁止发送邮件',
|
||||
initSuccess: 'Successfully initialized',
|
||||
initSuccess: '初始化成功',
|
||||
noDomainPermAdd: '你没有权限添加该域名邮箱',
|
||||
noDomainPermReg: '你没有权限注册该域名邮箱',
|
||||
noDomainPermRegKey: '你的注册码没有权限注册该域名邮箱',
|
||||
noDomainPermSend: '你没有权限使用该域名邮箱发送邮件',
|
||||
JWTMismatch: 'jwt_secret 不匹配',
|
||||
perms: {
|
||||
"邮件": "邮件",
|
||||
"邮件发送": "邮件发送",
|
||||
|
||||
@@ -8,7 +8,7 @@ const init = {
|
||||
const secret = c.req.param('secret');
|
||||
|
||||
if (secret !== c.env.jwt_secret) {
|
||||
return c.text(t('initSuccess'));
|
||||
return c.text('jwt_secret 不匹配');
|
||||
}
|
||||
|
||||
await this.intDB(c);
|
||||
@@ -19,12 +19,17 @@ const init = {
|
||||
await this.v1_4DB(c);
|
||||
await this.v1_5DB(c);
|
||||
await settingService.refresh(c);
|
||||
return c.text('初始化成功');
|
||||
return c.text(t('initSuccess'));
|
||||
},
|
||||
|
||||
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: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) {
|
||||
|
||||
@@ -126,7 +126,6 @@ app.use('*', async (c, next) => {
|
||||
const userPaths = permKeyToPaths(permKeys);
|
||||
|
||||
const userPermIndex = userPaths.findIndex(item => {
|
||||
console.log(item)
|
||||
return path.startsWith(item);
|
||||
});
|
||||
|
||||
|
||||
@@ -48,9 +48,17 @@ const accountService = {
|
||||
const userRow = await userService.selectById(c, userId);
|
||||
const roleRow = await roleService.selectById(c, userRow.type);
|
||||
|
||||
if (roleRow.accountCount && userRow.email !== c.env.admin) {
|
||||
const userAccountCount = await accountService.countUserAccount(c, userId)
|
||||
if(userAccountCount >= roleRow.accountCount) throw new BizError(t('accountLimit'), 403);
|
||||
if (userRow.email !== c.env.admin) {
|
||||
|
||||
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)) {
|
||||
|
||||
@@ -159,6 +159,7 @@ const emailService = {
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (attDataList.length > 0 && !r2Domain) {
|
||||
throw new BizError(t('noOsDomainSendPic'));
|
||||
}
|
||||
@@ -186,6 +187,18 @@ const emailService = {
|
||||
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 resendToken = resendTokens[domain];
|
||||
|
||||
@@ -194,10 +207,6 @@ const emailService = {
|
||||
}
|
||||
|
||||
|
||||
if (accountRow.userId !== userId) {
|
||||
throw new BizError(t('sendEmailNotCurUser'));
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
name = emailUtils.getName(accountRow.email);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,11 @@ const loginService = {
|
||||
|
||||
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) {
|
||||
throw new BizError(t('regDisabled'));
|
||||
@@ -76,10 +80,6 @@ const loginService = {
|
||||
throw new BizError(t('isRegAccount'));
|
||||
}
|
||||
|
||||
if (await settingService.isRegisterVerify(c)) {
|
||||
await turnstileService.verify(c,token)
|
||||
}
|
||||
|
||||
const { salt, hash } = await saltHashUtils.hashPassword(password);
|
||||
|
||||
let defType = null
|
||||
@@ -89,6 +89,21 @@ const loginService = {
|
||||
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 });
|
||||
|
||||
await userService.updateUserInfo(c, userId, true);
|
||||
|
||||
@@ -9,12 +9,13 @@ import userService from './user-service';
|
||||
import user from '../entity/user';
|
||||
import verifyUtils from '../utils/verify-utils';
|
||||
import { t } from '../i18n/i18n.js';
|
||||
import emailUtils from '../utils/email-utils';
|
||||
|
||||
const roleService = {
|
||||
|
||||
async add(c, params, userId) {
|
||||
|
||||
let { name, permIds, banEmail } = params;
|
||||
let { name, permIds, banEmail, availDomain } = params;
|
||||
|
||||
if (!name) {
|
||||
throw new BizError(t('emptyRoleName'));
|
||||
@@ -32,9 +33,11 @@ const roleService = {
|
||||
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) {
|
||||
return;
|
||||
@@ -55,7 +58,8 @@ const roleService = {
|
||||
.where(eq(perm.type, permConst.type.BUTTON)).all();
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -64,7 +68,7 @@ const roleService = {
|
||||
|
||||
async setRole(c, params) {
|
||||
|
||||
let { name, permIds, roleId, banEmail } = params;
|
||||
let { name, permIds, roleId, banEmail, availDomain } = params;
|
||||
|
||||
if (!name) {
|
||||
throw new BizError(t('emptyRoleName'));
|
||||
@@ -80,7 +84,9 @@ const roleService = {
|
||||
|
||||
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();
|
||||
|
||||
if (permIds.length > 0) {
|
||||
@@ -150,6 +156,24 @@ const roleService = {
|
||||
|
||||
selectByUserId(c, userId) {
|
||||
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
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -69,30 +69,41 @@ const settingService = {
|
||||
|
||||
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) {
|
||||
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);
|
||||
return key;
|
||||
return background;
|
||||
},
|
||||
|
||||
async physicsDeleteAll(c) {
|
||||
|
||||
@@ -27,7 +27,7 @@ const fileUtils = {
|
||||
* @returns {File} File 对象
|
||||
*/
|
||||
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) {
|
||||
throw new Error('Invalid base64 data format');
|
||||
}
|
||||
@@ -35,7 +35,7 @@ const fileUtils = {
|
||||
const type = match[1]; // image 或 video
|
||||
const ext = match[2]; // jpg, png, mp4 等
|
||||
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 byteArrays = [];
|
||||
|
||||
@@ -32,7 +32,7 @@ run_worker_first = true
|
||||
crons = ["0 16 * * *"]
|
||||
|
||||
[vars]
|
||||
orm_log = true
|
||||
domain = ["example.com", "example2.com"]
|
||||
orm_log = false
|
||||
domain = ["example.com", "example2.com", "example3.com", "example4.com"]
|
||||
admin = "admin@example.com"
|
||||
jwt_secret = "b7f29a1d-18e2-4d3b-941f-f6b2c97c02fd"
|
||||
|
||||
Reference in New Issue
Block a user