Spaces:
Paused
Paused
Download server.js from willy880305/notepad: direct link, hf CLI and curl.
- Browser
- Download file 24 kB
-
https://huggingface.co/spaces/willy880305/notepad/resolve/main/server.js
- Command line
-
hf download hf://spaces/willy880305/notepad/server.js
-
curl -L -o server.js https://huggingface.co/spaces/willy880305/notepad/resolve/main/server.js
24 kB
| const express = require('express'); | |
| const mongoose = require('mongoose'); | |
| const bodyParser = require('body-parser'); | |
| const cookieSession = require('cookie-session'); | |
| const { marked } = require('marked'); | |
| const sanitizeHtml = require('sanitize-html'); | |
| const crypto = require('crypto'); | |
| const app = express(); | |
| const port = 7860; | |
| // --- 1. 資料庫連線與 Schema --- | |
| mongoose.connect(process.env.MONGODB_URI) | |
| .then(() => console.log('MongoDB 連線成功')) | |
| .catch(err => console.error(err)); | |
| const NoteSchema = new mongoose.Schema({ | |
| path: { type: String, required: true, unique: true }, // 網址路徑 | |
| title: { type: String, default: "" }, // 自定義標題 | |
| content: { type: String, default: "" }, | |
| ownerToken: { type: String, default: null }, // 認主金鑰 (隱藏) | |
| accessPassword: { type: String, default: null }, // 訪問(閱讀)密碼 | |
| editPassword: { type: String, default: null }, // 編輯密碼 | |
| expireAt: { type: Date, default: null }, // 自動銷毀 | |
| views: { type: Number, default: 0 }, | |
| lastUpdated: { type: Date, default: Date.now } | |
| }); | |
| // 設定 TTL 自動刪除 | |
| NoteSchema.index({ expireAt: 1 }, { expireAfterSeconds: 0 }); | |
| const Note = mongoose.model('Note', NoteSchema); | |
| // --- 2. Middleware 設定 --- | |
| app.use(bodyParser.urlencoded({ extended: true })); | |
| app.use(bodyParser.json()); | |
| app.use(cookieSession({ | |
| name: 'session', | |
| keys: ['secret-key-A', 'secret-key-B'], | |
| maxAge: 365 * 24 * 60 * 60 * 1000 // Session 1年有效 | |
| })); | |
| // --- 3. 頁面渲染函式 (前端 UI) --- | |
| const renderPage = (note, viewMode = 'editor', isOwner = false, canEdit = false, isAdmin = false) => { | |
| const content = (note && note.content) ? note.content : ''; | |
| const title = (note && note.title) ? note.title : '未命名筆記'; | |
| const path = note ? note.path : 'new'; | |
| // 計算剩餘時間 | |
| let timeRemaining = ''; | |
| if (note && note.expireAt) { | |
| const diff = new Date(note.expireAt) - new Date(); | |
| const mins = Math.floor(diff / 60000); | |
| timeRemaining = `(⏳ ${mins} 分鐘後銷毀)`; | |
| } | |
| const htmlContent = viewMode === 'markdown' | |
| ? sanitizeHtml(marked.parse(content)) | |
| : content; | |
| // 狀態判斷 | |
| const readOnlyAttr = canEdit ? '' : 'readonly'; | |
| const readOnlyClass = canEdit ? '' : 'readonly-mode'; | |
| const permissionLabel = isOwner ? '👑 擁有者' : (canEdit ? '✏️ 可編輯' : '👀 純瀏覽'); | |
| return ` | |
| <!DOCTYPE html> | |
| <html lang="zh-TW"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>${title} | 雲端筆記</title> | |
| <style> | |
| :root { --bg: #ffffff; --text: #333; --area-bg: #f9f9f9; --bar-bg: #e0e0e0; --border: #ccc; } | |
| [data-theme="dark"] { --bg: #1e1e1e; --text: #e0e0e0; --area-bg: #2d2d2d; --bar-bg: #333; --border: #555; } | |
| body { margin: 0; display: flex; flex-direction: column; height: 100vh; font-family: "Microsoft JhengHei", monospace; background: var(--bg); color: var(--text); transition: 0.3s; } | |
| /* 頂部導航列 */ | |
| header { padding: 10px 20px; background: var(--bar-bg); display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); } | |
| .meta-info { display: flex; align-items: center; gap: 10px; flex: 1; } | |
| .path-badge { background: #555; color: #fff; padding: 2px 6px; border-radius: 4px; font-size: 0.8em; } | |
| .title-input { background: transparent; border: none; border-bottom: 1px dashed #999; font-size: 1.1em; color: var(--text); font-weight: bold; width: 200px; } | |
| .title-input:focus { outline: none; border-bottom: 1px solid #007bff; } | |
| .controls { display: flex; gap: 8px; } | |
| button { padding: 6px 12px; cursor: pointer; border: 1px solid var(--border); border-radius: 4px; background: var(--bg); color: var(--text); font-size: 14px; } | |
| button:hover { background: var(--bar-bg); } | |
| button.primary { background: #007bff; color: white; border: none; } | |
| button.danger { background: #dc3545; color: white; border: none; } | |
| /* 編輯區域 */ | |
| .editor-container { flex: 1; position: relative; display: flex; } | |
| textarea { flex: 1; width: 100%; border: none; padding: 20px; font-size: 16px; line-height: 1.6; background: var(--area-bg); color: var(--text); resize: none; outline: none; } | |
| textarea.readonly-mode { color: #666; cursor: default; } | |
| .markdown-view { flex: 1; padding: 20px; overflow-y: auto; background: var(--area-bg); line-height: 1.6; } | |
| /* 彈窗與提示 */ | |
| .modal { display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: var(--bg); padding: 25px; border: 1px solid var(--border); border-radius: 8px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); z-index: 100; min-width: 300px; } | |
| .overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 90; } | |
| .status-bar { font-size: 12px; opacity: 0.7; margin-left: 10px; } | |
| #toast { position: fixed; bottom: 20px; right: 20px; background: #333; color: #fff; padding: 10px 20px; border-radius: 5px; opacity: 0; transition: opacity 0.5s; pointer-events: none; z-index: 200; } | |
| label { display: block; margin-top: 10px; margin-bottom: 5px; font-weight: bold; } | |
| input[type="password"], select { width: 100%; padding: 8px; margin-bottom: 10px; background: var(--area-bg); border: 1px solid var(--border); color: var(--text); box-sizing: border-box; } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="meta-info"> | |
| <span class="path-badge">/${path}</span> | |
| <input type="text" id="noteTitle" class="title-input" value="${title}" placeholder="在此輸入標題" ${canEdit ? '' : 'disabled'}> | |
| <span class="status-bar">${permissionLabel} ${timeRemaining}</span> | |
| <span id="saveStatus" class="status-bar"></span> | |
| </div> | |
| <div class="controls"> | |
| <button onclick="toggleTheme()">🌗</button> | |
| <button onclick="toggleMarkdown()">${viewMode === 'editor' ? '👁️ 預覽' : '✏️ 編輯'}</button> | |
| ${canEdit ? `<button onclick="openSettings()">⚙️ 設定</button>` : ''} | |
| ${!canEdit && !isOwner ? `<button onclick="unlockEdit()" class="primary">🔓 解鎖編輯</button>` : ''} | |
| ${isAdmin ? '<a href="/admin"><button class="danger">管理後台</button></a>' : ''} | |
| </div> | |
| </header> | |
| <div class="editor-container"> | |
| ${viewMode === 'markdown' | |
| ? `<div class="markdown-view">${htmlContent}</div>` | |
| : `<textarea id="content" placeholder="${canEdit ? '開始輸入內容...' : '唯讀模式:您沒有編輯權限。'}" spellcheck="false" ${readOnlyAttr}>${content}</textarea>` | |
| } | |
| </div> | |
| <div class="overlay" id="overlay" onclick="closeSettings()"></div> | |
| <div class="modal" id="settingsModal"> | |
| <h3>⚙️ 筆記設定</h3> | |
| <p style="font-size:0.9em; color: #888;">只有擁有者可以更改這些設定。</p> | |
| <label>🔒 訪問密碼 (留空則公開)</label> | |
| <input type="password" id="setAccessPass" placeholder="設定後,看筆記需要密碼"> | |
| <label>✏️ 編輯密碼 (留空則僅擁有者可編輯)</label> | |
| <input type="password" id="setEditPass" placeholder="分享此密碼給他人協作"> | |
| <label>⏱️ 自動銷毀 (閱後即焚)</label> | |
| <select id="ttlSelect"> | |
| <option value="">永不銷毀</option> | |
| <option value="10">10 分鐘後</option> | |
| <option value="60">1 小時後</option> | |
| <option value="1440">1 天後</option> | |
| <option value="10080">7 天後</option> | |
| </select> | |
| <div style="margin-top: 20px; text-align: right;"> | |
| <button onclick="closeSettings()">取消</button> | |
| <button onclick="saveSettings()" class="primary">儲存設定</button> | |
| </div> | |
| </div> | |
| <div class="modal" id="unlockModal"> | |
| <h3>🔓 解鎖編輯權限</h3> | |
| <p>請輸入此筆記的編輯密碼:</p> | |
| <input type="password" id="inputEditPass" autofocus> | |
| <div style="text-align: right;"> | |
| <button onclick="document.getElementById('unlockModal').style.display='none';document.getElementById('overlay').style.display='none';">取消</button> | |
| <button onclick="submitUnlock()" class="primary">解鎖</button> | |
| </div> | |
| </div> | |
| <div id="toast">已儲存</div> | |
| <script> | |
| const notePath = "${path}"; | |
| // 簡單的防抖動 | |
| let timeoutId; | |
| // 初始化主題 | |
| if(localStorage.getItem('theme') === 'dark') document.body.setAttribute('data-theme', 'dark'); | |
| function toggleTheme() { | |
| const isDark = document.body.getAttribute('data-theme') === 'dark'; | |
| document.body.setAttribute('data-theme', isDark ? 'light' : 'dark'); | |
| localStorage.setItem('theme', isDark ? 'light' : 'dark'); | |
| } | |
| // 自動儲存邏輯 (標題或內容變動都存) | |
| const textarea = document.getElementById('content'); | |
| const titleInput = document.getElementById('noteTitle'); | |
| function triggerSave() { | |
| document.getElementById('saveStatus').innerText = '儲存中...'; | |
| clearTimeout(timeoutId); | |
| timeoutId = setTimeout(saveContent, 800); | |
| } | |
| if(textarea && !textarea.readOnly) textarea.addEventListener('input', triggerSave); | |
| if(titleInput && !titleInput.disabled) titleInput.addEventListener('input', triggerSave); | |
| async function saveContent() { | |
| const content = textarea ? textarea.value : ''; | |
| const title = titleInput ? titleInput.value : ''; | |
| try { | |
| const res = await fetch('/' + notePath, { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ content, title }) | |
| }); | |
| const data = await res.json(); | |
| if(data.success) { | |
| document.getElementById('saveStatus').innerText = '已同步'; | |
| showToast('已儲存'); | |
| } else { | |
| document.getElementById('saveStatus').innerText = '儲存失敗'; | |
| alert('儲存失敗:' + (data.error || '權限不足')); | |
| location.reload(); | |
| } | |
| } catch(e) { console.error(e); } | |
| } | |
| // 切換預覽 | |
| function toggleMarkdown() { | |
| const url = new URL(window.location); | |
| if("${viewMode}" === 'editor') url.searchParams.set('mode', 'markdown'); | |
| else url.searchParams.delete('mode'); | |
| window.location = url; | |
| } | |
| // 設定相關 UI | |
| function openSettings() { | |
| document.getElementById('settingsModal').style.display = 'block'; | |
| document.getElementById('overlay').style.display = 'block'; | |
| } | |
| function closeSettings() { | |
| document.querySelectorAll('.modal').forEach(m => m.style.display = 'none'); | |
| document.getElementById('overlay').style.display = 'none'; | |
| } | |
| function unlockEdit() { | |
| document.getElementById('unlockModal').style.display = 'block'; | |
| document.getElementById('overlay').style.display = 'block'; | |
| } | |
| async function saveSettings() { | |
| const accessPass = document.getElementById('setAccessPass').value; | |
| const editPass = document.getElementById('setEditPass').value; | |
| const ttl = document.getElementById('ttlSelect').value; | |
| const payload = { settings: true }; | |
| if(accessPass !== "") payload.accessPassword = accessPass; // 空字串不傳送,避免意外清空,除非後端特別處理 | |
| if(accessPass === "") payload.clearAccessPass = true; // 特殊標記來清空 | |
| if(editPass !== "") payload.editPassword = editPass; | |
| if(editPass === "") payload.clearEditPass = true; | |
| if(ttl) payload.ttlMinutes = parseInt(ttl); | |
| else payload.ttlMinutes = 0; // 0 代表取消 | |
| const res = await fetch('/' + notePath + '/settings', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify(payload) | |
| }); | |
| const data = await res.json(); | |
| if(data.success) { | |
| alert('設定已更新'); | |
| location.reload(); | |
| } else { | |
| alert('錯誤:' + data.error); | |
| } | |
| } | |
| async function submitUnlock() { | |
| const password = document.getElementById('inputEditPass').value; | |
| const res = await fetch('/' + notePath + '/auth-edit', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ password }) | |
| }); | |
| const data = await res.json(); | |
| if(data.success) { | |
| location.reload(); | |
| } else { | |
| alert('密碼錯誤'); | |
| } | |
| } | |
| function showToast(msg) { | |
| const t = document.getElementById('toast'); | |
| t.innerText = msg; | |
| t.style.opacity = 1; | |
| setTimeout(() => t.style.opacity = 0, 2000); | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| `; | |
| }; | |
| // 密碼輸入頁面 | |
| const renderPasswordPage = (path, error = "") => { | |
| return ` | |
| <html> | |
| <head><title>受保護的筆記</title></head> | |
| <body style="display:flex;justify-content:center;align-items:center;height:100vh;font-family:'Microsoft JhengHei', sans-serif;background:#f4f4f4;"> | |
| <form method="POST" action="/${path}/auth-access" style="text-align:center;background:white;padding:40px;border-radius:8px;box-shadow:0 4px 10px rgba(0,0,0,0.1)"> | |
| <h2>🔒 此筆記受密碼保護</h2> | |
| <p>請輸入密碼以繼續訪問。</p> | |
| <input type="password" name="password" placeholder="輸入訪問密碼" autofocus style="padding:10px;width:200px;margin-bottom:10px;border:1px solid #ccc;border-radius:4px;"><br> | |
| <button type="submit" style="padding:10px 20px;cursor:pointer;background:#007bff;color:white;border:none;border-radius:4px;">解鎖</button> | |
| ${error ? `<p style="color:red;margin-top:10px;">${error}</p>` : ''} | |
| </form> | |
| </body> | |
| </html>`; | |
| }; | |
| // 管理員後台頁面 | |
| const renderAdminPage = (notes, error = "") => { | |
| return ` | |
| <html> | |
| <head><title>管理員後台</title> | |
| <style> | |
| body{font-family:'Microsoft JhengHei',monospace;padding:20px;max-width:1200px;margin:0 auto;} | |
| table{width:100%;border-collapse:collapse;margin-top:20px;} | |
| th,td{border:1px solid #ddd;padding:10px;text-align:left;} | |
| th{background-color:#f8f9fa;} | |
| tr:nth-child(even){background-color:#f9f9f9;} | |
| .tag{font-size:12px;padding:2px 6px;border-radius:4px;color:white;} | |
| .tag-lock{background:#d63384;} .tag-edit{background:#fd7e14;} | |
| .btn{padding:5px 10px;cursor:pointer;border-radius:4px;border:none;color:white;} | |
| .btn-del{background:#dc3545;} .btn-view{background:#007bff;text-decoration:none;padding:5px 10px;border-radius:4px;color:white;} | |
| </style> | |
| </head> | |
| <body> | |
| <div style="display:flex;justify-content:space-between;align-items:center;"> | |
| <h1>🛠️ 系統管理後台</h1> | |
| <a href="/" style="text-decoration:none;">回首頁</a> | |
| </div> | |
| ${error ? `<p style="color:red">${error}</p>` : ''} | |
| <table> | |
| <tr><th>標題 (路徑)</th><th>字數</th><th>瀏覽數</th><th>最後更新</th><th>到期時間</th><th>安全狀態</th><th>操作</th></tr> | |
| ${notes.map(n => ` | |
| <tr> | |
| <td><b>${n.title || '(無標題)'}</b><br><span style="color:#666;font-size:0.9em;">/${n.path}</span></td> | |
| <td>${n.content.length}</td> | |
| <td>${n.views}</td> | |
| <td>${new Date(n.lastUpdated).toLocaleString()}</td> | |
| <td>${n.expireAt ? new Date(n.expireAt).toLocaleString() : '永久'}</td> | |
| <td> | |
| ${n.accessPassword ? '<span class="tag tag-lock">鎖</span>' : ''} | |
| ${n.editPassword ? '<span class="tag tag-edit">編</span>' : ''} | |
| </td> | |
| <td> | |
| <a href="/${n.path}" target="_blank" class="btn-view">查看</a> | |
| <form action="/admin/delete" method="POST" style="display:inline" onsubmit="return confirm('確定要刪除 /${n.path} 嗎?');"> | |
| <input type="hidden" name="path" value="${n.path}"> | |
| <button type="submit" class="btn btn-del">刪除</button> | |
| </form> | |
| </td> | |
| </tr>`).join('')} | |
| </table> | |
| </body> | |
| </html>`; | |
| }; | |
| // 管理員登入頁 | |
| const renderAdminLogin = () => ` | |
| <html><body style="display:flex;justify-content:center;align-items:center;height:100vh;font-family:'Microsoft JhengHei';background:#f4f4f4;"> | |
| <form method="POST" action="/admin/login" style="text-align:center;padding:30px;background:white;border-radius:8px;"> | |
| <h2>管理員登入</h2> | |
| <input type="password" name="password" placeholder="請輸入 Admin 密碼" style="padding:10px;"><br><br> | |
| <button type="submit" style="padding:8px 20px;cursor:pointer;">登入</button> | |
| </form> | |
| </body></html>`; | |
| // --- 4. 路由邏輯 --- | |
| // Helper: 檢查是否為擁有者或有編輯權限 | |
| const checkPermission = (note, session) => { | |
| if (!note) return { isOwner: true, canEdit: true }; // 新筆記預設全是 | |
| if (note.ownerToken && session.ownerTokens && session.ownerTokens.includes(note.ownerToken)) { | |
| return { isOwner: true, canEdit: true }; | |
| } | |
| if (session.unlockedEdits && session.unlockedEdits.includes(note.path)) { | |
| return { isOwner: false, canEdit: true }; | |
| } | |
| // 如果沒有設定 Owner (舊資料) 且沒有編輯密碼,視為公開編輯 (Backward Compatibility) | |
| // 但根據新邏輯,我們希望儘量鎖定。這裡設為:若無 ownerToken 且無 editPassword,暫時開放編輯,一旦編輯就認主。 | |
| if (!note.ownerToken && !note.editPassword) { | |
| return { isOwner: true, canEdit: true }; // 暫時給權限,下次存檔會補上 ownerToken | |
| } | |
| return { isOwner: false, canEdit: false }; | |
| }; | |
| // Admin 路由 | |
| app.get('/admin', async (req, res) => { | |
| if (req.session.isAdmin) { | |
| const notes = await Note.find().sort({ lastUpdated: -1 }); | |
| res.send(renderAdminPage(notes)); | |
| } else { | |
| res.send(renderAdminLogin()); | |
| } | |
| }); | |
| app.post('/admin/login', (req, res) => { | |
| if (req.body.password === process.env.ADMIN_PASSWORD) { | |
| req.session.isAdmin = true; | |
| res.redirect('/admin'); | |
| } else { | |
| res.send('密碼錯誤'); | |
| } | |
| }); | |
| app.post('/admin/delete', async (req, res) => { | |
| if (!req.session.isAdmin) return res.status(403).send('Forbidden'); | |
| await Note.deleteOne({ path: req.body.path }); | |
| res.redirect('/admin'); | |
| }); | |
| // 筆記路由 | |
| app.get('/:path', async (req, res) => { | |
| const path = req.params.path; | |
| if(path === 'favicon.ico') return res.status(404).end(); | |
| let note = await Note.findOne({ path }); | |
| // 1. 訪問權限檢查 (Access Password) | |
| if (note && note.accessPassword) { | |
| if (!req.session.unlockedAccess || !req.session.unlockedAccess.includes(path)) { | |
| // 如果是擁有者,直接跳過訪問密碼檢查 | |
| const perms = checkPermission(note, req.session); | |
| if (!perms.isOwner) { | |
| return res.send(renderPasswordPage(path)); | |
| } | |
| } | |
| } | |
| // 2. 編輯/擁有權限檢查 | |
| const { isOwner, canEdit } = checkPermission(note, req.session); | |
| if(note) { | |
| note.views += 1; | |
| await note.save(); | |
| } | |
| const viewMode = req.query.mode === 'markdown' ? 'markdown' : 'editor'; | |
| res.send(renderPage(note ? note : { path }, viewMode, isOwner, canEdit, req.session.isAdmin)); | |
| }); | |
| // 解鎖訪問 (閱讀) | |
| app.post('/:path/auth-access', async (req, res) => { | |
| const path = req.params.path; | |
| const { password } = req.body; | |
| const note = await Note.findOne({ path }); | |
| if (note && note.accessPassword === password) { | |
| req.session.unlockedAccess = req.session.unlockedAccess || []; | |
| req.session.unlockedAccess.push(path); | |
| res.redirect('/' + path); | |
| } else { | |
| res.send(renderPasswordPage(path, "密碼錯誤")); | |
| } | |
| }); | |
| // 解鎖編輯 (協作者) | |
| app.post('/:path/auth-edit', async (req, res) => { | |
| const path = req.params.path; | |
| const { password } = req.body; | |
| const note = await Note.findOne({ path }); | |
| if (note && note.editPassword === password) { | |
| req.session.unlockedEdits = req.session.unlockedEdits || []; | |
| req.session.unlockedEdits.push(path); | |
| res.json({ success: true }); | |
| } else { | |
| res.json({ success: false }); | |
| } | |
| }); | |
| // 儲存內容 (POST /:path) | |
| app.post('/:path', async (req, res) => { | |
| const path = req.params.path; | |
| const { content, title } = req.body; | |
| let note = await Note.findOne({ path }); | |
| // 檢查權限 | |
| const { isOwner, canEdit } = checkPermission(note, req.session); | |
| if (note && !canEdit) { | |
| return res.status(403).json({ success: false, error: "您只有閱讀權限" }); | |
| } | |
| if (!note) { | |
| // 建立新筆記 | |
| note = new Note({ path }); | |
| // 生成並分配 Owner Token | |
| const newToken = crypto.randomUUID(); | |
| note.ownerToken = newToken; | |
| req.session.ownerTokens = req.session.ownerTokens || []; | |
| req.session.ownerTokens.push(newToken); | |
| } | |
| else if (!note.ownerToken) { | |
| // 如果是舊筆記沒有主人,第一個編輯的人變成主人 | |
| const newToken = crypto.randomUUID(); | |
| note.ownerToken = newToken; | |
| req.session.ownerTokens = req.session.ownerTokens || []; | |
| req.session.ownerTokens.push(newToken); | |
| } | |
| note.content = content; | |
| if(title !== undefined) note.title = title; | |
| note.lastUpdated = Date.now(); | |
| await note.save(); | |
| res.json({ success: true }); | |
| }); | |
| // 儲存設定 | |
| app.post('/:path/settings', async (req, res) => { | |
| const path = req.params.path; | |
| const { accessPassword, clearAccessPass, editPassword, clearEditPass, ttlMinutes } = req.body; | |
| let note = await Note.findOne({ path }); | |
| // 只有擁有者 (Owner) 才能改設定! | |
| const { isOwner } = checkPermission(note, req.session); | |
| if (!isOwner) { | |
| return res.status(403).json({ success: false, error: "只有筆記擁有者可以更改設定" }); | |
| } | |
| if (!note) return res.status(404).json({error: "Note not found"}); | |
| if (clearAccessPass) note.accessPassword = null; | |
| else if (accessPassword) note.accessPassword = accessPassword; | |
| if (clearEditPass) note.editPassword = null; | |
| else if (editPassword) note.editPassword = editPassword; | |
| if (ttlMinutes > 0) { | |
| note.expireAt = new Date(Date.now() + ttlMinutes * 60000); | |
| } else if (ttlMinutes === 0) { | |
| note.expireAt = null; | |
| } | |
| await note.save(); | |
| res.json({ success: true }); | |
| }); | |
| app.get('/', (req, res) => { | |
| const randomStr = Math.random().toString(36).substring(7); | |
| res.redirect(`/${randomStr}`); | |
| }); | |
| app.listen(port, () => console.log(`Server running on port ${port}`)); |