Files
springboot-demo/target/classes/static/access.html
2026-03-23 13:05:44 +08:00

390 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spring Learning Login</title>
<style>
:root {
--bg: linear-gradient(135deg, #eef5ff 0%, #f8fff4 100%);
--card: rgba(255, 255, 255, 0.94);
--line: #dbe6f2;
--text: #122033;
--muted: #5d7288;
--brand: #177245;
--accent: #0f67b5;
--shadow: 0 20px 48px rgba(18, 32, 51, 0.12);
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif;
color: var(--text);
background:
radial-gradient(circle at top left, rgba(23, 114, 69, 0.13), transparent 30%),
radial-gradient(circle at bottom right, rgba(15, 103, 181, 0.12), transparent 24%),
var(--bg);
}
.page {
max-width: 1180px;
margin: 0 auto;
padding: 24px;
}
.hero,
.card {
background: var(--card);
border: 1px solid var(--line);
border-radius: 28px;
box-shadow: var(--shadow);
}
.hero {
padding: 28px;
margin-bottom: 18px;
}
.eyebrow {
display: inline-flex;
padding: 7px 12px;
border-radius: 999px;
background: rgba(23, 114, 69, 0.1);
color: var(--brand);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.1em;
text-transform: uppercase;
}
h1, h2, h3 { margin: 10px 0 12px; }
p {
margin: 0;
color: var(--muted);
line-height: 1.8;
}
.layout {
display: grid;
grid-template-columns: 1.1fr 0.9fr;
gap: 18px;
}
.card {
padding: 22px;
}
.field {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 14px;
}
label {
font-size: 13px;
font-weight: 700;
color: #21384f;
}
input {
width: 100%;
border: 1px solid var(--line);
border-radius: 14px;
padding: 12px 14px;
background: transparent;
color: var(--text);
outline: none;
font: inherit;
}
.actions,
.chips {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn,
.btn-soft {
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
border-radius: 999px;
padding: 12px 18px;
cursor: pointer;
font-weight: 700;
}
.btn {
color: #fff;
background: linear-gradient(135deg, var(--brand), #35a465);
}
.btn-soft {
color: var(--accent);
background: #eaf3ff;
}
.note-list {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 14px;
}
.note {
padding: 16px;
border: 1px solid var(--line);
border-radius: 18px;
background: rgba(255, 255, 255, 0.5);
}
.status {
min-height: 22px;
margin-top: 12px;
color: var(--muted);
}
.status.error { color: #d64545; }
.status.success { color: #177245; }
pre {
margin: 16px 0 0;
min-height: 220px;
padding: 16px;
border-radius: 18px;
background: #0f1621;
color: #deebff;
white-space: pre-wrap;
font-family: Consolas, "Courier New", monospace;
overflow: auto;
}
.chips span {
display: inline-flex;
padding: 6px 10px;
border-radius: 999px;
background: rgba(15, 103, 181, 0.08);
color: var(--accent);
font-size: 12px;
font-weight: 700;
}
@media (max-width: 920px) {
.layout { grid-template-columns: 1fr; }
}
@media (max-width: 720px) {
.page { padding: 14px; }
}
</style>
</head>
<body>
<div class="page">
<section class="hero">
<div class="eyebrow" data-i18n="heroBadge"></div>
<h1 data-i18n="heroTitle"></h1>
<p data-i18n="heroText"></p>
<div class="actions" style="margin-top:18px;">
<a class="btn-soft" href="/" data-i18n="homeLink"></a>
<a class="btn-soft" href="/users.html" data-i18n="usersLink"></a>
<a class="btn-soft" href="/aop.html" data-i18n="aopLink"></a>
<a class="btn-soft" href="/events.html" data-i18n="eventsLink"></a>
</div>
</section>
<div class="layout">
<section class="card">
<div class="eyebrow" data-i18n="loginBadge"></div>
<h2 data-i18n="loginTitle"></h2>
<p data-i18n="loginText"></p>
<div class="field">
<label for="username" data-i18n="usernameLabel"></label>
<input id="username" value="admin" autocomplete="username">
</div>
<div class="field">
<label for="password" data-i18n="passwordLabel"></label>
<input id="password" type="password" value="admin123" autocomplete="current-password">
</div>
<div class="actions" style="margin-top:18px;">
<button class="btn" type="button" id="loginButton" data-i18n="loginButton" onclick="login()"></button>
<button class="btn-soft" type="button" id="checkButton" data-i18n="checkButton" onclick="introspectToken()"></button>
<button class="btn-soft" type="button" id="logoutButton" data-i18n="logoutButton" onclick="logout()"></button>
</div>
<div class="status" id="statusBox"></div>
<pre id="resultBox"></pre>
</section>
<aside class="card">
<div class="eyebrow" data-i18n="guideBadge"></div>
<h2 data-i18n="guideTitle"></h2>
<div class="note-list">
<div class="note">
<strong data-i18n="note1Title"></strong>
<p data-i18n="note1Text"></p>
</div>
<div class="note">
<strong data-i18n="note2Title"></strong>
<p data-i18n="note2Text"></p>
</div>
<div class="note">
<strong data-i18n="note3Title"></strong>
<p data-i18n="note3Text"></p>
</div>
</div>
<div class="eyebrow" style="margin-top:20px;" data-i18n="accountBadge"></div>
<div class="chips" style="margin-top:12px;">
<span>admin / admin123</span>
<span>user / user123</span>
</div>
</aside>
</div>
</div>
<script src="/learning-shell.js"></script>
<script>
const I18N = {
zh: {
title: "Spring \u5b66\u4e60\u767b\u5f55\u9875",
heroBadge: "\u5b66\u4e60\u9274\u6743\u5165\u53e3",
heroTitle: "\u5148\u5b8c\u6210\u6f14\u793a\u767b\u5f55\uff0c\u518d\u8fdb\u5165\u53d7\u4fdd\u62a4\u5b9e\u9a8c\u3002",
heroText: "\u8fd9\u4e2a\u9875\u9762\u628a\u540e\u7aef\u5df2\u6709\u7684 JWT \u767b\u5f55\u80fd\u529b\u53d8\u6210\u771f\u6b63\u53ef\u89c1\u7684\u5b66\u4e60\u5165\u53e3\uff0c\u65b9\u4fbf\u4f60\u5728\u516c\u5f00 VPS \u73af\u5883\u91cc\u5148\u767b\u5f55\uff0c\u518d\u4f53\u9a8c\u7528\u6237\u3001AOP\u3001\u4e8b\u4ef6\u548c\u9ad8\u7ea7\u5b9e\u9a8c\u3002",
homeLink: "\u8fd4\u56de\u9996\u9875",
usersLink: "\u6253\u5f00\u7528\u6237\u5b9e\u9a8c",
aopLink: "\u6253\u5f00 AOP \u5b9e\u9a8c",
eventsLink: "\u6253\u5f00\u4e8b\u4ef6\u5b9e\u9a8c",
loginBadge: "\u767b\u5f55\u8868\u5355",
loginTitle: "\u4f7f\u7528\u6f14\u793a\u8d26\u53f7\u6362\u53d6 Bearer Token",
loginText: "\u767b\u5f55\u6210\u529f\u540e\uff0ctoken \u4f1a\u4fdd\u5b58\u5230\u6d4f\u89c8\u5668\u672c\u5730\u3002\u4e4b\u540e\u672c\u9879\u76ee\u4e2d\u7684\u524d\u7aef\u9875\u9762\u4f1a\u81ea\u52a8\u643a\u5e26 Authorization \u5934\u8bbf\u95ee\u53d7\u4fdd\u62a4\u63a5\u53e3\u3002",
usernameLabel: "\u7528\u6237\u540d",
passwordLabel: "\u5bc6\u7801",
loginButton: "\u7acb\u5373\u767b\u5f55",
checkButton: "\u68c0\u67e5\u5f53\u524d\u4ee4\u724c",
logoutButton: "\u6e05\u9664\u4ee4\u724c",
guideBadge: "\u5b66\u4e60\u63d0\u793a",
guideTitle: "\u8fd9\u4e00\u6b65\u89e3\u51b3\u4e86\u4ec0\u4e48\u95ee\u9898",
note1Title: "1. \u8ba9\u767b\u5f55\u4e0d\u518d\u53ea\u662f\u540e\u7aef\u63a5\u53e3",
note1Text: "\u4e4b\u524d\u540e\u7aef\u63a5\u53e3\u5df2\u7ecf\u5b58\u5728\uff0c\u4f46\u9875\u9762\u91cc\u6ca1\u6709\u5165\u53e3\u3002\u73b0\u5728\u4f60\u53ef\u4ee5\u4ece\u524d\u7aef\u5b8c\u6210\u767b\u5f55\u3001\u4ee4\u724c\u6301\u4e45\u5316\u548c\u4ee4\u724c\u68c0\u67e5\u3002",
note2Title: "2. \u628a\u53d7\u4fdd\u62a4\u8def\u7531\u53d8\u6210\u53ef\u89c2\u5bdf\u6982\u5ff5",
note2Text: "\u767b\u5f55\u540e\u518d\u8bbf\u95ee /api/users/**\u3001/aop/**\u3001/learn/** \u548c /api/lab/**\uff0c\u5c31\u80fd\u6e05\u6670\u5bf9\u6bd4\u767b\u5f55\u524d\u540e\u7684\u884c\u4e3a\u5dee\u5f02\u3002",
note3Title: "3. \u4e3a\u4e2d\u82f1\u5207\u6362\u9884\u7559\u7edf\u4e00\u5165\u53e3",
note3Text: "\u9875\u9762\u9876\u90e8\u5df2\u7ecf\u63a5\u4e0a\u5171\u4eab\u5de5\u4f5c\u53f0\u811a\u672c\uff0c\u8bed\u8a00\u504f\u597d\u548c\u767b\u5f55\u72b6\u6001\u4f1a\u5728\u9875\u9762\u95f4\u540c\u6b65\u3002",
accountBadge: "\u6f14\u793a\u8d26\u53f7",
ready: "\u51c6\u5907\u5c31\u7eea\u3002\u4f60\u53ef\u4ee5\u76f4\u63a5\u4f7f\u7528\u9ed8\u8ba4\u8d26\u53f7\u767b\u5f55\u3002",
loginSuccess: "\u767b\u5f55\u6210\u529f\uff0c\u4ee4\u724c\u5df2\u4fdd\u5b58\u5230\u672c\u5730\u3002",
logoutSuccess: "\u672c\u5730\u4ee4\u724c\u5df2\u6e05\u9664\u3002",
introspectSuccess: "\u4ee4\u724c\u81ea\u68c0\u5b8c\u6210\u3002",
missingToken: "\u5f53\u524d\u6ca1\u6709\u53ef\u68c0\u67e5\u7684\u4ee4\u724c\uff0c\u8bf7\u5148\u767b\u5f55\u3002"
},
en: {
title: "Spring Learning Login",
heroBadge: "Auth Entry",
heroTitle: "Complete demo login before entering protected labs.",
heroText: "This page turns the existing backend JWT capability into a visible learning entry so you can authenticate first and then explore users, AOP, events, and advanced labs on a public VPS.",
homeLink: "Back home",
usersLink: "Open user lab",
aopLink: "Open AOP lab",
eventsLink: "Open event lab",
loginBadge: "Login Form",
loginTitle: "Exchange demo credentials for a Bearer token",
loginText: "After login, the token is stored locally. Frontend pages in this project will automatically attach the Authorization header when calling protected APIs.",
usernameLabel: "Username",
passwordLabel: "Password",
loginButton: "Login now",
checkButton: "Inspect token",
logoutButton: "Clear token",
guideBadge: "Study Notes",
guideTitle: "What this step solves",
note1Title: "1. Login is no longer only a backend endpoint",
note1Text: "The backend endpoint already existed, but there was no visible entry on the frontend. Now you can complete login, token persistence, and token inspection in the browser.",
note2Title: "2. Protected routes become observable",
note2Text: "After login, revisit /api/users/**, /aop/**, /learn/**, and /api/lab/** to compare protected behavior.",
note3Title: "3. Shared groundwork for language switching",
note3Text: "The shared workspace script is already mounted at the top of the page, so language preference and login state now sync across pages.",
accountBadge: "Demo Accounts",
ready: "Ready. You can sign in with the default accounts.",
loginSuccess: "Login succeeded and the token was saved locally.",
logoutSuccess: "Local token has been cleared.",
introspectSuccess: "Token introspection completed.",
missingToken: "There is no token to inspect yet. Please log in first."
}
};
function pageText() {
return I18N[window.learningShell.getLanguage()] || I18N.zh;
}
function applyTranslations(text) {
document.title = text.title;
document.querySelectorAll("[data-i18n]").forEach(function (element) {
const key = element.getAttribute("data-i18n");
if (Object.prototype.hasOwnProperty.call(text, key)) {
element.textContent = text[key];
}
});
}
function renderLanguage() {
const text = pageText();
applyTranslations(text);
const resultBox = document.getElementById("resultBox");
if (!resultBox.dataset.state || resultBox.dataset.state === "idle") {
resultBox.textContent = text.ready;
resultBox.dataset.state = "idle";
}
}
function setStatus(message, type) {
const box = document.getElementById("statusBox");
box.textContent = message;
box.className = type ? "status " + type : "status";
}
async function login() {
const username = document.getElementById("username").value.trim();
const password = document.getElementById("password").value;
try {
const payload = await window.learningShell.requestJson("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: username, password: password })
});
window.learningShell.saveAuth(payload.data.token, payload.data.username);
setStatus(pageText().loginSuccess, "success");
document.getElementById("resultBox").textContent = JSON.stringify(payload, null, 2);
document.getElementById("resultBox").dataset.state = "live";
} catch (error) {
setStatus(window.learningShell.describeError(error), "error");
document.getElementById("resultBox").textContent = JSON.stringify(
error.payload || { message: error.message },
null,
2
);
document.getElementById("resultBox").dataset.state = "live";
}
}
async function introspectToken() {
const token = window.learningShell.getToken();
if (!token) {
setStatus(pageText().missingToken, "error");
return;
}
try {
const payload = await window.learningShell.requestJson("/api/auth/introspect");
setStatus(pageText().introspectSuccess, "success");
document.getElementById("resultBox").textContent = JSON.stringify(payload, null, 2);
document.getElementById("resultBox").dataset.state = "live";
} catch (error) {
setStatus(window.learningShell.describeError(error), "error");
document.getElementById("resultBox").textContent = JSON.stringify(
error.payload || { message: error.message },
null,
2
);
document.getElementById("resultBox").dataset.state = "live";
}
}
function logout() {
window.learningShell.clearAuth();
setStatus(pageText().logoutSuccess, "success");
document.getElementById("resultBox").textContent = JSON.stringify({ cleared: true }, null, 2);
document.getElementById("resultBox").dataset.state = "live";
}
window.learningShell.mountShell({ onLanguageChange: renderLanguage });
renderLanguage();
</script>
</body>
</html>