保存
This commit is contained in:
20
mail-worker/src/api/account-api.js
Normal file
20
mail-worker/src/api/account-api.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import app from '../hono/hono';
|
||||
import accountService from '../service/account-service';
|
||||
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));
|
||||
return c.json(result.ok(list));
|
||||
});
|
||||
|
||||
app.delete('/account/delete', async (c) => {
|
||||
await accountService.delete(c, c.req.query(), await 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));
|
||||
return c.json(result.ok(account));
|
||||
});
|
||||
|
||||
26
mail-worker/src/api/email-api.js
Normal file
26
mail-worker/src/api/email-api.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import app from '../hono/hono';
|
||||
import emailService from '../service/email-service';
|
||||
import result from '../model/result';
|
||||
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));
|
||||
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));
|
||||
return c.json(result.ok(list));
|
||||
})
|
||||
|
||||
app.delete('/email/delete', async (c) => {
|
||||
await emailService.delete(c, c.req.query(), await 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));
|
||||
return c.json(result.ok(attList));
|
||||
})
|
||||
|
||||
19
mail-worker/src/api/login-api.js
Normal file
19
mail-worker/src/api/login-api.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import app from '../hono/hono';
|
||||
import loginService from '../service/login-service';
|
||||
import result from '../model/result';
|
||||
import userContext from '../security/user-context';
|
||||
|
||||
app.post('/login', async (c) => {
|
||||
const token = await loginService.login(c, await c.req.json());
|
||||
return c.json(result.ok({ token: token }));
|
||||
});
|
||||
|
||||
app.post('/register', async (c) => {
|
||||
const jwt = await loginService.register(c, await c.req.json());
|
||||
return c.json(result.ok(jwt));
|
||||
});
|
||||
|
||||
app.delete('/logout', async (c) => {
|
||||
await loginService.logout(c, await userContext.getUserId(c));
|
||||
return c.json(result.ok());
|
||||
});
|
||||
13
mail-worker/src/api/setting-api.js
Normal file
13
mail-worker/src/api/setting-api.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import app from '../hono/hono';
|
||||
import result from '../model/result';
|
||||
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);
|
||||
return c.json(result.ok(setting));
|
||||
})
|
||||
19
mail-worker/src/api/star-api.js
Normal file
19
mail-worker/src/api/star-api.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import app from '../hono/hono';
|
||||
import startService 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));
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
app.get('/star/list', async (c) => {
|
||||
const data = await startService.list(c, c.req.query(), await 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));
|
||||
return c.json(result.ok());
|
||||
});
|
||||
1
mail-worker/src/api/test-api.js
Normal file
1
mail-worker/src/api/test-api.js
Normal file
@@ -0,0 +1 @@
|
||||
import app from '../hono/hono';
|
||||
23
mail-worker/src/api/user-api.js
Normal file
23
mail-worker/src/api/user-api.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import app from '../hono/hono';
|
||||
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));
|
||||
return c.json(result.ok());
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
10
mail-worker/src/const/constant.js
Normal file
10
mail-worker/src/const/constant.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const constant = {
|
||||
|
||||
TOKEN_HEADER: 'Authorization',
|
||||
JWT_UID: 'user_id:',
|
||||
JWT_TOKEN: 'token:',
|
||||
TOKEN_EXPIRE: 60 * 60 * 24 * 30,
|
||||
ATTACHMENT_PREFIX: 'attachments/'
|
||||
}
|
||||
|
||||
export default constant
|
||||
42
mail-worker/src/const/entity-const.js
Normal file
42
mail-worker/src/const/entity-const.js
Normal file
@@ -0,0 +1,42 @@
|
||||
export const userConst = {
|
||||
type: {
|
||||
COMMON: 1,
|
||||
ADMIN: 0,
|
||||
},
|
||||
}
|
||||
|
||||
export const settingConst = {
|
||||
register: {
|
||||
OPEN: 0,
|
||||
CLOSE: 1,
|
||||
},
|
||||
receive: {
|
||||
OPEN: 0,
|
||||
CLOSE: 1,
|
||||
},
|
||||
send: {
|
||||
OPEN: 0,
|
||||
CLOSE: 1,
|
||||
},
|
||||
addEmail: {
|
||||
OPEN: 0,
|
||||
CLOSE: 1,
|
||||
},
|
||||
manyEmail: {
|
||||
OPEN: 0,
|
||||
CLOSE: 1,
|
||||
},
|
||||
registerVerify: {
|
||||
OPEN: 0,
|
||||
CLOSE: 1,
|
||||
},
|
||||
addEmailVerify: {
|
||||
OPEN: 0,
|
||||
CLOSE: 1,
|
||||
}
|
||||
}
|
||||
|
||||
export const isDel = {
|
||||
DELETE: 1,
|
||||
NORMAL: 0
|
||||
}
|
||||
6
mail-worker/src/const/kv-const.js
Normal file
6
mail-worker/src/const/kv-const.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const KvConst = {
|
||||
AUTH_INFO: 'auth-uid:',
|
||||
SETTING: 'setting:',
|
||||
}
|
||||
|
||||
export default KvConst;
|
||||
77
mail-worker/src/email/email.js
Normal file
77
mail-worker/src/email/email.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import PostalMime from 'postal-mime';
|
||||
import emailService from '../service/email-service';
|
||||
import accountService from '../service/account-service';
|
||||
import { isDel } from '../const/entity-const';
|
||||
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) {
|
||||
|
||||
try {
|
||||
|
||||
if (!await settingService.isReceive({ env })) {
|
||||
return;
|
||||
}
|
||||
|
||||
const account = await accountService.selectByEmailIncludeDel({ env: env }, message.to);
|
||||
|
||||
const reader = message.raw.getReader();
|
||||
let content = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
content += new TextDecoder().decode(value);
|
||||
}
|
||||
|
||||
const email = await PostalMime.parse(content);
|
||||
|
||||
const params = {
|
||||
sendEmail: email.from.address,
|
||||
name: email.from.name,
|
||||
receiveEmail: message.to,
|
||||
subject: email.subject,
|
||||
content: email.html,
|
||||
text: email.text,
|
||||
userId: account.userId,
|
||||
accountId: account.accountId
|
||||
};
|
||||
|
||||
const emailRow = await emailService.receive({ env }, params);
|
||||
|
||||
|
||||
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);
|
||||
|
||||
if (!env.r2) {
|
||||
console.log('r2对象存储未配置, 附件取消保存');
|
||||
return;
|
||||
}
|
||||
|
||||
for (let attachment of attachments) {
|
||||
await r2Service.putObj({ env }, attachment.key, attachment.content, {
|
||||
contentType: attachment.mimeType,
|
||||
contentDisposition: `attachment; filename="${attachment.filename}"`
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('邮件接收异常: ', e);
|
||||
}
|
||||
}
|
||||
12
mail-worker/src/entity/account.js
Normal file
12
mail-worker/src/entity/account.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { sqliteTable, text, integer} from 'drizzle-orm/sqlite-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
export const account = sqliteTable('account', {
|
||||
accountId: integer('account_id').primaryKey({ autoIncrement: true }),
|
||||
email: text('email').notNull(),
|
||||
status: integer('status').default(0).notNull(),
|
||||
latestEmailTime: text('latest_email_time'),
|
||||
createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`),
|
||||
userId: integer('user_id').notNull(),
|
||||
isDel: integer('is_del').default(0).notNull(),
|
||||
});
|
||||
export default account
|
||||
19
mail-worker/src/entity/att.js
Normal file
19
mail-worker/src/entity/att.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const att = sqliteTable('attachments', {
|
||||
attId: integer('att_id').primaryKey({ autoIncrement: true }),
|
||||
userId: integer('user_id').notNull(),
|
||||
emailId: integer('email_id').notNull(),
|
||||
accountId: integer('account_id').notNull(),
|
||||
key: text('key').notNull(),
|
||||
filename: text('filename'),
|
||||
mimeType: text('mime_type'),
|
||||
size: integer('size'),
|
||||
disposition: text('disposition'),
|
||||
related: text('related'),
|
||||
contentId: text('content_id'),
|
||||
encoding: text('encoding'),
|
||||
createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
});
|
||||
|
||||
16
mail-worker/src/entity/email.js
Normal file
16
mail-worker/src/entity/email.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { sqliteTable, text, integer} from 'drizzle-orm/sqlite-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
export const email = sqliteTable('email', {
|
||||
emailId: integer('email_id').primaryKey({ autoIncrement: true }),
|
||||
sendEmail: text('send_email'),
|
||||
name: text('name'),
|
||||
receiveEmail: text('receive_email').notNull(),
|
||||
accountId: integer('account_id').notNull(),
|
||||
userId: integer('user_id').notNull(),
|
||||
subject: text('subject'),
|
||||
text: text('text'),
|
||||
content: text('content'),
|
||||
createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
isDel: integer('is_del').default(0).notNull()
|
||||
});
|
||||
export default email
|
||||
5
mail-worker/src/entity/orm.js
Normal file
5
mail-worker/src/entity/orm.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { drizzle } from 'drizzle-orm/d1';
|
||||
|
||||
export default function orm(c) {
|
||||
return drizzle(c.env.db,{logger: false})
|
||||
}
|
||||
12
mail-worker/src/entity/setting.js
Normal file
12
mail-worker/src/entity/setting.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { sqliteTable, text, integer} from 'drizzle-orm/sqlite-core';
|
||||
export const setting = sqliteTable('setting', {
|
||||
register: integer('register').default(0).notNull(),
|
||||
receive: integer('receive').default(0).notNull(),
|
||||
title: text('title').default('').notNull(),
|
||||
manyEmail: integer('many_email').default(1).notNull(),
|
||||
addEmail: integer('add_email').default(0).notNull(),
|
||||
autoRefreshTime: integer('auto_refresh_time').default(0).notNull(),
|
||||
registerVerify: integer('register_verify').default(1).notNull(),
|
||||
addEmailVerify: integer('add_email_verify').default(1).notNull()
|
||||
});
|
||||
export default setting
|
||||
11
mail-worker/src/entity/star.js
Normal file
11
mail-worker/src/entity/star.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const star = sqliteTable('star', {
|
||||
starId: integer('star_id').primaryKey({ autoIncrement: true }),
|
||||
userId: integer('user_id').notNull(),
|
||||
emailId: integer('email_id').notNull(),
|
||||
createTime: text('create_time')
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
});
|
||||
14
mail-worker/src/entity/user.js
Normal file
14
mail-worker/src/entity/user.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { sqliteTable, text, integer} from 'drizzle-orm/sqlite-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
const user = sqliteTable('user', {
|
||||
userId: integer('user_id').primaryKey({ autoIncrement: true }),
|
||||
email: text('email').notNull(),
|
||||
type: integer('type').default(1).notNull(),
|
||||
password: text('password').notNull(),
|
||||
salt: text('salt').notNull(),
|
||||
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(),
|
||||
});
|
||||
export default user
|
||||
9
mail-worker/src/error/biz-error.js
Normal file
9
mail-worker/src/error/biz-error.js
Normal file
@@ -0,0 +1,9 @@
|
||||
class BizError extends Error {
|
||||
constructor(message, code) {
|
||||
super(message);
|
||||
this.code = code ? code : 501;
|
||||
this.name = 'BizError';
|
||||
}
|
||||
}
|
||||
|
||||
export default BizError;
|
||||
50
mail-worker/src/hono/hono.js
Normal file
50
mail-worker/src/hono/hono.js
Normal file
@@ -0,0 +1,50 @@
|
||||
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);
|
||||
}else {
|
||||
console.error(err);
|
||||
}
|
||||
return c.json(result.fail(err.message, err.code));
|
||||
});
|
||||
|
||||
export default app;
|
||||
|
||||
|
||||
9
mail-worker/src/hono/webs.js
Normal file
9
mail-worker/src/hono/webs.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import app from './hono';
|
||||
import '../api/email-api';
|
||||
import '../api/user-api';
|
||||
import '../api/login-api';
|
||||
import '../api/setting-api';
|
||||
import '../api/account-api';
|
||||
import '../api/star-api';
|
||||
import '../api/test-api';
|
||||
export default app;
|
||||
17
mail-worker/src/index.js
Normal file
17
mail-worker/src/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import app from './hono/webs';
|
||||
import { email } from './email/email';
|
||||
|
||||
export default {
|
||||
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)
|
||||
return app.fetch(req, env, ctx);
|
||||
}
|
||||
|
||||
return env.ASSETS.fetch(req);
|
||||
},
|
||||
email: email
|
||||
};
|
||||
4
mail-worker/src/init/init-cache.js
Normal file
4
mail-worker/src/init/init-cache.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import settingService from '../service/setting-service';
|
||||
export default async function initCache(c) {
|
||||
await settingService.refresh(c)
|
||||
}
|
||||
96
mail-worker/src/init/init-db.js
Normal file
96
mail-worker/src/init/init-db.js
Normal file
@@ -0,0 +1,96 @@
|
||||
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();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
9
mail-worker/src/model/result.js
Normal file
9
mail-worker/src/model/result.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const result = {
|
||||
ok(data) {
|
||||
return { code: 200, message: 'success', data: data ? data : null };
|
||||
},
|
||||
fail(message, code = 500) {
|
||||
return { code, message };
|
||||
}
|
||||
};
|
||||
export default result;
|
||||
60
mail-worker/src/security/security.js
Normal file
60
mail-worker/src/security/security.js
Normal file
@@ -0,0 +1,60 @@
|
||||
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';
|
||||
|
||||
const exclude = [
|
||||
'/login',
|
||||
'/register',
|
||||
'/setting/query'
|
||||
];
|
||||
|
||||
const adminPerm = [
|
||||
'/setting/set'
|
||||
]
|
||||
|
||||
const verify = async (c) => {
|
||||
|
||||
if (c.req.path.startsWith('/test')) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (exclude.includes(c.req.path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const jwt = c.req.header(constant.TOKEN_HEADER);
|
||||
|
||||
const result = await jwtUtils.verifyToken(c, jwt)
|
||||
|
||||
if (!result) {
|
||||
throw new BizError('身份认证失效,请重新登录',401)
|
||||
}
|
||||
|
||||
const {userId, token} = result
|
||||
const authInfo = await c.env.kv.get(KvConst.AUTH_INFO + userId,{ type: 'json' });
|
||||
|
||||
if (!authInfo) {
|
||||
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)
|
||||
}
|
||||
|
||||
if (dayjs().diff(dayjs(authInfo.createTime), 'day') >= 1) {
|
||||
authInfo.refreshTime = new Date().toISOString()
|
||||
await c.env.kv.put(KvConst.AUTH_INFO + userId, JSON.stringify(authInfo), { expirationTtl: constant.TOKEN_EXPIRE });
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
};
|
||||
|
||||
export default verify;
|
||||
16
mail-worker/src/security/user-context.js
Normal file
16
mail-worker/src/security/user-context.js
Normal file
@@ -0,0 +1,16 @@
|
||||
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);
|
||||
},
|
||||
async getToken(c) {
|
||||
const jwt = c.req.header(constant.TOKEN_HEADER);
|
||||
const { token } = JwtUtils.verifyToken(c,jwt);
|
||||
return token;
|
||||
},
|
||||
};
|
||||
export default userContext;
|
||||
127
mail-worker/src/service/account-service.js
Normal file
127
mail-worker/src/service/account-service.js
Normal file
@@ -0,0 +1,127 @@
|
||||
import BizError from '../error/biz-error';
|
||||
import verifyUtils from '../utils/verify-utils';
|
||||
import emailUtils from '../utils/email-utils';
|
||||
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 { isDel } from '../const/entity-const';
|
||||
import settingService from './setting-service';
|
||||
import turnstileService from './turnstile-service';
|
||||
|
||||
const accountService = {
|
||||
|
||||
async add(c, params, userId) {
|
||||
|
||||
|
||||
if (!await settingService.isAddEmail(c)) {
|
||||
throw new BizError('添加邮箱功能已关闭');
|
||||
}
|
||||
|
||||
let { email, token } = params;
|
||||
|
||||
if (!email) {
|
||||
throw new BizError('邮箱不能为空');
|
||||
}
|
||||
|
||||
if (!verifyUtils.isEmail(email)) {
|
||||
throw new BizError('非法邮箱');
|
||||
}
|
||||
|
||||
if (!c.env.domain.includes(emailUtils.getDomain(email))) {
|
||||
throw new BizError('未配置改邮箱域名');
|
||||
}
|
||||
|
||||
const accountRow = await this.selectByEmailIncludeDel(c, email);
|
||||
|
||||
if (accountRow && accountRow.isDel === isDel.DELETE) {
|
||||
throw new BizError('该邮箱已被注销');
|
||||
}
|
||||
|
||||
if (accountRow) {
|
||||
throw new BizError('该邮箱已被注册');
|
||||
}
|
||||
|
||||
if (await settingService.isAddEmailVerify(c)) {
|
||||
await turnstileService.verify(c, token);
|
||||
}
|
||||
|
||||
return orm(c).insert(account).values({ email: email, userId: userId }).returning().get();
|
||||
},
|
||||
|
||||
selectByEmailIncludeDel(c, email) {
|
||||
return orm(c).select().from(account).where(eq(account.email, email)).get();
|
||||
},
|
||||
|
||||
selectByEmail(c, email) {
|
||||
return orm(c).select().from(account).where(
|
||||
and(
|
||||
eq(account.email, email),
|
||||
eq(account.isDel, isDel.NORMAL)))
|
||||
.get();
|
||||
},
|
||||
|
||||
list(c, params, userId) {
|
||||
|
||||
let { accountId, size } = params;
|
||||
|
||||
accountId = Number(accountId);
|
||||
size = Number(size);
|
||||
|
||||
if (size > 30) {
|
||||
size = 30;
|
||||
}
|
||||
|
||||
if (!accountId) {
|
||||
accountId = 0;
|
||||
}
|
||||
return orm(c).select().from(account).where(
|
||||
and(
|
||||
eq(account.userId, userId),
|
||||
eq(account.isDel, isDel.NORMAL),
|
||||
gt(account.accountId, accountId)))
|
||||
.orderBy(asc(account.accountId))
|
||||
.limit(size)
|
||||
.all();
|
||||
},
|
||||
|
||||
async delete(c, params, userId) {
|
||||
|
||||
let { accountId } = params;
|
||||
|
||||
const user = await userService.selectById(c, userId);
|
||||
const accountRow = await this.selectById(c, accountId);
|
||||
|
||||
if (accountRow.email === user.email) {
|
||||
throw new BizError('不可以删除自己的邮箱');
|
||||
}
|
||||
|
||||
if (accountRow.userId !== user.userId) {
|
||||
throw new BizError('该邮箱不属于当前用户');
|
||||
}
|
||||
|
||||
await orm(c).update(account).set({ isDel: isDel.DELETE }).where(
|
||||
and(eq(account.userId, userId),
|
||||
eq(account.accountId, accountId)))
|
||||
.run();
|
||||
await emailService.removeByAccountId(c, accountId);
|
||||
},
|
||||
|
||||
selectById(c, accountId) {
|
||||
return orm(c).select().from(account).where(
|
||||
and(eq(account.accountId, accountId),
|
||||
eq(account.isDel, isDel.NORMAL)))
|
||||
.get();
|
||||
},
|
||||
|
||||
async insert(c, params) {
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
export default accountService;
|
||||
22
mail-worker/src/service/att-service.js
Normal file
22
mail-worker/src/service/att-service.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import orm from '../entity/orm';
|
||||
import { att } from '../entity/att';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
|
||||
const attService = {
|
||||
async addAtt(c, params) {
|
||||
await orm(c).insert(att).values(params).run();
|
||||
},
|
||||
async list(c, params, userId) {
|
||||
const { emailId } = params;
|
||||
|
||||
const list = await orm(c).select().from(att).where(
|
||||
and(
|
||||
eq(att.emailId, emailId),
|
||||
eq(att.userId, userId)))
|
||||
.all();
|
||||
|
||||
return list;
|
||||
}
|
||||
};
|
||||
|
||||
export default attService;
|
||||
102
mail-worker/src/service/email-service.js
Normal file
102
mail-worker/src/service/email-service.js
Normal file
@@ -0,0 +1,102 @@
|
||||
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 { star } from '../entity/star';
|
||||
|
||||
const emailService = {
|
||||
|
||||
async list(c, params, userId) {
|
||||
|
||||
let { emailId, accountId, size } = params;
|
||||
size = Number(size);
|
||||
emailId = Number(emailId);
|
||||
|
||||
if (size > 30) {
|
||||
size = 30;
|
||||
}
|
||||
|
||||
if (!emailId) {
|
||||
emailId = 9999999999;
|
||||
}
|
||||
|
||||
const list = await orm(c)
|
||||
.select({
|
||||
...email,
|
||||
starId: star.starId
|
||||
})
|
||||
.from(email)
|
||||
.leftJoin(
|
||||
star,
|
||||
and(
|
||||
eq(star.emailId, email.emailId),
|
||||
eq(star.userId, userId)
|
||||
)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
lt(email.emailId, emailId),
|
||||
eq(email.accountId, accountId),
|
||||
eq(email.userId, userId),
|
||||
eq(email.isDel, isDel.NORMAL)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(email.emailId))
|
||||
.limit(size)
|
||||
.all();
|
||||
|
||||
const resultList = list.map(item => ({
|
||||
...item,
|
||||
isStar: item.starId != null ? 1 : 0
|
||||
}));
|
||||
|
||||
return { list: resultList };
|
||||
},
|
||||
|
||||
async delete(c, params, userId) {
|
||||
const { emailIds } = params;
|
||||
const emailIdList = emailIds.split(',').map(Number);
|
||||
await orm(c).update(email).set({ isDel: isDel.DELETE }).where(
|
||||
and(
|
||||
eq(email.userId, userId),
|
||||
inArray(email.emailId, emailIdList)))
|
||||
.run();
|
||||
},
|
||||
|
||||
receive(c, params) {
|
||||
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 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),
|
||||
eq(email.isDel, isDel.NORMAL)))
|
||||
.get();
|
||||
},
|
||||
|
||||
latest(c, params, userId) {
|
||||
let { emailId,accountId } = params;
|
||||
return orm(c).select().from(email).where(
|
||||
and(
|
||||
eq(email.userId, userId),
|
||||
eq(email.isDel, isDel.NORMAL),
|
||||
eq(email.accountId, accountId),
|
||||
gt(email.emailId, emailId)
|
||||
))
|
||||
.orderBy(desc(email.emailId))
|
||||
.limit(20);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export default emailService;
|
||||
113
mail-worker/src/service/login-service.js
Normal file
113
mail-worker/src/service/login-service.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import BizError from '../error/biz-error';
|
||||
import userService from './user-service';
|
||||
import emailUtils from '../utils/email-utils';
|
||||
import { isDel, userConst } from '../const/entity-const';
|
||||
import JwtUtils from '../utils/jwt-utils';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import KvConst from '../const/kv-const';
|
||||
import constant from '../const/constant';
|
||||
import userContext from '../security/user-context';
|
||||
import verifyUtils from '../utils/verify-utils';
|
||||
import accountService from './account-service';
|
||||
import settingService from './setting-service';
|
||||
import saltHashUtils from '../utils/crypto-utils';
|
||||
import cryptoUtils from '../utils/crypto-utils';
|
||||
import turnstileService from './turnstile-service';
|
||||
|
||||
|
||||
const loginService = {
|
||||
|
||||
async register(c, params) {
|
||||
|
||||
const { email, password, token } = params;
|
||||
|
||||
if (!await settingService.isRegister(c)) {
|
||||
throw new BizError('注册功能已关闭');
|
||||
}
|
||||
|
||||
if (!verifyUtils.isEmail(email)) {
|
||||
throw new BizError('非法邮箱');
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
throw new BizError('密码必须大于6位');
|
||||
}
|
||||
|
||||
if (!c.env.domain.includes(emailUtils.getDomain(email))) {
|
||||
throw new BizError('非法邮箱域名');
|
||||
}
|
||||
|
||||
const userRow = await userService.selectByEmailIncludeDel(c, email);
|
||||
|
||||
if (userRow && userRow.isDel === isDel.DELETE) {
|
||||
throw new BizError('该邮箱已被注销');
|
||||
}
|
||||
|
||||
if (userRow) {
|
||||
throw new BizError('该邮箱已被注册');
|
||||
}
|
||||
|
||||
if (await settingService.isRegisterVerify(c)) {
|
||||
await turnstileService.verify(c,token)
|
||||
}
|
||||
|
||||
const { salt, hash } = await saltHashUtils.hashPassword(password);
|
||||
|
||||
const userId = await userService.insert(c, { email, password: hash, salt, type: userConst.type.COMMON });
|
||||
await accountService.insert(c, { userId: userId, email });
|
||||
},
|
||||
|
||||
async login(c, params) {
|
||||
|
||||
const { email, password } = params;
|
||||
|
||||
if (!email || !password) {
|
||||
throw new BizError('邮箱和密码不能为空');
|
||||
}
|
||||
|
||||
const userRow = await userService.selectByEmail(c, email);
|
||||
|
||||
if (!userRow) {
|
||||
throw new BizError('该邮箱不存在');
|
||||
}
|
||||
|
||||
if (!await cryptoUtils.verifyPassword(password, userRow.salt, userRow.password)) {
|
||||
throw new BizError('密码输入错误');
|
||||
}
|
||||
|
||||
const uuid = uuidv4();
|
||||
const jwt = await JwtUtils.generateToken(c,{ userId: userRow.userId, token: uuid });
|
||||
|
||||
let authInfo = await c.env.kv.get(KvConst.AUTH_INFO + userRow.userId, { type: 'json' });
|
||||
|
||||
if (authInfo) {
|
||||
|
||||
authInfo.tokens.push(uuid);
|
||||
|
||||
} else {
|
||||
|
||||
authInfo = {
|
||||
tokens: [],
|
||||
user: userRow,
|
||||
refreshTime: new Date().toISOString()
|
||||
};
|
||||
|
||||
authInfo.tokens.push(uuid);
|
||||
|
||||
}
|
||||
|
||||
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 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);
|
||||
await c.env.kv.put(KvConst.AUTH_INFO + userId, JSON.stringify(authInfo));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export default loginService;
|
||||
11
mail-worker/src/service/r2-service.js
Normal file
11
mail-worker/src/service/r2-service.js
Normal file
@@ -0,0 +1,11 @@
|
||||
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, {
|
||||
httpMetadata: {...metadata}
|
||||
});
|
||||
}
|
||||
};
|
||||
export default r2Service;
|
||||
54
mail-worker/src/service/setting-service.js
Normal file
54
mail-worker/src/service/setting-service.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import KvConst from '../const/kv-const';
|
||||
import setting from '../entity/setting';
|
||||
import orm from '../entity/orm';
|
||||
import { settingConst } from '../const/entity-const';
|
||||
|
||||
const settingService = {
|
||||
|
||||
async refresh(c) {
|
||||
const settingRow = await orm(c).select().from(setting).get();
|
||||
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;
|
||||
domainList = domainList.map(item => '@' + item);
|
||||
setting.domainList = domainList;
|
||||
setting.siteKey = c.env.site_key;
|
||||
setting.r2Domain = c.env.r2_domain;
|
||||
return setting;
|
||||
},
|
||||
|
||||
async set(c, params) {
|
||||
await orm(c).update(setting).set({ ...params }).returning().get();
|
||||
await this.refresh(c);
|
||||
},
|
||||
|
||||
async isRegister(c) {
|
||||
const { register } = await this.query(c);
|
||||
return register === settingConst.register.OPEN;
|
||||
},
|
||||
|
||||
async isReceive(c) {
|
||||
const { receive } = await this.query(c);
|
||||
return receive === settingConst.receive.OPEN;
|
||||
},
|
||||
|
||||
async isAddEmail(c) {
|
||||
const { addEmail, manyEmail } = await this.query(c);
|
||||
return addEmail === settingConst.addEmail.OPEN && manyEmail === settingConst.manyEmail.OPEN;
|
||||
},
|
||||
|
||||
async isRegisterVerify(c) {
|
||||
const { registerVerify } = await this.query(c);
|
||||
return registerVerify === settingConst.registerVerify.OPEN;
|
||||
},
|
||||
|
||||
async isAddEmailVerify(c) {
|
||||
const { addEmailVerify } = await this.query(c);
|
||||
return addEmailVerify === settingConst.addEmailVerify.OPEN;
|
||||
}
|
||||
};
|
||||
|
||||
export default settingService;
|
||||
69
mail-worker/src/service/star-service.js
Normal file
69
mail-worker/src/service/star-service.js
Normal file
@@ -0,0 +1,69 @@
|
||||
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 email from '../entity/email';
|
||||
import { isDel } from '../const/entity-const';
|
||||
|
||||
const startService = {
|
||||
|
||||
async add(c, params, userId) {
|
||||
const { emailId } = params;
|
||||
const email = await emailService.selectById(c, emailId);
|
||||
if (!email) {
|
||||
throw new BizError('星标的邮件不存在');
|
||||
}
|
||||
if (!email.userId === userId) {
|
||||
throw new BizError('星标的邮件非当前用户所有');
|
||||
}
|
||||
const exist = await orm(c).select().from(star).where(
|
||||
and(
|
||||
eq(star.userId, userId),
|
||||
eq(star.emailId, emailId)))
|
||||
.get()
|
||||
|
||||
if (exist) {
|
||||
return
|
||||
}
|
||||
|
||||
await orm(c).insert(star).values({ userId, emailId }).run();
|
||||
},
|
||||
|
||||
async cancel(c, params, userId) {
|
||||
const { emailId } = params;
|
||||
await orm(c).delete(star).where(
|
||||
and(
|
||||
eq(star.userId, userId),
|
||||
eq(star.emailId, emailId)))
|
||||
.run();
|
||||
},
|
||||
|
||||
async list(c, params, userId) {
|
||||
let { emailId, size } = params;
|
||||
emailId = Number(emailId);
|
||||
size = Number(size);
|
||||
|
||||
if (!emailId) {
|
||||
emailId = 9999999999;
|
||||
}
|
||||
|
||||
const list = await orm(c).select({
|
||||
isStar: sql`1`.as('isStar'),
|
||||
starId: star.starId
|
||||
, ...email
|
||||
}).from(star)
|
||||
.leftJoin(email, eq(email.emailId, star.emailId))
|
||||
.where(
|
||||
and(
|
||||
eq(star.userId, userId),
|
||||
eq(email.isDel, isDel.NORMAL),
|
||||
lt(star.emailId, emailId)))
|
||||
.orderBy(desc(star.emailId))
|
||||
.limit(size)
|
||||
.all();
|
||||
return { list };
|
||||
}
|
||||
};
|
||||
|
||||
export default startService;
|
||||
30
mail-worker/src/service/turnstile-service.js
Normal file
30
mail-worker/src/service/turnstile-service.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import BizError from '../error/biz-error';
|
||||
|
||||
const turnstileService = {
|
||||
|
||||
async verify(c, token) {
|
||||
|
||||
if (!token) {
|
||||
throw new BizError('验证token不能为空');
|
||||
}
|
||||
const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
secret: c.env.secret_key,
|
||||
response: token,
|
||||
remoteip: c.req.header('cf-connecting-ip')
|
||||
})
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
console.log(result)
|
||||
if (!result.success) {
|
||||
throw new BizError('人机验证失败,请重试',400)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default turnstileService;
|
||||
67
mail-worker/src/service/user-service.js
Normal file
67
mail-worker/src/service/user-service.js
Normal file
@@ -0,0 +1,67 @@
|
||||
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 kvConst from '../const/kv-const';
|
||||
import cryptoUtils from '../utils/crypto-utils';
|
||||
|
||||
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;
|
||||
user.accountId = account.accountId;
|
||||
user.type = c.env.admin === user.email ? 0 : 1;
|
||||
return user;
|
||||
},
|
||||
|
||||
async resetPassword(c, params, userId) {
|
||||
|
||||
const { password } = params;
|
||||
|
||||
if (password < 6) {
|
||||
throw new BizError('密码不能小于6位');
|
||||
}
|
||||
const { salt, hash } = await cryptoUtils.hashPassword(password);
|
||||
await orm(c).update(user).set({ password: hash, salt: salt }).where(eq(user.userId, userId)).run();
|
||||
},
|
||||
|
||||
selectByEmail(c, email) {
|
||||
return orm(c).select().from(user).where(
|
||||
and(
|
||||
eq(user.email, email),
|
||||
eq(user.isDel, isDel.NORMAL)))
|
||||
.get();
|
||||
},
|
||||
|
||||
async insert(c, params) {
|
||||
const { userId } = await orm(c).insert(user).values({ ...params }).returning().get();
|
||||
return userId;
|
||||
},
|
||||
|
||||
selectByEmailIncludeDel(c, email) {
|
||||
return orm(c).select().from(user).where(eq(user.email, email)).get();
|
||||
},
|
||||
|
||||
selectById(c, userId) {
|
||||
return orm(c).select().from(user).where(
|
||||
and(
|
||||
eq(user.userId, userId),
|
||||
eq(user.isDel, isDel.NORMAL)))
|
||||
.get();
|
||||
},
|
||||
|
||||
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)
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
export default userService;
|
||||
31
mail-worker/src/utils/crypto-utils.js
Normal file
31
mail-worker/src/utils/crypto-utils.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const saltHashUtils = {
|
||||
|
||||
generateSalt(length = 16) {
|
||||
const array = new Uint8Array(length);
|
||||
crypto.getRandomValues(array);
|
||||
return btoa(String.fromCharCode(...array));
|
||||
},
|
||||
|
||||
|
||||
async hashPassword(password) {
|
||||
const salt = this.generateSalt();
|
||||
const hash = await this.genHashPassword(password, salt);
|
||||
return { salt, hash };
|
||||
},
|
||||
|
||||
async genHashPassword(password, salt) {
|
||||
const data = encoder.encode(salt + password);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
return btoa(String.fromCharCode(...hashArray));
|
||||
},
|
||||
|
||||
async verifyPassword(inputPassword, salt, storedHash) {
|
||||
const hash = await this.genHashPassword(inputPassword, salt);
|
||||
return hash === storedHash;
|
||||
}
|
||||
};
|
||||
|
||||
export default saltHashUtils;
|
||||
9
mail-worker/src/utils/email-utils.js
Normal file
9
mail-worker/src/utils/email-utils.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const emailUtils = {
|
||||
getDomain(email) {
|
||||
if (typeof email !== 'string') return ''
|
||||
const parts = email.split('@')
|
||||
return parts.length === 2 ? parts[1] : ''
|
||||
}
|
||||
}
|
||||
|
||||
export default emailUtils
|
||||
9
mail-worker/src/utils/file-utils.js
Normal file
9
mail-worker/src/utils/file-utils.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const fileUtils = {
|
||||
getExtFileName(filename) {
|
||||
const index = filename.lastIndexOf('.');
|
||||
return index !== -1 ? filename.slice(index) : '';
|
||||
}
|
||||
};
|
||||
|
||||
export default fileUtils
|
||||
|
||||
87
mail-worker/src/utils/jwt-utils.js
Normal file
87
mail-worker/src/utils/jwt-utils.js
Normal file
@@ -0,0 +1,87 @@
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
const base64url = (input) => {
|
||||
const str = btoa(String.fromCharCode(...new Uint8Array(input)));
|
||||
return str.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
||||
};
|
||||
|
||||
const base64urlDecode = (str) => {
|
||||
str = str.replace(/-/g, '+').replace(/_/g, '/');
|
||||
while (str.length % 4) str += '=';
|
||||
return Uint8Array.from(atob(str), c => c.charCodeAt(0));
|
||||
};
|
||||
|
||||
const jwtUtils = {
|
||||
async generateToken(c, payload, expiresInSeconds) {
|
||||
const header = {
|
||||
alg: 'HS256',
|
||||
typ: 'JWT'
|
||||
};
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const exp = expiresInSeconds ? now + expiresInSeconds : undefined;
|
||||
|
||||
const fullPayload = {
|
||||
...payload,
|
||||
iat: now,
|
||||
...(exp ? { exp } : {})
|
||||
};
|
||||
|
||||
const headerStr = base64url(encoder.encode(JSON.stringify(header)));
|
||||
const payloadStr = base64url(encoder.encode(JSON.stringify(fullPayload)));
|
||||
const data = `${headerStr}.${payloadStr}`;
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(c.env.jwt_secret),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['sign']
|
||||
);
|
||||
|
||||
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
|
||||
const signatureStr = base64url(signature);
|
||||
|
||||
return `${data}.${signatureStr}`;
|
||||
},
|
||||
|
||||
async verifyToken(c, token) {
|
||||
try {
|
||||
const [headerB64, payloadB64, signatureB64] = token.split('.');
|
||||
|
||||
if (!headerB64 || !payloadB64 || !signatureB64) return null;
|
||||
|
||||
const data = `${headerB64}.${payloadB64}`;
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(c.env.jwt_secret),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['verify']
|
||||
);
|
||||
|
||||
const valid = await crypto.subtle.verify(
|
||||
'HMAC',
|
||||
key,
|
||||
base64urlDecode(signatureB64),
|
||||
encoder.encode(data)
|
||||
);
|
||||
|
||||
if (!valid) return null;
|
||||
|
||||
const payloadJson = decoder.decode(base64urlDecode(payloadB64));
|
||||
const payload = JSON.parse(payloadJson);
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (payload.exp && payload.exp < now) return null;
|
||||
|
||||
return payload;
|
||||
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default jwtUtils;
|
||||
7
mail-worker/src/utils/verify-utils.js
Normal file
7
mail-worker/src/utils/verify-utils.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const verifyUtils = {
|
||||
isEmail(str) {
|
||||
return /^[a-zA-Z0-9]+@([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]+$/.test(str);
|
||||
}
|
||||
}
|
||||
|
||||
export default verifyUtils
|
||||
Reference in New Issue
Block a user