mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-17 14:47:35 +08:00
feat: add USER_ROLES && admin pages search when keybord enter && auto trim (#348)
* feat: add USER_ROLES * feat: admin pages search when keybord enter && auto trim * feat: update version to v0.6.0
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { Context } from 'hono';
|
||||
|
||||
import { CONSTANTS } from '../constants';
|
||||
import { getJsonSetting, saveSetting, checkUserPassword, getDomains } from '../utils';
|
||||
import { getJsonSetting, saveSetting, checkUserPassword, getDomains, getUserRoles } from '../utils';
|
||||
import { UserSettings, GeoData, UserInfo } from "../models";
|
||||
import { handleListQuery } from '../common'
|
||||
import { HonoCustomType } from '../types';
|
||||
@@ -38,18 +38,22 @@ export default {
|
||||
const { limit, offset, query } = c.req.query();
|
||||
if (query) {
|
||||
return await handleListQuery(c,
|
||||
`SELECT u.id, u.user_email, u.created_at, u.updated_at,`
|
||||
`SELECT u.id as id, u.user_email, u.created_at, u.updated_at,`
|
||||
+ ` ur.role_text as role_text,`
|
||||
+ ` (SELECT COUNT(*) FROM users_address WHERE user_id = u.id) AS address_count`
|
||||
+ ` FROM users u`
|
||||
+ ` LEFT JOIN user_roles ur ON u.id = ur.user_id`
|
||||
+ ` where u.user_email like ?`,
|
||||
`SELECT count(*) as count FROM users where user_email like ?`,
|
||||
[`%${query}%`], limit, offset
|
||||
);
|
||||
}
|
||||
return await handleListQuery(c,
|
||||
`SELECT u.id, u.user_email, u.created_at, u.updated_at,`
|
||||
`SELECT u.id as id, u.user_email, u.created_at, u.updated_at,`
|
||||
+ ` ur.role_text as role_text,`
|
||||
+ ` (SELECT COUNT(*) FROM users_address WHERE user_id = u.id) AS address_count`
|
||||
+ ` FROM users u`,
|
||||
+ ` FROM users u`
|
||||
+ ` LEFT JOIN user_roles ur ON u.id = ur.user_id`,
|
||||
`SELECT count(*) as count FROM users`,
|
||||
[], limit, offset
|
||||
);
|
||||
@@ -114,4 +118,30 @@ export default {
|
||||
}
|
||||
return c.json({ success: true });
|
||||
},
|
||||
updateUserRoles: async (c: Context<HonoCustomType>) => {
|
||||
const { user_id, role_text } = await c.req.json();
|
||||
if (!user_id) return c.text("Invalid user_id", 400);
|
||||
if (!role_text) {
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM user_roles WHERE user_id = ?`
|
||||
).bind(user_id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to update user roles", 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
}
|
||||
const user_roles = getUserRoles(c);
|
||||
if (!user_roles.find((r) => r.role === role_text)) {
|
||||
return c.text("Invalid role_text", 400)
|
||||
}
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`INSERT INTO user_roles (user_id, role_text)`
|
||||
+ ` VALUES (?, ?)`
|
||||
+ ` ON CONFLICT(user_id) DO UPDATE SET role_text = ?, updated_at = datetime('now')`
|
||||
).bind(user_id, role_text, role_text).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to update user roles", 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Hono } from 'hono'
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
import { HonoCustomType } from '../types'
|
||||
import { sendAdminInternalMail, getJsonSetting, saveSetting } from '../utils'
|
||||
import { sendAdminInternalMail, getJsonSetting, saveSetting, getUserRoles } from '../utils'
|
||||
import { newAddress, handleListQuery } from '../common'
|
||||
import { CONSTANTS } from '../constants'
|
||||
import cleanup_api from './cleanup_api'
|
||||
@@ -40,7 +40,7 @@ api.post('/admin/new_address', async (c) => {
|
||||
return c.text("Please provide a name", 400)
|
||||
}
|
||||
try {
|
||||
const res = await newAddress(c, name, domain, enablePrefix, false);
|
||||
const res = await newAddress(c, name, domain, enablePrefix, false, null, false);
|
||||
return c.json(res);
|
||||
} catch (e) {
|
||||
return c.text(`Failed create address: ${(e as Error).message}`, 400)
|
||||
@@ -292,5 +292,7 @@ api.get('/admin/users', admin_user_api.getUsers)
|
||||
api.delete('/admin/users/:user_id', admin_user_api.deleteUser)
|
||||
api.post('/admin/users', admin_user_api.createUser)
|
||||
api.post('/admin/users/:user_id/reset_password', admin_user_api.resetPassword)
|
||||
api.get('/admin/user_roles', async (c) => c.json(getUserRoles(c)))
|
||||
api.post('/admin/user_roles', admin_user_api.updateUserRoles)
|
||||
api.get("/admin/webhook/settings", webhook_settings.getWebhookSettings);
|
||||
api.post("/admin/webhook/settings", webhook_settings.saveWebhookSettings);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Hono } from 'hono'
|
||||
|
||||
import { getDomains, getPasswords, getBooleanValue, getIntValue, getStringArray } from './utils';
|
||||
import { getDomains, getPasswords, getBooleanValue, getIntValue, getStringArray, getDefaultDomains } from './utils';
|
||||
import { CONSTANTS } from './constants';
|
||||
import { HonoCustomType } from './types';
|
||||
import { isS3Enabled } from './mails_api/s3_attachment';
|
||||
@@ -20,6 +20,7 @@ api.get('/open_api/settings', async (c) => {
|
||||
"prefix": c.env.PREFIX,
|
||||
"minAddressLen": getIntValue(c.env.MIN_ADDRESS_LEN, 1),
|
||||
"maxAddressLen": getIntValue(c.env.MAX_ADDRESS_LEN, 30),
|
||||
"defaultDomains": getDefaultDomains(c),
|
||||
"domains": getDomains(c),
|
||||
"domainLabels": getStringArray(c.env.DOMAIN_LABELS),
|
||||
"needAuth": needAuth,
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { Context } from 'hono';
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
import { getBooleanValue, getDomains, getStringValue, getIntValue } from './utils';
|
||||
import { HonoCustomType } from './types';
|
||||
import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles, getDefaultDomains } from './utils';
|
||||
import { HonoCustomType, UserRole } from './types';
|
||||
import { unbindTelegramByAddress } from './telegram_api/common';
|
||||
|
||||
export const newAddress = async (
|
||||
c: Context<HonoCustomType>,
|
||||
name: string, domain: string | undefined | null,
|
||||
enablePrefix: boolean,
|
||||
checkLengthByConfig: boolean = true
|
||||
checkLengthByConfig: boolean = true,
|
||||
addressPrefix: string | undefined | null = null,
|
||||
checkAllowDomains: boolean = true
|
||||
): Promise<{ address: string, jwt: string }> => {
|
||||
// remove special characters
|
||||
name = name.replace(/[^a-z0-9]/g, '')
|
||||
@@ -30,14 +32,16 @@ export const newAddress = async (
|
||||
if (name.length > maxAddressLength) {
|
||||
throw new Error(`Name too long (max ${maxAddressLength})`);
|
||||
}
|
||||
// create address
|
||||
if (enablePrefix) {
|
||||
// create address with prefix
|
||||
if (typeof addressPrefix === "string") {
|
||||
name = addressPrefix + name;
|
||||
} else if (enablePrefix) {
|
||||
name = getStringValue(c.env.PREFIX) + name;
|
||||
}
|
||||
// check domain, generate random domain
|
||||
const domains = getDomains(c);
|
||||
if (!domain || !domains.includes(domain)) {
|
||||
domain = domains[Math.floor(Math.random() * domains.length)];
|
||||
// check domain
|
||||
const allowDomains = checkAllowDomains ? await getAllowDomains(c) : getDomains(c);
|
||||
if (!domain || !allowDomains.includes(domain)) {
|
||||
throw new Error("Invalid domain")
|
||||
}
|
||||
// create address
|
||||
name = name + "@" + domain;
|
||||
@@ -217,3 +221,34 @@ export const commonParseMail = async (raw_mail: string | undefined | null): Prom
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const commonGetUserRole = async (
|
||||
c: Context<HonoCustomType>, user_id: number
|
||||
): Promise<UserRole | undefined | null> => {
|
||||
const user_roles = getUserRoles(c);
|
||||
const role_text = await c.env.DB.prepare(
|
||||
`SELECT role_text FROM user_roles where user_id = ?`
|
||||
).bind(user_id).first<string | undefined | null>("role_text");
|
||||
return role_text ? user_roles.find((r) => r.role === role_text) : null;
|
||||
}
|
||||
|
||||
export const getAddressPrefix = async (c: Context<HonoCustomType>): Promise<string | undefined> => {
|
||||
const user = c.get("userPayload");
|
||||
if (!user) {
|
||||
return c.env.PREFIX;
|
||||
}
|
||||
const user_role = await commonGetUserRole(c, user.user_id);
|
||||
if (typeof user_role?.prefix === "string") {
|
||||
return user_role.prefix;
|
||||
}
|
||||
return c.env.PREFIX;
|
||||
}
|
||||
|
||||
export const getAllowDomains = async (c: Context<HonoCustomType>): Promise<string[]> => {
|
||||
const user = c.get("userPayload");
|
||||
if (!user) {
|
||||
return getDefaultDomains(c);
|
||||
}
|
||||
const user_role = await commonGetUserRole(c, user.user_id);
|
||||
return user_role?.domains || getDefaultDomains(c);;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const CONSTANTS = {
|
||||
VERSION: 'v0.5.5',
|
||||
VERSION: 'v0.6.0',
|
||||
|
||||
// DB settings
|
||||
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Hono } from 'hono'
|
||||
|
||||
import { HonoCustomType } from "../types";
|
||||
import { getBooleanValue, getJsonSetting, checkCfTurnstile } from '../utils';
|
||||
import { newAddress, handleListQuery, deleteAddressWithData } from '../common'
|
||||
import { newAddress, handleListQuery, deleteAddressWithData, getAddressPrefix, getAllowDomains } from '../common'
|
||||
import { CONSTANTS } from '../constants'
|
||||
import auto_reply from './auto_reply'
|
||||
import webhook_settings from './webhook_settings';
|
||||
@@ -118,7 +118,8 @@ api.post('/api/new_address', async (c) => {
|
||||
console.error(error);
|
||||
}
|
||||
try {
|
||||
const res = await newAddress(c, name, domain, true);
|
||||
const addressPrefix = await getAddressPrefix(c);
|
||||
const res = await newAddress(c, name, domain, true, true, addressPrefix);
|
||||
return c.json(res);
|
||||
} catch (e) {
|
||||
return c.text(`Failed create address: ${(e as Error).message}`, 400)
|
||||
|
||||
8
worker/src/types.d.ts
vendored
8
worker/src/types.d.ts
vendored
@@ -1,3 +1,9 @@
|
||||
export type UserRole = {
|
||||
domains: string[] | undefined | null,
|
||||
role: string,
|
||||
prefix: string | undefined | null
|
||||
}
|
||||
|
||||
export type Bindings = {
|
||||
// bindings
|
||||
DB: D1Database
|
||||
@@ -10,7 +16,9 @@ export type Bindings = {
|
||||
PREFIX: string | undefined
|
||||
MIN_ADDRESS_LEN: string | number | undefined
|
||||
MAX_ADDRESS_LEN: string | number | undefined
|
||||
DEFAULT_DOMAINS: string | string[] | undefined
|
||||
DOMAINS: string | string[] | undefined
|
||||
USER_ROLES: string | UserRole[] | undefined
|
||||
DOMAIN_LABELS: string | string[] | undefined
|
||||
PASSWORDS: string | string[] | undefined
|
||||
ADMIN_PASSWORDS: string | string[] | undefined
|
||||
|
||||
@@ -2,8 +2,9 @@ import { Context } from "hono";
|
||||
|
||||
import { HonoCustomType } from "../types";
|
||||
import { UserSettings } from "../models";
|
||||
import { getJsonSetting } from "../utils"
|
||||
import { getJsonSetting, getUserRoles } from "../utils"
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { commonGetUserRole } from "../common";
|
||||
|
||||
export default {
|
||||
openSettings: async (c: Context<HonoCustomType>) => {
|
||||
@@ -19,10 +20,14 @@ export default {
|
||||
// check if user exists
|
||||
const db_user_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM users where id = ?`
|
||||
).bind(user.user_id).first("id");
|
||||
).bind(user.user_id).first<number | undefined | null>("id");
|
||||
if (!db_user_id) {
|
||||
return c.text("User not found", 400);
|
||||
}
|
||||
return c.json(user);
|
||||
const user_role = await commonGetUserRole(c, db_user_id);
|
||||
return c.json({
|
||||
...user,
|
||||
user_role: user_role
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Context } from "hono";
|
||||
import { createMimeMessage } from "mimetext";
|
||||
import { HonoCustomType } from "./types";
|
||||
import { HonoCustomType, UserRole } from "./types";
|
||||
import { User } from "telegraf/types";
|
||||
|
||||
export const getJsonSetting = async (
|
||||
c: Context<HonoCustomType>, key: string
|
||||
@@ -97,6 +98,12 @@ export const getStringArray = (
|
||||
return value;
|
||||
}
|
||||
|
||||
export const getDefaultDomains = (c: Context<HonoCustomType>): string[] => {
|
||||
const domains = getStringArray(c.env.DEFAULT_DOMAINS);
|
||||
if (domains && domains.length > 0) return domains;
|
||||
return getDomains(c);
|
||||
}
|
||||
|
||||
export const getDomains = (c: Context<HonoCustomType>): string[] => {
|
||||
if (!c.env.DOMAINS) {
|
||||
return [];
|
||||
@@ -113,6 +120,22 @@ export const getDomains = (c: Context<HonoCustomType>): string[] => {
|
||||
return c.env.DOMAINS;
|
||||
}
|
||||
|
||||
export const getUserRoles = (c: Context<HonoCustomType>): UserRole[] => {
|
||||
if (!c.env.USER_ROLES) {
|
||||
return [];
|
||||
}
|
||||
// check if USER_ROLES is an array, if not use json.parse
|
||||
if (!Array.isArray(c.env.USER_ROLES)) {
|
||||
try {
|
||||
return JSON.parse(c.env.USER_ROLES);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse USER_ROLES", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return c.env.USER_ROLES;
|
||||
}
|
||||
|
||||
export const getPasswords = (c: Context<HonoCustomType>): string[] => {
|
||||
if (!c.env.PASSWORDS) {
|
||||
return [];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Hono } from 'hono'
|
||||
import { Context, Hono } from 'hono'
|
||||
import { cors } from 'hono/cors';
|
||||
import { jwt } from 'hono/jwt'
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
@@ -13,11 +13,16 @@ import { api as telegramApi } from './telegram_api'
|
||||
import { email } from './email';
|
||||
import { scheduled } from './scheduled';
|
||||
import { getAdminPasswords, getPasswords, getBooleanValue } from './utils';
|
||||
import { HonoCustomType } from './types';
|
||||
import { HonoCustomType, UserPayload } from './types';
|
||||
|
||||
const app = new Hono<HonoCustomType>()
|
||||
//cors
|
||||
app.use('/*', cors());
|
||||
// error handler
|
||||
app.onError((err, c) => {
|
||||
console.error(err)
|
||||
return c.text(`${err.name} ${err.message}`, 500)
|
||||
})
|
||||
// rate limit
|
||||
app.use('/*', async (c, next) => {
|
||||
if (
|
||||
@@ -50,6 +55,26 @@ app.use('/*', async (c, next) => {
|
||||
}
|
||||
await next()
|
||||
});
|
||||
|
||||
const checkUserPayload = async (
|
||||
c: Context<HonoCustomType>
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const token = c.req.raw.headers.get("x-user-token");
|
||||
if (!token) return;
|
||||
const payload = await Jwt.verify(token, c.env.JWT_SECRET, "HS256");
|
||||
// check expired
|
||||
if (!payload.exp) return;
|
||||
// exp is in seconds
|
||||
if (payload.exp < Math.floor(Date.now() / 1000)) {
|
||||
return;
|
||||
}
|
||||
c.set("userPayload", payload as UserPayload);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// api auth
|
||||
app.use('/api/*', async (c, next) => {
|
||||
// check header x-custom-auth
|
||||
@@ -61,6 +86,7 @@ app.use('/api/*', async (c, next) => {
|
||||
}
|
||||
}
|
||||
if (c.req.path.startsWith("/api/new_address")) {
|
||||
await checkUserPayload(c);
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
@@ -87,7 +113,7 @@ app.use('/user_api/*', async (c, next) => {
|
||||
if (payload.exp < Math.floor(Date.now() / 1000)) {
|
||||
return c.text("Token Expired", 401)
|
||||
}
|
||||
c.set("userPayload", payload);
|
||||
c.set("userPayload", payload as UserPayload);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return c.text("Need User Token", 401)
|
||||
|
||||
Reference in New Issue
Block a user