<template>
<div class="landing">
<nav class="auth-nav auth-nav--sticky">
<div class="auth-nav-brand">
<div class="auth-logo">
<q-icon name="shield" size="26px" />
</div>
<span class="auth-nav-title">Dpz.Core</span>
<span class="auth-nav-sub">认证中心</span>
</div>
<div class="auth-nav-end">
<div v-if="user" class="auth-nav-actions">
<q-btn
v-for="item in navActions"
:key="item.label"
dense
flat
no-caps
class="auth-nav-link"
:icon="item.icon"
:label="item.label"
@click="navigate(item.to)"
/>
</div>
<q-btn v-if="user" flat round icon="logout" @click="signOut">
<q-tooltip>退出登录</q-tooltip>
</q-btn>
<q-btn
round
flat
:icon="$q.dark.isActive ? 'light_mode' : 'dark_mode'"
@click="$q.dark.toggle()"
>
<q-tooltip>切换主题</q-tooltip>
</q-btn>
</div>
</nav>
<main v-if="user" class="landing-main">
<section class="dashboard">
<div class="dashboard-hero">
<div class="dashboard-mesh" aria-hidden="true" />
<div class="dashboard-glow dashboard-glow--tl" />
<div class="dashboard-glow dashboard-glow--br" />
<div class="dashboard-orbit" aria-hidden="true" />
<div class="dashboard-profile">
<q-avatar
size="86px"
color="primary"
text-color="white"
class="dashboard-avatar"
>
<q-icon v-if="!user.avatar" name="person" size="44px" />
<img v-else :src="user.avatar" alt="" />
</q-avatar>
<div class="dashboard-profile-text">
<div class="dashboard-kicker">
<q-icon name="verified" size="16px" />
<span>已通过身份验证</span>
</div>
<div class="dashboard-name-row">
<span class="dashboard-name">{{ user.name }}</span>
<q-chip
v-if="isAdmin"
dense
color="primary"
text-color="white"
size="sm"
label="管理员"
/>
</div>
<div class="dashboard-account">
<span>{{ user.id }}</span>
<span v-if="user.email">{{ user.email }}</span>
</div>
</div>
</div>
<div class="dashboard-divider" />
<div class="dashboard-stats">
<div
v-for="status in dashboardStatuses"
:key="status.label"
class="dashboard-stat"
:class="status.colorClass"
>
<div class="dashboard-stat-icon">
<q-icon :name="status.icon" size="22px" />
</div>
<div>
<div class="dashboard-stat-value">{{ status.value }}</div>
<div class="dashboard-stat-label">{{ status.label }}</div>
</div>
</div>
</div>
</div>
<div class="dashboard-cards">
<div class="dashboard-cards-header">
<div>
<div class="section-kicker">SECURITY CONSOLE</div>
<h3 class="dashboard-cards-title">快捷操作</h3>
</div>
<div class="dashboard-cards-hint">
<q-icon name="bolt" size="18px" />
<span>安全配置入口</span>
</div>
</div>
<div class="dashboard-cards-grid">
<q-card
v-for="card in userCards"
:key="card.label"
flat
bordered
class="dash-card"
@click="navigate(card.to)"
>
<q-card-section class="dash-card-section">
<div class="dash-card-orbit" aria-hidden="true" />
<div class="dash-card-icon" :class="card.colorClass">
<q-icon :name="card.icon" size="28px" />
</div>
<div class="dash-card-copy">
<div class="dash-card-title">{{ card.label }}</div>
<div class="dash-card-desc">{{ card.desc }}</div>
</div>
<q-icon
class="dash-card-arrow"
name="arrow_forward"
size="20px"
/>
</q-card-section>
</q-card>
</div>
</div>
</section>
</main>
<main v-else class="landing-main">
<section class="hero">
<div class="hero-decor" aria-hidden="true">
<div class="hero-grid" />
<div class="hero-scan" />
<div class="hero-beam hero-beam--a" />
<div class="hero-beam hero-beam--b" />
<svg class="hero-svg hero-svg--rings" viewBox="0 0 620 620">
<circle
cx="310"
cy="310"
r="282"
fill="none"
stroke="currentColor"
stroke-width="0.6"
opacity="0.28"
/>
<circle
cx="310"
cy="310"
r="218"
fill="none"
stroke="currentColor"
stroke-width="0.75"
opacity="0.42"
stroke-dasharray="18 26"
/>
<circle
cx="310"
cy="310"
r="142"
fill="none"
stroke="currentColor"
stroke-width="0.9"
opacity="0.5"
/>
</svg>
<div class="hero-gateway">
<div class="gateway-core">
<q-icon name="shield" size="58px" />
</div>
<div class="gateway-ring gateway-ring--outer" />
<div class="gateway-ring gateway-ring--inner" />
<div class="gateway-node gateway-node--oauth">
<q-icon name="vpn_key" size="18px" />
<span>OAuth</span>
</div>
<div class="gateway-node gateway-node--oidc">
<q-icon name="verified_user" size="18px" />
<span>OIDC</span>
</div>
<div class="gateway-node gateway-node--passkey">
<q-icon name="fingerprint" size="18px" />
<span>Passkey</span>
</div>
</div>
</div>
<div class="hero-content">
<div class="hero-badge">
<q-icon name="verified_user" size="16px" />
<span>OpenID Connect · OAuth 2.0</span>
</div>
<h1 class="hero-title">
统一认证
<br />
<span class="hero-title--gradient">安全入口</span>
</h1>
<p class="hero-desc">
为所有应用提供安全、统一、现代的认证体验。支持密码、双因素、 Passkey
多种方式,自动适配深色与浅色模式。
</p>
<div class="hero-actions">
<q-btn
color="primary"
size="19px"
icon="login"
label="前往登录"
href="/sign-in"
unelevated
padding="12px 40px"
class="hero-btn hero-btn--primary"
/>
<q-btn
outline
size="19px"
icon="fingerprint"
label="Passkey 登录"
href="/sign-in"
padding="12px 40px"
class="hero-btn hero-btn--ghost"
/>
</div>
<div class="hero-stats">
<div v-for="stat in heroStats" :key="stat.value" class="hero-stat">
<span class="hero-stat-num">{{ stat.value }}</span>
<span class="hero-stat-label">{{ stat.label }}</span>
</div>
</div>
</div>
</section>
<section class="section features">
<div class="section-header">
<div class="section-kicker">IDENTITY CAPABILITIES</div>
<h2 class="section-title">认证能力</h2>
<p class="section-subtitle">
基于 OpenIddict 构建,为企业级应用提供完整的身份认证解决方案
</p>
</div>
<div class="features-grid">
<q-card
v-for="feat in features"
:key="feat.title"
flat
bordered
class="feature-card"
>
<q-card-section class="feature-card-section">
<div class="feature-card-shine" aria-hidden="true" />
<div class="feature-card-icon" :class="feat.colorClass">
<q-icon :name="feat.icon" size="26px" />
</div>
<div class="feature-card-title">{{ feat.title }}</div>
<div class="feature-card-desc">{{ feat.desc }}</div>
</q-card-section>
</q-card>
</div>
</section>
<section class="section passkey-section">
<div class="section-header passkey-section-header">
<div class="section-kicker">PASSKEY MANAGEMENT</div>
<h2 class="section-title">删除 Passkey 前先看这里</h2>
<p class="section-subtitle">
本站删除的是服务端绑定记录,不会远程删除你设备或浏览器里的通行密钥。
</p>
</div>
<div class="passkey-grid">
<q-card
v-for="item in passkeyNotes"
:key="item.title"
flat
bordered
class="passkey-card"
>
<q-card-section class="passkey-card-section">
<div class="passkey-card-top">
<div class="passkey-card-icon" :class="item.colorClass">
<q-icon :name="item.icon" size="26px" />
</div>
<div class="passkey-card-title">{{ item.title }}</div>
</div>
<div class="passkey-card-desc">{{ item.desc }}</div>
<div class="passkey-card-note">
<q-icon name="info" size="16px" />
<span>{{ item.note }}</span>
</div>
</q-card-section>
</q-card>
</div>
<div class="passkey-local">
<div class="passkey-local-head">
<div class="passkey-local-head-icon">
<q-icon name="travel_explore" size="24px" />
</div>
<div>
<div class="passkey-local-title">常见设备上的本地删除入口</div>
<div class="passkey-local-copy">
系统和浏览器版本不同,入口名称可能略有差异;关键词通常是“密码”“Passkey”“通行密钥”。
</div>
</div>
</div>
<div class="passkey-local-grid">
<div
v-for="entry in localPasskeyEntries"
:key="entry.title"
class="passkey-local-entry"
>
<q-icon :name="entry.icon" size="21px" />
<div>
<div class="passkey-local-entry-title">{{ entry.title }}</div>
<div class="passkey-local-entry-path">{{ entry.path }}</div>
</div>
</div>
</div>
</div>
</section>
</main>
<footer class="landing-footer">
<div />
<div class="landing-footer-text">
Dpz.Core 认证中心 © {{ currentYear }}
</div>
<div />
</footer>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { useQuasar } from "quasar";
import { PageContext } from "../../services/PageContext";
import {
UserPermissions,
type UserSecurityBindingStatus,
type VmUserInfo,
} from "../../models/AuthModels";
import { UserSecurityService } from "../../services/UserSecurityService";
const $q = useQuasar();
$q.dark.set("auto");
const context = PageContext.read();
const security = new UserSecurityService();
const securityStatus = ref<UserSecurityBindingStatus | null>(null);
const currentYear = computed(() => new Date().getFullYear());
const user = computed<VmUserInfo | null>(() => context.user ?? null);
const isAdmin = computed(() =>
user.value?.permissions != null
? (user.value.permissions & UserPermissions.System) ===
UserPermissions.System
: false,
);
onMounted(async () => {
if (!user.value) {
return;
}
const result = await security.bindings();
securityStatus.value = result.success ? (result.data ?? null) : null;
});
const heroStats = [
{ value: "OAuth 2.0", label: "授权协议" },
{ value: "OIDC", label: "认证标准" },
{ value: "PKCE", label: "安全流程" },
{ value: "Passkey", label: "无密码" },
];
const dashboardStatuses = [
{
icon: "verified_user",
value: "OpenID Connect",
label: "统一身份",
colorClass: "ds--blue",
},
{
icon: "fingerprint",
value: "Passkey",
label: "无密码凭证",
colorClass: "ds--green",
},
{
icon: "phonelink_lock",
value: "双因素",
label: "高风险保护",
colorClass: "ds--purple",
},
];
const features = [
{
icon: "security",
title: "OAuth 2.0 / OIDC",
desc: "Authorization Code + PKCE 流程,支持机密与公共客户端。",
colorClass: "fc--blue",
},
{
icon: "phonelink_lock",
title: "双因素认证",
desc: "基于 TOTP 的双因素验证,为高安全场景提供额外保护。",
colorClass: "fc--green",
},
{
icon: "fingerprint",
title: "Passkey 通行密钥",
desc: "WebAuthn/FIDO2 标准,支持指纹、面部、安全密钥。",
colorClass: "fc--purple",
},
{
icon: "manage_accounts",
title: "统一授权管理",
desc: "集中管理授权范围与策略,用户可随时撤销客户端访问。",
colorClass: "fc--orange",
},
{
icon: "palette",
title: "自适应主题",
desc: "自动跟随系统深色/浅色模式,也可手动切换。",
colorClass: "fc--teal",
},
{
icon: "devices",
title: "全设备响应式",
desc: "适配桌面、平板、手机等所有屏幕尺寸。",
colorClass: "fc--red",
},
];
const passkeyNotes = [
{
icon: "delete",
title: "站点内删除",
desc: "进入 Passkey 管理页删除凭证后,这个 Passkey 将不再能用于本系统登录或敏感操作。",
note: "当前已登录会话通常仍会保留,直到你主动退出或会话过期。",
colorClass: "pk--blue",
},
{
icon: "devices",
title: "本地仍保留",
desc: "服务端没有权限远程擦除你电脑、手机或浏览器里的通行密钥。",
note: "若要彻底移除本地 Passkey,需要到系统、浏览器或密码管理器里手动删除。",
colorClass: "pk--green",
},
{
icon: "warning",
title: "删除后的后果",
desc: "之后该凭证无法再用于登录,也不能再完成依赖该 Passkey 的验证。",
note: "建议先保留至少一个备用方式,例如 2FA 或另一个 Passkey。",
colorClass: "pk--amber",
},
];
const localPasskeyEntries = [
{
icon: "desktop_windows",
title: "Windows Hello",
path: "设置 → 账户 → 通行密钥 / Passkeys → 选择本站 → 删除",
},
{
icon: "language",
title: "Chrome / Google 密码管理器",
path: "Chrome → 更多 → 密码和自动填充 → Google 密码管理器 → Passkeys",
},
{
icon: "web_asset",
title: "Edge / Microsoft 密码管理器",
path: "Edge → 设置 → 个人资料 → 密码 / Microsoft Password Manager → Passkeys",
},
{
icon: "public",
title: "Firefox",
path: "Firefox 通常调用系统或外接安全密钥保存 Passkey,请到 Windows Hello、系统密码、Google 密码管理器或安全密钥管理工具里删除",
},
{
icon: "phone_iphone",
title: "iPhone / iPad / Mac",
path: "设置或系统设置 → 密码 → 搜索本站 → 删除对应通行密钥",
},
{
icon: "android",
title: "Android",
path: "设置 → Google → 密码管理工具 → 搜索本站 → 删除 Passkey",
},
];
const baseCards = [
{
icon: "space_dashboard",
label: "用户中心",
desc: "查看个人资料、安全状态、授权应用和内容管理入口。",
to: "/user",
colorClass: "dc--green",
},
{
icon: "password",
label: "修改密码",
desc: "更新登录密码。修改后将撤销所有现有授权并要求重新登录。",
to: "/change-password.html",
colorClass: "dc--blue",
},
{
icon: "fingerprint",
label: "Passkey 管理",
desc: "绑定指纹、面部识别或安全密钥,实现无密码登录。",
to: "/passkeys.html",
colorClass: "dc--purple",
},
];
const navActions = computed(() => {
if (!user.value) {
return [];
}
const twoFactorAction = securityStatus.value?.hasTwoFactor
? {
icon: "phonelink_erase",
label: "解绑 2FA",
to: "/unbind-two-factor.html",
}
: {
icon: "phonelink_lock",
label: "绑定 2FA",
to: "/two-factor.html",
};
const actions = [
{
icon: "password",
label: "修改密码",
to: "/change-password.html",
},
twoFactorAction,
{
icon: "fingerprint",
label: "Passkey",
to: "/passkeys.html",
},
];
if (isAdmin.value) {
actions.push({
icon: "admin_panel_settings",
label: "后台",
to: "/admin",
});
}
return actions;
});
const userCards = computed(() => {
const cards = [...baseCards];
if (isAdmin.value) {
cards.push({
icon: "admin_panel_settings",
label: "后台管理",
desc: "管理客户端应用、授权记录、令牌与访问申请。",
to: "/admin",
colorClass: "dc--amber",
});
}
return cards;
});
function navigate(url: string) {
window.location.href = url;
}
function signOut() {
$q.dialog({
title: "退出登录",
message: "确认退出当前认证中心会话?",
cancel: true,
persistent: true,
}).onOk(() => {
window.location.href = "/sign-out";
});
}
</script>
AI 正在分析代码…
评论加载中...