// ============================================================ // Secure Device 用户端(Web)—— 前后端分离演示 // // 后端地址:默认 localhost:8080(CORS 已放开)。 // 认证:登录/注册后获得 Bearer Token,存 localStorage, // 后续所有用户端接口自动携带 Authorization: Bearer 。 // ============================================================ 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();