Files
OpenFlare/frontend/proxy.ts
T
ryan 48d414e197 裁剪
swagger

移除 merchant

swagger

改造首页内容为通用后台管理系统定位

- 修改首页标题从 'LINUX DO Credit' 改为 'Modern Platform'
- 更新副标题为 '为二次开发而生'
- 更新首页描述为通用平台的特点
- 更新首页特性标签为 '开箱即用、高度可扩展、工业级基建'
- 修改展示卡片为技术栈和二次开发相关
- 更新开发者示例代码为通用的注册和 API Key 获取示例
- 更新页脚品牌名为 'Modern Platform'
- 调整页脚导航链接为通用平台相关内容

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

去除遗留

裁剪
移除 /api/v1/user/pay-key 相关代码

- 删除后端 UpdatePayKey 处理器函数和 UpdatePayKeyRequest 结构体
- 删除 User 模型中的 PayKey 字段
- 删除 User.VerifyPayKey 方法
- 删除 EncryptPayKeyFailed 错误常量
- 删除 /api/v1/user/pay-key PUT 路由
- 删除 OAuth 返回中的 IsPayKey 字段
- 删除前端 UserService.updatePayKey 方法
- 删除前端所有支付密钥 UI 和逻辑
- 更新相关的导出和注释

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

去除遗留

api 修正

系统配置

前端裁剪

后端裁剪

init
2026-06-08 20:34:28 +08:00

128 lines
3.7 KiB
TypeScript

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
/**
* Next.js 16 代理层
*
* 1. API 请求速率限制
* 2. 页面身份验证
*/
/* ==================== 速率限制 ==================== */
interface RateLimitEntry {
count: number
windowStart: number
}
const rateLimitStore = new Map<string, RateLimitEntry>()
/** 不需要速率限制的路径 */
const EXCLUDED_PREFIXES = [
'/api/v1/config',
'/epay/',
'/lpay/',
]
/** 速率限制规则: [最大请求数, 窗口时长(ms)] */
const RATE_LIMITS: Record<string, [number, number]> = {
'/api/v1/oauth/login': [1, 5000],
'/api/v1/oauth/callback': [1, 5000],
}
/** 默认限制: 60次/60秒 */
const DEFAULT_RATE_LIMIT: [number, number] = [60, 60000]
function getRateLimit(pathname: string): [number, number] {
if (RATE_LIMITS[pathname]) return RATE_LIMITS[pathname]
const match = Object.keys(RATE_LIMITS)
.filter(prefix => pathname.startsWith(prefix))
.sort((a, b) => b.length - a.length)[0]
return match ? RATE_LIMITS[match] : DEFAULT_RATE_LIMIT
}
function shouldRateLimit(pathname: string): boolean {
return !EXCLUDED_PREFIXES.some(prefix => pathname.startsWith(prefix))
}
function checkRateLimit(identifier: string, pathname: string): [boolean, number] {
const [maxRequests, windowMs] = getRateLimit(pathname)
const key = `${ identifier }:${ pathname }`
const now = Date.now()
const entry = rateLimitStore.get(key)
if (!entry || now - entry.windowStart >= windowMs) {
rateLimitStore.set(key, { count: 1, windowStart: now })
return [true, 0]
}
entry.count++
if (entry.count > maxRequests) {
return [false, Math.ceil((windowMs - (now - entry.windowStart)) / 1000)]
}
return [true, 0]
}
if (typeof setInterval !== 'undefined') {
setInterval(() => {
const now = Date.now()
for (const [key, entry] of rateLimitStore.entries()) {
if (now - entry.windowStart > 120000) rateLimitStore.delete(key)
}
}, 60000)
}
/* ==================== 代理主函数 ==================== */
export function proxy(request: NextRequest) {
const { pathname, search } = request.nextUrl
const sessionCookieName = process.env.LINUX_DO_CREDIT_SESSION_COOKIE_NAME || 'linux_do_credit_session_id'
const sessionCookie = request.cookies.get(sessionCookieName)
/* API 请求:速率限制后放行 */
if (pathname.startsWith('/api/')) {
const rateLimitEnabled = process.env.LINUX_DO_CREDIT_RATE_LIMIT_ENABLED === 'true'
if (rateLimitEnabled && shouldRateLimit(pathname)) {
const identifier = sessionCookie?.value ||
request.headers.get('x-forwarded-for')?.split(',')[0].trim() ||
request.headers.get('x-real-ip') ||
'anonymous'
const [allowed, waitTime] = checkRateLimit(identifier, pathname)
if (!allowed) {
return NextResponse.json(
{ error_code: 'RATE_LIMITED', error_msg: `请求过于频繁,请 ${ waitTime } 秒后重试` },
{ status: 429, headers: { 'Retry-After': String(waitTime) } }
)
}
}
return NextResponse.next()
}
/* 页面请求:公共路由放行 */
const publicRoutes = ['/', '/login', '/callback', '/privacy', '/terms']
const publicPrefixes = ['/docs/', '/epay/']
if (publicRoutes.includes(pathname) || publicPrefixes.some(p => pathname.startsWith(p))) {
return NextResponse.next()
}
if (!sessionCookie) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('callbackUrl', pathname + search)
return NextResponse.redirect(loginUrl)
}
return NextResponse.next()
}
export const config = {
matcher: [
'/api/:path*',
'/((?!_next|favicon.ico|robots.txt|sitemap.xml|.*\\.(?:jpg|jpeg|gif|png|svg|ico|webp)).*)',
],
}