Files
BYW-VoiceHub/server/utils/auth-route-policy.ts
T
Henry_Du 12dac95e1b feat(auth): 新增微信小程序登录(绑定策略:首次用学号密码绑定)
- 新增 POST /api/auth/wechat/login:用 wx.login 的 code 换 openid;已绑定直接签发 JWT 返回,未绑定返回 bindToken
- bind.post.ts:支持从请求体读取 bindToken(小程序不便维护 cookie),绑定成功后返回 token,供 Authorization: Bearer 使用
- auth-route-policy.ts:把新接口登记进免鉴权白名单,否则会被鉴权中间件拦截
- .env.example:新增 WECHAT_MINI_APPID / WECHAT_MINI_SECRET
2026-09-26 13:57:24 +08:00

103 lines
4.0 KiB
TypeScript

type HttpMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
interface RoutePolicy {
path: string
methods: readonly HttpMethod[]
}
const GET_METHODS = ['GET', 'HEAD'] as const
const WRITE_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'] as const
export const PASSWORD_CHANGE_ALLOWED_ROUTES: readonly RoutePolicy[] = [
{ path: '/api/auth/verify', methods: GET_METHODS },
{ path: '/api/auth/logout', methods: ['POST'] },
{ path: '/api/auth/change-password', methods: ['POST'] },
{ path: '/api/auth/set-initial-password', methods: ['POST'] },
{ path: '/api/auth/2fa/verify', methods: ['POST'] },
{ path: '/api/auth/2fa/send-email', methods: ['POST'] },
{ path: '/api/site-config', methods: GET_METHODS }
]
export const PUBLIC_API_EXACT_ROUTES: readonly RoutePolicy[] = [
{ path: '/api/auth/login', methods: ['POST'] },
{ path: '/api/auth/register', methods: ['POST'] },
{ path: '/api/auth/email-code', methods: ['POST'] },
{ path: '/api/auth/grade-class-options', methods: GET_METHODS },
{ path: '/api/auth/oauth-register-options', methods: GET_METHODS },
{ path: '/api/auth/bind', methods: ['POST'] },
// 微信小程序登录(wx.login 的 code 换 openid);未绑定时返回 bindToken,供上面的 /api/auth/bind 使用
{ path: '/api/auth/wechat/login', methods: ['POST'] },
{ path: '/api/auth/oauth-register', methods: ['POST'] },
{ path: '/api/auth/2fa/verify', methods: ['POST'] },
{ path: '/api/auth/2fa/send-email', methods: ['POST'] },
{ path: '/api/auth/forgot-password', methods: ['POST'] },
{ path: '/api/auth/reset-password', methods: ['POST'] },
{ path: '/api/auth/captcha', methods: GET_METHODS },
{ path: '/api/semesters/current', methods: GET_METHODS },
{ path: '/api/play-times', methods: GET_METHODS },
{ path: '/api/expected-play-dates', methods: GET_METHODS },
{ path: '/api/schedules/public', methods: GET_METHODS },
{ path: '/api/songs/count', methods: GET_METHODS },
{ path: '/api/songs/public', methods: GET_METHODS },
{ path: '/api/site-config', methods: GET_METHODS },
{ path: '/api/system/location', methods: GET_METHODS },
{ path: '/api/auth/webauthn/login', methods: ['POST'] },
{ path: '/api/music/resolve-url', methods: ['POST'] },
{ path: '/api/music/state', methods: ['POST'] },
{ path: '/api/music/websocket', methods: GET_METHODS },
{ path: '/api/sys/time', methods: GET_METHODS }
]
export const PUBLIC_API_PREFIX_ROUTES: readonly RoutePolicy[] = [
{ path: '/api/proxy/', methods: GET_METHODS },
{ path: '/api/api-enhanced/', methods: [...GET_METHODS, ...WRITE_METHODS] },
{ path: '/api/native-api/', methods: [...GET_METHODS, 'POST'] },
{ path: '/api/open/', methods: [...GET_METHODS, ...WRITE_METHODS] },
{ path: '/api/auth/webauthn/login/', methods: ['POST'] }
]
function matchesMethod(policy: RoutePolicy, method: string): boolean {
return policy.methods.includes(method.toUpperCase() as HttpMethod)
}
export function isPublicApiPath(pathname: string, method: string): boolean {
return (
PUBLIC_API_EXACT_ROUTES.some(
(policy) => pathname === policy.path && matchesMethod(policy, method)
) ||
PUBLIC_API_PREFIX_ROUTES.some(
(policy) => pathname.startsWith(policy.path) && matchesMethod(policy, method)
)
)
}
export function isAllowedDuringPasswordChange(pathname: string, method: string): boolean {
return PASSWORD_CHANGE_ALLOWED_ROUTES.some(
(policy) => pathname === policy.path && matchesMethod(policy, method)
)
}
export function shouldBlockDuringPasswordChange(
pathname: string,
method: string,
requirePasswordChange: boolean
): boolean {
return (
requirePasswordChange &&
!isPublicApiPath(pathname, method) &&
!isAllowedDuringPasswordChange(pathname, method)
)
}
export function shouldBypassPublicApiAuthentication(
pathname: string,
method: string,
hasToken: boolean
): boolean {
return isPublicApiPath(pathname, method) && !hasToken
}
export function canBindOAuthIdentity(requirePasswordChange: boolean): boolean {
return !requirePasswordChange
}