新增邮件发送和管理员功能
This commit is contained in:
@@ -4,17 +4,17 @@ import result from '../model/result';
|
||||
import userContext from '../security/user-context';
|
||||
|
||||
app.get('/account/list', async (c) => {
|
||||
const list = await accountService.list(c, c.req.query(), await userContext.getUserId(c));
|
||||
const list = await accountService.list(c, c.req.query(), userContext.getUserId(c));
|
||||
return c.json(result.ok(list));
|
||||
});
|
||||
|
||||
app.delete('/account/delete', async (c) => {
|
||||
await accountService.delete(c, c.req.query(), await userContext.getUserId(c));
|
||||
await accountService.delete(c, c.req.query(), userContext.getUserId(c));
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
app.post('/account/add', async (c) => {
|
||||
const account = await accountService.add(c, await c.req.json(), await userContext.getUserId(c));
|
||||
const account = await accountService.add(c, await c.req.json(), userContext.getUserId(c));
|
||||
return c.json(result.ok(account));
|
||||
});
|
||||
|
||||
|
||||
@@ -5,22 +5,27 @@ import userContext from '../security/user-context';
|
||||
import attService from '../service/att-service';
|
||||
|
||||
app.get('/email/list', async (c) => {
|
||||
const data = await emailService.list(c, c.req.query(), await userContext.getUserId(c));
|
||||
const data = await emailService.list(c, c.req.query(), userContext.getUserId(c));
|
||||
return c.json(result.ok(data));
|
||||
});
|
||||
|
||||
app.get('/email/latest',async (c) => {
|
||||
const list = await emailService.latest(c, c.req.query(), await userContext.getUserId(c));
|
||||
app.get('/email/latest', async (c) => {
|
||||
const list = await emailService.latest(c, c.req.query(), userContext.getUserId(c));
|
||||
return c.json(result.ok(list));
|
||||
})
|
||||
});
|
||||
|
||||
app.delete('/email/delete', async (c) => {
|
||||
await emailService.delete(c, c.req.query(), await userContext.getUserId(c));
|
||||
await emailService.delete(c, c.req.query(), userContext.getUserId(c));
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
app.get('/email/attList', async (c) => {
|
||||
const attList = await attService.list(c, c.req.query(), await userContext.getUserId(c));
|
||||
const attList = await attService.list(c, c.req.query(), userContext.getUserId(c));
|
||||
return c.json(result.ok(attList));
|
||||
})
|
||||
});
|
||||
|
||||
app.post('/email/send', async (c) => {
|
||||
const email = await emailService.send(c, await c.req.json(), userContext.getUserId(c));
|
||||
return c.json(result.ok(email));
|
||||
});
|
||||
|
||||
|
||||
6
mail-worker/src/api/init-api.js
Normal file
6
mail-worker/src/api/init-api.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import app from '../hono/hono';
|
||||
import initService from '../service/init-service';
|
||||
|
||||
app.get('/init/:secret', (c) => {
|
||||
return initService.init(c);
|
||||
})
|
||||
@@ -14,6 +14,7 @@ app.post('/register', async (c) => {
|
||||
});
|
||||
|
||||
app.delete('/logout', async (c) => {
|
||||
await loginService.logout(c, await userContext.getUserId(c));
|
||||
await loginService.logout(c, userContext.getUserId(c));
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
|
||||
21
mail-worker/src/api/my-api.js
Normal file
21
mail-worker/src/api/my-api.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import app from '../hono/hono';
|
||||
import userService from '../service/user-service';
|
||||
import result from '../model/result';
|
||||
import userContext from '../security/user-context';
|
||||
|
||||
app.get('/my/loginUserInfo', async (c) => {
|
||||
const user = await userService.loginUserInfo(c, userContext.getUserId(c));
|
||||
return c.json(result.ok(user));
|
||||
});
|
||||
|
||||
app.put('/my/resetPassword', async (c) => {
|
||||
await userService.resetPassword(c, await c.req.json(), userContext.getUserId(c));
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
app.delete('/my/delete', async (c) => {
|
||||
await userService.delete(c, userContext.getUserId(c));
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
|
||||
16
mail-worker/src/api/r2-api.js
Normal file
16
mail-worker/src/api/r2-api.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import r2Service from '../service/r2-service';
|
||||
import app from '../hono/hono';
|
||||
|
||||
|
||||
app.get('/file/*', async (c) => {
|
||||
const key = c.req.path.split('/file/')[1];
|
||||
const obj = await r2Service.getObj(c, key);
|
||||
return new Response(obj.body, {
|
||||
headers: {
|
||||
'Content-Type': obj.httpMetadata?.contentType || 'application/octet-stream',
|
||||
'Content-Disposition': obj.httpMetadata?.contentDisposition || null
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
10
mail-worker/src/api/resend-api.js
Normal file
10
mail-worker/src/api/resend-api.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import resendService from '../service/resend-service';
|
||||
import app from '../hono/hono';
|
||||
app.post('/webhooks',async (c) => {
|
||||
try {
|
||||
await resendService.webhooks(c, await c.req.json());
|
||||
return c.text('success', 200)
|
||||
} catch (e) {
|
||||
return c.text(e.message, 500)
|
||||
}
|
||||
})
|
||||
44
mail-worker/src/api/role-api.js
Normal file
44
mail-worker/src/api/role-api.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import app from '../hono/hono';
|
||||
import roleService from '../service/role-service';
|
||||
import userContext from '../security/user-context';
|
||||
import result from '../model/result';
|
||||
import permService from '../service/perm-service';
|
||||
|
||||
app.post('/role/add', async (c) => {
|
||||
await roleService.add(c, await c.req.json(), userContext.getUserId(c));
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
app.put('/role/setDefault', async (c) => {
|
||||
await roleService.setDefault(c, await c.req.json());
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
|
||||
app.put('/role/set', async (c) => {
|
||||
await roleService.setRole(c, await c.req.json());
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
app.get('/role/permTree', async (c) => {
|
||||
const tree = await permService.tree(c);
|
||||
return c.json(result.ok(tree));
|
||||
});
|
||||
|
||||
app.delete('/role/delete', async (c) => {
|
||||
await roleService.delete(c, c.req.query());
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
app.get('/role/list', async (c) => {
|
||||
const roleList = await roleService.roleList(c);
|
||||
return c.json(result.ok(roleList));
|
||||
});
|
||||
|
||||
app.get('/role/selectUse', async (c) => {
|
||||
const roleList = await roleService.roleSelectUse(c);
|
||||
return c.json(result.ok(roleList));
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -5,9 +5,19 @@ import settingService from '../service/setting-service';
|
||||
app.put('/setting/set', async (c) => {
|
||||
await settingService.set(c, await c.req.json());
|
||||
return c.json(result.ok());
|
||||
})
|
||||
});
|
||||
|
||||
app.get('/setting/query', async (c) => {
|
||||
const setting = await settingService.query(c);
|
||||
const setting = await settingService.get(c);
|
||||
return c.json(result.ok(setting));
|
||||
})
|
||||
});
|
||||
|
||||
app.put('/setting/setBackground', async (c) => {
|
||||
const key = await settingService.setBackground(c, await c.req.json());
|
||||
return c.json(result.ok(key));
|
||||
});
|
||||
|
||||
app.delete('/setting/physicsDeleteAll', async (c) => {
|
||||
await settingService.physicsDeleteAll(c);
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import app from '../hono/hono';
|
||||
import startService from '../service/star-service';
|
||||
import starService from '../service/star-service';
|
||||
import userContext from '../security/user-context';
|
||||
import result from '../model/result';
|
||||
|
||||
app.post('/star/add', async (c) => {
|
||||
await startService.add(c, await c.req.json(), await userContext.getUserId(c));
|
||||
await starService.add(c, await c.req.json(), userContext.getUserId(c));
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
app.get('/star/list', async (c) => {
|
||||
const data = await startService.list(c, c.req.query(), await userContext.getUserId(c));
|
||||
const data = await starService.list(c, c.req.query(), userContext.getUserId(c));
|
||||
return c.json(result.ok(data));
|
||||
});
|
||||
|
||||
app.delete('/star/cancel', async (c) => {
|
||||
await startService.cancel(c, await c.req.query(), await userContext.getUserId(c));
|
||||
await starService.cancel(c, await c.req.query(), userContext.getUserId(c));
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
13
mail-worker/src/api/sys-email-api.js
Normal file
13
mail-worker/src/api/sys-email-api.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import app from '../hono/hono';
|
||||
import emailService from '../service/email-service';
|
||||
import result from '../model/result';
|
||||
|
||||
app.get('/sys-email/list',async (c) => {
|
||||
const data = await emailService.allList(c, c.req.query());
|
||||
return c.json(result.ok(data));
|
||||
})
|
||||
|
||||
app.delete('/sys-email/delete',async (c) => {
|
||||
const list = await emailService.physicsDelete(c, c.req.query());
|
||||
return c.json(result.ok(list));
|
||||
})
|
||||
@@ -3,21 +3,42 @@ import userService from '../service/user-service';
|
||||
import result from '../model/result';
|
||||
import userContext from '../security/user-context';
|
||||
|
||||
app.get('/user/loginUserInfo', async (c) => {
|
||||
const user = await userService.loginUserInfo(c, await userContext.getUserId(c));
|
||||
return c.json(result.ok(user));
|
||||
});
|
||||
|
||||
app.put('/user/resetPassword', async (c) => {
|
||||
await userService.resetPassword(c, await c.req.json(), await userContext.getUserId(c));
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
app.delete('/user/delete', async (c) => {
|
||||
await userService.delete(c, await userContext.getUserId(c));
|
||||
await userService.physicsDelete(c, c.req.query());
|
||||
return c.json(result.ok());
|
||||
})
|
||||
});
|
||||
|
||||
app.put('/user/setPwd', async (c) => {
|
||||
await userService.setPwd(c, await c.req.json());
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
app.put('/user/setStatus', async (c) => {
|
||||
await userService.setStatus(c, await c.req.json());
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
app.put('/user/setType', async (c) => {
|
||||
await userService.setType(c, await c.req.json());
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
app.get('/user/list', async (c) => {
|
||||
const data = await userService.list(c, c.req.query(), userContext.getUserId(c));
|
||||
return c.json(result.ok(data));
|
||||
});
|
||||
|
||||
app.post('/user/add', async (c) => {
|
||||
await userService.add(c, await c.req.json());
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
app.put('/user/resetSendCount', async (c) => {
|
||||
await userService.resetSendCount(c, await c.req.json());
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
app.put('/user/restore', async (c) => {
|
||||
await userService.restore(c, await c.req.json());
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
const constant = {
|
||||
|
||||
TOKEN_HEADER: 'Authorization',
|
||||
JWT_UID: 'user_id:',
|
||||
JWT_TOKEN: 'token:',
|
||||
TOKEN_EXPIRE: 60 * 60 * 24 * 30,
|
||||
ATTACHMENT_PREFIX: 'attachments/'
|
||||
ATTACHMENT_PREFIX: 'attachments/',
|
||||
BACKGROUND_PREFIX: 'static/background/',
|
||||
ADMIN_ROLE: {
|
||||
name: '超级管理员',
|
||||
sendCount: 0,
|
||||
sendType: 'count',
|
||||
accountCount: 0
|
||||
}
|
||||
}
|
||||
|
||||
export default constant
|
||||
|
||||
@@ -1,8 +1,51 @@
|
||||
export const userConst = {
|
||||
type: {
|
||||
COMMON: 1,
|
||||
ADMIN: 0,
|
||||
status: {
|
||||
NORMAL: 0,
|
||||
BAN: 1
|
||||
}
|
||||
}
|
||||
|
||||
export const roleConst = {
|
||||
isDefault: {
|
||||
CLOSE: 0,
|
||||
OPEN: 1
|
||||
},
|
||||
sendType: {
|
||||
COUNT: 'count',
|
||||
DAY: 'day'
|
||||
}
|
||||
}
|
||||
|
||||
export const permConst = {
|
||||
type: {
|
||||
BUTTON: 2,
|
||||
}
|
||||
}
|
||||
|
||||
export const emailConst = {
|
||||
type: {
|
||||
SEND: 1,
|
||||
RECEIVE: 0,
|
||||
},
|
||||
status: {
|
||||
RECEIVE: 0,
|
||||
SENT: 1,
|
||||
DELIVERED: 2,
|
||||
BOUNCED: 3,
|
||||
COMPLAINED: 4,
|
||||
DELAYED: 5
|
||||
}
|
||||
}
|
||||
|
||||
export const attConst = {
|
||||
status: {
|
||||
NORMAL: 0,
|
||||
UNUSED: 1
|
||||
},
|
||||
type: {
|
||||
ATT: 0,
|
||||
EMBED: 1
|
||||
}
|
||||
}
|
||||
|
||||
export const settingConst = {
|
||||
@@ -16,7 +59,7 @@ export const settingConst = {
|
||||
},
|
||||
send: {
|
||||
OPEN: 0,
|
||||
CLOSE: 1,
|
||||
CLOSE: 1
|
||||
},
|
||||
addEmail: {
|
||||
OPEN: 0,
|
||||
@@ -36,6 +79,7 @@ export const settingConst = {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const isDel = {
|
||||
DELETE: 1,
|
||||
NORMAL: 0
|
||||
|
||||
@@ -3,9 +3,7 @@ import emailService from '../service/email-service';
|
||||
import accountService from '../service/account-service';
|
||||
import settingService from '../service/setting-service';
|
||||
import attService from '../service/att-service';
|
||||
import r2Service from '../service/r2-service';
|
||||
import constant from '../const/constant';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import fileUtils from '../utils/file-utils';
|
||||
|
||||
export async function email(message, env, ctx) {
|
||||
@@ -40,35 +38,29 @@ export async function email(message, env, ctx) {
|
||||
accountId: account.accountId
|
||||
};
|
||||
|
||||
const emailRow = await emailService.receive({ env }, params);
|
||||
const attachments = [];
|
||||
const cidAttachments = [];
|
||||
|
||||
if (!env.r2) {
|
||||
console.warn('r2对象存储未配置, 附件取消保存');
|
||||
return;
|
||||
}
|
||||
|
||||
if (email.attachments.length > 0) {
|
||||
|
||||
let attachments = email.attachments.map(item => {
|
||||
let attachment = { ...item };
|
||||
attachment.emailId = emailRow.emailId;
|
||||
attachment.userId = emailRow.userId;
|
||||
attachment.accountId = emailRow.accountId;
|
||||
attachment.key = constant.ATTACHMENT_PREFIX + uuidv4() + fileUtils.getExtFileName(item.filename);
|
||||
attachment.size = item.content.length ?? item.content.byteLength;
|
||||
return attachment;
|
||||
});
|
||||
|
||||
await attService.addAtt({ env }, attachments);
|
||||
|
||||
for (let attachment of attachments) {
|
||||
await r2Service.putObj({ env }, attachment.key, attachment.content, {
|
||||
contentType: attachment.mimeType,
|
||||
contentDisposition: `attachment; filename="${attachment.filename}"`
|
||||
});
|
||||
for(let item of email.attachments) {
|
||||
let attachment = { ...item };
|
||||
attachment.key = constant.ATTACHMENT_PREFIX + await fileUtils.getBuffHash(attachment.content) + fileUtils.getExtFileName(item.filename);
|
||||
attachment.size = item.content.length ?? item.content.byteLength;
|
||||
attachments.push(attachment);
|
||||
if (attachment.contentId) {
|
||||
cidAttachments.push(attachment)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const emailRow = await emailService.receive({ env }, params, cidAttachments);
|
||||
|
||||
attachments.forEach(attachment => {
|
||||
attachment.emailId = emailRow.emailId;
|
||||
attachment.userId = emailRow.userId;
|
||||
attachment.accountId = emailRow.accountId;
|
||||
})
|
||||
|
||||
await attService.addAtt({ env }, attachments);
|
||||
|
||||
} catch (e) {
|
||||
console.error('邮件接收异常: ', e);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ export const att = sqliteTable('attachments', {
|
||||
filename: text('filename'),
|
||||
mimeType: text('mime_type'),
|
||||
size: integer('size'),
|
||||
status: text('status').default(0).notNull(),
|
||||
type: integer('type').default(0).notNull(),
|
||||
disposition: text('disposition'),
|
||||
related: text('related'),
|
||||
contentId: text('content_id'),
|
||||
|
||||
@@ -10,6 +10,10 @@ export const email = sqliteTable('email', {
|
||||
subject: text('subject'),
|
||||
text: text('text'),
|
||||
content: text('content'),
|
||||
type: integer('type').default(0).notNull(),
|
||||
status: integer('status').default(0).notNull(),
|
||||
resendEmailId: text('resend_email_id'),
|
||||
message: text('message'),
|
||||
createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
isDel: integer('is_del').default(0).notNull()
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { drizzle } from 'drizzle-orm/d1';
|
||||
|
||||
export default function orm(c) {
|
||||
return drizzle(c.env.db,{logger: false})
|
||||
return drizzle(c.env.db,{logger: c.env.orm_log})
|
||||
}
|
||||
|
||||
10
mail-worker/src/entity/perm.js
Normal file
10
mail-worker/src/entity/perm.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { sqliteTable, text, integer} from 'drizzle-orm/sqlite-core';
|
||||
export const perm = sqliteTable('perm', {
|
||||
permId: integer('perm_id').primaryKey({ autoIncrement: true }),
|
||||
name: text('name').notNull(),
|
||||
permKey: text('perm_key'),
|
||||
pid: integer('pid').notNull().default(0),
|
||||
type: integer('type').notNull().default(2),
|
||||
sort: integer('sort')
|
||||
});
|
||||
export default perm
|
||||
7
mail-worker/src/entity/role-perm.js
Normal file
7
mail-worker/src/entity/role-perm.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { sqliteTable, text, integer} from 'drizzle-orm/sqlite-core';
|
||||
export const rolePerm = sqliteTable('role_perm', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
roleId: integer('role_id'),
|
||||
permId: integer('perm_id')
|
||||
});
|
||||
export default rolePerm
|
||||
16
mail-worker/src/entity/role.js
Normal file
16
mail-worker/src/entity/role.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { sqliteTable, text, integer} from 'drizzle-orm/sqlite-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
export const role = sqliteTable('role', {
|
||||
roleId: integer('role_id').primaryKey({ autoIncrement: true }),
|
||||
name: text('name').notNull(),
|
||||
key: text('key').notNull(),
|
||||
description: text('description'),
|
||||
sort: integer('sort'),
|
||||
isDefault: integer('is_default').default(0),
|
||||
createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
userId: integer('user_id'),
|
||||
sendCount: integer('send_count'),
|
||||
sendType: text('send_type').default('count'),
|
||||
accountCount: integer('account_count')
|
||||
});
|
||||
export default role
|
||||
@@ -6,7 +6,13 @@ export const setting = sqliteTable('setting', {
|
||||
manyEmail: integer('many_email').default(1).notNull(),
|
||||
addEmail: integer('add_email').default(0).notNull(),
|
||||
autoRefreshTime: integer('auto_refresh_time').default(0).notNull(),
|
||||
addEmailVerify: integer('add_email_verify').default(1).notNull(),
|
||||
registerVerify: integer('register_verify').default(1).notNull(),
|
||||
addEmailVerify: integer('add_email_verify').default(1).notNull()
|
||||
send: integer('send').default(1).notNull(),
|
||||
r2Domain: text('r2_domain'),
|
||||
secretKey: text('secret_key'),
|
||||
siteKey: text('site_key'),
|
||||
background: text('background'),
|
||||
resendTokens: text('resend_tokens').default("{}").notNull(),
|
||||
});
|
||||
export default setting
|
||||
|
||||
@@ -9,6 +9,13 @@ const user = sqliteTable('user', {
|
||||
status: integer('status').default(0).notNull(),
|
||||
createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`),
|
||||
activeTime: text('active_time'),
|
||||
isDel: integer('is_del').default(0).notNull(),
|
||||
createIp: text('create_ip'),
|
||||
activeIp: text('active_ip'),
|
||||
os: text('os'),
|
||||
browser: text('browser'),
|
||||
device: text('device'),
|
||||
sort: text('sort').default(0),
|
||||
sendCount: text('send_count').default(0),
|
||||
isDel: integer('is_del').default(0).notNull()
|
||||
});
|
||||
export default user
|
||||
|
||||
@@ -1,41 +1,11 @@
|
||||
import { Hono } from 'hono';
|
||||
|
||||
const app = new Hono();
|
||||
import initDB from '../init/init-db';
|
||||
import initCache from '../init/init-cache';
|
||||
import verify from '../security/security';
|
||||
|
||||
import result from '../model/result';
|
||||
import { cors } from 'hono/cors';
|
||||
|
||||
app.use('*', cors());
|
||||
|
||||
let initStatus = true;
|
||||
app.use('*', async (c, next) => {
|
||||
if (initStatus) {
|
||||
await initDB(c);
|
||||
initStatus = false;
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
|
||||
let initCacheStatus = true;
|
||||
app.use('*', async (c, next) => {
|
||||
if (initCacheStatus) {
|
||||
await initCache(c);
|
||||
initCacheStatus = false;
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
|
||||
app.use('*', async (c, next) => {
|
||||
if(await verify(c)) {
|
||||
await next();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
app.onError((err, c) => {
|
||||
if (err.name === 'BizError') {
|
||||
console.log(err.message);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import app from './hono';
|
||||
import '../security/security'
|
||||
|
||||
import '../api/email-api';
|
||||
import '../api/user-api';
|
||||
import '../api/login-api';
|
||||
@@ -6,4 +8,11 @@ import '../api/setting-api';
|
||||
import '../api/account-api';
|
||||
import '../api/star-api';
|
||||
import '../api/test-api';
|
||||
import '../api/r2-api';
|
||||
import '../api/resend-api';
|
||||
import '../api/user-api';
|
||||
import '../api/my-api';
|
||||
import '../api/role-api'
|
||||
import '../api/sys-email-api'
|
||||
import '../api/init-api'
|
||||
export default app;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import app from './hono/webs';
|
||||
import { email } from './email/email';
|
||||
|
||||
import userService from './service/user-service';
|
||||
export default {
|
||||
fetch(req, env, ctx) {
|
||||
async fetch(req, env, ctx) {
|
||||
const url = new URL(req.url)
|
||||
console.log(url.pathname)
|
||||
|
||||
|
||||
if (url.pathname.startsWith('/api/')) {
|
||||
url.pathname = url.pathname.replace('/api', '')
|
||||
req = new Request(url.toString(), req)
|
||||
@@ -13,5 +14,8 @@ export default {
|
||||
|
||||
return env.assets.fetch(req);
|
||||
},
|
||||
email: email
|
||||
email: email,
|
||||
async scheduled(c, env, ctx) {
|
||||
await userService.resetDaySendCount({ env })
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import settingService from '../service/setting-service';
|
||||
export default async function initCache(c) {
|
||||
await settingService.refresh(c)
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
export default async function initDB(c) {
|
||||
await c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS email (
|
||||
email_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
send_email TEXT,
|
||||
name TEXT,
|
||||
receive_email TEXT NOT NULL,
|
||||
account_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
subject TEXT,
|
||||
content TEXT,
|
||||
text TEXT,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
is_del INTEGER DEFAULT 0 NOT NULL
|
||||
)
|
||||
`).run();
|
||||
|
||||
await c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS star (
|
||||
star_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
email_id INTEGER NOT NULL,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
)
|
||||
`).run();
|
||||
|
||||
await c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS attachments (
|
||||
att_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
email_id INTEGER NOT NULL,
|
||||
account_id INTEGER NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
filename TEXT,
|
||||
mime_type TEXT,
|
||||
size INTEGER,
|
||||
disposition TEXT,
|
||||
related TEXT,
|
||||
content_id TEXT,
|
||||
encoding TEXT,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
)
|
||||
`).run();
|
||||
|
||||
await c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS user (
|
||||
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL,
|
||||
type INTEGER DEFAULT 1 NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
salt TEXT NOT NULL,
|
||||
status INTEGER DEFAULT 0 NOT NULL,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
active_time DATETIME,
|
||||
is_del INTEGER DEFAULT 0 NOT NULL
|
||||
)
|
||||
`).run();
|
||||
|
||||
await c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS account (
|
||||
account_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL,
|
||||
status INTEGER DEFAULT 0 NOT NULL,
|
||||
latest_email_time DATETIME,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
user_id INTEGER NOT NULL,
|
||||
is_del INTEGER DEFAULT 0 NOT NULL
|
||||
)
|
||||
`).run();
|
||||
|
||||
await c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS setting (
|
||||
register INTEGER NOT NULL,
|
||||
receive INTEGER NOT NULL,
|
||||
add_email INTEGER NOT NULL,
|
||||
many_email INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
auto_refresh_time INTEGER NOT NULL,
|
||||
register_verify INTEGER NOT NULL,
|
||||
add_email_verify INTEGER NOT NULL
|
||||
)
|
||||
`).run();
|
||||
|
||||
await c.env.db.prepare(`
|
||||
INSERT INTO setting (register, receive,add_email,many_email,title,auto_refresh_time,register_verify,add_email_verify)
|
||||
SELECT 0, 0, 0, 1, 'Cloud 邮箱', 0, 1, 1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM setting)
|
||||
`).run();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,59 +2,152 @@ import BizError from '../error/biz-error';
|
||||
import constant from '../const/constant';
|
||||
import jwtUtils from '../utils/jwt-utils';
|
||||
import KvConst from '../const/kv-const';
|
||||
import { userConst } from '../const/entity-const';
|
||||
import dayjs from 'dayjs';
|
||||
import userService from '../service/user-service';
|
||||
import permService from '../service/perm-service';
|
||||
import app from '../hono/hono';
|
||||
|
||||
const exclude = [
|
||||
'/login',
|
||||
'/register',
|
||||
'/setting/query'
|
||||
'/setting/query',
|
||||
'/file',
|
||||
'/webhooks',
|
||||
'/init'
|
||||
];
|
||||
|
||||
const adminPerm = [
|
||||
'/setting/set'
|
||||
]
|
||||
const requirePerms = [
|
||||
'/email/send',
|
||||
'/email/delete',
|
||||
'/account/list',
|
||||
'/account/delete',
|
||||
'/account/add',
|
||||
'/my/delete',
|
||||
'/role/add',
|
||||
'/role/list',
|
||||
'/role/delete',
|
||||
'/role/tree',
|
||||
'/role/set',
|
||||
'/role/setDefault',
|
||||
'/sys-email/list',
|
||||
'/sys-email/delete',
|
||||
'/setting/physicsDeleteAll',
|
||||
'/setting/setBackground',
|
||||
'/setting/set',
|
||||
'/user/delete',
|
||||
'/user/setPwd',
|
||||
'/user/setStatus',
|
||||
'/user/setType',
|
||||
'/user/star',
|
||||
'/user/list',
|
||||
'/user/resetSendCount',
|
||||
'/user/add'
|
||||
];
|
||||
|
||||
const verify = async (c) => {
|
||||
const premKey = {
|
||||
'email:delete': ['/email/delete'],
|
||||
'email:send': ['/email/send'],
|
||||
'account:add': ['/account/add'],
|
||||
'account:query': ['/account/list'],
|
||||
'account:delete': ['/account/delete'],
|
||||
'my:delete': ['/my/delete'],
|
||||
'role:add': ['/role/add'],
|
||||
'role:set': ['/role/set','/role/setDefault'],
|
||||
'role:query': ['/role/list', '/role/tree'],
|
||||
'role:delete': ['/role/delete'],
|
||||
'user:query': ['/user/list'],
|
||||
'user:add': ['/user/add'],
|
||||
'user:reset-send': ['/user/resetSendCount'],
|
||||
'user:set-pwd': ['/user/setPwd'],
|
||||
'user:set-status': ['/user/setStatus'],
|
||||
'user:set-type': ['/user/setType'],
|
||||
'user:star': ['/user/star'],
|
||||
'user:delete': ['/user/delete'],
|
||||
'sys-email:query': ['/sys-email/list'],
|
||||
'sys-email:delete': ['/sys-email/delete'],
|
||||
'setting:query': [],
|
||||
'setting:set': ['/setting/set', '/setting/setBackground'],
|
||||
'setting:clean': ['/setting/physicsDeleteAll']
|
||||
};
|
||||
|
||||
if (c.req.path.startsWith('/test')) {
|
||||
return true
|
||||
app.use('*', async (c, next) => {
|
||||
|
||||
const path = c.req.path;
|
||||
|
||||
if (path.startsWith('/test')) {
|
||||
return await next();
|
||||
}
|
||||
|
||||
if (exclude.includes(c.req.path)) {
|
||||
return true;
|
||||
const index = exclude.findIndex(item => {
|
||||
return path.startsWith(item);
|
||||
});
|
||||
|
||||
if (index > -1) {
|
||||
return await next();
|
||||
}
|
||||
|
||||
|
||||
const jwt = c.req.header(constant.TOKEN_HEADER);
|
||||
|
||||
const result = await jwtUtils.verifyToken(c, jwt)
|
||||
const result = await jwtUtils.verifyToken(c, jwt);
|
||||
|
||||
if (!result) {
|
||||
throw new BizError('身份认证失效,请重新登录',401)
|
||||
throw new BizError('身份认证失效,请重新登录', 401);
|
||||
}
|
||||
|
||||
const {userId, token} = result
|
||||
const authInfo = await c.env.kv.get(KvConst.AUTH_INFO + userId,{ type: 'json' });
|
||||
const { userId, token } = result;
|
||||
const authInfo = await c.env.kv.get(KvConst.AUTH_INFO + userId, { type: 'json' });
|
||||
|
||||
if (!authInfo) {
|
||||
throw new BizError('身份认证失效,请重新登录',401)
|
||||
throw new BizError('身份认证失效,请重新登录', 401);
|
||||
}
|
||||
|
||||
if(!authInfo.tokens.includes(token)) {
|
||||
throw new BizError('身份认证失效,请重新登录',401)
|
||||
if (!authInfo.tokens.includes(token)) {
|
||||
throw new BizError('身份认证失效,请重新登录', 401);
|
||||
}
|
||||
|
||||
if (adminPerm.includes(c.req.path) && c.env.admin !== authInfo.user.email) {
|
||||
throw new BizError('权限不足',403)
|
||||
|
||||
const permIndex = requirePerms.findIndex(item => {
|
||||
return path.startsWith(item);
|
||||
});
|
||||
|
||||
if (permIndex > -1) {
|
||||
|
||||
const permKeys = await permService.userPermKeys(c, authInfo.user.userId);
|
||||
|
||||
const userPaths = permKeyToPaths(permKeys);
|
||||
|
||||
const userPermIndex = userPaths.findIndex(item => {
|
||||
return path.startsWith(item);
|
||||
});
|
||||
|
||||
if (userPermIndex === -1 && authInfo.user.email !== c.env.admin) {
|
||||
throw new BizError('权限不足', 403);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (dayjs().diff(dayjs(authInfo.createTime), 'day') >= 1) {
|
||||
authInfo.refreshTime = new Date().toISOString()
|
||||
const refreshTime = dayjs(authInfo.refreshTime).startOf('day');
|
||||
const nowTime = dayjs().startOf('day')
|
||||
|
||||
if (!nowTime.isSame(refreshTime)) {
|
||||
authInfo.refreshTime = dayjs().toISOString();
|
||||
await userService.updateUserInfo(c, authInfo.user.userId);
|
||||
await c.env.kv.put(KvConst.AUTH_INFO + userId, JSON.stringify(authInfo), { expirationTtl: constant.TOKEN_EXPIRE });
|
||||
}
|
||||
|
||||
return true
|
||||
c.set('user',authInfo.user)
|
||||
|
||||
};
|
||||
return await next();
|
||||
});
|
||||
|
||||
export default verify;
|
||||
function permKeyToPaths(permKeys) {
|
||||
const paths = [];
|
||||
for (const key of permKeys) {
|
||||
const routeList = premKey[key];
|
||||
if (routeList && Array.isArray(routeList)) {
|
||||
paths.push(...routeList);
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@ import JwtUtils from '../utils/jwt-utils';
|
||||
import constant from '../const/constant';
|
||||
|
||||
const userContext = {
|
||||
async getUserId(c) {
|
||||
const jwt = c.req.header(constant.TOKEN_HEADER);
|
||||
const { userId } = await JwtUtils.verifyToken(c, jwt);
|
||||
return Number(userId);
|
||||
getUserId(c) {
|
||||
return c.get('user').userId;
|
||||
},
|
||||
|
||||
getUser(c) {
|
||||
return c.get('user');
|
||||
},
|
||||
|
||||
async getToken(c) {
|
||||
const jwt = c.req.header(constant.TOKEN_HEADER);
|
||||
const { token } = JwtUtils.verifyToken(c,jwt);
|
||||
|
||||
@@ -5,16 +5,16 @@ import userService from './user-service';
|
||||
import emailService from './email-service';
|
||||
import orm from '../entity/orm';
|
||||
import account from '../entity/account';
|
||||
import { and, asc, eq, gt } from 'drizzle-orm';
|
||||
import { and, asc, eq, gt, inArray, count, like } from 'drizzle-orm';
|
||||
import { isDel } from '../const/entity-const';
|
||||
import settingService from './setting-service';
|
||||
import turnstileService from './turnstile-service';
|
||||
import roleService from './role-service';
|
||||
|
||||
const accountService = {
|
||||
|
||||
async add(c, params, userId) {
|
||||
|
||||
|
||||
if (!await settingService.isAddEmail(c)) {
|
||||
throw new BizError('添加邮箱功能已关闭');
|
||||
}
|
||||
@@ -30,7 +30,7 @@ const accountService = {
|
||||
}
|
||||
|
||||
if (!c.env.domain.includes(emailUtils.getDomain(email))) {
|
||||
throw new BizError('未配置改邮箱域名');
|
||||
throw new BizError('不存在的邮箱域名');
|
||||
}
|
||||
|
||||
const accountRow = await this.selectByEmailIncludeDel(c, email);
|
||||
@@ -43,6 +43,16 @@ const accountService = {
|
||||
throw new BizError('该邮箱已被注册');
|
||||
}
|
||||
|
||||
const userRow = await userService.selectById(c, userId);
|
||||
const roleRow = await roleService.selectById(c, userRow.type);
|
||||
|
||||
if (roleRow.accountCount) {
|
||||
|
||||
const userAccountCount = await accountService.countUserAccount(c, userId)
|
||||
if(userAccountCount >= roleRow.accountCount) throw new BizError(`添加邮箱数量限制${roleRow.accountCount}个`, 403);
|
||||
|
||||
}
|
||||
|
||||
if (await settingService.isAddEmailVerify(c)) {
|
||||
await turnstileService.verify(c, token);
|
||||
}
|
||||
@@ -105,7 +115,6 @@ const accountService = {
|
||||
and(eq(account.userId, userId),
|
||||
eq(account.accountId, accountId)))
|
||||
.run();
|
||||
await emailService.removeByAccountId(c, accountId);
|
||||
},
|
||||
|
||||
selectById(c, accountId) {
|
||||
@@ -119,8 +128,50 @@ const accountService = {
|
||||
await orm(c).insert(account).values({ ...params }).returning();
|
||||
},
|
||||
|
||||
async removeByUserId(c, userId) {
|
||||
await orm(c).update(account).set({ isDel: isDel.DELETE }).where(eq(account.userId, userId)).run();
|
||||
async physicsDeleteAll(c) {
|
||||
const accountIdsRow = await orm(c).select({accountId: account.accountId}).from(account).where(eq(account.isDel,isDel.DELETE)).limit(99);
|
||||
if (accountIdsRow.length === 0) {
|
||||
return;
|
||||
}
|
||||
const accountIds = accountIdsRow.map(item => item.accountId)
|
||||
await emailService.physicsDeleteAccountIds(c, accountIds);
|
||||
await orm(c).delete(account).where(inArray(account.accountId,accountIds)).run();
|
||||
if (accountIdsRow.length === 99) {
|
||||
await this.physicsDeleteAll(c)
|
||||
}
|
||||
},
|
||||
|
||||
async physicsDeleteByUserIds(c, userIds) {
|
||||
await emailService.physicsDeleteUserIds(c, userIds);
|
||||
await orm(c).delete(account).where(inArray(account.userId,userIds)).run();
|
||||
},
|
||||
|
||||
async selectUserAccountCountList(c, userIds, del = isDel.NORMAL) {
|
||||
const result = await orm(c)
|
||||
.select({
|
||||
userId: account.userId,
|
||||
count: count(account.accountId)
|
||||
})
|
||||
.from(account)
|
||||
.where(and(
|
||||
inArray(account.userId, userIds),
|
||||
eq(account.isDel, del)
|
||||
))
|
||||
.groupBy(account.userId)
|
||||
return result;
|
||||
},
|
||||
|
||||
async countUserAccount(c, userId) {
|
||||
const { num } = await orm(c).select({num: count()}).from(account).where(and(eq(account.userId, userId),eq(account.isDel, isDel.NORMAL))).get();
|
||||
return num;
|
||||
},
|
||||
|
||||
async restoreByEmail(c, email) {
|
||||
await orm(c).update(account).set({isDel: isDel.NORMAL}).where(eq(account.email, email)).run();
|
||||
},
|
||||
|
||||
async restoreByUserId(c, userId) {
|
||||
await orm(c).update(account).set({isDel: isDel.NORMAL}).where(eq(account.userId, userId)).run();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,21 +1,160 @@
|
||||
import orm from '../entity/orm';
|
||||
import { att } from '../entity/att';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import { and, eq, isNull, inArray, notInArray } from 'drizzle-orm';
|
||||
import r2Service from './r2-service';
|
||||
import constant from '../const/constant';
|
||||
import fileUtils from '../utils/file-utils';
|
||||
import { attConst } from '../const/entity-const';
|
||||
import { parseHTML } from 'linkedom';
|
||||
|
||||
const attService = {
|
||||
async addAtt(c, params) {
|
||||
await orm(c).insert(att).values(params).run();
|
||||
|
||||
async addAtt(c, attachments) {
|
||||
|
||||
for (let attachment of attachments) {
|
||||
await r2Service.putObj(c, attachment.key, attachment.content, {
|
||||
contentType: attachment.mimeType,
|
||||
contentDisposition: `attachment; filename="${attachment.filename}"`
|
||||
});
|
||||
}
|
||||
|
||||
await orm(c).insert(att).values(attachments).run();
|
||||
},
|
||||
async list(c, params, userId) {
|
||||
|
||||
list(c, params, userId) {
|
||||
const { emailId } = params;
|
||||
|
||||
const list = await orm(c).select().from(att).where(
|
||||
return orm(c).select().from(att).where(
|
||||
and(
|
||||
eq(att.emailId, emailId),
|
||||
eq(att.userId, userId)))
|
||||
.all();
|
||||
eq(att.userId, userId),
|
||||
eq(att.type, attConst.type.ATT),
|
||||
isNull(att.contentId)
|
||||
)
|
||||
).all();
|
||||
},
|
||||
|
||||
return list;
|
||||
async toImageUrlHtml(c, content, r2Domain) {
|
||||
|
||||
const { document } = parseHTML(content);
|
||||
|
||||
const images = Array.from(document.querySelectorAll('img'));
|
||||
|
||||
const attDataList = [];
|
||||
|
||||
for (const img of images) {
|
||||
|
||||
const src = img.getAttribute('src');
|
||||
if (src && src.startsWith('data:image')) {
|
||||
const file = fileUtils.base64ToFile(src);
|
||||
const buff = await file.arrayBuffer();
|
||||
const key = constant.ATTACHMENT_PREFIX + await fileUtils.getBuffHash(buff) + fileUtils.getExtFileName(file.name);
|
||||
img.setAttribute('src', r2Domain + '/' + key);
|
||||
|
||||
const attData = {};
|
||||
attData.key = key;
|
||||
attData.filename = file.name;
|
||||
attData.mimeType = file.type;
|
||||
attData.size = file.size;
|
||||
attData.buff = buff;
|
||||
|
||||
attDataList.push(attData);
|
||||
}
|
||||
|
||||
const hasInlineWidth = img.hasAttribute('width');
|
||||
const style = img.getAttribute('style') || '';
|
||||
const hasStyleWidth = /(^|\s)width\s*:\s*[^;]+/.test(style);
|
||||
|
||||
if (!hasInlineWidth && !hasStyleWidth) {
|
||||
const newStyle = (style ? style.trim().replace(/;$/, '') + '; ' : '') + 'max-width: 100%;';
|
||||
img.setAttribute('style', newStyle);
|
||||
}
|
||||
}
|
||||
return { attDataList, html: document.toString() };
|
||||
},
|
||||
|
||||
async saveSendAtt(c, attList, userId, accountId, emailId) {
|
||||
|
||||
const attDataList = [];
|
||||
|
||||
for (let att of attList) {
|
||||
att.buff = fileUtils.base64ToUint8Array(att.content);
|
||||
att.key = constant.ATTACHMENT_PREFIX + await fileUtils.getBuffHash(att.buff) + fileUtils.getExtFileName(att.filename);
|
||||
const attData = { userId, accountId, emailId };
|
||||
attData.key = att.key;
|
||||
attData.size = att.buff.length;
|
||||
attData.filename = att.filename;
|
||||
attData.mimeType = att.type;
|
||||
attData.type = attConst.type.ATT;
|
||||
attDataList.push(attData);
|
||||
}
|
||||
|
||||
await orm(c).insert(att).values(attDataList).run();
|
||||
|
||||
for (let att of attList) {
|
||||
await r2Service.putObj(c, att.key, att.buff, {
|
||||
contentType: att.type,
|
||||
contentDisposition: `attachment; filename="${att.filename}"`
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
async saveArticleAtt(c, attDataList, userId, accountId, emailId) {
|
||||
|
||||
for (let attData of attDataList) {
|
||||
attData.userId = userId;
|
||||
attData.emailId = emailId;
|
||||
attData.accountId = accountId;
|
||||
attData.type = attConst.type.EMBED;
|
||||
await r2Service.putObj(c, attData.key, attData.buff, {
|
||||
contentType: attData.mimeType
|
||||
});
|
||||
}
|
||||
|
||||
await orm(c).insert(att).values(attDataList).run();
|
||||
|
||||
},
|
||||
|
||||
async removeByUserIds(c, userIds) {
|
||||
await this.removeAttByField(c, att.userId, userIds);
|
||||
},
|
||||
|
||||
async removeByEmailIds(c, emailIds) {
|
||||
await this.removeAttByField(c, att.emailId, emailIds);
|
||||
},
|
||||
|
||||
async removeByAccountIds(c, accountIds) {
|
||||
await this.removeAttByField(c, att.accountId, accountIds);
|
||||
},
|
||||
|
||||
async removeAttByField(c, fieldName, fieldValues) {
|
||||
|
||||
const condition = inArray(fieldName, fieldValues);
|
||||
const attList = await orm(c).select().from(att).where(condition).limit(99);
|
||||
|
||||
if (attList.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attIds = attList.map(attRow => attRow.attId);
|
||||
const keys = attList.map(attRow => attRow.key);
|
||||
await orm(c).delete(att).where(inArray(att.attId, attIds)).run();
|
||||
|
||||
const existAttRows = await orm(c).select().from(att).where(inArray(att.key, keys)).all();
|
||||
const existKeys = existAttRows.map(attRow => attRow.key);
|
||||
const delKeyList = keys.filter(key => !existKeys.includes(key));
|
||||
if (delKeyList.length > 0) {
|
||||
await c.env.r2.delete(delKeyList);
|
||||
}
|
||||
|
||||
if (attList.length >= 99) {
|
||||
await this.removeAttByField(c, fieldName, fieldValues);
|
||||
}
|
||||
},
|
||||
|
||||
selectByEmailIds(c, emailIds) {
|
||||
return orm(c).select().from(att).where(and(inArray(att.emailId,emailIds),eq(att.type, attConst.type.ATT))).all();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,26 +1,47 @@
|
||||
import orm from '../entity/orm';
|
||||
import email from '../entity/email';
|
||||
import { isDel } from '../const/entity-const';
|
||||
import { and, desc, eq, gt, inArray, lt, sql } from 'drizzle-orm';
|
||||
import { emailConst, isDel, settingConst } from '../const/entity-const';
|
||||
import { and, desc, eq, gt, inArray, lt, count, asc, like } from 'drizzle-orm';
|
||||
import { star } from '../entity/star';
|
||||
import settingService from './setting-service';
|
||||
import accountService from './account-service';
|
||||
import BizError from '../error/biz-error';
|
||||
import emailUtils from '../utils/email-utils';
|
||||
import { Resend } from 'resend';
|
||||
import attService from './att-service';
|
||||
import { parseHTML } from 'linkedom';
|
||||
import userService from './user-service';
|
||||
import roleService from './role-service';
|
||||
import user from '../entity/user';
|
||||
import account from '../entity/account';
|
||||
import starService from './star-service';
|
||||
|
||||
const emailService = {
|
||||
|
||||
async list(c, params, userId) {
|
||||
|
||||
let { emailId, accountId, size } = params;
|
||||
let { emailId, type, accountId, size, timeSort } = params;
|
||||
|
||||
size = Number(size);
|
||||
emailId = Number(emailId);
|
||||
timeSort = Number(timeSort);
|
||||
|
||||
if (size > 30) {
|
||||
size = 30;
|
||||
}
|
||||
|
||||
if (!emailId) {
|
||||
emailId = 9999999999;
|
||||
|
||||
if (timeSort) {
|
||||
emailId = 0;
|
||||
} else {
|
||||
emailId = 9999999999;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const list = await orm(c)
|
||||
|
||||
const query = orm(c)
|
||||
.select({
|
||||
...email,
|
||||
starId: star.starId
|
||||
@@ -35,22 +56,57 @@ const emailService = {
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
lt(email.emailId, emailId),
|
||||
timeSort ? gt(email.emailId, emailId) : lt(email.emailId, emailId),
|
||||
eq(email.accountId, accountId),
|
||||
eq(email.userId, userId),
|
||||
eq(email.type, type),
|
||||
eq(email.isDel, isDel.NORMAL)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(email.emailId))
|
||||
.limit(size)
|
||||
.all();
|
||||
);
|
||||
|
||||
const resultList = list.map(item => ({
|
||||
if (timeSort) {
|
||||
query.orderBy(asc(email.emailId));
|
||||
} else {
|
||||
query.orderBy(desc(email.emailId));
|
||||
}
|
||||
|
||||
const listQuery = query.limit(size).all();
|
||||
|
||||
const totalQuery = orm(c).select({ total: count() }).from(email).where(
|
||||
and(
|
||||
eq(email.accountId, accountId),
|
||||
eq(email.userId, userId),
|
||||
eq(email.type, type),
|
||||
eq(email.isDel, isDel.NORMAL)
|
||||
)
|
||||
).get();
|
||||
|
||||
const latestEmailQuery = orm(c).select().from(email).where(
|
||||
and(
|
||||
eq(email.accountId, accountId),
|
||||
eq(email.userId, userId),
|
||||
eq(email.type, type),
|
||||
eq(email.isDel, isDel.NORMAL)
|
||||
))
|
||||
.orderBy(desc(email.emailId)).limit(1).get();
|
||||
|
||||
let [list, totalRow, latestEmail] = await Promise.all([listQuery, totalQuery, latestEmailQuery]);
|
||||
|
||||
list = list.map(item => ({
|
||||
...item,
|
||||
isStar: item.starId != null ? 1 : 0
|
||||
}));
|
||||
|
||||
return { list: resultList };
|
||||
const emailIds = list.map(item => item.emailId);
|
||||
|
||||
const attsList = await attService.selectByEmailIds(c, emailIds);
|
||||
|
||||
list.forEach(emailRow => {
|
||||
const atts = attsList.filter(attsRow => attsRow.emailId === emailRow.emailId);
|
||||
emailRow.attList = atts;
|
||||
});
|
||||
|
||||
return { list, total: totalRow.total, latestEmail };
|
||||
},
|
||||
|
||||
async delete(c, params, userId) {
|
||||
@@ -63,20 +119,139 @@ const emailService = {
|
||||
.run();
|
||||
},
|
||||
|
||||
receive(c, params) {
|
||||
receive(c, params, cidAttList) {
|
||||
|
||||
const { document } = parseHTML(params.content);
|
||||
|
||||
const images = Array.from(document.querySelectorAll('img'));
|
||||
|
||||
for (const img of images) {
|
||||
|
||||
const src = img.getAttribute('src');
|
||||
if (src && src.startsWith('cid:')) {
|
||||
|
||||
const cid = src.replace(/^cid:/, '');
|
||||
const attCidIndex = cidAttList.findIndex(cidAtt => cidAtt.contentId.replace(/^<|>$/g, '') === cid);
|
||||
|
||||
if (attCidIndex > -1) {
|
||||
const cidAtt = cidAttList[attCidIndex];
|
||||
img.setAttribute('src', '{{domain}}' + cidAtt.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
params.content = document.toString();
|
||||
return orm(c).insert(email).values({ ...params }).returning().get();
|
||||
},
|
||||
|
||||
async removeByAccountId(c, accountId) {
|
||||
await orm(c).update(email)
|
||||
.set({ isDel: isDel.DELETE })
|
||||
.where(eq(email.accountId, accountId))
|
||||
.run();
|
||||
async send(c, params, userId) {
|
||||
|
||||
let { accountId, name, receiveEmail, text, content, subject, attachments } = params;
|
||||
|
||||
const { resendTokens, r2Domain, send } = await settingService.query(c);
|
||||
|
||||
|
||||
const { attDataList, html } = await attService.toImageUrlHtml(c, content, r2Domain);
|
||||
|
||||
if (attDataList.length > 0 && !r2Domain) {
|
||||
throw new BizError('r2域名未配置不能发送正文图片');
|
||||
}
|
||||
|
||||
if (attDataList.length > 0 && !c.env.r2) {
|
||||
throw new BizError('r2对象存储未配置不能发送正文图片');
|
||||
}
|
||||
|
||||
if (attachments.length > 0 && !r2Domain) {
|
||||
throw new BizError('r2域名未配置不能发送附件');
|
||||
}
|
||||
|
||||
if (attachments.length > 0 && !c.env.r2) {
|
||||
throw new BizError('r2对象存储未配置不能发送附件');
|
||||
}
|
||||
|
||||
if (send === settingConst.send.CLOSE) {
|
||||
throw new BizError('邮箱发送功能已停用', 403);
|
||||
}
|
||||
|
||||
|
||||
const userRow = await userService.selectById(c, userId);
|
||||
const roleRow = await roleService.selectById(c, userRow.type);
|
||||
|
||||
if (userRow.sendCount >= roleRow.sendCount && c.env.admin !== userRow.email) {
|
||||
if (roleRow.sendType === 'day') throw new BizError('已到达每日发送数量限制', 403);
|
||||
if (roleRow.sendType === 'count') throw new BizError('已到达发送数量限制', 403);
|
||||
}
|
||||
|
||||
|
||||
const accountRow = await accountService.selectById(c, accountId);
|
||||
|
||||
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('非当前用户所属邮箱');
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
name = emailUtils.getName(accountRow.email);
|
||||
}
|
||||
|
||||
const resend = new Resend(resendToken);
|
||||
|
||||
const { data, error } = await resend.emails.send({
|
||||
from: `${name} <${accountRow.email}>`,
|
||||
to: [receiveEmail],
|
||||
subject: subject,
|
||||
text: text,
|
||||
html: html,
|
||||
attachments: attachments
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
throw new BizError(error.message);
|
||||
}
|
||||
|
||||
const emailData = {};
|
||||
emailData.sendEmail = accountRow.email;
|
||||
emailData.name = name;
|
||||
emailData.subject = subject;
|
||||
emailData.receiveEmail = receiveEmail;
|
||||
emailData.content = html;
|
||||
emailData.text = text;
|
||||
emailData.accountId = accountId;
|
||||
emailData.type = emailConst.type.SEND;
|
||||
emailData.userId = userId;
|
||||
emailData.status = emailConst.status.SENT;
|
||||
emailData.resendEmailId = data.id;
|
||||
|
||||
const emailRow = await orm(c).insert(email).values(emailData).returning().get();
|
||||
|
||||
if (attDataList.length > 0) {
|
||||
await attService.saveArticleAtt(c, attDataList, userId, accountId, emailRow.emailId);
|
||||
}
|
||||
|
||||
if (roleRow.sendCount) {
|
||||
await userService.incrUserService(c, 1, userId);
|
||||
}
|
||||
|
||||
if (attachments?.length > 0 && c.env.r2) {
|
||||
await attService.saveSendAtt(c, attachments, userId, accountId, emailRow.emailId);
|
||||
}
|
||||
|
||||
const attsList = await attService.selectByEmailIds(c, [emailRow.emailId]);
|
||||
emailRow.attList = attsList;
|
||||
|
||||
return emailRow;
|
||||
},
|
||||
|
||||
async removeByUserId(c, userId) {
|
||||
await orm(c).update(email).set({ isDel: isDel.DELETE }).where(eq(email.userId, userId)).run();
|
||||
},
|
||||
selectById(c, emailId) {
|
||||
return orm(c).select().from(email).where(
|
||||
and(eq(email.emailId, emailId),
|
||||
@@ -84,19 +259,186 @@ const emailService = {
|
||||
.get();
|
||||
},
|
||||
|
||||
latest(c, params, userId) {
|
||||
let { emailId,accountId } = params;
|
||||
return orm(c).select().from(email).where(
|
||||
async latest(c, params, userId) {
|
||||
let { emailId, accountId } = params;
|
||||
const list = orm(c).select().from(email).where(
|
||||
and(
|
||||
eq(email.userId, userId),
|
||||
eq(email.isDel, isDel.NORMAL),
|
||||
eq(email.accountId, accountId),
|
||||
eq(email.type, emailConst.type.RECEIVE),
|
||||
gt(email.emailId, emailId)
|
||||
))
|
||||
.orderBy(desc(email.emailId))
|
||||
.limit(20);
|
||||
}
|
||||
|
||||
const emailIds = list.map(item => item.emailId);
|
||||
const attsList = await attService.selectByEmailIds(c, emailIds);
|
||||
|
||||
list.forEach(emailRow => {
|
||||
const atts = attsList.filter(attsRow => attsRow.emailId === emailRow.emailId);
|
||||
emailRow.attList = atts;
|
||||
});
|
||||
|
||||
return list;
|
||||
},
|
||||
|
||||
|
||||
async physicsDeleteAll(c) {
|
||||
const emailIdsRow = await orm(c).select({ emailId: email.emailId }).from(email).where(eq(email.isDel, isDel.DELETE)).limit(99);
|
||||
if (emailIdsRow.length === 0) {
|
||||
return;
|
||||
}
|
||||
const emailIds = emailIdsRow.map(row => row.emailId);
|
||||
await attService.removeByEmailIds(c, emailIds);
|
||||
await starService.removeByEmailIds(c, emailIds);
|
||||
await orm(c).delete(email).where(inArray(email.emailId, emailIds)).run();
|
||||
if (emailIdsRow.length === 99) {
|
||||
await this.physicsDeleteAll(c);
|
||||
}
|
||||
},
|
||||
|
||||
async physicsDelete(c, params) {
|
||||
let { emailIds } = params;
|
||||
emailIds = emailIds.split(',').map(Number);
|
||||
await attService.removeByEmailIds(c, emailIds);
|
||||
await starService.removeByEmailIds(c, emailIds);
|
||||
await orm(c).delete(email).where(inArray(email.emailId, emailIds)).run();
|
||||
},
|
||||
|
||||
async physicsDeleteAccountIds(c, accountIds) {
|
||||
await attService.removeByAccountIds(c, accountIds);
|
||||
await orm(c).delete(email).where(inArray(email.accountId, accountIds)).run();
|
||||
},
|
||||
|
||||
async physicsDeleteUserIds(c, userIds) {
|
||||
await attService.removeByUserIds(c, userIds);
|
||||
await orm(c).delete(email).where(inArray(email.userId, userIds)).run();
|
||||
},
|
||||
|
||||
updateEmailStatus(c, params) {
|
||||
const { status, resendEmailId, message } = params;
|
||||
return orm(c).update(email).set({
|
||||
status: status,
|
||||
message: message
|
||||
}).where(eq(email.resendEmailId, resendEmailId)).returning().get();
|
||||
},
|
||||
|
||||
async selectUserEmailCountList(c, userIds, type, del = isDel.NORMAL) {
|
||||
const result = await orm(c)
|
||||
.select({
|
||||
userId: email.userId,
|
||||
count: count(email.emailId)
|
||||
})
|
||||
.from(email)
|
||||
.where(and(
|
||||
inArray(email.userId, userIds),
|
||||
eq(email.type, type),
|
||||
eq(email.isDel, del)
|
||||
))
|
||||
.groupBy(email.userId);
|
||||
return result;
|
||||
},
|
||||
|
||||
async allList(c, params) {
|
||||
|
||||
let { emailId, size, name, subject, accountEmail, userEmail, type, timeSort } = params;
|
||||
|
||||
size = Number(size);
|
||||
|
||||
emailId = Number(emailId);
|
||||
timeSort = Number(timeSort);
|
||||
|
||||
if (size > 30) {
|
||||
size = 30;
|
||||
}
|
||||
|
||||
if (!emailId) {
|
||||
|
||||
if (timeSort) {
|
||||
emailId = 0;
|
||||
} else {
|
||||
emailId = 9999999999;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const conditions = [];
|
||||
|
||||
|
||||
if (type === 'send') {
|
||||
conditions.push(eq(email.type, emailConst.type.SEND));
|
||||
}
|
||||
|
||||
if (type === 'receive') {
|
||||
conditions.push(eq(email.type, emailConst.type.RECEIVE));
|
||||
}
|
||||
|
||||
if (type === 'delete') {
|
||||
conditions.push(eq(email.isDel, isDel.DELETE));
|
||||
}
|
||||
|
||||
if (userEmail) {
|
||||
conditions.push(like(user.email, `${userEmail}%`));
|
||||
}
|
||||
|
||||
if (accountEmail) {
|
||||
conditions.push(like(account.email, `${accountEmail}%`));
|
||||
}
|
||||
|
||||
if (name) {
|
||||
conditions.push(like(email.name, `${name}%`));
|
||||
}
|
||||
|
||||
if (subject) {
|
||||
conditions.push(like(email.subject, `${subject}%`));
|
||||
}
|
||||
|
||||
const countConditions = [...conditions];
|
||||
|
||||
if (timeSort) {
|
||||
conditions.push(gt(email.emailId, emailId));
|
||||
} else {
|
||||
conditions.push(lt(email.emailId, emailId));
|
||||
}
|
||||
|
||||
const query = orm(c).select({ ...email, userEmail: user.email, accountEmail: account.email })
|
||||
.from(email)
|
||||
.leftJoin(user, eq(email.userId, user.userId))
|
||||
.leftJoin(account, eq(email.accountId, account.accountId))
|
||||
.where(and(...conditions));
|
||||
|
||||
const queryCount = orm(c).select({ total: count() })
|
||||
.from(email)
|
||||
.leftJoin(user, eq(email.userId, user.userId))
|
||||
.leftJoin(account, eq(email.accountId, account.accountId))
|
||||
.where(and(...countConditions));
|
||||
|
||||
if (timeSort) {
|
||||
query.orderBy(asc(email.emailId));
|
||||
} else {
|
||||
query.orderBy(desc(email.emailId));
|
||||
}
|
||||
|
||||
const listQuery = await query.limit(size).all();
|
||||
const totalQuery = await queryCount.get();
|
||||
|
||||
const [list, totalRow] = await Promise.all([listQuery, totalQuery]);
|
||||
|
||||
const emailIds = list.map(item => item.emailId);
|
||||
const attsList = await attService.selectByEmailIds(c, emailIds);
|
||||
|
||||
list.forEach(emailRow => {
|
||||
const atts = attsList.filter(attsRow => attsRow.emailId === emailRow.emailId);
|
||||
emailRow.attList = atts;
|
||||
});
|
||||
|
||||
return { list: list, total: totalRow.total };
|
||||
},
|
||||
|
||||
async restoreByUserId(c, userId) {
|
||||
await orm(c).update(email).set({ isDel: isDel.NORMAL }).where(eq(email.userId, userId)).run();
|
||||
}
|
||||
};
|
||||
|
||||
export default emailService;
|
||||
|
||||
249
mail-worker/src/service/init-service.js
Normal file
249
mail-worker/src/service/init-service.js
Normal file
@@ -0,0 +1,249 @@
|
||||
import settingService from './setting-service';
|
||||
|
||||
const initService = {
|
||||
async init(c) {
|
||||
|
||||
const secret = c.req.param('secret');
|
||||
|
||||
if (secret !== c.env.jwt_secret) {
|
||||
return c.text('secret不匹配');
|
||||
}
|
||||
|
||||
await this.intDB(c);
|
||||
await this.v1DB(c);
|
||||
await settingService.refresh(c);
|
||||
return c.text('初始化成功');
|
||||
},
|
||||
|
||||
async v1DB(c) {
|
||||
// 添加字段
|
||||
const ADD_COLUMN_SQL_LIST = [
|
||||
`ALTER TABLE email ADD COLUMN type INTEGER NOT NULL DEFAULT 0;`,
|
||||
`ALTER TABLE email ADD COLUMN status INTEGER NOT NULL DEFAULT 0;`,
|
||||
`ALTER TABLE email ADD COLUMN resend_email_id TEXT;`,
|
||||
`ALTER TABLE email ADD COLUMN message TEXT;`,
|
||||
|
||||
`ALTER TABLE setting ADD COLUMN resend_tokens TEXT NOT NULL DEFAULT '{}';`,
|
||||
`ALTER TABLE setting ADD COLUMN send INTEGER NOT NULL DEFAULT 0;`,
|
||||
`ALTER TABLE setting ADD COLUMN r2_domain TEXT;`,
|
||||
`ALTER TABLE setting ADD COLUMN site_key TEXT;`,
|
||||
`ALTER TABLE setting ADD COLUMN secret_key TEXT;`,
|
||||
`ALTER TABLE setting ADD COLUMN background TEXT;`,
|
||||
|
||||
`ALTER TABLE user ADD COLUMN create_ip TEXT;`,
|
||||
`ALTER TABLE user ADD COLUMN active_ip TEXT;`,
|
||||
`ALTER TABLE user ADD COLUMN os TEXT;`,
|
||||
`ALTER TABLE user ADD COLUMN browser TEXT;`,
|
||||
`ALTER TABLE user ADD COLUMN device TEXT;`,
|
||||
`ALTER TABLE user ADD COLUMN sort INTEGER NOT NULL DEFAULT 0;`,
|
||||
`ALTER TABLE user ADD COLUMN send_count INTEGER NOT NULL DEFAULT 0;`,
|
||||
|
||||
`ALTER TABLE attachments ADD COLUMN status INTEGER NOT NULL DEFAULT 0;`,
|
||||
`ALTER TABLE attachments ADD COLUMN type INTEGER NOT NULL DEFAULT 0;`
|
||||
];
|
||||
|
||||
for (let sql of ADD_COLUMN_SQL_LIST) {
|
||||
try {
|
||||
await c.env.db.prepare(sql).run();
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段添加,原因:${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建 perm 表并初始化
|
||||
await c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS perm (
|
||||
perm_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
perm_key TEXT,
|
||||
pid INTEGER NOT NULL DEFAULT 0,
|
||||
type INTEGER NOT NULL DEFAULT 2,
|
||||
sort INTEGER
|
||||
)
|
||||
`).run();
|
||||
|
||||
const {permTotal} = await c.env.db.prepare(`SELECT COUNT(*) as permTotal FROM perm`).first();
|
||||
|
||||
if (permTotal === 0) {
|
||||
await c.env.db.prepare(`
|
||||
INSERT INTO perm (perm_id, name, perm_key, pid, type, sort) VALUES
|
||||
(1, '邮件', NULL, 0, 0, 0),
|
||||
(2, '邮件删除', 'email:delete', 1, 2, 1),
|
||||
(3, '邮件发送', 'email:send', 1, 2, 0),
|
||||
(4, '个人设置', '', 0, 1, 2),
|
||||
(5, '用户注销', 'my:delete', 4, 2, 0),
|
||||
(6, '用户信息', NULL, 0, 1, 3),
|
||||
(7, '用户查看', 'user:query', 6, 2, 0),
|
||||
(8, '密码修改', 'user:set-pwd', 6, 2, 2),
|
||||
(9, '状态修改', 'user:set-status', 6, 2, 3),
|
||||
(10, '权限修改', 'user:set-type', 6, 2, 4),
|
||||
(11, '用户删除', 'user:delete', 6, 2, 7),
|
||||
(12, '用户收藏', 'user:star', 6, 2, 5),
|
||||
(13, '权限控制', '', 0, 1, 5),
|
||||
(14, '身份查看', 'role:query', 13, 2, 0),
|
||||
(15, '身份修改', 'role:set', 13, 2, 1),
|
||||
(16, '身份删除', 'role:delete', 13, 2, 2),
|
||||
(17, '系统设置', '', 0, 1, 6),
|
||||
(18, '设置查看', 'setting:query', 17, 2, 0),
|
||||
(19, '设置修改', 'setting:set', 17, 2, 1),
|
||||
(20, '物理清空', 'seting:clear', 17, 2, 2),
|
||||
(21, '邮箱侧栏', '', 0, 0, 1),
|
||||
(22, '邮箱查看', 'account:query', 21, 2, 0),
|
||||
(23, '邮箱添加', 'account:add', 21, 2, 1),
|
||||
(24, '邮箱删除', 'account:delete', 21, 2, 2),
|
||||
(25, '用户添加', 'user:add', 6, 2, 1),
|
||||
(26, '发件重置', 'user:reset-send', 6, 2, 6),
|
||||
(27, '邮件列表', '', 0, 1, 4),
|
||||
(28, '邮件查看', 'sys-email:query', 27, 2, 0),
|
||||
(29, '邮件删除', 'sys-email:delete', 27, 2, 0)
|
||||
`).run();
|
||||
}
|
||||
|
||||
// 创建 role 表并插入默认身份
|
||||
await c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS role (
|
||||
role_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
key TEXT,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
sort INTEGER DEFAULT 0,
|
||||
description TEXT,
|
||||
user_id INTEGER,
|
||||
is_default INTEGER DEFAULT 0,
|
||||
send_count INTEGER,
|
||||
send_type TEXT NOT NULL DEFAULT 'count',
|
||||
account_count INTEGER
|
||||
)
|
||||
`).run();
|
||||
|
||||
const { roleCount } = await c.env.db.prepare(`SELECT COUNT(*) as roleCount FROM role`).first();
|
||||
if (roleCount === 0) {
|
||||
await c.env.db.prepare(`
|
||||
INSERT INTO role (
|
||||
role_id, name, key, create_time, sort, description, user_id, is_default, send_count, send_type, account_count
|
||||
) VALUES (
|
||||
1, '普通用户', NULL, '0000-00-00 00:00:00', 0, '只有普通使用权限', 0, 1, NULL, 'count', 10
|
||||
)
|
||||
`).run();
|
||||
}
|
||||
|
||||
// 创建 role_perm 表并初始化数据
|
||||
await c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS role_perm (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
role_id INTEGER,
|
||||
perm_id INTEGER
|
||||
)
|
||||
`).run();
|
||||
|
||||
const {rolePermCount} = await c.env.db.prepare(`SELECT COUNT(*) as rolePermCount FROM role_perm`).first();
|
||||
if (rolePermCount === 0) {
|
||||
await c.env.db.prepare(`
|
||||
INSERT INTO role_perm (id, role_id, perm_id) VALUES
|
||||
(100, 1, 2),
|
||||
(101, 1, 21),
|
||||
(102, 1, 22),
|
||||
(103, 1, 23),
|
||||
(104, 1, 24),
|
||||
(105, 1, 4),
|
||||
(106, 1, 5),
|
||||
(107, 1, 1)
|
||||
`).run();
|
||||
}
|
||||
},
|
||||
|
||||
async intDB(c) {
|
||||
// 初始化数据库表结构
|
||||
await c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS email (
|
||||
email_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
send_email TEXT,
|
||||
name TEXT,
|
||||
receive_email TEXT NOT NULL,
|
||||
account_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
subject TEXT,
|
||||
content TEXT,
|
||||
text TEXT,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
is_del INTEGER DEFAULT 0 NOT NULL
|
||||
)
|
||||
`).run();
|
||||
|
||||
await c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS star (
|
||||
star_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
email_id INTEGER NOT NULL,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
)
|
||||
`).run();
|
||||
|
||||
await c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS attachments (
|
||||
att_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
email_id INTEGER NOT NULL,
|
||||
account_id INTEGER NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
filename TEXT,
|
||||
mime_type TEXT,
|
||||
size INTEGER,
|
||||
disposition TEXT,
|
||||
related TEXT,
|
||||
content_id TEXT,
|
||||
encoding TEXT,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
)
|
||||
`).run();
|
||||
|
||||
await c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS user (
|
||||
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL,
|
||||
type INTEGER DEFAULT 1 NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
salt TEXT NOT NULL,
|
||||
status INTEGER DEFAULT 0 NOT NULL,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
active_time DATETIME,
|
||||
is_del INTEGER DEFAULT 0 NOT NULL
|
||||
)
|
||||
`).run();
|
||||
|
||||
await c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS account (
|
||||
account_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL,
|
||||
status INTEGER DEFAULT 0 NOT NULL,
|
||||
latest_email_time DATETIME,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
user_id INTEGER NOT NULL,
|
||||
is_del INTEGER DEFAULT 0 NOT NULL
|
||||
)
|
||||
`).run();
|
||||
|
||||
await c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS setting (
|
||||
register INTEGER NOT NULL,
|
||||
receive INTEGER NOT NULL,
|
||||
add_email INTEGER NOT NULL,
|
||||
many_email INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
auto_refresh_time INTEGER NOT NULL,
|
||||
register_verify INTEGER NOT NULL,
|
||||
add_email_verify INTEGER NOT NULL
|
||||
)
|
||||
`).run();
|
||||
|
||||
await c.env.db.prepare(`
|
||||
INSERT INTO setting (
|
||||
register, receive, add_email, many_email, title, auto_refresh_time, register_verify, add_email_verify
|
||||
)
|
||||
SELECT 0, 0, 0, 1, 'Cloud 邮箱', 0, 1, 1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM setting)
|
||||
`).run();
|
||||
}
|
||||
};
|
||||
|
||||
export default initService;
|
||||
@@ -13,7 +13,8 @@ import settingService from './setting-service';
|
||||
import saltHashUtils from '../utils/crypto-utils';
|
||||
import cryptoUtils from '../utils/crypto-utils';
|
||||
import turnstileService from './turnstile-service';
|
||||
|
||||
import roleService from './role-service';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const loginService = {
|
||||
|
||||
@@ -37,13 +38,13 @@ const loginService = {
|
||||
throw new BizError('非法邮箱域名');
|
||||
}
|
||||
|
||||
const userRow = await userService.selectByEmailIncludeDel(c, email);
|
||||
const accountRow = await accountService.selectByEmailIncludeDel(c, email);
|
||||
|
||||
if (userRow && userRow.isDel === isDel.DELETE) {
|
||||
if (accountRow && accountRow.isDel === isDel.DELETE) {
|
||||
throw new BizError('该邮箱已被注销');
|
||||
}
|
||||
|
||||
if (userRow) {
|
||||
if (accountRow) {
|
||||
throw new BizError('该邮箱已被注册');
|
||||
}
|
||||
|
||||
@@ -53,7 +54,12 @@ const loginService = {
|
||||
|
||||
const { salt, hash } = await saltHashUtils.hashPassword(password);
|
||||
|
||||
const userId = await userService.insert(c, { email, password: hash, salt, type: userConst.type.COMMON });
|
||||
const roleRow = await roleService.selectDefaultRole(c);
|
||||
|
||||
const userId = await userService.insert(c, { email, password: hash, salt, type: roleRow.roleId });
|
||||
|
||||
await userService.updateUserInfo(c, userId);
|
||||
|
||||
await accountService.insert(c, { userId: userId, email });
|
||||
},
|
||||
|
||||
@@ -65,12 +71,20 @@ const loginService = {
|
||||
throw new BizError('邮箱和密码不能为空');
|
||||
}
|
||||
|
||||
const userRow = await userService.selectByEmail(c, email);
|
||||
const userRow = await userService.selectByEmailIncludeDel(c, email);
|
||||
|
||||
if (!userRow) {
|
||||
throw new BizError('该邮箱不存在');
|
||||
}
|
||||
|
||||
if(userRow.isDel === isDel.DELETE) {
|
||||
throw new BizError('该邮箱已被注销');
|
||||
}
|
||||
|
||||
if(userRow.status === userConst.status.BAN) {
|
||||
throw new BizError('该邮箱已被禁用');
|
||||
}
|
||||
|
||||
if (!await cryptoUtils.verifyPassword(password, userRow.salt, userRow.password)) {
|
||||
throw new BizError('密码输入错误');
|
||||
}
|
||||
@@ -89,19 +103,22 @@ const loginService = {
|
||||
authInfo = {
|
||||
tokens: [],
|
||||
user: userRow,
|
||||
refreshTime: new Date().toISOString()
|
||||
refreshTime: dayjs().toISOString()
|
||||
};
|
||||
|
||||
authInfo.tokens.push(uuid);
|
||||
|
||||
}
|
||||
|
||||
|
||||
await userService.updateUserInfo(c, userRow.userId);
|
||||
|
||||
await c.env.kv.put(KvConst.AUTH_INFO + userRow.userId, JSON.stringify(authInfo), { expirationTtl: constant.TOKEN_EXPIRE });
|
||||
return jwt;
|
||||
},
|
||||
|
||||
async logout(c, userId) {
|
||||
const token = await userContext.getToken(c);
|
||||
const token =userContext.getToken(c);
|
||||
const authInfo = await c.env.kv.get(KvConst.AUTH_INFO + userId, { type: 'json' });
|
||||
const index = authInfo.tokens.findIndex(item => item === token);
|
||||
authInfo.tokens.splice(index, 1);
|
||||
|
||||
31
mail-worker/src/service/perm-service.js
Normal file
31
mail-worker/src/service/perm-service.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import orm from '../entity/orm';
|
||||
import perm from '../entity/perm';
|
||||
import { eq, ne, and, asc } from 'drizzle-orm';
|
||||
import rolePerm from '../entity/role-perm';
|
||||
import user from '../entity/user';
|
||||
import role from '../entity/role';
|
||||
import { permConst } from '../const/entity-const';
|
||||
|
||||
const permService = {
|
||||
async tree(c) {
|
||||
const pList = await orm(c).select().from(perm).where(eq(perm.pid, 0)).orderBy(asc(perm.sort)).all();
|
||||
const cList = await orm(c).select().from(perm).where(ne(perm.pid, 0)).orderBy(asc(perm.sort)).all();
|
||||
|
||||
pList.forEach(pItem => {
|
||||
pItem.children = cList.filter(cItem => cItem.pid === pItem.permId)
|
||||
})
|
||||
return pList;
|
||||
},
|
||||
|
||||
async userPermKeys(c, userId) {
|
||||
const userPerms = await orm(c).select({permKey: perm.permKey}).from(user)
|
||||
.leftJoin(role, eq(role.roleId,user.type))
|
||||
.rightJoin(rolePerm, eq(rolePerm.roleId,role.roleId))
|
||||
.leftJoin(perm, eq(rolePerm.permId,perm.permId))
|
||||
.where(and(eq(user.userId,userId),eq(perm.type,permConst.type.BUTTON)))
|
||||
.all();
|
||||
return userPerms.map(perm => perm.permKey);
|
||||
}
|
||||
}
|
||||
|
||||
export default permService
|
||||
@@ -1,11 +1,20 @@
|
||||
import attService from './att-service';
|
||||
import constant from '../const/constant';
|
||||
|
||||
const r2Service = {
|
||||
async putObj(c, key, content, metadata) {
|
||||
const body = typeof content === 'string'
|
||||
? new TextEncoder().encode(content)
|
||||
: content;
|
||||
await c.env.r2.put(key, body, {
|
||||
await c.env.r2.put(key, content, {
|
||||
httpMetadata: {...metadata}
|
||||
});
|
||||
},
|
||||
|
||||
async getObj(c, key) {
|
||||
return await c.env.r2.get(key);
|
||||
},
|
||||
|
||||
async delete(c, key) {
|
||||
await c.env.r2.delete(key);
|
||||
}
|
||||
|
||||
};
|
||||
export default r2Service;
|
||||
|
||||
46
mail-worker/src/service/resend-service.js
Normal file
46
mail-worker/src/service/resend-service.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import emailService from './email-service';
|
||||
import { emailConst } from '../const/entity-const';
|
||||
import BizError from '../error/biz-error';
|
||||
|
||||
const resendService = {
|
||||
|
||||
async webhooks(c, body) {
|
||||
|
||||
const params = {}
|
||||
|
||||
if (body.type === 'email.delivered') {
|
||||
params.status = emailConst.status.DELIVERED
|
||||
params.resendEmailId = body.data.email_id
|
||||
params.message = null
|
||||
}
|
||||
|
||||
if (body.type === 'email.complained') {
|
||||
params.status = emailConst.status.COMPLAINED
|
||||
params.resendEmailId = body.data.email_id
|
||||
params.message = null
|
||||
}
|
||||
|
||||
if (body.type === 'email.bounced') {
|
||||
let bounce = body.data.bounce
|
||||
bounce = JSON.stringify(bounce);
|
||||
params.status = emailConst.status.BOUNCED
|
||||
params.resendEmailId = body.data.email_id
|
||||
params.message = bounce
|
||||
}
|
||||
|
||||
if (body.type === 'email.delivery_delayed') {
|
||||
params.status = emailConst.status.DELAYED
|
||||
params.resendEmailId = body.data.email_id
|
||||
params.message = null
|
||||
}
|
||||
|
||||
const emailRow = await emailService.updateEmailStatus(c, params)
|
||||
|
||||
if (!emailRow) {
|
||||
throw new BizError('更新邮件状态记录失败');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default resendService
|
||||
132
mail-worker/src/service/role-service.js
Normal file
132
mail-worker/src/service/role-service.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import role from '../entity/role';
|
||||
import orm from '../entity/orm';
|
||||
import { eq, asc, inArray, and } from 'drizzle-orm';
|
||||
import BizError from '../error/biz-error';
|
||||
import rolePerm from '../entity/role-perm';
|
||||
import perm from '../entity/perm';
|
||||
import { permConst, roleConst } from '../const/entity-const';
|
||||
import userService from './user-service';
|
||||
|
||||
const roleService = {
|
||||
|
||||
async add(c, params, userId) {
|
||||
|
||||
let { name, permIds } = params;
|
||||
|
||||
if (!name) {
|
||||
throw new BizError('身份名不能为空');
|
||||
}
|
||||
|
||||
let roleRow = await orm(c).select().from(role).where(eq(role.name, name)).get();
|
||||
|
||||
if (roleRow) {
|
||||
throw new BizError('身份名已存在');
|
||||
}
|
||||
|
||||
roleRow = await orm(c).insert(role).values({...params, userId}).returning().get();
|
||||
|
||||
if (permIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rolePermList = permIds.map(permId => ({ permId, roleId: roleRow.roleId }));
|
||||
|
||||
await orm(c).insert(rolePerm).values(rolePermList).run();
|
||||
|
||||
|
||||
},
|
||||
|
||||
async roleList(c) {
|
||||
|
||||
const roleList = await orm(c).select().from(role).orderBy(asc(role.sort)).all();
|
||||
const permList = await orm(c).select({ permId: perm.permId, roleId: rolePerm.roleId }).from(rolePerm)
|
||||
.leftJoin(perm, eq(perm.permId, rolePerm.permId))
|
||||
.where(eq(perm.type, permConst.type.BUTTON)).all();
|
||||
|
||||
roleList.forEach(role => {
|
||||
role.permIds = permList.filter(perm => perm.roleId === role.roleId).map(perm => perm.permId);
|
||||
});
|
||||
|
||||
return roleList;
|
||||
},
|
||||
|
||||
async setRole(c, params) {
|
||||
|
||||
let { name, permIds, roleId } = params;
|
||||
|
||||
if (!name) {
|
||||
throw new BizError('名字不能为空');
|
||||
}
|
||||
|
||||
delete params.isDefault
|
||||
|
||||
await orm(c).update(role).set({...params}).where(eq(role.roleId, roleId)).run();
|
||||
await orm(c).delete(rolePerm).where(eq(rolePerm.roleId, roleId)).run();
|
||||
|
||||
if (permIds.length > 0) {
|
||||
const rolePermList = permIds.map(permId => ({ permId, roleId: roleId }));
|
||||
await orm(c).insert(rolePerm).values(rolePermList).run();
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
async delete(c, params) {
|
||||
|
||||
const { roleId } = params;
|
||||
|
||||
const roleRow = await orm(c).select().from(role).where(eq(role.roleId, roleId)).get();
|
||||
|
||||
if (!roleRow) {
|
||||
throw new BizError('身份不存在');
|
||||
}
|
||||
|
||||
if (roleRow.isDefault) {
|
||||
throw new BizError('默认身份不能删除');
|
||||
}
|
||||
|
||||
const defRoleRow = await orm(c).select().from(role).where(eq(role.isDefault, roleConst.isDefault.OPEN)).get();
|
||||
|
||||
await userService.updateAllUserType(c, defRoleRow.roleId, roleId);
|
||||
|
||||
await orm(c).delete(rolePerm).where(eq(rolePerm.roleId, roleId)).run();
|
||||
await orm(c).delete(role).where(eq(role.roleId, roleId)).run();
|
||||
|
||||
},
|
||||
|
||||
roleSelectUse(c) {
|
||||
return orm(c).select({ name: role.name, roleId: role.roleId }).from(role).orderBy(asc(role.sort)).all();
|
||||
},
|
||||
|
||||
async selectDefaultRole(c) {
|
||||
return await orm(c).select().from(role).where(eq(role.isDefault, roleConst.isDefault.OPEN)).get();
|
||||
},
|
||||
|
||||
async setDefault(c, params) {
|
||||
const roleRow = await orm(c).select().from(role).where(eq(role.roleId, params.roleId)).get();
|
||||
if (!roleRow) {
|
||||
throw new BizError('身份不存在');
|
||||
}
|
||||
await orm(c).update(role).set({ isDefault: 0 }).run();
|
||||
await orm(c).update(role).set({ isDefault: 1 }).where(eq(role.roleId, params.roleId)).run();
|
||||
},
|
||||
|
||||
selectById(c, roleId) {
|
||||
return orm(c).select().from(role).where(eq(role.roleId, roleId)).get();
|
||||
},
|
||||
|
||||
selectByIdsHasPermKey(c, types, permKey) {
|
||||
return orm(c).select({ roleId: role.roleId, sendType: role.sendType, sendCount: role.sendCount }).from(perm)
|
||||
.leftJoin(rolePerm, eq(perm.permId, rolePerm.permId))
|
||||
.leftJoin(role, eq(role.roleId, rolePerm.roleId))
|
||||
.where(and(eq(perm.permKey, permKey), inArray(role.roleId, types))).all();
|
||||
},
|
||||
|
||||
selectByIdsAndSendType(c, permKey, sendType) {
|
||||
return orm(c).select({ roleId: role.roleId }).from(perm)
|
||||
.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();
|
||||
}
|
||||
};
|
||||
|
||||
export default roleService;
|
||||
@@ -2,31 +2,51 @@ import KvConst from '../const/kv-const';
|
||||
import setting from '../entity/setting';
|
||||
import orm from '../entity/orm';
|
||||
import { settingConst } from '../const/entity-const';
|
||||
import BizError from "../error/biz-error";
|
||||
import fileUtils from '../utils/file-utils';
|
||||
import r2Service from './r2-service';
|
||||
import emailService from './email-service';
|
||||
import accountService from './account-service';
|
||||
import userService from './user-service';
|
||||
import starService from './star-service';
|
||||
import constant from '../const/constant';
|
||||
import BizError from '../error/biz-error';
|
||||
|
||||
const settingService = {
|
||||
|
||||
async refresh(c) {
|
||||
const settingRow = await orm(c).select().from(setting).get();
|
||||
settingRow.resendTokens = JSON.parse(settingRow.resendTokens);
|
||||
await c.env.kv.put(KvConst.SETTING, JSON.stringify(settingRow));
|
||||
},
|
||||
|
||||
async query(c) {
|
||||
const setting = await c.env.kv.get(KvConst.SETTING, { type: 'json' });
|
||||
let domainList = c.env.domain;
|
||||
if (typeof domainList === 'string') {
|
||||
throw new BizError('环境变量domain必须是JSON类型');
|
||||
}
|
||||
domainList = domainList.map(item => '@' + item);
|
||||
setting.domainList = domainList;
|
||||
setting.siteKey = c.env.site_key;
|
||||
setting.r2Domain = c.env.r2_domain;
|
||||
return setting;
|
||||
},
|
||||
|
||||
async get(c) {
|
||||
const settingRow = await this.query(c);
|
||||
settingRow.siteKey = settingRow.siteKey ? `${settingRow.siteKey.slice(0, 11)}******` : null ;
|
||||
settingRow.secretKey = settingRow.secretKey ? `${settingRow.secretKey.slice(0, 11)}******`: null ;
|
||||
Object.keys(settingRow.resendTokens).forEach(key => {
|
||||
settingRow.resendTokens[key] = `${settingRow.resendTokens[key].slice(0, 11)}******`;
|
||||
});
|
||||
return settingRow
|
||||
},
|
||||
|
||||
async set(c, params) {
|
||||
if (params.registerVerify === 0 || params.addEmailVerify === 0) {
|
||||
if (!c.env.site_key || !c.env.secret_key) {
|
||||
throw new BizError('Turnstile密钥未配置,不能开启人机验证')
|
||||
}
|
||||
}
|
||||
const settingData = await this.query(c)
|
||||
let resendTokens = {...settingData.resendTokens,...params.resendTokens}
|
||||
Object.keys(resendTokens).forEach(domain => {
|
||||
if(!resendTokens[domain]) delete resendTokens[domain]
|
||||
})
|
||||
params.resendTokens = JSON.stringify(resendTokens)
|
||||
await orm(c).update(setting).set({ ...params }).returning().get();
|
||||
await this.refresh(c);
|
||||
},
|
||||
@@ -54,6 +74,42 @@ const settingService = {
|
||||
async isAddEmailVerify(c) {
|
||||
const { addEmailVerify } = await this.query(c);
|
||||
return addEmailVerify === settingConst.addEmailVerify.OPEN;
|
||||
},
|
||||
|
||||
async setBackground(c, params) {
|
||||
|
||||
const settingRow = await this.query(c);
|
||||
|
||||
|
||||
if (!c.env.r2) {
|
||||
throw new BizError('r2对象存储未配置不能上传背景');
|
||||
}
|
||||
|
||||
if (!settingRow.r2Domain) {
|
||||
throw new BizError('r2域名未配置不上传背景');
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
await orm(c).update(setting).set({ background: key }).run();
|
||||
await this.refresh(c);
|
||||
return key;
|
||||
},
|
||||
|
||||
async physicsDeleteAll(c) {
|
||||
await emailService.physicsDeleteAll(c);
|
||||
await accountService.physicsDeleteAll(c);
|
||||
await userService.physicsDeleteAll(c);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@ import orm from '../entity/orm';
|
||||
import { star } from '../entity/star';
|
||||
import emailService from './email-service';
|
||||
import BizError from '../error/biz-error';
|
||||
import { and, desc, eq, lt, sql } from 'drizzle-orm';
|
||||
import { and, desc, eq, lt, sql, inArray } from 'drizzle-orm';
|
||||
import email from '../entity/email';
|
||||
import { isDel } from '../const/entity-const';
|
||||
import { att } from '../entity/att';
|
||||
import userService from './user-service';
|
||||
|
||||
const startService = {
|
||||
const starService = {
|
||||
|
||||
async add(c, params, userId) {
|
||||
const { emailId } = params;
|
||||
@@ -63,7 +65,10 @@ const startService = {
|
||||
.limit(size)
|
||||
.all();
|
||||
return { list };
|
||||
},
|
||||
async removeByEmailIds(c, emailIds) {
|
||||
await orm(c).delete(star).where(inArray(star.emailId, emailIds)).run();
|
||||
}
|
||||
};
|
||||
|
||||
export default startService;
|
||||
export default starService;
|
||||
|
||||
@@ -20,7 +20,7 @@ const turnstileService = {
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
console.log(result)
|
||||
|
||||
if (!result.success) {
|
||||
throw new BizError('人机验证失败,请重试',400)
|
||||
}
|
||||
|
||||
@@ -2,23 +2,48 @@ import BizError from '../error/biz-error';
|
||||
import accountService from './account-service';
|
||||
import orm from '../entity/orm';
|
||||
import user from '../entity/user';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { isDel } from '../const/entity-const';
|
||||
import { and, asc, count, desc, eq, inArray, like, sql } from 'drizzle-orm';
|
||||
import { emailConst, isDel, roleConst, userConst } from '../const/entity-const';
|
||||
import kvConst from '../const/kv-const';
|
||||
import KvConst from '../const/kv-const';
|
||||
import cryptoUtils from '../utils/crypto-utils';
|
||||
import emailService from './email-service';
|
||||
import { UAParser } from 'ua-parser-js';
|
||||
import dayjs from 'dayjs';
|
||||
import permService from './perm-service';
|
||||
import roleService from './role-service';
|
||||
import emailUtils from '../utils/email-utils';
|
||||
import saltHashUtils from '../utils/crypto-utils';
|
||||
import constant from '../const/constant';
|
||||
|
||||
const userService = {
|
||||
|
||||
async loginUserInfo(c, userId) {
|
||||
let user = await userService.selectById(c, userId);
|
||||
let account = await accountService.selectByEmailIncludeDel(c, user.email);
|
||||
delete user.password;
|
||||
delete user.salt;
|
||||
|
||||
const userRow = await userService.selectById(c, userId);
|
||||
|
||||
const [account, roleRow, permKeys] = await Promise.all([
|
||||
accountService.selectByEmailIncludeDel(c, userRow.email),
|
||||
roleService.selectById(c, userRow.type),
|
||||
userRow.email === c.env.admin ? Promise.resolve(['*']) : permService.userPermKeys(c, userId)
|
||||
]);
|
||||
|
||||
const user = {};
|
||||
user.userId = userRow.userId;
|
||||
user.sendCount = userRow.sendCount;
|
||||
user.email = userRow.email;
|
||||
user.accountId = account.accountId;
|
||||
user.type = c.env.admin === user.email ? 0 : 1;
|
||||
user.permKeys = permKeys;
|
||||
user.role = roleRow
|
||||
|
||||
if (c.env.admin === userRow.email) {
|
||||
user.role = constant.ADMIN_ROLE
|
||||
}
|
||||
|
||||
return user;
|
||||
},
|
||||
|
||||
|
||||
async resetPassword(c, params, userId) {
|
||||
|
||||
const { password } = params;
|
||||
@@ -57,10 +82,311 @@ const userService = {
|
||||
|
||||
async delete(c, userId) {
|
||||
await orm(c).update(user).set({ isDel: isDel.DELETE }).where(eq(user.userId, userId)).run();
|
||||
await Promise.all([
|
||||
c.env.kv.delete(kvConst.AUTH_INFO + userId),
|
||||
accountService.removeByUserId(c, userId)
|
||||
await c.env.kv.delete(kvConst.AUTH_INFO + userId)
|
||||
},
|
||||
|
||||
|
||||
async physicsDeleteAll(c) {
|
||||
const userIdsRow = await orm(c).select().from(user).where(eq(user.isDel, isDel.DELETE)).limit(99);
|
||||
if (userIdsRow.length === 0) {
|
||||
return;
|
||||
}
|
||||
const userIds = userIdsRow.map(item => item.userId);
|
||||
await accountService.physicsDeleteByUserIds(c, userIds);
|
||||
await orm(c).delete(user).where(inArray(user.userId, userIds)).run();
|
||||
if (userIdsRow.length === 99) {
|
||||
await this.physicsDeleteAll(c);
|
||||
}
|
||||
},
|
||||
|
||||
async physicsDelete(c, params) {
|
||||
const { userId } = params
|
||||
await accountService.physicsDeleteByUserIds(c, [userId])
|
||||
await orm(c).delete(user).where(eq(user.userId, userId)).run();
|
||||
},
|
||||
|
||||
async list(c, params, userId) {
|
||||
|
||||
let { num, size, email, timeSort, status } = params;
|
||||
|
||||
size = Number(size);
|
||||
num = Number(num);
|
||||
timeSort = Number(timeSort);
|
||||
params.isDel = Number(params.isDel);
|
||||
if (size > 50) {
|
||||
size = 50;
|
||||
}
|
||||
|
||||
num = (num - 1) * size;
|
||||
|
||||
const conditions = [];
|
||||
|
||||
if (status > -1) {
|
||||
conditions.push(eq(user.status, status));
|
||||
conditions.push(eq(user.isDel, isDel.NORMAL));
|
||||
}
|
||||
|
||||
|
||||
if (email) {
|
||||
conditions.push(like(user.email, `${email}%`));
|
||||
}
|
||||
|
||||
if (params.isDel) {
|
||||
conditions.push(eq(user.isDel, params.isDel));
|
||||
}
|
||||
|
||||
|
||||
const query = orm(c).select().from(user)
|
||||
.where(and(...conditions));
|
||||
|
||||
|
||||
if (timeSort) {
|
||||
query.orderBy(asc(user.userId));
|
||||
} else {
|
||||
query.orderBy(desc(user.userId));
|
||||
}
|
||||
|
||||
const list = await query.limit(size).offset(num);
|
||||
|
||||
const { total } = await orm(c)
|
||||
.select({ total: count() })
|
||||
.from(user)
|
||||
.where(and(...conditions)).get();
|
||||
const userIds = list.map(user => user.userId);
|
||||
|
||||
const types = [...new Set(list.map(user => user.type))];
|
||||
|
||||
const [emailCounts, delEmailCounts, sendCounts, delSendCounts, accountCounts, delAccountCounts, roleList] = await Promise.all([
|
||||
emailService.selectUserEmailCountList(c, userIds, emailConst.type.RECEIVE),
|
||||
emailService.selectUserEmailCountList(c, userIds, emailConst.type.RECEIVE, isDel.DELETE),
|
||||
emailService.selectUserEmailCountList(c, userIds, emailConst.type.SEND),
|
||||
emailService.selectUserEmailCountList(c, userIds, emailConst.type.SEND, isDel.DELETE),
|
||||
accountService.selectUserAccountCountList(c, userIds),
|
||||
accountService.selectUserAccountCountList(c, userIds, isDel.DELETE),
|
||||
roleService.selectByIdsHasPermKey(c, types,'email:send')
|
||||
]);
|
||||
|
||||
const receiveMap = Object.fromEntries(emailCounts.map(item => [item.userId, item.count]));
|
||||
const sendMap = Object.fromEntries(sendCounts.map(item => [item.userId, item.count]));
|
||||
const accountMap = Object.fromEntries(accountCounts.map(item => [item.userId, item.count]));
|
||||
|
||||
const delReceiveMap = Object.fromEntries(delEmailCounts.map(item => [item.userId, item.count]));
|
||||
const delSendMap = Object.fromEntries(delSendCounts.map(item => [item.userId, item.count]));
|
||||
const delAccountMap = Object.fromEntries(delAccountCounts.map(item => [item.userId, item.count]));
|
||||
|
||||
for (const user of list) {
|
||||
|
||||
const userId = user.userId;
|
||||
|
||||
user.receiveEmailCount = receiveMap[userId] || 0;
|
||||
user.sendEmailCount = sendMap[userId] || 0;
|
||||
user.accountCount = accountMap[userId] || 0;
|
||||
|
||||
user.delReceiveEmailCount = delReceiveMap[userId] || 0;
|
||||
user.delSendEmailCount = delSendMap[userId] || 0;
|
||||
user.delAccountCount = delAccountMap[userId] || 0;
|
||||
|
||||
const roleIndex = roleList.findIndex(roleRow => user.type === roleRow.roleId);
|
||||
let sendAction = {};
|
||||
|
||||
if (roleIndex > -1) {
|
||||
sendAction.sendType = roleList[roleIndex].sendType;
|
||||
sendAction.sendCount = roleList[roleIndex].sendCount;
|
||||
sendAction.hasPerm = true;
|
||||
} else {
|
||||
sendAction.hasPerm = false;
|
||||
}
|
||||
|
||||
if (user.email === c.env.admin) {
|
||||
sendAction.sendType = constant.ADMIN_ROLE.sendType;
|
||||
sendAction.sendCount = constant.ADMIN_ROLE.sendCount;
|
||||
sendAction.hasPerm = true;
|
||||
user.type = 0
|
||||
}
|
||||
|
||||
user.sendAction = sendAction;
|
||||
}
|
||||
|
||||
return { list, total };
|
||||
},
|
||||
|
||||
async updateUserInfo(c, userId, recordCreateIp = false) {
|
||||
|
||||
const ua = c.req.header('user-agent') || '';
|
||||
console.log(ua);
|
||||
const parser = new UAParser(ua);
|
||||
const { browser, device, os } = parser.getResult();
|
||||
|
||||
let browserInfo = null;
|
||||
let osInfo = null;
|
||||
|
||||
if (browser.name) {
|
||||
browserInfo = browser.name + ' ' + browser.version;
|
||||
}
|
||||
|
||||
if (os.name) {
|
||||
osInfo = os.name + os.version;
|
||||
}
|
||||
|
||||
let deviceInfo = 'Desktop';
|
||||
|
||||
const hasVendor = !!device?.vendor;
|
||||
const hasModel = !!device?.model;
|
||||
|
||||
if (hasVendor || hasModel) {
|
||||
const vendor = device.vendor || '';
|
||||
const model = device.model || '';
|
||||
const type = device.type || '';
|
||||
|
||||
const namePart = [vendor, model].filter(Boolean).join(' ');
|
||||
const typePart = type ? ` (${type})` : '';
|
||||
deviceInfo = (namePart + typePart).trim();
|
||||
}
|
||||
|
||||
const userIp = c.req.header('cf-connecting-ip') || '';
|
||||
|
||||
const params = {
|
||||
os: osInfo,
|
||||
browser: browserInfo,
|
||||
device: deviceInfo,
|
||||
activeIp: userIp,
|
||||
activeTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||
};
|
||||
|
||||
if (recordCreateIp) {
|
||||
params.createIp = userIp;
|
||||
}
|
||||
|
||||
await orm(c)
|
||||
.update(user)
|
||||
.set(params)
|
||||
.where(eq(user.userId, userId))
|
||||
.run();
|
||||
},
|
||||
|
||||
async setPwd(c, params) {
|
||||
|
||||
const { password, userId } = params;
|
||||
await this.resetPassword(c, { password }, userId);
|
||||
},
|
||||
|
||||
async setStatus(c, params) {
|
||||
|
||||
const { status, userId } = params;
|
||||
|
||||
await orm(c)
|
||||
.update(user)
|
||||
.set({ status })
|
||||
.where(eq(user.userId, userId))
|
||||
.run();
|
||||
|
||||
if (status === userConst.status.BAN) {
|
||||
c.env.kv.delete(KvConst.AUTH_INFO + userId);
|
||||
}
|
||||
},
|
||||
|
||||
async setType(c, params) {
|
||||
|
||||
const { type, userId } = params;
|
||||
|
||||
const roleRow = await roleService.selectById(c, type);
|
||||
|
||||
if (!roleRow) {
|
||||
throw new BizError('身份不存在');
|
||||
}
|
||||
|
||||
await orm(c)
|
||||
.update(user)
|
||||
.set({ type })
|
||||
.where(eq(user.userId, userId))
|
||||
.run();
|
||||
|
||||
if (type) {
|
||||
const authInfo = await c.env.kv.get(KvConst.AUTH_INFO + userId, { type: 'json' });
|
||||
if (authInfo) {
|
||||
authInfo.user.type = type;
|
||||
await c.env.kv.put(KvConst.AUTH_INFO + userId, JSON.stringify(authInfo));
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
async incrUserService(c, quantity, userId) {
|
||||
await orm(c).update(user).set({
|
||||
sendCount: sql`${user.sendCount}
|
||||
+
|
||||
${quantity}`
|
||||
}).where(eq(user.userId, userId)).run();
|
||||
},
|
||||
|
||||
async updateAllUserType(c, type, curType) {
|
||||
await orm(c)
|
||||
.update(user)
|
||||
.set({ type })
|
||||
.where(eq(user.type, curType))
|
||||
.run();
|
||||
},
|
||||
|
||||
async add(c, params) {
|
||||
|
||||
const { email, type, password } = params;
|
||||
|
||||
if (!c.env.domain.includes(emailUtils.getDomain(email))) {
|
||||
throw new BizError('非法邮箱域名');
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
throw new BizError('密码必须大于6位');
|
||||
}
|
||||
|
||||
const accountRow = await accountService.selectByEmailIncludeDel(c, email);
|
||||
|
||||
if (accountRow && accountRow.isDel === isDel.DELETE) {
|
||||
throw new BizError('该邮箱已被注销');
|
||||
}
|
||||
|
||||
if (accountRow) {
|
||||
throw new BizError('该邮箱已被注册');
|
||||
}
|
||||
|
||||
const role = roleService.selectById(c, type);
|
||||
|
||||
if (!role) {
|
||||
throw new BizError('权限身份不存在');
|
||||
}
|
||||
|
||||
const { salt, hash } = await saltHashUtils.hashPassword(password);
|
||||
|
||||
const userId = await userService.insert(c, { email, password: hash, salt, type });
|
||||
|
||||
await accountService.insert(c, { userId: userId, email, type });
|
||||
},
|
||||
|
||||
async resetDaySendCount(c) {
|
||||
const roleList = await roleService.selectByIdsAndSendType(c, 'email:send', roleConst.sendType.DAY);
|
||||
const roleIds = roleList.map(action => action.roleId);
|
||||
await orm(c).update(user).set({ sendCount: 0 }).where(inArray(user.type, roleIds)).run();
|
||||
},
|
||||
|
||||
async resetSendCount(c, params) {
|
||||
await orm(c).update(user).set({ sendCount: 0 }).where(eq(user.userId, params.userId)).run();
|
||||
},
|
||||
|
||||
async restore(c, params) {
|
||||
const { userId, type } = params
|
||||
await orm(c)
|
||||
.update(user)
|
||||
.set({ isDel: isDel.NORMAL })
|
||||
.where(eq(user.userId, userId))
|
||||
.run();
|
||||
const userRow = await this.selectById(c, userId);
|
||||
await accountService.restoreByEmail(c, userRow.email);
|
||||
|
||||
if (type) {
|
||||
await emailService.restoreByUserId(c, userId);
|
||||
await accountService.restoreByUserId(c, userId);
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,12 @@ const emailUtils = {
|
||||
if (typeof email !== 'string') return ''
|
||||
const parts = email.split('@')
|
||||
return parts.length === 2 ? parts[1] : ''
|
||||
},
|
||||
|
||||
getName(email) {
|
||||
if (typeof email !== 'string') return ''
|
||||
const parts = email.trim().split('@')
|
||||
return parts.length === 2 ? parts[0] : ''
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,60 @@ const fileUtils = {
|
||||
getExtFileName(filename) {
|
||||
const index = filename.lastIndexOf('.');
|
||||
return index !== -1 ? filename.slice(index) : '';
|
||||
},
|
||||
|
||||
async getBuffHash(buff) {
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', buff);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
},
|
||||
|
||||
base64ToUint8Array(base64) {
|
||||
const binaryStr = atob(base64);
|
||||
const len = binaryStr.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binaryStr.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
},
|
||||
|
||||
/**
|
||||
* 将 Base64 数据转换为 File 对象(自动识别 MIME 类型和文件扩展名)
|
||||
* @param {string} base64Data 带有 data: 前缀的 base64 数据
|
||||
* @param {string} [customFilename] 可选,传入自定义文件名(不含扩展名)
|
||||
* @returns {File} File 对象
|
||||
*/
|
||||
base64ToFile(base64Data, customFilename) {
|
||||
const match = base64Data.match(/^data:(image|video)\/([a-zA-Z0-9.+-]+);base64,/);
|
||||
if (!match) {
|
||||
throw new Error('Invalid base64 data format');
|
||||
}
|
||||
|
||||
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 byteCharacters = atob(cleanBase64);
|
||||
const byteArrays = [];
|
||||
|
||||
for (let offset = 0; offset < byteCharacters.length; offset += 1024) {
|
||||
const slice = byteCharacters.slice(offset, offset + 1024);
|
||||
const byteNumbers = new Array(slice.length);
|
||||
for (let i = 0; i < slice.length; i++) {
|
||||
byteNumbers[i] = slice.charCodeAt(i);
|
||||
}
|
||||
byteArrays.push(new Uint8Array(byteNumbers));
|
||||
}
|
||||
|
||||
const blob = new Blob(byteArrays, { type: mimeType });
|
||||
|
||||
const filename = `${customFilename || `${type}_${Date.now()}`}.${ext}`;
|
||||
return new File([blob], filename, { type: mimeType });
|
||||
}
|
||||
};
|
||||
|
||||
export default fileUtils
|
||||
|
||||
export default fileUtils;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user