新增注册码、草稿箱、权限拦截邮件、发件菜单隐藏

This commit is contained in:
eoao
2025-07-15 20:09:56 +08:00
parent 7e5a383bc6
commit 254325431b
55 changed files with 1819 additions and 361 deletions

View File

@@ -48,7 +48,7 @@ const accountService = {
if (roleRow.accountCount && userRow.email !== c.env.admin) {
const userAccountCount = await accountService.countUserAccount(c, userId)
if(userAccountCount >= roleRow.accountCount) throw new BizError(`添加邮箱数量限制${roleRow.accountCount}`, 403);
if(userAccountCount >= roleRow.accountCount) throw new BizError(`添加邮箱数量到达限制`, 403);
}
if (await settingService.isAddEmailVerify(c)) {
@@ -181,6 +181,9 @@ const accountService = {
async setName(c, params, userId) {
const { name, accountId } = params
if (name.length > 30) {
throw new BizError('用户名长度超出限制');
}
await orm(c).update(account).set({name}).where(and(eq(account.userId, userId),eq(account.accountId, accountId))).run();
}
};

View File

@@ -5,10 +5,7 @@ import { desc, count, eq, and, ne, isNotNull } from 'drizzle-orm';
import { emailConst } from '../const/entity-const';
import kvConst from '../const/kv-const';
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'
dayjs.extend(utc)
dayjs.extend(timezone)
import { toUtc } from '../utils/date-uitil';
const analysisService = {
async echarts(c) {
@@ -62,7 +59,7 @@ const analysisService = {
},
filterEmptyDay(data) {
const today = dayjs().tz('Asia/Shanghai').subtract(1, 'day');
const today = toUtc().tz('Asia/Shanghai').subtract(1, 'day');
const previousDays = Array.from({ length: 15 }, (_, i) => {
return today.subtract(i, 'day').format('YYYY-MM-DD');
}).reverse();

View File

@@ -150,7 +150,7 @@ const emailService = {
}
if (send === settingConst.send.CLOSE) {
throw new BizError('邮发送功能已停用', 403);
throw new BizError('邮发送功能已停用', 403);
}
if (attachments.length > 0 && manyType === 'divide') {
@@ -163,13 +163,17 @@ const emailService = {
if (c.env.admin !== userRow.email && roleRow.sendCount) {
if (roleRow.sendCount < 0) {
throw new BizError('用户无发送次数', 403);
}
if (userRow.sendCount >= roleRow.sendCount) {
if (roleRow.sendType === 'day') throw new BizError('已到达每日发送次数限制', 403);
if (roleRow.sendType === 'count') throw new BizError('已到达发送次数限制', 403);
if (roleRow.sendType === 'day') throw new BizError('发送次数已到达每日限制', 403);
if (roleRow.sendType === 'count') throw new BizError('发送次数已到达限制', 403);
}
if (userRow.sendCount + receiveEmail.length > roleRow.sendCount) {
if (roleRow.sendType === 'day') throw new BizError('剩余每日发送次数不足', 403);
if (roleRow.sendType === 'day') throw new BizError('当日剩余发送次数不足', 403);
if (roleRow.sendType === 'count') throw new BizError('剩余发送次数不足', 403);
}
@@ -178,18 +182,20 @@ const emailService = {
const accountRow = await accountService.selectById(c, accountId);
if (!accountRow) {
throw new BizError('发件人邮箱不存在');
}
const domain = emailUtils.getDomain(accountRow.email);
const resendToken = resendTokens[domain];
if (!resendToken) {
throw new BizError('resend密钥未配置');
}
if (!accountRow) {
throw new BizError('邮箱不存在');
}
if (accountRow.userId !== userId) {
throw new BizError('非当前用户所属邮箱');
throw new BizError('发件人邮箱非当前用户所');
}
if (!name) {
@@ -488,7 +494,7 @@ const emailService = {
async allList(c, params) {
let { emailId, size, name, subject, accountEmail, sendEmail, userEmail, type, timeSort } = params;
let { emailId, size, name, subject, accountEmail, userEmail, type, timeSort } = params;
size = Number(size);

View File

@@ -1,7 +1,7 @@
import BizError from '../error/biz-error';
import userService from './user-service';
import emailUtils from '../utils/email-utils';
import { isDel, userConst } from '../const/entity-const';
import { isDel, settingConst, userConst } from '../const/entity-const';
import JwtUtils from '../utils/jwt-utils';
import { v4 as uuidv4 } from 'uuid';
import KvConst from '../const/kv-const';
@@ -14,15 +14,19 @@ import saltHashUtils from '../utils/crypto-utils';
import cryptoUtils from '../utils/crypto-utils';
import turnstileService from './turnstile-service';
import roleService from './role-service';
import regKeyService from './reg-key-service';
import dayjs from 'dayjs';
import { formatDetailDate, toUtc } from '../utils/date-uitil';
const loginService = {
async register(c, params) {
const { email, password, token } = params;
const { email, password, token, code } = params;
if (!await settingService.isRegister(c)) {
const {regKey, register} = await settingService.query(c)
if (register === settingConst.register.CLOSE) {
throw new BizError('注册功能已关闭');
}
@@ -30,7 +34,15 @@ const loginService = {
throw new BizError('非法邮箱');
}
if (password.length < 6) {
if (password.length > 30) {
throw new BizError('密码长度超出限制');
}
if (emailUtils.getName(email).length > 30) {
throw new BizError('邮箱长度超出限制');
}
if (password.length > 6) {
throw new BizError('密码必须大于6位');
}
@@ -38,6 +50,21 @@ const loginService = {
throw new BizError('非法邮箱域名');
}
let type = null;
let regKeyId = 0
if (regKey === settingConst.regKey.OPEN) {
const result = await this.handleOpenRegKey(c, regKey, code)
type = result.type
regKeyId = result.regKeyId
}
if (regKey === settingConst.regKey.OPTIONAL) {
const result = await this.handleOpenOptional(c, regKey, code)
type = result.type
regKeyId = result.regKeyId
}
const accountRow = await accountService.selectByEmailIncludeDelNoCase(c, email);
if (accountRow && accountRow.isDel === isDel.DELETE) {
@@ -45,7 +72,7 @@ const loginService = {
}
if (accountRow) {
throw new BizError('该邮箱已被其他用户绑定');
throw new BizError('该邮箱已被注册');
}
if (await settingService.isRegisterVerify(c)) {
@@ -54,13 +81,71 @@ const loginService = {
const { salt, hash } = await saltHashUtils.hashPassword(password);
const roleRow = await roleService.selectDefaultRole(c);
let defType = null
const userId = await userService.insert(c, { email, password: hash, salt, type: roleRow.roleId });
if (!type) {
const roleRow = await roleService.selectDefaultRole(c);
defType = roleRow.roleId
}
const userId = await userService.insert(c, { email, regKeyId,password: hash, salt, type: type || defType });
await userService.updateUserInfo(c, userId, true);
await accountService.insert(c, { userId: userId, email, name: emailUtils.getName(email) });
if (regKey !== settingConst.regKey.CLOSE && type) {
await regKeyService.reduceCount(c, code, 1);
}
},
async handleOpenRegKey(c, regKey, code) {
if (!code) {
throw new BizError('注册码不能为空');
}
const regKeyRow = await regKeyService.selectByCode(c, code);
if (!regKeyRow) {
throw new BizError('注册码不存在');
}
if (regKeyRow.count <= 0) {
throw new BizError('注册码使用次数已耗尽');
}
const today = toUtc().tz('Asia/Shanghai').startOf('day')
const expireTime = toUtc(regKeyRow.expireTime).tz('Asia/Shanghai').startOf('day');
if (expireTime.isBefore(today)) {
throw new BizError('注册码已过期');
}
return { type: regKeyRow.roleId, regKeyId: regKeyRow.regKeyId };
},
async handleOpenOptional(c, regKey, code) {
if (!code) {
return null
}
const regKeyRow = await regKeyService.selectByCode(c, code);
if (!regKeyRow) {
return null
}
const today = toUtc().tz('Asia/Shanghai').startOf('day')
const expireTime = toUtc(regKeyRow.expireTime).tz('Asia/Shanghai').startOf('day');
if (regKeyRow.count <= 0 || expireTime.isBefore(today)) {
return null
}
return { type: regKeyRow.roleId, regKeyId: regKeyRow.regKeyId };
},
async login(c, params) {

View File

@@ -1,6 +1,3 @@
import attService from './att-service';
import constant from '../const/constant';
const r2Service = {
async putObj(c, key, content, metadata) {
await c.env.r2.put(key, content, {

View File

@@ -0,0 +1,100 @@
import orm from '../entity/orm';
import regKey from '../entity/reg-key';
import { inArray, like, eq, desc, sql, or } from 'drizzle-orm';
import roleService from './role-service';
import BizError from '../error/biz-error';
import { formatDetailDate, toUtc } from '../utils/date-uitil';
import userService from './user-service';
const regKeyService = {
async add(c, params, userId) {
let {code,roleId,count,expireTime} = params;
if (!code) {
throw new BizError('注册码不能为空');
}
if (!count) {
throw new BizError('使用次数不能为空');
}
if (!expireTime) {
throw new BizError('有效时间不能为空');
}
const regKeyRow = await orm(c).select().from(regKey).where(eq(regKey.code, code)).get();
if (regKeyRow) {
throw new BizError('注册码已存在');
}
const roleRow = roleService.selectById(c, roleId);
if (!roleRow) {
throw new BizError('权限身份不存在');
}
expireTime = formatDetailDate(expireTime)
await orm(c).insert(regKey).values({code,roleId,count,userId,expireTime}).run();
},
async delete(c, params) {
let {regKeyIds} = params;
regKeyIds = regKeyIds.split(',').map(id => Number(id));
await orm(c).delete(regKey).where(inArray(regKey.regKeyId,regKeyIds)).run();
},
async clearNotUse(c) {
let now = formatDetailDate(toUtc().tz('Asia/Shanghai').startOf('day'))
await orm(c).delete(regKey).where(or(eq(regKey.count, 0),sql`datetime(${regKey.expireTime}, '+8 hours') < datetime(${now})`)).run();
},
selectByCode(c, code) {
return orm(c).select().from(regKey).where(eq(regKey.code, code)).get();
},
async list(c, params) {
const {code} = params
let query = orm(c).select().from(regKey)
if (code) {
query = query.where(like(regKey.code, `${code}%`))
}
const regKeyList = await query.orderBy(desc(regKey.regKeyId)).all();
const roleList = await roleService.roleSelectUse(c);
const today = toUtc().tz('Asia/Shanghai').startOf('day')
regKeyList.forEach(regKeyRow => {
const index = roleList.findIndex(roleRow => roleRow.roleId === regKeyRow.roleId)
regKeyRow.roleName = index > -1 ? roleList[index].name : ''
const expireTime = toUtc(regKeyRow.expireTime).tz('Asia/Shanghai').startOf('day');
if (expireTime.isBefore(today)) {
regKeyRow.expireTime = null
}
})
return regKeyList;
},
async reduceCount(c, code, count) {
await orm(c).update(regKey).set({
count: sql`${regKey.count}
-
${count}`
}).where(eq(regKey.code, code)).run();
},
async history(c, params) {
const { regKeyId } = params;
return userService.listByRegKeyId(c, regKeyId);
}
}
export default regKeyService;

View File

@@ -6,12 +6,15 @@ import rolePerm from '../entity/role-perm';
import perm from '../entity/perm';
import { permConst, roleConst } from '../const/entity-const';
import userService from './user-service';
import user from '../entity/user';
import emailUtils from '../utils/email-utils';
import verifyUtils from '../utils/verify-utils';
const roleService = {
async add(c, params, userId) {
let { name, permIds } = params;
let { name, permIds, banEmail } = params;
if (!name) {
throw new BizError('身份名不能为空');
@@ -23,7 +26,15 @@ const roleService = {
throw new BizError('身份名已存在');
}
roleRow = await orm(c).insert(role).values({...params, userId}).returning().get();
const notEmailIndex = banEmail.findIndex(item => !verifyUtils.isEmail(item))
if (notEmailIndex > -1) {
throw new BizError('非法邮箱');
}
banEmail = banEmail.join(',')
roleRow = await orm(c).insert(role).values({...params, banEmail, userId}).returning().get();
if (permIds.length === 0) {
return;
@@ -44,6 +55,7 @@ const roleService = {
.where(eq(perm.type, permConst.type.BUTTON)).all();
roleList.forEach(role => {
role.banEmail = role.banEmail.split(",").filter(item => item !== "")
role.permIds = permList.filter(perm => perm.roleId === role.roleId).map(perm => perm.permId);
});
@@ -52,7 +64,7 @@ const roleService = {
async setRole(c, params) {
let { name, permIds, roleId } = params;
let { name, permIds, roleId, banEmail } = params;
if (!name) {
throw new BizError('名字不能为空');
@@ -60,7 +72,15 @@ const roleService = {
delete params.isDefault
await orm(c).update(role).set({...params}).where(eq(role.roleId, roleId)).run();
const notEmailIndex = banEmail.findIndex(item => !verifyUtils.isEmail(item))
if (notEmailIndex > -1) {
throw new BizError('非法邮箱');
}
banEmail = banEmail.join(',')
await orm(c).update(role).set({...params, banEmail}).where(eq(role.roleId, roleId)).run();
await orm(c).delete(rolePerm).where(eq(rolePerm.roleId, roleId)).run();
if (permIds.length > 0) {
@@ -126,6 +146,10 @@ const roleService = {
.leftJoin(rolePerm, eq(perm.permId, rolePerm.permId))
.leftJoin(role, eq(role.roleId, rolePerm.roleId))
.where(and(eq(perm.permKey, permKey), eq(role.sendType, sendType))).all();
},
selectByUserId(c, userId) {
return orm(c).select(role).from(user).leftJoin(role, eq(role.roleId, user.type)).where(eq(user.userId, userId)).get();
}
};

View File

@@ -49,16 +49,6 @@ const settingService = {
await this.refresh(c);
},
async isRegister(c) {
const { register } = await this.query(c);
return register === settingConst.register.OPEN;
},
async isReceive(c) {
const { receive } = await this.query(c);
return receive === settingConst.receive.OPEN;
},
async isAddEmail(c) {
const { addEmail, manyEmail } = await this.query(c);
return addEmail === settingConst.addEmail.OPEN && manyEmail === settingConst.manyEmail.OPEN;
@@ -125,7 +115,8 @@ const settingService = {
siteKey: settingRow.siteKey,
background: settingRow.background,
loginOpacity: settingRow.loginOpacity,
domainList:settingRow.domainList
domainList:settingRow.domainList,
regKey: settingRow.regKey
};
}
};

View File

@@ -382,6 +382,15 @@ const userService = {
await accountService.restoreByUserId(c, userId);
}
},
listByRegKeyId(c, regKeyId) {
return orm(c)
.select({email: user.email,createTime: user.createTime})
.from(user)
.where(eq(user.regKeyId, regKeyId))
.orderBy(desc(user.userId))
.all();
}
};