import { Context, Hono } from 'hono' import { cors } from 'hono/cors'; import { Jwt } from 'hono/utils/jwt' import { addressJwtAuth } from './address_auth'; import { api as commonApi } from './commom_api'; import { api as openAuthApi } from './open_api/auth'; import { api as mailsApi } from './mails_api' import { api as userApi } from './user_api'; import { api as adminApi } from './admin_api'; import { api as apiSendMail } from './mails_api/send_mail_api' import { api as telegramApi } from './telegram_api' import { api as redeemApi } from './redeem_api' import i18n from './i18n'; import { ErrorCode } from './error_codes'; import { email } from './email'; import { scheduled } from './scheduled'; import { getPasswords, getBooleanValue, getDomains, checkIsAdmin, getEnvStringList } from './utils'; import { checkAccessControl } from './ip_blacklist'; const API_PATHS = [ "/api/", "/open_api/", "/user_api/", "/admin/", "/telegram/", "/external/", "/redeem_api/", ]; const app = new Hono() //cors app.use('/*', cors()); // error handler app.onError((err, c) => { console.error(err) return c.json({ code: ErrorCode.INTERNAL_SERVER_ERROR, message: `${err.name} ${err.message}` }, 500) }) // global middlewares app.use('/*', async (c, next) => { // check if the request is for static files if (c.env.ASSETS && !API_PATHS.some(path => c.req.path.startsWith(path))) { const url = new URL(c.req.raw.url); if (!url.pathname.includes('.')) { url.pathname = "" } return c.env.ASSETS.fetch(url); } // save language in context const lang = c.req.raw.headers.get("x-lang"); if (lang) { c.set("lang", lang); } const msgs = i18n.getMessages(lang || c.env.DEFAULT_LANG); // check header x-custom-auth const passwords = getPasswords(c); if (!c.req.path.startsWith("/open_api") && !c.req.path.startsWith("/telegram/") && passwords && passwords.length > 0 ) { const auth = c.req.raw.headers.get("x-custom-auth"); if (!auth || !passwords.includes(auth)) { return c.json({ code: ErrorCode.AUTH_SITE_PASSWORD_INVALID, message: msgs.CustomAuthPasswordMsg }, 401) } } // rate limit for specific endpoints if ( c.req.path.startsWith("/api/new_address") || c.req.path.startsWith("/api/send_mail") || c.req.path.startsWith("/external/api/send_mail") || (c.req.path.startsWith("/user_api/address/") && c.req.path.endsWith("/send_mail")) || c.req.path.startsWith("/user_api/register") || c.req.path.startsWith("/user_api/verify_code") || c.req.path.startsWith("/redeem_api/") ) { const reqIp = c.req.raw.headers.get("cf-connecting-ip") if (reqIp && c.env.RATE_LIMITER) { const { success } = await c.env.RATE_LIMITER.limit( { key: `${c.req.path}|${reqIp}` } ) if (!success) { return c.text(`IP=${reqIp} Rate limit exceeded for ${c.req.path}`, 429) } } // Check access control (blacklist and daily limit) const accessControlResponse = await checkAccessControl(c); if (accessControlResponse) { return accessControlResponse; } } // webhook check if ( c.req.path.startsWith("/api/webhook") || c.req.path.startsWith("/admin/webhook") || c.req.path.startsWith("/admin/mail_webhook") ) { if (!c.env.KV) { return c.text(msgs.KVNotAvailableMsg, 400); } if (!getBooleanValue(c.env.ENABLE_WEBHOOK)) { return c.text(msgs.WebhookNotEnabledMsg, 403); } } if (!c.env.DB) { return c.text(msgs.DBNotAvailableMsg, 400); } if (!c.env.JWT_SECRET) { return c.text(msgs.JWTSecretNotSetMsg, 400); } await next() }); const checkUserPayload = async ( c: Context ): Promise => { 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); } } const checkoutUserRolePayload = async ( c: Context, userId?: number ): Promise => { try { const token = c.req.raw.headers.get("x-user-access-token"); if (!token) return; const payload = await Jwt.verify(token, c.env.JWT_SECRET, { alg: "HS256", exp: false }); // check expired if (!payload.exp) return; // exp is in seconds if (payload.exp < Math.floor(Date.now() / 1000)) { return c.json({ code: ErrorCode.AUTH_USER_ACCESS_TOKEN_EXPIRED, message: i18n.getMessagesbyContext(c).UserAcceesTokenExpiredMsg }, 401); } if (typeof payload?.user_role !== "string") return; if (userId !== undefined && payload.user_id !== userId) return; c.set("userRolePayload", payload.user_role); } catch (e) { console.error(e); } } // api auth app.use('/api/*', async (c, next) => { if (c.req.path.startsWith("/api/new_address")) { await checkUserPayload(c); await next(); return; } if (c.req.path.startsWith("/api/settings") || c.req.path.startsWith("/api/send_mail") ) { const response = await checkoutUserRolePayload(c); if (response) return response; } if (c.req.path.startsWith("/api/address_login")) { await next(); return; } try { return await addressJwtAuth(c, next); } catch (e) { console.warn(e); const lang = c.get("lang") || c.env.DEFAULT_LANG; const msgs = i18n.getMessages(lang); return c.text(msgs.InvalidAddressCredentialMsg, 401) } }); // user_api auth app.use('/user_api/*', async (c, next) => { if ( c.req.path.startsWith("/user_api/open_settings") || c.req.path.startsWith("/user_api/register") || c.req.path.startsWith("/user_api/login") || c.req.path.startsWith("/user_api/verify_code") || c.req.path.startsWith("/user_api/passkey/authenticate_") || c.req.path.startsWith("/user_api/oauth2") ) { await next(); return; } const lang = c.req.raw.headers.get("x-lang") || c.env.DEFAULT_LANG; const msgs = i18n.getMessages(lang); try { const token = c.req.raw.headers.get("x-user-token"); if (!token) return c.text(msgs.UserTokenExpiredMsg, 401) const payload = await Jwt.verify(token, c.env.JWT_SECRET, "HS256"); // check expired if (!payload.exp) return c.text(msgs.UserTokenExpiredMsg, 401); // exp is in seconds if (payload.exp < Math.floor(Date.now() / 1000)) { return c.text(msgs.UserTokenExpiredMsg, 401) } c.set("userPayload", payload as UserPayload); } catch (e) { console.error(e); return c.text(msgs.UserTokenExpiredMsg, 401) } if ( c.req.path.startsWith("/user_api/bind_address") || c.req.path.startsWith("/user_api/address/") ) { const { user_id } = c.get("userPayload"); const response = await checkoutUserRolePayload(c, user_id); if (response) return response; } if (c.req.path.startsWith('/user_api/bind_address') && c.req.method === 'POST' ) { return addressJwtAuth(c, next); } await next(); }); // admin auth app.use('/admin/*', async (c, next) => { const lang = c.req.raw.headers.get("x-lang") || c.env.DEFAULT_LANG; const msgs = i18n.getMessages(lang); try { const ipWhitelist = getEnvStringList(c.env.ADMIN_API_IP_WHITELIST) .filter(ip => typeof ip === "string") .map(ip => ip.trim()) .filter(Boolean); if (ipWhitelist.length > 0) { const reqIp = c.req.raw.headers.get("cf-connecting-ip")?.trim(); if (!reqIp || !ipWhitelist.includes(reqIp)) { return c.text(msgs.AdminApiIpNotAllowedMsg, 403); } } } catch (e) { console.error("Failed to check admin API IP whitelist", e); } // check header x-admin-auth if (checkIsAdmin(c)) { await next(); return; } // check if user is admin const access_token = c.req.raw.headers.get("x-user-access-token"); if (c.env.ADMIN_USER_ROLE && access_token) { try { const payload = await Jwt.verify(access_token, c.env.JWT_SECRET, { alg: "HS256", exp: false }); // check expired if (!payload.exp) return c.json({ code: ErrorCode.AUTH_ADMIN_CREDENTIAL_INVALID, message: msgs.UserAcceesTokenExpiredMsg }, 401); // exp is in seconds if (payload.exp < Math.floor(Date.now() / 1000)) { if (getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK)) return await next(); return c.json({ code: ErrorCode.AUTH_USER_ACCESS_TOKEN_EXPIRED, message: msgs.UserAcceesTokenExpiredMsg }, 401); } if (payload.user_role !== c.env.ADMIN_USER_ROLE) { return c.json({ code: ErrorCode.AUTH_ADMIN_CREDENTIAL_INVALID, message: msgs.UserRoleIsNotAdminMsg }, 401) } await next(); return; } catch (e) { console.error(e); } } // disable admin api check if (getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK)) { await next(); return; } return c.json({ code: ErrorCode.AUTH_ADMIN_CREDENTIAL_INVALID, message: msgs.NeedAdminPasswordMsg }, 401) }); app.route('/', commonApi) app.route('/', openAuthApi) app.route('/', mailsApi) app.route('/', userApi) app.route('/', adminApi) app.route('/', apiSendMail) app.route('/', telegramApi) app.route('/', redeemApi) const health_check = async (c: Context) => { const lang = c.req.raw.headers.get("x-lang") || c.env.DEFAULT_LANG; const msgs = i18n.getMessages(lang); if (!c.env.DB) { return c.text(msgs.DBNotAvailableMsg, 400); } if (!c.env.JWT_SECRET) { return c.text(msgs.JWTSecretNotSetMsg, 400); } if (getDomains(c).length === 0) { return c.text(msgs.DomainsNotSetMsg, 400); } return c.text("OK"); } app.get('/', health_check) app.get('/health_check', health_check) app.all('/*', async c => c.text("Not Found", 404)) export default { fetch: app.fetch, email: email, scheduled: scheduled, }