forked from wangziqi/gongxue-base
fix: guard admin h5 entry pages
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -59,10 +59,12 @@ export default function PlatformWorkbenchPage() {
|
||||
const [grants, setGrants] = useState<PlatformQuestionBankGrant[]>([]);
|
||||
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 (
|
||||
<View className='platform-page'>
|
||||
<View className='platform-shell'>
|
||||
<View className='platform-header'>
|
||||
<Text className='platform-kicker'>Platform Admin</Text>
|
||||
<Text className='platform-title'>正在校验平台权限</Text>
|
||||
<Text className='platform-subtitle'>请先完成登录,系统会确认当前账号是否拥有平台管理员权限。</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
async function exportAuditLogs() {
|
||||
setBusy('audit-export');
|
||||
setError('');
|
||||
@@ -154,9 +170,10 @@ export default function PlatformWorkbenchPage() {
|
||||
|
||||
<View className='platform-section'>
|
||||
<Text className='platform-section-title'>后台模块</Text>
|
||||
<View className='platform-list'>
|
||||
<View className='platform-module-grid'>
|
||||
{modules.map(item => (
|
||||
<View className='platform-row' key={item.path} onClick={() => Taro.navigateTo({ url: item.path })}>
|
||||
<View className='platform-module-card' key={item.path} onClick={() => Taro.navigateTo({ url: item.path })}>
|
||||
<Text className='platform-module-label'>Console</Text>
|
||||
<Text className='platform-row-main'>{item.name}</Text>
|
||||
<Text className='platform-row-meta'>{item.meta}</Text>
|
||||
</View>
|
||||
|
||||
@@ -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 (
|
||||
<View className='student-page'>
|
||||
<View className='student-topbar'>
|
||||
<View className='student-title-block'>
|
||||
<Text className='student-kicker'>{tenant?.branding.shortName || tenant?.branding.brandName || '题库系统'}</Text>
|
||||
<Text className='student-title'>手机号登录</Text>
|
||||
<Text className='student-subtitle'>短信登录会由后端校验租户、验证码、会话和用户身份。</Text>
|
||||
<View className='student-page login-page'>
|
||||
<View className='login-hero'>
|
||||
<View className='login-avatar'>
|
||||
<Text className='login-avatar-mark'>{(tenant?.branding.shortName || tenant?.branding.brandName || '题').slice(0, 1)}</Text>
|
||||
</View>
|
||||
<Text className='login-kicker'>{tenant?.branding.shortName || tenant?.branding.brandName || '题库系统'}</Text>
|
||||
<Text className='login-title'>欢迎回来</Text>
|
||||
<Text className='login-subtitle'>登录后继续刷题、查看错题和学习报告。</Text>
|
||||
</View>
|
||||
|
||||
<View className='list-stack'>
|
||||
<View className='login-card list-stack'>
|
||||
<Text className='row-main'>手机号登录</Text>
|
||||
<Text className='row-meta'>短信验证码将用于确认当前租户和账号身份。</Text>
|
||||
{reason ? <Text className='error-text'>{reason}</Text> : null}
|
||||
<Input className='input' type='number' maxlength={11} placeholder='请输入手机号' value={phone} onInput={event => setPhone(String(event.detail.value || ''))} />
|
||||
<View className='toolbar'>
|
||||
<Input className='input' type='number' maxlength={6} placeholder='验证码' value={code} onInput={event => setCode(String(event.detail.value || ''))} />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<TenantDashboard | null>(null);
|
||||
const [overview, setOverview] = useState<TenantOverview | null>(null);
|
||||
const [permissions, setPermissions] = useState<TenantPermissionsPayload>({});
|
||||
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 (
|
||||
<View className='admin-page'>
|
||||
<View className='admin-shell'>
|
||||
<View className='admin-header'>
|
||||
<Text className='admin-kicker'>Tenant Admin</Text>
|
||||
<Text className='admin-title'>正在校验后台权限</Text>
|
||||
<Text className='admin-subtitle'>请先完成登录,系统会确认当前账号是否拥有租户后台权限。</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='admin-page'>
|
||||
<View className='admin-shell'>
|
||||
@@ -118,9 +134,10 @@ export default function TenantWorkbenchPage() {
|
||||
|
||||
<View className='admin-section'>
|
||||
<Text className='admin-section-title'>后台模块</Text>
|
||||
<View className='admin-list'>
|
||||
<View className='admin-module-grid'>
|
||||
{modules.map(item => (
|
||||
<View className='admin-row' key={item.path} onClick={() => Taro.navigateTo({ url: item.path })}>
|
||||
<View className={`admin-module-card module-${item.key}`} key={item.path} onClick={() => Taro.navigateTo({ url: item.path })}>
|
||||
<Text className='admin-module-label'>Module</Text>
|
||||
<Text className='admin-row-main'>{item.name}</Text>
|
||||
<Text className='admin-row-meta'>{item.meta}</Text>
|
||||
</View>
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
Reference in New Issue
Block a user