Files
pdnode-account/public/third-party-app.html
Bret 66a901c676 v.0.2.0-beta
目前oauth已经可以正常使用
2025-07-29 17:20:26 -07:00

289 lines
12 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>第三方应用示例</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.app-container {
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.status-card {
transition: all 0.3s ease;
}
.status-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
<div class="container">
<div class="row justify-content-center align-items-center min-vh-100">
<div class="col-md-8 col-lg-6">
<div class="app-container p-4">
<div class="text-center mb-4">
<i class="fas fa-rocket fa-3x text-primary mb-3"></i>
<h2 class="fw-bold">第三方应用示例</h2>
<p class="text-muted">演示OAuth 2.0授权流程</p>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<div class="card status-card h-100">
<div class="card-body text-center">
<i class="fas fa-user-lock fa-2x text-warning mb-2"></i>
<h5 class="card-title">用户状态</h5>
<p class="card-text" id="userStatus">未登录</p>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card status-card h-100">
<div class="card-body text-center">
<i class="fas fa-key fa-2x text-success mb-2"></i>
<h5 class="card-title">访问令牌</h5>
<p class="card-text" id="tokenStatus">未获取</p>
</div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">
<i class="fas fa-cog me-2"></i>应用配置
</h5>
<div class="row">
<div class="col-md-6">
<label class="form-label">客户端ID</label>
<input type="text" class="form-control" id="clientId" placeholder="输入客户端ID">
</div>
<div class="col-md-6">
<label class="form-label">客户端密钥</label>
<input type="password" class="form-control" id="clientSecret" placeholder="输入客户端密钥">
</div>
</div>
</div>
</div>
<div class="d-grid gap-2">
<button class="btn btn-primary btn-lg" onclick="startOAuthFlow()">
<i class="fas fa-sign-in-alt me-2"></i>开始OAuth授权
</button>
<button class="btn btn-outline-secondary" onclick="getUserInfo()" id="getUserInfoBtn" disabled>
<i class="fas fa-user me-2"></i>获取用户信息
</button>
</div>
<div class="mt-4">
<div class="alert alert-info" role="alert">
<h6><i class="fas fa-info-circle me-2"></i>使用说明</h6>
<ol class="mb-0">
<li>在个人中心创建OAuth客户端</li>
<li>将客户端ID和密钥填入上方配置</li>
<li>点击"开始OAuth授权"按钮</li>
<li>在授权页面选择同意或拒绝</li>
<li>授权成功后可以获取用户信息</li>
</ol>
</div>
</div>
<div class="mt-3">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-terminal me-2"></i>API响应
</h6>
</div>
<div class="card-body">
<pre id="apiResponse" class="bg-light p-3 rounded"
style="max-height: 200px; overflow-y: auto;">等待操作...</pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
let accessToken = '';
let userInfo = null;
// 检查URL参数
function checkUrlParams() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const error = urlParams.get('error');
const state = urlParams.get('state');
if (code) {
logResponse('收到授权码', { code, state });
exchangeCodeForToken(code);
} else if (error) {
logResponse('授权被拒绝', { error, error_description: urlParams.get('error_description'), state });
updateStatus('用户拒绝授权', 'error');
}
}
// 开始OAuth流程
function startOAuthFlow() {
const clientId = document.getElementById('clientId').value.trim();
const clientSecret = document.getElementById('clientSecret').value.trim();
if (!clientId || !clientSecret) {
alert('请输入客户端ID和密钥');
return;
}
// 保存到 localStorage
localStorage.setItem('client_id', clientId);
localStorage.setItem('client_secret', clientSecret);
const redirectUri = encodeURIComponent(window.location.origin + window.location.pathname);
const scope = encodeURIComponent('read write');
const state = 'test123';
const authUrl = `http://localhost:3001/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&state=${state}&response_type=code`;
logResponse('重定向到授权页面', { authUrl });
window.location.href = authUrl;
}
// 使用授权码交换访问令牌
async function exchangeCodeForToken(code) {
let clientId = document.getElementById('clientId').value.trim();
let clientSecret = document.getElementById('clientSecret').value.trim();
if (!clientId || !clientSecret) {
clientId = localStorage.getItem('client_id');
clientSecret = localStorage.getItem('client_secret');
}
if (!clientId || !clientSecret) {
logResponse('错误', { message: '请先配置客户端ID和密钥' });
return;
}
try {
const redirectUri = window.location.origin + window.location.pathname;
// const redirectUri = window.location.href;
console.log(redirectUri)
const response = await fetch('http://localhost:3000/api/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: clientId,
client_secret: clientSecret,
code: code,
redirect_uri: redirectUri
})
});
const result = await response.json();
if (result.success) {
accessToken = result.data.access_token;
updateStatus('已登录', 'success');
updateTokenStatus('已获取');
document.getElementById('getUserInfoBtn').disabled = false;
logResponse('令牌交换成功', result.data);
} else {
logResponse('令牌交换失败', result);
updateStatus('令牌获取失败', 'error');
}
} catch (error) {
logResponse('请求错误', { error: error.message });
updateStatus('网络错误', 'error');
}
}
// 获取用户信息
async function getUserInfo() {
if (!accessToken) {
logResponse('错误', { message: '没有访问令牌' });
return;
}
try {
const response = await fetch('http://localhost:3000/api/oauth/userinfo', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
const result = await response.json();
if (result.success) {
userInfo = result.data;
updateStatus(`已登录: ${userInfo.username}`, 'success');
logResponse('用户信息获取成功', result.data);
} else {
logResponse('用户信息获取失败', result);
}
} catch (error) {
logResponse('请求错误', { error: error.message });
}
}
// 更新状态显示
function updateStatus(status, type = 'info') {
const statusElement = document.getElementById('userStatus');
statusElement.textContent = status;
statusElement.className = `card-text text-${type === 'success' ? 'success' : type === 'error' ? 'danger' : 'warning'}`;
}
function updateTokenStatus(status) {
document.getElementById('tokenStatus').textContent = status;
}
// 记录API响应
function logResponse(title, data) {
const responseElement = document.getElementById('apiResponse');
const timestamp = new Date().toLocaleTimeString();
const log = `[${timestamp}] ${title}:\n${JSON.stringify(data, null, 2)}\n\n`;
responseElement.textContent += log;
responseElement.scrollTop = responseElement.scrollHeight;
}
// 页面加载时检查URL参数
window.addEventListener('load', () => {
checkUrlParams();
// 从 localStorage 读取客户端ID和密钥
const savedClientId = localStorage.getItem('client_id');
const savedClientSecret = localStorage.getItem('client_secret');
if (savedClientId) {
document.getElementById('clientId').value = savedClientId;
}
if (savedClientSecret) {
document.getElementById('clientSecret').value = savedClientSecret;
}
});
</script>
</body>
</html>