From d8e3db79d291fa65046206162a7aa2685f2d4057 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 2 Jul 2026 01:41:00 +0800 Subject: [PATCH] fix: guard admin h5 entry pages --- apps/taro/src/pages/bootstrap/index.tsx | 5 +- .../src/pages/platform-admin/platform.css | 56 ++++++++++++ .../pages/platform-admin/workbench/index.tsx | 21 ++++- apps/taro/src/pages/student/login/index.tsx | 23 +++-- apps/taro/src/pages/student/student.css | 86 +++++++++++++++++++ apps/taro/src/pages/tenant-admin/admin.css | 59 +++++++++++++ .../pages/tenant-admin/workbench/index.tsx | 25 +++++- apps/taro/src/services/routeGuard.ts | 24 +++++- 8 files changed, 283 insertions(+), 16 deletions(-) diff --git a/apps/taro/src/pages/bootstrap/index.tsx b/apps/taro/src/pages/bootstrap/index.tsx index 47cb8a7a..27051504 100644 --- a/apps/taro/src/pages/bootstrap/index.tsx +++ b/apps/taro/src/pages/bootstrap/index.tsx @@ -3,7 +3,7 @@ import Taro from '@tarojs/taro'; import { Button, Text, View } from '@tarojs/components'; import { appEnv, assertFrontendSecretsAreAbsent, ensureRuntimeConfigLoaded, isH5Runtime } from '@/env'; import { resolveTenant } from '@/services/api'; -import { landingPath, requireSignedIn } from '@/services/routeGuard'; +import { landingPath, requirePlatformAdmin, requireTenantAdmin } from '@/services/routeGuard'; import './index.css'; function hostFromRuntime() { @@ -22,7 +22,8 @@ export default function BootstrapPage() { .then(async () => { if (appEnv.portal === 'student') return null; setStatus('正在校验登录状态'); - return requireSignedIn(landingPath()); + if (appEnv.portal === 'platform-admin') return requirePlatformAdmin(landingPath()); + return requireTenantAdmin(landingPath()); }) .then(authPayload => { if (appEnv.portal !== 'student' && !authPayload) return; diff --git a/apps/taro/src/pages/platform-admin/platform.css b/apps/taro/src/pages/platform-admin/platform.css index 755b1a4c..2af04bef 100644 --- a/apps/taro/src/pages/platform-admin/platform.css +++ b/apps/taro/src/pages/platform-admin/platform.css @@ -172,6 +172,62 @@ box-shadow: 0 5px 16px rgba(15, 23, 42, 0.04); } +.platform-module-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; +} + +.platform-module-card { + position: relative; + overflow: hidden; + min-height: 170px; + padding: 22px; + border-radius: 28px; + background: linear-gradient(135deg, #1152d4, #38bdf8); + box-shadow: 0 12px 24px rgba(17, 82, 212, 0.14); +} + +.platform-module-card:nth-child(2) { background: linear-gradient(135deg, #10b981, #2dd4bf); } +.platform-module-card:nth-child(3) { background: linear-gradient(135deg, #7c3aed, #a78bfa); } +.platform-module-card:nth-child(4) { background: linear-gradient(135deg, #f97316, #fbbf24); } + +.platform-module-card::after { + content: ""; + position: absolute; + right: -28px; + bottom: -44px; + width: 130px; + height: 130px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.16); +} + +.platform-module-card .platform-row-main, +.platform-module-card .platform-row-meta, +.platform-module-label { + position: relative; + z-index: 1; +} + +.platform-module-card .platform-row-main { + margin-top: 30px; + color: #fff; + font-size: 30px; + font-weight: 950; +} + +.platform-module-card .platform-row-meta { + color: rgba(255, 255, 255, 0.82); +} + +.platform-module-label { + display: block; + color: rgba(255, 255, 255, 0.82); + font-size: 20px; + font-weight: 850; +} + .platform-row-main { display: block; color: var(--tiku-text); diff --git a/apps/taro/src/pages/platform-admin/workbench/index.tsx b/apps/taro/src/pages/platform-admin/workbench/index.tsx index a9db7c7e..696f7a5f 100644 --- a/apps/taro/src/pages/platform-admin/workbench/index.tsx +++ b/apps/taro/src/pages/platform-admin/workbench/index.tsx @@ -59,10 +59,12 @@ export default function PlatformWorkbenchPage() { const [grants, setGrants] = useState([]); const [error, setError] = useState(''); const [busy, setBusy] = useState(''); + const [authorized, setAuthorized] = useState(false); useEffect(() => { requirePlatformAdmin('/pages/platform-admin/workbench/index').then(payload => { if (!payload) return; + setAuthorized(true); return Promise.all([ loadPlatformOverview().catch(() => ({ item: null })), loadPlatformTenants({ limit: 6 }).catch(() => ({ items: [] })), @@ -100,6 +102,20 @@ export default function PlatformWorkbenchPage() { { name: '平台员工', path: '/pages/platform-admin/staff/index', meta: '员工账号、平台权限、禁用恢复' }, ]; + if (!authorized) { + return ( + + + + Platform Admin + 正在校验平台权限 + 请先完成登录,系统会确认当前账号是否拥有平台管理员权限。 + + + + ); + } + async function exportAuditLogs() { setBusy('audit-export'); setError(''); @@ -154,9 +170,10 @@ export default function PlatformWorkbenchPage() { 后台模块 - + {modules.map(item => ( - Taro.navigateTo({ url: item.path })}> + Taro.navigateTo({ url: item.path })}> + Console {item.name} {item.meta} diff --git a/apps/taro/src/pages/student/login/index.tsx b/apps/taro/src/pages/student/login/index.tsx index fba836b2..83875f77 100644 --- a/apps/taro/src/pages/student/login/index.tsx +++ b/apps/taro/src/pages/student/login/index.tsx @@ -14,10 +14,15 @@ export default function StudentLoginPage() { const [debugCode, setDebugCode] = useState(''); const [message, setMessage] = useState(''); const [error, setError] = useState(''); + const [reason, setReason] = useState(''); const [sending, setSending] = useState(false); const [verifying, setVerifying] = useState(false); useEffect(() => { + if (router.params?.reason) { + setReason(decodeURIComponent(router.params.reason)); + return; + } loadCurrentUser() .then(() => redirectAfterLogin(router.params?.redirect || landingPath())) .catch(() => undefined); @@ -53,16 +58,20 @@ export default function StudentLoginPage() { } return ( - - - - {tenant?.branding.shortName || tenant?.branding.brandName || '题库系统'} - 手机号登录 - 短信登录会由后端校验租户、验证码、会话和用户身份。 + + + + {(tenant?.branding.shortName || tenant?.branding.brandName || '题').slice(0, 1)} + {tenant?.branding.shortName || tenant?.branding.brandName || '题库系统'} + 欢迎回来 + 登录后继续刷题、查看错题和学习报告。 - + + 手机号登录 + 短信验证码将用于确认当前租户和账号身份。 + {reason ? {reason} : null} setPhone(String(event.detail.value || ''))} /> setCode(String(event.detail.value || ''))} /> diff --git a/apps/taro/src/pages/student/student.css b/apps/taro/src/pages/student/student.css index 62bdd42c..94957421 100644 --- a/apps/taro/src/pages/student/student.css +++ b/apps/taro/src/pages/student/student.css @@ -4,6 +4,92 @@ background: var(--tiku-page); } +.login-page { + display: flex; + min-height: 100vh; + flex-direction: column; + justify-content: center; + gap: 24px; +} + +.login-hero { + position: relative; + overflow: hidden; + padding: 34px 30px; + border-radius: 30px; + background: linear-gradient(135deg, #1152d4 0%, #2563eb 58%, #60a5fa 100%); + box-shadow: var(--tiku-shadow-blue); +} + +.login-hero::after { + content: ""; + position: absolute; + right: -52px; + bottom: -72px; + width: 220px; + height: 220px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.14); +} + +.login-avatar, +.login-kicker, +.login-title, +.login-subtitle { + position: relative; + z-index: 1; +} + +.login-avatar { + display: flex; + align-items: center; + justify-content: center; + width: 82px; + height: 82px; + border: 4px solid rgba(255, 255, 255, 0.5); + border-radius: 999px; + background: #fff; +} + +.login-avatar-mark { + color: var(--tiku-primary); + font-size: 36px; + font-weight: 950; +} + +.login-kicker { + display: block; + margin-top: 24px; + color: rgba(255, 255, 255, 0.82); + font-size: 23px; + font-weight: 850; +} + +.login-title { + display: block; + margin-top: 10px; + color: #fff; + font-size: 48px; + font-weight: 950; + line-height: 1.12; +} + +.login-subtitle { + display: block; + margin-top: 12px; + color: rgba(255, 255, 255, 0.84); + font-size: 25px; + line-height: 1.45; +} + +.login-card { + padding: 28px; + border: 1px solid var(--tiku-border); + border-radius: 28px; + background: #fff; + box-shadow: var(--tiku-shadow); +} + .student-topbar { display: flex; align-items: center; diff --git a/apps/taro/src/pages/tenant-admin/admin.css b/apps/taro/src/pages/tenant-admin/admin.css index c4267e8e..fdd9ed75 100644 --- a/apps/taro/src/pages/tenant-admin/admin.css +++ b/apps/taro/src/pages/tenant-admin/admin.css @@ -211,6 +211,65 @@ background: var(--tiku-primary-soft); } +.admin-module-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; +} + +.admin-module-card { + position: relative; + overflow: hidden; + min-height: 170px; + padding: 22px; + border: 0; + border-radius: 28px; + background: linear-gradient(135deg, #1152d4, #38bdf8); + box-shadow: 0 12px 24px rgba(17, 82, 212, 0.14); +} + +.admin-module-card::after { + content: ""; + position: absolute; + right: -28px; + bottom: -44px; + width: 130px; + height: 130px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.16); +} + +.admin-module-card.module-students { background: linear-gradient(135deg, #10b981, #2dd4bf); } +.admin-module-card.module-content { background: linear-gradient(135deg, #7c3aed, #a78bfa); } +.admin-module-card.module-marketing { background: linear-gradient(135deg, #f97316, #fbbf24); } +.admin-module-card.module-commerce { background: linear-gradient(135deg, #ec4899, #f472b6); } +.admin-module-card.module-settings { background: linear-gradient(135deg, #334155, #64748b); } + +.admin-module-card .admin-row-main, +.admin-module-card .admin-row-meta, +.admin-module-label { + position: relative; + z-index: 1; +} + +.admin-module-card .admin-row-main { + margin-top: 30px; + color: #fff; + font-size: 30px; + font-weight: 950; +} + +.admin-module-card .admin-row-meta { + color: rgba(255, 255, 255, 0.82); +} + +.admin-module-label { + display: block; + color: rgba(255, 255, 255, 0.82); + font-size: 20px; + font-weight: 850; +} + .admin-row-main { display: block; color: var(--tiku-text); diff --git a/apps/taro/src/pages/tenant-admin/workbench/index.tsx b/apps/taro/src/pages/tenant-admin/workbench/index.tsx index b9156b7e..43e1d31a 100644 --- a/apps/taro/src/pages/tenant-admin/workbench/index.tsx +++ b/apps/taro/src/pages/tenant-admin/workbench/index.tsx @@ -10,7 +10,7 @@ import { type TenantOverview, type TenantPermissionsPayload, } from '@/services/tenantAdmin'; -import { requireSignedIn } from '@/services/routeGuard'; +import { requireTenantAdmin } from '@/services/routeGuard'; import '../admin.css'; interface AdminModule { @@ -72,11 +72,13 @@ export default function TenantWorkbenchPage() { const [dashboard, setDashboard] = useState(null); const [overview, setOverview] = useState(null); const [permissions, setPermissions] = useState({}); + const [authorized, setAuthorized] = useState(false); useEffect(() => { - requireSignedIn('/pages/tenant-admin/workbench/index') + requireTenantAdmin('/pages/tenant-admin/workbench/index') .then(payload => { if (!payload) return; + setAuthorized(true); loadTenantDashboard('30d').then(next => setDashboard(next.item || null)).catch(() => setDashboard(null)); loadTenantOverview().then(next => setOverview(next.item || null)).catch(() => setOverview(null)); loadTenantPermissions().then(next => setPermissions(next)).catch(() => setPermissions({})); @@ -88,6 +90,20 @@ export default function TenantWorkbenchPage() { const payment = dashboard?.paymentStats || {}; const modules = MODULES.filter(item => canOpenModule(permissions, item)); + if (!authorized) { + return ( + + + + Tenant Admin + 正在校验后台权限 + 请先完成登录,系统会确认当前账号是否拥有租户后台权限。 + + + + ); + } + return ( @@ -118,9 +134,10 @@ export default function TenantWorkbenchPage() { 后台模块 - + {modules.map(item => ( - Taro.navigateTo({ url: item.path })}> + Taro.navigateTo({ url: item.path })}> + Module {item.name} {item.meta} diff --git a/apps/taro/src/services/routeGuard.ts b/apps/taro/src/services/routeGuard.ts index 4e50b00f..3ffffeb2 100644 --- a/apps/taro/src/services/routeGuard.ts +++ b/apps/taro/src/services/routeGuard.ts @@ -19,6 +19,10 @@ function loginUrl(redirectPath: string) { return `/pages/student/login/index?redirect=${encodeURIComponent(safeRedirectPath(redirectPath))}`; } +function forbiddenUrl(reason: string, redirectPath: string) { + return `/pages/student/login/index?reason=${encodeURIComponent(reason)}&redirect=${encodeURIComponent(safeRedirectPath(redirectPath))}`; +} + function currentPagePath() { const instance = Taro.getCurrentInstance(); const path = instance.router?.path || ''; @@ -69,7 +73,21 @@ export async function requirePlatformAdmin(redirectPath: string) { const user = payload.user || payload.item; const roles = user?.roles || []; if (user?.primaryRole === 'platform_admin' || roles.includes('platform_admin')) return payload; - throw new Error('当前账号不是平台管理员。'); + Taro.redirectTo({ url: forbiddenUrl('当前账号不是平台管理员', redirectPath) }); + return null; +} + +export async function requireTenantAdmin(redirectPath: string) { + const payload = await requireSignedIn(redirectPath); + if (!payload) return null; + const user = payload.user || payload.item; + const roles = user?.roles || []; + const allowedRoles = new Set(['tenant_owner', 'tenant_admin', 'tenant_operator', 'teacher', 'sales', 'agent']); + if (user?.primaryRole === 'platform_admin') return payload; + if (user?.primaryRole && allowedRoles.has(user.primaryRole)) return payload; + if (roles.some(role => allowedRoles.has(role) || role === 'platform_admin')) return payload; + Taro.redirectTo({ url: forbiddenUrl('当前账号没有租户后台权限', redirectPath) }); + return null; } export async function guardCurrentRoute() { @@ -82,6 +100,10 @@ export async function guardCurrentRoute() { await requirePlatformAdmin(path); return; } + if (appEnv.portal === 'tenant-admin') { + await requireTenantAdmin(path); + return; + } await requireSignedIn(path); } finally { pendingGuardPath = '';