Bump secrets-mcp to 0.2.2 and sync Cargo.lock. Add home.html landing with SEO and footer link to the refining/secrets repository; serve it at / and expose /login for sign-in. Update OAuth error redirects and dashboard unauthenticated redirects to /login. Improve login page meta tags, back-home link, and OAuth error alert. Refresh llms.txt and robots.txt. Made-with: Cursor
187 lines
9.3 KiB
HTML
187 lines
9.3 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="robots" content="noindex, follow">
|
|
<meta name="description" content="登录 Secrets MCP Web 控制台,安全管理跨设备加密 secrets。">
|
|
<meta name="keywords" content="Secrets MCP,登录,OAuth,密钥管理">
|
|
<link rel="canonical" href="{{ base_url }}/login">
|
|
<link rel="icon" href="/favicon.svg?v={{ version }}" type="image/svg+xml">
|
|
<title>登录 — Secrets MCP</title>
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:url" content="{{ base_url }}/login">
|
|
<meta property="og:title" content="登录 — Secrets MCP">
|
|
<meta property="og:description" content="登录 Web 控制台,管理加密存储的密钥与配置。">
|
|
<meta name="twitter:card" content="summary">
|
|
<meta name="twitter:title" content="登录 — Secrets MCP">
|
|
<meta name="twitter:description" content="登录 Web 控制台,管理加密存储的密钥与配置。">
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
|
|
:root {
|
|
--bg: #0d1117;
|
|
--surface: #161b22;
|
|
--border: #30363d;
|
|
--text: #e6edf3;
|
|
--text-muted: #8b949e;
|
|
--accent: #58a6ff;
|
|
--accent-hover: #79b8ff;
|
|
--google: #4285f4;
|
|
--danger: #f85149;
|
|
}
|
|
body { background: var(--bg); color: var(--text); font-family: 'Inter', sans-serif;
|
|
min-height: 100vh; display: flex; align-items: center; justify-content: center; }
|
|
.card {
|
|
background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
|
|
padding: 48px 40px; width: 100%; max-width: 400px;
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
|
}
|
|
.topbar { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; gap: 12px; }
|
|
.back-home {
|
|
font-size: 13px; color: var(--accent); text-decoration: none; white-space: nowrap;
|
|
}
|
|
.back-home:hover { text-decoration: underline; }
|
|
.lang-bar { display: flex; gap: 2px; background: rgba(255,255,255,0.04); border-radius: 6px; padding: 2px; flex-shrink: 0; }
|
|
.lang-btn { padding: 3px 9px; border: none; background: none; color: var(--text-muted);
|
|
font-size: 12px; cursor: pointer; border-radius: 4px; }
|
|
.lang-btn.active { background: var(--border); color: var(--text); }
|
|
.oauth-alert {
|
|
display: none;
|
|
margin-bottom: 16px; padding: 10px 12px; border-radius: 8px;
|
|
font-size: 13px; line-height: 1.4;
|
|
background: rgba(248, 81, 73, 0.12);
|
|
border: 1px solid rgba(248, 81, 73, 0.35);
|
|
color: #ffa198;
|
|
}
|
|
.oauth-alert.visible { display: block; }
|
|
h1 { font-size: 22px; font-weight: 600; margin-bottom: 8px; }
|
|
.subtitle { color: var(--text-muted); font-size: 14px; margin-bottom: 32px; }
|
|
.btn {
|
|
display: flex; align-items: center; justify-content: center; gap: 12px;
|
|
width: 100%; padding: 12px 20px; border: 1px solid var(--border); border-radius: 8px;
|
|
background: var(--surface); color: var(--text); font-size: 14px; font-weight: 500;
|
|
cursor: pointer; text-decoration: none; transition: all 0.2s;
|
|
}
|
|
.btn:hover { background: var(--border); border-color: var(--text-muted); }
|
|
.btn + .btn { margin-top: 12px; }
|
|
.btn svg { flex-shrink: 0; }
|
|
.footer { margin-top: 28px; text-align: center; color: var(--text-muted); font-size: 12px; }
|
|
.footer a { color: var(--accent); text-decoration: none; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="card">
|
|
<div class="topbar">
|
|
<a class="back-home" href="/" data-i18n="backHome">返回首页</a>
|
|
<div class="lang-bar">
|
|
<button type="button" class="lang-btn" onclick="setLang('zh-CN')">简</button>
|
|
<button type="button" class="lang-btn" onclick="setLang('zh-TW')">繁</button>
|
|
<button type="button" class="lang-btn" onclick="setLang('en')">EN</button>
|
|
</div>
|
|
</div>
|
|
<div id="oauth-alert" class="oauth-alert" role="alert"></div>
|
|
<h1 data-i18n="title">登录</h1>
|
|
<p class="subtitle" data-i18n="subtitle">安全管理你的跨设备 secrets。</p>
|
|
|
|
{% if has_google %}
|
|
<a href="/auth/google" class="btn">
|
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
|
<path d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844a4.14 4.14 0 01-1.796 2.716v2.259h2.908c1.702-1.567 2.684-3.875 2.684-6.615z" fill="#4285F4"/>
|
|
<path d="M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 009 18z" fill="#34A853"/>
|
|
<path d="M3.964 10.71A5.41 5.41 0 013.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 000 9c0 1.452.348 2.827.957 4.042l3.007-2.332z" fill="#FBBC05"/>
|
|
<path d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 00.957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58z" fill="#EA4335"/>
|
|
</svg>
|
|
<span data-i18n="google">使用 Google 登录</span>
|
|
</a>
|
|
{% endif %}
|
|
|
|
{% if !has_google %}
|
|
<p style="text-align:center; color: var(--text-muted); font-size: 14px;" data-i18n="noProviders">
|
|
未配置登录方式,请联系管理员。
|
|
</p>
|
|
{% endif %}
|
|
</div>
|
|
<script>
|
|
const T = {
|
|
'zh-CN': {
|
|
docTitle: '登录 — Secrets MCP',
|
|
backHome: '返回首页',
|
|
title: '登录',
|
|
subtitle: '安全管理你的跨设备 secrets。',
|
|
google: '使用 Google 登录',
|
|
noProviders: '未配置登录方式,请联系管理员。',
|
|
err_oauth_error: '登录失败:授权提供方返回错误,请重试。',
|
|
err_oauth_missing_code: '登录失败:未收到授权码,请重试。',
|
|
err_oauth_missing_state: '登录失败:缺少安全校验参数,请重试。',
|
|
err_oauth_state: '登录失败:会话校验不匹配(可能因 Cookie 策略或服务器重启)。请返回首页再试。',
|
|
},
|
|
'zh-TW': {
|
|
docTitle: '登入 — Secrets MCP',
|
|
backHome: '返回首頁',
|
|
title: '登入',
|
|
subtitle: '安全管理你的跨裝置 secrets。',
|
|
google: '使用 Google 登入',
|
|
noProviders: '尚未設定登入方式,請聯絡管理員。',
|
|
err_oauth_error: '登入失敗:授權方回傳錯誤,請再試一次。',
|
|
err_oauth_missing_code: '登入失敗:未取得授權碼,請再試一次。',
|
|
err_oauth_missing_state: '登入失敗:缺少安全校驗參數,請再試一次。',
|
|
err_oauth_state: '登入失敗:工作階段校驗不符(可能與 Cookie 政策或伺服器重啟有關)。請回到首頁再試。',
|
|
},
|
|
'en': {
|
|
docTitle: 'Sign in — Secrets MCP',
|
|
backHome: 'Back to home',
|
|
title: 'Sign in',
|
|
subtitle: 'Manage your cross-device secrets securely.',
|
|
google: 'Continue with Google',
|
|
noProviders: 'No login providers configured. Please contact your administrator.',
|
|
err_oauth_error: 'Sign-in failed: the identity provider returned an error. Please try again.',
|
|
err_oauth_missing_code: 'Sign-in failed: no authorization code was returned. Please try again.',
|
|
err_oauth_missing_state: 'Sign-in failed: missing security state. Please try again.',
|
|
err_oauth_state: 'Sign-in failed: session state mismatch (often cookies or server restart). Open the home page and try again.',
|
|
}
|
|
};
|
|
|
|
let currentLang = localStorage.getItem('lang') || 'zh-CN';
|
|
|
|
function t(key) { return T[currentLang][key] || T['en'][key] || key; }
|
|
|
|
function showOAuthError() {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const code = params.get('error');
|
|
const el = document.getElementById('oauth-alert');
|
|
if (!code || !code.startsWith('oauth_')) {
|
|
el.classList.remove('visible');
|
|
el.textContent = '';
|
|
return;
|
|
}
|
|
const key = 'err_' + code;
|
|
el.textContent = t(key) || t('err_oauth_error');
|
|
el.classList.add('visible');
|
|
}
|
|
|
|
function applyLang() {
|
|
document.documentElement.lang = currentLang;
|
|
document.title = t('docTitle');
|
|
document.querySelectorAll('[data-i18n]').forEach(el => {
|
|
const key = el.getAttribute('data-i18n');
|
|
el.textContent = t(key);
|
|
});
|
|
document.querySelectorAll('.lang-btn').forEach(btn => {
|
|
const map = { 'zh-CN': '简', 'zh-TW': '繁', 'en': 'EN' };
|
|
btn.classList.toggle('active', btn.textContent === map[currentLang]);
|
|
});
|
|
showOAuthError();
|
|
}
|
|
|
|
function setLang(lang) {
|
|
currentLang = lang;
|
|
localStorage.setItem('lang', lang);
|
|
applyLang();
|
|
}
|
|
|
|
applyLang();
|
|
</script>
|
|
</body>
|
|
</html>
|