feat: 更新SproutGate前后端代码

This commit is contained in:
2026-04-01 22:04:01 +08:00
parent 90590c7cb0
commit 650e1c7707
49 changed files with 3609 additions and 768 deletions

View File

@@ -0,0 +1,207 @@
import React, { useMemo, useState } from "react";
import {
buildAuthDenyCallbackUrl,
formatOAuthRedirectLabel,
formatWebsiteLabel
} from "../config";
import icons from "../icons";
import { MailtoEmail } from "./common";
import AvatarImg from "./AvatarImg";
const DEFAULT_PERMISSIONS = [
{
title: "账户标识",
desc: "获取账户名account用于在该应用中唯一识别你的萌芽身份。"
},
{
title: "基本资料",
desc: "获取昵称、头像、邮箱、等级、萌芽币、个人主页与简介等(以「验证令牌」「当前用户」等接口实际返回为准)。"
},
{
title: "访问令牌",
desc: "向你选择的应用颁发 JWT应用仅在令牌有效期内、经你同意后代表你请求萌芽账户认证中心接口不会获得你的登录密码。"
},
{
title: "应用接入记录",
desc: "在「应用接入」中记录该应用的 client_id 与名称,便于你在用户中心查看已与哪些应用建立关联。"
}
];
function scopeLabel(s) {
const lower = String(s || "").toLowerCase().trim();
const map = {
openid: "OpenID账户标识",
profile: "个人资料(昵称、头像等展示信息)",
email: "邮箱地址",
offline_access: "离线访问(延长令牌有效期的扩展能力,若应用支持)"
};
return map[lower] || s;
}
export default function OAuthConsentScreen({
authFlow,
user,
onAllow,
onSwitchAccount,
onPreviewImage
}) {
const [permsOpen, setPermsOpen] = useState(true);
const appLabel = useMemo(() => {
const name = (authFlow?.clientName || "").trim();
const id = (authFlow?.clientId || "").trim();
if (name) return name;
if (id) return id;
return "第三方应用";
}, [authFlow]);
const redirectLabel = formatOAuthRedirectLabel(authFlow?.redirectUri);
const redirectUrl = (authFlow?.redirectUri || "").trim();
const extraScopes = useMemo(() => {
const raw = authFlow?.scopes || [];
if (!raw.length) return [];
return raw.map(scopeLabel);
}, [authFlow]);
const handleDeny = () => {
const url = buildAuthDenyCallbackUrl(authFlow?.redirectUri, authFlow?.state || "");
if (url) window.location.replace(url);
};
return (
<div className="oauth-consent">
<div className="oauth-consent-brand">
<div className="oauth-consent-icon-wrap" aria-hidden>
<svg className="oauth-consent-lock" viewBox="0 0 24 24" width="28" height="28">
<rect x="5" y="10" width="14" height="11" rx="2" fill="none" stroke="currentColor" strokeWidth="1.8" />
<path d="M8 10V8a4 4 0 0 1 8 0v2" fill="none" stroke="currentColor" strokeWidth="1.8" />
</svg>
</div>
<h1 className="oauth-consent-app-name">{appLabel}</h1>
<p className="oauth-consent-lead">请求访问你的<strong>萌芽统一账户</strong></p>
</div>
<div className="oauth-consent-card oauth-consent-card--identity">
<div className="oauth-consent-identity">
<AvatarImg
user={user}
placeholderSize={56}
alt="avatar"
className="oauth-consent-avatar previewable-image"
previewable
onPreviewImage={onPreviewImage}
previewLabel={user?.username || user?.account || "avatar"}
/>
<div className="oauth-consent-id-text">
<div className="oauth-consent-display">{user?.username || user?.account || "用户"}</div>
<div className="oauth-consent-as">
<span className="mono oauth-consent-account">@{user?.account}</span> 的身份授权
</div>
{user?.email ? (
<div className="oauth-consent-email">
<MailtoEmail address={user.email} className="profile-external-link">{user.email}</MailtoEmail>
</div>
) : (
<div className="muted oauth-consent-email">未绑定邮箱</div>
)}
</div>
</div>
</div>
<div className="oauth-consent-card oauth-consent-card--detail">
<div className="oauth-consent-section-label">应用信息</div>
<ul className="oauth-consent-meta">
<li>
<span className="icon oauth-consent-meta-icon">{icons.link}</span>
<div>
<div className="oauth-consent-meta-key">回跳地址</div>
{redirectUrl.match(/^https?:\/\//i) ? (
<a
href={redirectUrl}
target="_blank"
rel="noopener noreferrer"
className="oauth-consent-meta-link"
>
{redirectLabel}
</a>
) : (
<span className="mono oauth-consent-meta-val">{redirectLabel}</span>
)}
</div>
</li>
{(authFlow?.clientId || "").trim() ? (
<li>
<span className="icon oauth-consent-meta-icon">{icons.apps}</span>
<div>
<div className="oauth-consent-meta-key">应用 IDclient_id</div>
<span className="mono oauth-consent-meta-val">{authFlow.clientId.trim()}</span>
</div>
</li>
) : null}
{(authFlow?.clientName || "").trim() ? (
<li>
<span className="icon oauth-consent-meta-icon">{icons.username}</span>
<div>
<div className="oauth-consent-meta-key">应用名称</div>
<span className="oauth-consent-meta-val">{authFlow.clientName.trim()}</span>
</div>
</li>
) : null}
</ul>
<div className="oauth-consent-section-label oauth-consent-section-label--perm">将获取以下信息与权限</div>
<button
type="button"
className={`oauth-consent-perm-toggle${permsOpen ? " is-open" : ""}`}
onClick={() => setPermsOpen((v) => !v)}
aria-expanded={permsOpen}
>
<span className="oauth-consent-perm-toggle-text">查看详情</span>
<span className="oauth-consent-chevron" aria-hidden></span>
</button>
{permsOpen && (
<ul className="oauth-consent-perms">
{DEFAULT_PERMISSIONS.map((p) => (
<li key={p.title}>
<span className="oauth-consent-perm-icon">{icons.token}</span>
<div>
<div className="oauth-consent-perm-title">{p.title}</div>
<div className="oauth-consent-perm-desc">{p.desc}</div>
</div>
</li>
))}
{extraScopes.length > 0 ? (
<li>
<span className="oauth-consent-perm-icon">{icons.level}</span>
<div>
<div className="oauth-consent-perm-title">应用请求的 scope</div>
<ul className="oauth-consent-scope-list">
{extraScopes.map((s) => (
<li key={s}>{s}</li>
))}
</ul>
</div>
</li>
) : null}
</ul>
)}
</div>
<div className="oauth-consent-actions">
<button type="button" className="primary oauth-consent-allow" onClick={onAllow}>
允许
</button>
<button type="button" className="ghost oauth-consent-deny" onClick={handleDeny}>
拒绝
</button>
<button type="button" className="oauth-consent-switch text-button" onClick={onSwitchAccount}>
使用其他账号登录
</button>
</div>
<p className="oauth-consent-footnote">
点击允许你将返回应用 <span className="mono">{formatWebsiteLabel(redirectUrl) || redirectLabel}</span>并在浏览器地址的 Hash 中携带访问令牌
</p>
</div>
);
}