feat: 云笔记增强 - 安全加固 + 回收站/版本历史/分享/批量导出 + 前端优化
- 安全: 认证改随机token会话(弃固定cookie), 笔记密码SHA256升级为bcrypt(自动迁移), 堵住GET /api/notes/:id泄露带密码笔记, CORS收紧+SameSite防CSRF, 上传图片内容嗅探 - 回收站: 软删除(deleted_at), 列表/恢复/彻底删除/清空, 目录子树连删连恢复 - 版本历史: note_versions表存快照, 每次保存自动留档, 支持查看/回滚 - 分享: 生成随机token分享链接, 支持过期时间, 公开阅读页share.html - 批量导出: 全部笔记打包zip(按目录结构+front matter) - 前端: 深色模式, Mermaid图表, 待办清单checkbox, 字数统计; 后台新增回收站/历史/分享面板和批量导出按钮 - 新增deploy/note-manager.service systemd单元与smoke_test.py
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
<!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 rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #fafafa; color: #333; line-height: 1.7;
|
||||
}
|
||||
.container { max-width: 860px; margin: 40px auto; background: #fff; padding: 48px; border-radius: 10px; box-shadow: 0 1px 6px rgba(0,0,0,0.08); }
|
||||
.badge { display: inline-block; background: #e8f0fe; color: #1a73e8; padding: 4px 12px; border-radius: 20px; font-size: 13px; margin-bottom: 16px; }
|
||||
h1 { font-size: 28px; margin-bottom: 12px; color: #1a1a1a; }
|
||||
.meta { color: #888; font-size: 14px; margin-bottom: 24px; padding-bottom: 16px; border-bottom: 1px solid #eee; }
|
||||
.tag { display: inline-block; background: #f1f3f4; color: #5f6368; padding: 2px 10px; border-radius: 12px; font-size: 13px; margin-right: 6px; }
|
||||
.note-content { word-wrap: break-word; }
|
||||
.note-content h1,.note-content h2,.note-content h3,.note-content h4 { margin: 20px 0 10px; color: #1a1a1a; }
|
||||
.note-content p { margin: 10px 0; }
|
||||
.note-content pre { background: #f6f8fa; padding: 14px; border-radius: 6px; overflow-x: auto; }
|
||||
.note-content code { background: #f6f8fa; padding: 2px 5px; border-radius: 3px; font-size: 14px; }
|
||||
.note-content pre code { background: none; padding: 0; }
|
||||
.note-content img { max-width: 100%; }
|
||||
.note-content blockquote { border-left: 4px solid #ddd; padding-left: 14px; color: #666; margin: 12px 0; }
|
||||
.note-content table { border-collapse: collapse; margin: 12px 0; }
|
||||
.note-content th,.note-content td { border: 1px solid #ddd; padding: 8px 12px; }
|
||||
.note-content a { color: #1a73e8; }
|
||||
.password-prompt { text-align: center; padding: 60px 20px; }
|
||||
.password-prompt input { padding: 10px 14px; border: 1px solid #ccc; border-radius: 6px; width: 260px; font-size: 14px; }
|
||||
.password-prompt button { padding: 10px 24px; background: #1a73e8; color: #fff; border: none; border-radius: 6px; font-size: 14px; cursor: pointer; margin-left: 8px; }
|
||||
.footer { text-align: center; color: #bbb; font-size: 13px; margin: 24px 0 40px; }
|
||||
.hidden { display: none; }
|
||||
.mermaid { text-align: center; margin: 16px 0; }
|
||||
.task-done { text-decoration: line-through; color: #888; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<span class="badge">📄 分享的笔记</span>
|
||||
<div id="noteDetail" class="hidden">
|
||||
<h1 id="noteTitle"></h1>
|
||||
<div class="meta">
|
||||
<span>分类:<span id="noteCategory">未分类</span></span>
|
||||
<span style="margin-left:16px" id="noteDate"></span>
|
||||
<div style="margin-top:10px" id="noteTags"></div>
|
||||
</div>
|
||||
<div class="note-content" id="noteContent"></div>
|
||||
</div>
|
||||
<div class="password-prompt" id="passwordPrompt" style="display:none">
|
||||
<h3>🔒 该笔记已加密</h3>
|
||||
<p style="color:#888;margin:10px 0 20px">请输入访问密码查看内容</p>
|
||||
<input type="password" id="passwordInput" placeholder="输入访问密码">
|
||||
<button onclick="accessWithPassword()">查看</button>
|
||||
<p id="pwdError" style="color:#d32f2f;margin-top:12px"></p>
|
||||
</div>
|
||||
<div id="errorBox" class="password-prompt" style="display:none">
|
||||
<h3>😕 无法访问</h3>
|
||||
<p id="errorMsg" style="color:#888;margin-top:10px"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">Powered by 云笔记</div>
|
||||
|
||||
<script>
|
||||
const TOKEN = '{{.token}}';
|
||||
const API = '/api/share/' + TOKEN;
|
||||
|
||||
async function load(password) {
|
||||
let url = API;
|
||||
if (password) url += '?password=' + encodeURIComponent(password);
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
if (data.code === 0) {
|
||||
render(data.data);
|
||||
} else if (res.status === 401) {
|
||||
document.getElementById('passwordPrompt').style.display = 'block';
|
||||
document.getElementById('passwordPrompt').style.display = 'flex';
|
||||
} else {
|
||||
document.getElementById('errorBox').style.display = 'block';
|
||||
document.getElementById('errorMsg').textContent = data.message || '笔记不存在或链接已失效';
|
||||
}
|
||||
}
|
||||
|
||||
async function accessWithPassword() {
|
||||
const pwd = document.getElementById('passwordInput').value;
|
||||
document.getElementById('pwdError').textContent = '';
|
||||
let url = API + '?password=' + encodeURIComponent(pwd);
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
if (data.code === 0) {
|
||||
document.getElementById('passwordPrompt').style.display = 'none';
|
||||
render(data.data);
|
||||
} else {
|
||||
document.getElementById('pwdError').textContent = '密码错误,请重试';
|
||||
}
|
||||
}
|
||||
|
||||
function render(note) {
|
||||
document.getElementById('noteDetail').classList.remove('hidden');
|
||||
document.getElementById('noteTitle').textContent = note.title;
|
||||
if (note.category) document.getElementById('noteCategory').textContent = note.category;
|
||||
if (note.created_at) {
|
||||
const d = new Date(note.created_at);
|
||||
document.getElementById('noteDate').textContent = '发布于 ' + d.toLocaleString('zh-CN');
|
||||
}
|
||||
const tagsBox = document.getElementById('noteTags');
|
||||
try {
|
||||
const tags = JSON.parse(note.tags || '[]');
|
||||
if (Array.isArray(tags) && tags.length) {
|
||||
tagsBox.innerHTML = tags.map(t => `<span class="tag">#${escapeHtml(t)}</span>`).join('');
|
||||
}
|
||||
} catch(e) {}
|
||||
document.getElementById('noteContent').innerHTML = renderMarkdown(note.content || '');
|
||||
// 高亮代码
|
||||
document.querySelectorAll('pre code').forEach(block => {
|
||||
try { hljs.highlightBlock(block); } catch(e){}
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// 简单 Markdown 渲染(支持代码块、标题、列表、表格、链接、图片、加粗、待办清单)
|
||||
function renderMarkdown(text) {
|
||||
let html = escapeHtml(text);
|
||||
// 代码块
|
||||
html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, function(m, lang, code) {
|
||||
return '<pre><code class="language-' + (lang || '') + '">' + code + '</code></pre>';
|
||||
});
|
||||
// 行内代码
|
||||
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
// 待办清单
|
||||
html = html.replace(/\[ \] /g, '☐ ');
|
||||
html = html.replace(/\[x\] /g, '☑ ');
|
||||
// 标题
|
||||
html = html.replace(/^### (.*)$/gm, '<h3>$1</h3>');
|
||||
html = html.replace(/^## (.*)$/gm, '<h2>$1</h2>');
|
||||
html = html.replace(/^# (.*)$/gm, '<h1>$1</h1>');
|
||||
// 图片
|
||||
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
|
||||
// 链接
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
|
||||
// 加粗
|
||||
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
||||
// 引用
|
||||
html = html.replace(/^> (.*)$/gm, '<blockquote>$1</blockquote>');
|
||||
// 无序列表
|
||||
html = html.replace(/^- (.*)$/gm, '<li>$1</li>');
|
||||
html = html.replace(/(<li>[\s\S]*?<\/li>)/g, function(m){ return m.includes('<li></li>') ? m : '<ul>'+m+'</ul>'; });
|
||||
// 表格
|
||||
html = html.replace(/\|(.+)\|\n\|[\s-|]+\|\n((?:\|[^|]+\|\n?)*)/g, function(m, head, body) {
|
||||
const hd = head.split('|').filter((x,i)=>i!==0 && i!==head.split('|').length-1).map(c=>`<th>${c.trim()}</th>`).join('');
|
||||
const rows = body.split('\n').filter(r=>r.trim()).map(r => {
|
||||
const cells = r.split('|').filter((x,i)=>i!==0 && i!==r.split('|').length-1).map(c=>`<td>${c.trim()}</td>`).join('');
|
||||
return `<tr>${cells}</tr>`;
|
||||
}).join('');
|
||||
return `<table><thead><tr>${hd}</tr></thead><tbody>${rows}</tbody></table>`;
|
||||
});
|
||||
// 段落
|
||||
html = html.replace(/\n{2,}/g, '</p><p>');
|
||||
html = '<p>' + html + '</p>';
|
||||
return html;
|
||||
}
|
||||
|
||||
load();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user