具体改动: 1. feat(投稿表单): 移除 RequestForm.vue 联合投稿功能整套模板+状态+payload+CSS(10处残留) 2. feat(图标): Icon.vue 新增 sparkles/star 图标; 首页档期卡片📅🎵✨和排期列表空状态🎵📅换成 lucide SVG Icon 3. fix(浅色适配): ScheduleList.vue 日期选择弹窗桌面端+移动端(content/header/h3/close-btn/item)全部改为 bg-primary + var text + indigo 主色, 修复竖屏选择日期标题白字看不见 4. feat(歌曲列表): SongList.vue scheduled-tag 内联追加 formatScheduleDate, 徽章+日期双胶囊显示 5. fix(登录): OAuthButtons.vue showPlaceholderHint 强制 false, 隐藏虚线框 .env 配置提示 6. fix(权限): server/middleware/auth.ts + auth-route-policy.ts 双白名单同步追加 /api/expected-play-dates GET 公共访问, 修复未登录 Authentication expired 401
101 lines
3.8 KiB
TypeScript
101 lines
3.8 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'] },
|
|
{ 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
|
|
}
|