Files
secure-device-demo/web-client/app.js
TongTongStudio 93499fa189 feat: 重构为前后端分离架构并完善设备端演示
- 新增 Web 用户端(登录体系 + 统一 REST API 调用)
- 后端增加用户认证、统一 ApiResponse、CORS 支持
- Android 设备端迁移至 MVVM + DataBinding + Retrofit 网络层
- 完善 README 架构说明与密码学原理文档
- 新增 .gitignore 与持久化数据表说明
2026-08-21 04:34:58 +08:00

254 lines
8.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// Secure Device 用户端Web—— 前后端分离演示
//
// 后端地址:默认 localhost:8080CORS 已放开)。
// 认证:登录/注册后获得 Bearer Token存 localStorage
// 后续所有用户端接口自动携带 Authorization: Bearer <token>。
// ============================================================
const API_BASE = 'http://localhost:8080';
// ---------- 状态 ----------
let token = localStorage.getItem('sd_token') || '';
let userId = localStorage.getItem('sd_userId') || '';
// ---------- DOM 引用 ----------
const $ = (id) => document.getElementById(id);
const sections = {
auth: $('authSection'),
device: $('deviceSection'),
recover: $('recoverSection'),
photo: $('photoSection'),
};
// 登录后才需要显示的面板(登录区 auth 始终显示,不在其中)
const authedSections = {
device: $('deviceSection'),
recover: $('recoverSection'),
photo: $('photoSection'),
};
// ---------- 通用请求 ----------
async function api(path, { method = 'GET', body = null } = {}) {
const headers = { 'Content-Type': 'application/json' };
if (token) headers['Authorization'] = 'Bearer ' + token;
const resp = await fetch(API_BASE + path, {
method,
headers,
body: body ? JSON.stringify(body) : null,
});
// 统一响应格式 { code, message, data },即使 HTTP 4xx/5xx 也是该结构
const json = await resp.json().catch(() => null);
showRaw(method, path, resp.status, json);
if (!resp.ok || !json || json.code !== 0) {
throw new Error((json && json.message) || `HTTP ${resp.status}`);
}
return json.data;
}
// ---------- 原始响应展示 ----------
function showRaw(method, path, status, json) {
$('rawOutput').textContent =
`HTTP ${status} ${method} ${API_BASE}${path}\n` +
JSON.stringify(json, null, 2);
}
// ---------- 结果展示 ----------
function showResult(elId, text, isErr = false) {
const el = $(elId);
el.textContent = text;
el.className = 'result ' + (isErr ? 'err' : 'ok');
}
// ---------- 登录态 UI ----------
function refreshAuthUI() {
const loggedIn = !!token;
$('userInfo').classList.toggle('hidden', !loggedIn);
$('userLabel').textContent = loggedIn ? `当前用户:${userId}` : '';
// 登录区始终显示;仅「设备/恢复/照片」三个面板在登录后才显示
$('authSection').classList.toggle('hidden', false);
Object.values(authedSections).forEach((s) => s.classList.toggle('hidden', !loggedIn));
if (!loggedIn) $('authResult').textContent = '';
}
function login(user, pass) {
token = user;
userId = user;
localStorage.setItem('sd_token', token);
localStorage.setItem('sd_userId', userId);
refreshAuthUI();
}
function logout() {
token = '';
userId = '';
localStorage.removeItem('sd_token');
localStorage.removeItem('sd_userId');
refreshAuthUI();
}
// ============================================================
// 事件绑定
// ============================================================
// 登录
$('btnLogin').addEventListener('click', async () => {
const uid = $('authUserId').value.trim();
const pass = $('authPassword').value || '123456';
if (!uid) return showResult('authResult', '请输入用户名', true);
try {
const data = await api('/api/auth/login', {
method: 'POST',
body: { userId: uid, password: pass },
});
login(data.userId, data.token);
showResult('authResult', `登录成功Token: ${data.token.slice(0, 16)}...`);
} catch (e) {
showResult('authResult', `登录失败:${e.message}`, true);
}
});
// 注册
$('btnRegister').addEventListener('click', async () => {
const uid = $('authUserId').value.trim();
const pass = $('authPassword').value || '123456';
const phone = $('authPhone').value.trim() || '13800138000';
if (!uid) return showResult('authResult', '请输入用户名', true);
try {
const data = await api('/api/auth/register', {
method: 'POST',
body: { userId: uid, phone, password: pass },
});
login(data.userId, data.token);
showResult('authResult', `注册成功并已登录Token: ${data.token.slice(0, 16)}...`);
} catch (e) {
showResult('authResult', `注册失败:${e.message}`, true);
}
});
// 退出登录
$('btnLogout').addEventListener('click', () => {
logout();
showResult('authResult', '已退出登录');
});
// 绑定设备
$('btnBind').addEventListener('click', async () => {
const sn = $('bindSn').value.trim();
if (!sn) return showResult('bindResult', '请输入设备 SN', true);
try {
const data = await api('/api/device/bind', {
method: 'POST',
body: { sn },
});
showResult('bindResult', data.message || '绑定成功');
} catch (e) {
showResult('bindResult', `绑定失败:${e.message}`, true);
}
});
// 设备列表
$('btnListDevices').addEventListener('click', async () => {
try {
const data = await api('/api/user/devices');
const devices = data.devices || [];
if (devices.length === 0) {
showResult('devicesResult', '暂无绑定设备');
return;
}
const lines = devices.map(
(d) => `${d.sn} | deviceId: ${d.deviceId} | 状态: ${d.active ? '启用' : '停用'}`
);
showResult('devicesResult', lines.join('\n'));
} catch (e) {
showResult('devicesResult', `获取设备列表失败:${e.message}`, true);
}
});
// 发送短信验证码
$('btnSendSms').addEventListener('click', async () => {
const phone = $('recoverPhone').value.trim() || '13800138000';
try {
const data = await api('/api/device/sms/send', {
method: 'POST',
body: { phone },
});
showResult('recoverResult', data.message || '验证码已发送demo 固定 000000');
} catch (e) {
showResult('recoverResult', `发送失败:${e.message}`, true);
}
});
// 恢复授权
$('btnRecover').addEventListener('click', async () => {
const sn = $('recoverSn').value.trim();
const newPubKey = $('recoverNewPubKey').value.trim();
const smsCode = $('recoverSmsCode').value.trim();
if (!sn) return showResult('recoverResult', '请输入设备 SN', true);
if (!newPubKey) return showResult('recoverResult', '请输入新设备公钥(从设备端复制)', true);
if (!smsCode) return showResult('recoverResult', '请输入短信验证码', true);
try {
const data = await api('/api/device/recover', {
method: 'POST',
body: { sn, smsCode, newPublicKeyBase64: newPubKey },
});
showResult(
'recoverResult',
`恢复授权成功deviceId: ${data.deviceId}\n` +
`encryptedRecoveryToken: ${data.encryptedRecoveryToken.slice(0, 32)}...\n` +
`(设备端可凭此 Token 取回照片 DEK`
);
} catch (e) {
showResult('recoverResult', `恢复授权失败:${e.message}`, true);
}
});
// 照片列表
$('btnListPhotos').addEventListener('click', async () => {
try {
const data = await api('/api/user/photos');
const photos = data.photos || [];
if (photos.length === 0) {
showResult('photosResult', '暂无照片(请先在设备端上传)');
return;
}
showResult('photosResult', photos.map((p) => `${p}`).join('\n'));
} catch (e) {
showResult('photosResult', `获取照片列表失败:${e.message}`, true);
}
});
// 下载解密照片
$('btnDecrypt').addEventListener('click', async () => {
const photoId = $('decryptPhotoId').value.trim();
if (!photoId) return showResult('decryptResult', '请输入 photoId', true);
try {
const data = await api(`/api/photo/${photoId}/decrypt`);
const plaintext = decodeBase64Text(data.plaintextBase64);
showResult(
'decryptResult',
`解密成功photoId: ${data.photoId}\n明文内容:\n${plaintext}`
);
} catch (e) {
showResult('decryptResult', `解密失败:${e.message}`, true);
}
});
// ---------- 工具 ----------
function decodeBase64Text(b64) {
try {
const bin = atob(b64);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
return new TextDecoder('utf-8').decode(bytes);
} catch {
return '<无法解码为文本,可能为二进制照片数据>';
}
}
// ---------- 初始化 ----------
refreshAuthUI();