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

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

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>
<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">

View File

@@ -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 = [];
}
}

View File

@@ -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(),

View File

@@ -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",

View File

@@ -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: {
"邮件": "邮件",
"邮件发送": "邮件发送",

View File

@@ -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) {

View File

@@ -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);
});

View File

@@ -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)) {

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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
}
};

View File

@@ -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) {

View File

@@ -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 = [];

View File

@@ -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"