473 lines
24 KiB
JavaScript
473 lines
24 KiB
JavaScript
const Database = require('better-sqlite3');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const bcrypt = require('bcryptjs');
|
||
|
||
const DB_PATH = path.join(__dirname, 'data', 'rainweb.db');
|
||
const DATA_DIR = path.dirname(DB_PATH);
|
||
let db = null;
|
||
|
||
function getDb() {
|
||
if (db) return db;
|
||
// Ensure data directory exists
|
||
if (!fs.existsSync(DATA_DIR)) {
|
||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||
}
|
||
db = new Database(DB_PATH);
|
||
initTables();
|
||
migrateSchema();
|
||
seedAdmin();
|
||
seedDefaults();
|
||
seedSampleData();
|
||
return db;
|
||
}
|
||
|
||
function initTables() {
|
||
db.exec(`CREATE TABLE IF NOT EXISTS users (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL,
|
||
password TEXT NOT NULL, email TEXT DEFAULT '', email_verified INTEGER DEFAULT 0,
|
||
role TEXT DEFAULT 'user', avatar TEXT DEFAULT '',
|
||
nickname TEXT DEFAULT '', title TEXT DEFAULT '', title_color TEXT DEFAULT '', website TEXT DEFAULT '',
|
||
bio TEXT DEFAULT '', last_active_at TEXT DEFAULT '',
|
||
created_at DATETIME DEFAULT (datetime('now')))`);
|
||
|
||
db.exec(`CREATE TABLE IF NOT EXISTS pending_users (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL,
|
||
password TEXT NOT NULL, email TEXT NOT NULL, token TEXT UNIQUE NOT NULL,
|
||
created_at DATETIME DEFAULT (datetime('now')))`);
|
||
// M2:启动时清理超 15 分钟的 pending 注册记录(验证码过期,防表无限增长 / 验证码长期间可试)
|
||
db.exec("DELETE FROM pending_users WHERE created_at < datetime('now','-15 minutes')");
|
||
|
||
db.exec(`CREATE TABLE IF NOT EXISTS admin_links (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL,
|
||
url TEXT NOT NULL, embed_url TEXT DEFAULT '', description TEXT DEFAULT '',
|
||
icon TEXT DEFAULT '', category TEXT DEFAULT '默认', sort_order INTEGER DEFAULT 0,
|
||
use_proxy INTEGER DEFAULT 0, version TEXT DEFAULT '',
|
||
proxy_headers TEXT DEFAULT '', proxy_skip_tls_verify INTEGER DEFAULT 0,
|
||
permissions TEXT DEFAULT '', scale REAL DEFAULT 1.0, trusted INTEGER DEFAULT 1,
|
||
slug TEXT DEFAULT '',
|
||
created_at DATETIME DEFAULT (datetime('now')))`);
|
||
// Migration: add columns if missing
|
||
try { db.exec('ALTER TABLE admin_links ADD COLUMN use_proxy INTEGER DEFAULT 0'); } catch {}
|
||
try { db.exec('ALTER TABLE admin_links ADD COLUMN version TEXT DEFAULT ""'); } catch {}
|
||
|
||
db.exec(`CREATE TABLE IF NOT EXISTS announcements (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT DEFAULT '',
|
||
content TEXT NOT NULL, active INTEGER DEFAULT 1,
|
||
created_at DATETIME DEFAULT (datetime('now')),
|
||
updated_at DATETIME DEFAULT (datetime('now')))`);
|
||
|
||
db.exec(`CREATE TABLE IF NOT EXISTS forum_categories (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL,
|
||
description TEXT DEFAULT '', sort_order INTEGER DEFAULT 0,
|
||
announcement TEXT DEFAULT '', sub_categories TEXT DEFAULT '')`);
|
||
try { db.exec('ALTER TABLE forum_categories ADD COLUMN announcement TEXT DEFAULT ""'); } catch {}
|
||
try { db.exec('ALTER TABLE forum_categories ADD COLUMN sub_categories TEXT DEFAULT ""'); } catch {}
|
||
|
||
db.exec(`CREATE TABLE IF NOT EXISTS forum_posts (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT, category_id INTEGER NOT NULL,
|
||
title TEXT NOT NULL, content TEXT NOT NULL, author_id INTEGER NOT NULL,
|
||
use_markdown INTEGER DEFAULT 1, sub_category TEXT DEFAULT '',
|
||
created_at DATETIME DEFAULT (datetime('now')),
|
||
updated_at DATETIME DEFAULT (datetime('now')),
|
||
FOREIGN KEY (category_id) REFERENCES forum_categories(id),
|
||
FOREIGN KEY (author_id) REFERENCES users(id))`);
|
||
try { db.exec('ALTER TABLE forum_posts ADD COLUMN use_markdown INTEGER DEFAULT 1'); } catch {}
|
||
try { db.exec('ALTER TABLE forum_posts ADD COLUMN sub_category TEXT DEFAULT ""'); } catch {}
|
||
try { db.exec('ALTER TABLE forum_posts ADD COLUMN tags TEXT DEFAULT ""'); } catch {}
|
||
|
||
db.exec(`CREATE TABLE IF NOT EXISTS blog_comments (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT, post_id INTEGER NOT NULL,
|
||
content TEXT NOT NULL, author_id INTEGER,
|
||
author_name TEXT DEFAULT '', parent_id INTEGER DEFAULT 0,
|
||
status TEXT DEFAULT 'approved',
|
||
created_at DATETIME DEFAULT (datetime('now')),
|
||
FOREIGN KEY (post_id) REFERENCES blog_posts(id))`);
|
||
|
||
db.exec(`CREATE TABLE IF NOT EXISTS forum_replies (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT, post_id INTEGER NOT NULL,
|
||
content TEXT NOT NULL, author_id INTEGER NOT NULL,
|
||
created_at DATETIME DEFAULT (datetime('now')),
|
||
FOREIGN KEY (post_id) REFERENCES forum_posts(id),
|
||
FOREIGN KEY (author_id) REFERENCES users(id))`);
|
||
|
||
db.exec(`CREATE TABLE IF NOT EXISTS blog_posts (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL,
|
||
content TEXT NOT NULL, excerpt TEXT DEFAULT '', author_id INTEGER NOT NULL,
|
||
published INTEGER DEFAULT 1, use_markdown INTEGER DEFAULT 1,
|
||
tags TEXT DEFAULT '', views INTEGER DEFAULT 0,
|
||
created_at DATETIME DEFAULT (datetime('now')),
|
||
updated_at DATETIME DEFAULT (datetime('now')),
|
||
FOREIGN KEY (author_id) REFERENCES users(id))`);
|
||
|
||
db.exec(`CREATE TABLE IF NOT EXISTS post_likes (
|
||
post_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
|
||
created_at DATETIME DEFAULT (datetime('now')),
|
||
PRIMARY KEY (post_id, user_id))`);
|
||
|
||
db.exec(`CREATE TABLE IF NOT EXISTS user_settings (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER UNIQUE NOT NULL,
|
||
pin_hash TEXT DEFAULT '', kdf_salt TEXT DEFAULT '',
|
||
FOREIGN KEY (user_id) REFERENCES users(id))`);
|
||
|
||
db.exec(`CREATE TABLE IF NOT EXISTS password_entries (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL,
|
||
title TEXT NOT NULL, username TEXT DEFAULT '',
|
||
encrypted_password TEXT NOT NULL, url TEXT DEFAULT '', notes TEXT DEFAULT '',
|
||
created_at DATETIME DEFAULT (datetime('now')),
|
||
updated_at DATETIME DEFAULT (datetime('now')),
|
||
FOREIGN KEY (user_id) REFERENCES users(id))`);
|
||
|
||
db.exec(`CREATE TABLE IF NOT EXISTS attachments (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT, filename TEXT NOT NULL,
|
||
original_name TEXT NOT NULL, size INTEGER NOT NULL,
|
||
mime_type TEXT DEFAULT '', user_id INTEGER NOT NULL,
|
||
ref_type TEXT DEFAULT '', ref_id INTEGER DEFAULT 0,
|
||
created_at DATETIME DEFAULT (datetime('now')),
|
||
FOREIGN KEY (user_id) REFERENCES users(id))`);
|
||
|
||
db.exec(`CREATE TABLE IF NOT EXISTS site_settings (
|
||
key TEXT PRIMARY KEY, value TEXT DEFAULT '')`);
|
||
|
||
// 审计日志表(终端会话/密码失败等安全事件落库)
|
||
db.exec(`CREATE TABLE IF NOT EXISTS audit_logs (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
action TEXT NOT NULL,
|
||
user_id INTEGER DEFAULT 0,
|
||
username TEXT DEFAULT '',
|
||
detail TEXT DEFAULT '',
|
||
ip TEXT DEFAULT '',
|
||
created_at DATETIME DEFAULT (datetime('now')))`);
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_audit_logs_created ON audit_logs(created_at)');
|
||
// 审计保留策略:启动时惰性清理 90 天前的记录,防止无限增长
|
||
//(写入路径上 terminal.js 每 50 条也会触发一次同规则清理)
|
||
db.exec("DELETE FROM audit_logs WHERE created_at < datetime('now','-90 day')");
|
||
|
||
// Indexes for performance
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_blog_published ON blog_posts(published)');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_blog_comments_post ON blog_comments(post_id)');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_forum_posts_category ON forum_posts(category_id)');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_forum_replies_post ON forum_replies(post_id)');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_password_user ON password_entries(user_id)');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_attachments_ref ON attachments(ref_type, ref_id)');
|
||
|
||
// Legacy migrations for old database compatibility
|
||
try { db.exec("ALTER TABLE users ADD COLUMN email TEXT DEFAULT ''"); } catch {}
|
||
try { db.exec('ALTER TABLE users ADD COLUMN email_verified INTEGER DEFAULT 0'); } catch {}
|
||
try { db.exec("ALTER TABLE users ADD COLUMN avatar TEXT DEFAULT ''"); } catch {}
|
||
try { db.exec('ALTER TABLE blog_posts ADD COLUMN use_markdown INTEGER DEFAULT 1'); } catch {}
|
||
}
|
||
|
||
// 版本化迁移框架:PRAGMA user_version 记录当前 schema 版本,
|
||
// migrations 数组按 version 升序执行,仅当 current < m.version 时运行并推进版本号。
|
||
// 现有 try/catch ALTER 保留作为"确保列存在"的幂等层,未来新列迁移写入 migrations。
|
||
function migrateSchema() {
|
||
const current = db.pragma('user_version', { simple: true });
|
||
const migrations = [
|
||
// v1: 密码管理器 user_settings 增加 pin_iter 列(对应 routes/passwords.js 的 ensureSchema 逻辑,
|
||
// 默认 100000:存量用户保持旧迭代数,旧密文仍可解密)
|
||
{ version: 1, up: () => { try { db.exec("ALTER TABLE user_settings ADD COLUMN pin_iter INTEGER DEFAULT 100000"); } catch {} } },
|
||
// v2: 博客增强——博文标签/阅读量、评论嵌套与审核、点赞表
|
||
{ version: 2, up: () => {
|
||
try { db.exec("ALTER TABLE blog_posts ADD COLUMN tags TEXT DEFAULT ''"); } catch {}
|
||
try { db.exec("ALTER TABLE blog_posts ADD COLUMN views INTEGER DEFAULT 0"); } catch {}
|
||
try { db.exec("ALTER TABLE blog_comments ADD COLUMN parent_id INTEGER DEFAULT 0"); } catch {}
|
||
try { db.exec("ALTER TABLE blog_comments ADD COLUMN status TEXT DEFAULT 'approved'"); } catch {}
|
||
try { db.exec("CREATE TABLE IF NOT EXISTS post_likes (post_id INTEGER NOT NULL, user_id INTEGER NOT NULL, created_at DATETIME DEFAULT (datetime('now')), PRIMARY KEY (post_id, user_id))"); } catch {}
|
||
} },
|
||
// v3: RainID 单点登录——影子账号绑定键(sub)。部分唯一索引:空串(本地账号)不参与唯一约束
|
||
{ version: 3, up: () => {
|
||
try { db.exec("ALTER TABLE users ADD COLUMN rainid_user_id TEXT DEFAULT ''"); } catch {}
|
||
try { db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_users_rainid ON users(rainid_user_id) WHERE rainid_user_id <> ''"); } catch {}
|
||
} },
|
||
// v4: 论坛贴吧升级——置顶/加精/版主/图标
|
||
{ version: 4, up: () => {
|
||
try { db.exec("ALTER TABLE forum_posts ADD COLUMN is_pinned INTEGER DEFAULT 0"); } catch {}
|
||
try { db.exec("ALTER TABLE forum_posts ADD COLUMN is_essence INTEGER DEFAULT 0"); } catch {}
|
||
try { db.exec("ALTER TABLE forum_categories ADD COLUMN icon TEXT DEFAULT ''"); } catch {}
|
||
try { db.exec("ALTER TABLE forum_categories ADD COLUMN icon_color TEXT DEFAULT ''"); } catch {}
|
||
try { db.exec(`CREATE TABLE IF NOT EXISTS forum_moderators (
|
||
category_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
|
||
PRIMARY KEY (category_id, user_id),
|
||
FOREIGN KEY (category_id) REFERENCES forum_categories(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE)`); } catch {}
|
||
try { db.exec("CREATE INDEX IF NOT EXISTS idx_forum_posts_cat ON forum_posts(category_id, is_pinned DESC, created_at DESC)"); } catch {}
|
||
} },
|
||
// v5: 论坛版块级禁言
|
||
{ version: 5, up: () => {
|
||
try { db.exec(`CREATE TABLE IF NOT EXISTS forum_mutes (
|
||
category_id INTEGER NOT NULL,
|
||
user_id INTEGER NOT NULL,
|
||
muted_until DATETIME, -- NULL = 永久
|
||
created_at DATETIME DEFAULT (datetime('now')),
|
||
created_by INTEGER NOT NULL,
|
||
PRIMARY KEY (category_id, user_id),
|
||
FOREIGN KEY (category_id) REFERENCES forum_categories(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE)`); } catch {}
|
||
try { db.exec("CREATE INDEX IF NOT EXISTS idx_forum_mutes_cat ON forum_mutes(category_id)"); } catch {}
|
||
} },
|
||
// v6: RSS 版块级订阅——版块 feed 开关
|
||
{ version: 6, up: () => {
|
||
try { db.exec("ALTER TABLE forum_categories ADD COLUMN feed_enabled INTEGER DEFAULT 1"); } catch {}
|
||
} },
|
||
// v7: 论坛帖子二次编辑计数
|
||
{ version: 7, up: () => {
|
||
try { db.exec("ALTER TABLE forum_posts ADD COLUMN edit_count INTEGER DEFAULT 0"); } catch {}
|
||
} },
|
||
// v8: markdown [lock:] 锁定——博客/论坛帖子存锁定元数据 JSON([{type, hash}],不含块原文)
|
||
{ version: 8, up: () => {
|
||
try { db.exec("ALTER TABLE blog_posts ADD COLUMN locks TEXT DEFAULT '[]'"); } catch {}
|
||
try { db.exec("ALTER TABLE forum_posts ADD COLUMN locks TEXT DEFAULT '[]'"); } catch {}
|
||
} },
|
||
// v9: 用户 QQ 号字段(头像优先级:自传 > QQ 头像 > RainID 头像)
|
||
{ version: 9, up: () => {
|
||
try { db.exec("ALTER TABLE users ADD COLUMN qq TEXT DEFAULT ''"); } catch {}
|
||
} },
|
||
// v10: 公开个人主页——用户个性签名
|
||
{ version: 10, up: () => {
|
||
try { db.exec("ALTER TABLE users ADD COLUMN bio TEXT DEFAULT ''"); } catch {}
|
||
} },
|
||
// v11: 公开个人主页——最后活跃时间(datetime 字符串,跨天更新)
|
||
{ version: 11, up: () => {
|
||
try { db.exec("ALTER TABLE users ADD COLUMN last_active_at TEXT DEFAULT ''"); } catch {}
|
||
} },
|
||
// v12: 用户对外昵称(显示名:nickname||username)
|
||
{ version: 12, up: () => {
|
||
try { db.exec("ALTER TABLE users ADD COLUMN nickname TEXT DEFAULT ''"); } catch {}
|
||
} },
|
||
// v13: 自定义头衔 + 头衔颜色(帖子/评论作者位展示)
|
||
{ version: 13, up: () => {
|
||
try { db.exec("ALTER TABLE users ADD COLUMN title TEXT DEFAULT ''"); } catch {}
|
||
try { db.exec("ALTER TABLE users ADD COLUMN title_color TEXT DEFAULT ''"); } catch {}
|
||
} },
|
||
// v14: 个人博客链接(公开主页展示)
|
||
{ version: 14, up: () => {
|
||
try { db.exec("ALTER TABLE users ADD COLUMN website TEXT DEFAULT ''"); } catch {}
|
||
} },
|
||
// v15: 工作台 SPA 代理——面板链接加代理配置列
|
||
// (proxy_headers=转发自定义头 JSON、proxy_skip_tls_verify、permissions=iframe 委托权限 JSON 数组、
|
||
// scale=嵌入缩放、trusted=信任模型(1 保留 allow-same-origin)、slug=唯一标识)
|
||
{ version: 15, up: () => {
|
||
try { db.exec("ALTER TABLE admin_links ADD COLUMN proxy_headers TEXT DEFAULT ''"); } catch {}
|
||
try { db.exec("ALTER TABLE admin_links ADD COLUMN proxy_skip_tls_verify INTEGER DEFAULT 0"); } catch {}
|
||
try { db.exec("ALTER TABLE admin_links ADD COLUMN permissions TEXT DEFAULT ''"); } catch {}
|
||
try { db.exec("ALTER TABLE admin_links ADD COLUMN scale REAL DEFAULT 1.0"); } catch {}
|
||
try { db.exec("ALTER TABLE admin_links ADD COLUMN trusted INTEGER DEFAULT 1"); } catch {}
|
||
try { db.exec("ALTER TABLE admin_links ADD COLUMN slug TEXT DEFAULT ''"); } catch {}
|
||
} },
|
||
// v16: 站内工单——用户反馈、公开回复、内部备注及业务事件
|
||
{ version: 16, up: () => {
|
||
db.exec(`CREATE TABLE IF NOT EXISTS tickets (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
ticket_no TEXT NOT NULL UNIQUE,
|
||
requester_id INTEGER,
|
||
subject TEXT NOT NULL,
|
||
description TEXT NOT NULL,
|
||
category TEXT NOT NULL DEFAULT 'other',
|
||
priority TEXT NOT NULL DEFAULT 'normal',
|
||
status TEXT NOT NULL DEFAULT 'open',
|
||
source TEXT NOT NULL DEFAULT 'site',
|
||
source_url TEXT DEFAULT '',
|
||
source_type TEXT DEFAULT '',
|
||
source_id INTEGER DEFAULT 0,
|
||
browser_info TEXT DEFAULT '',
|
||
assignee_id INTEGER,
|
||
created_at DATETIME DEFAULT (datetime('now')),
|
||
updated_at DATETIME DEFAULT (datetime('now')),
|
||
last_reply_at DATETIME DEFAULT (datetime('now')),
|
||
first_response_at DATETIME,
|
||
resolved_at DATETIME,
|
||
closed_at DATETIME,
|
||
FOREIGN KEY (requester_id) REFERENCES users(id) ON DELETE SET NULL,
|
||
FOREIGN KEY (assignee_id) REFERENCES users(id) ON DELETE SET NULL
|
||
)`);
|
||
db.exec(`CREATE TABLE IF NOT EXISTS ticket_messages (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
ticket_id INTEGER NOT NULL,
|
||
author_id INTEGER,
|
||
content TEXT NOT NULL,
|
||
is_internal INTEGER NOT NULL DEFAULT 0,
|
||
created_at DATETIME DEFAULT (datetime('now')),
|
||
updated_at DATETIME DEFAULT (datetime('now')),
|
||
FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL
|
||
)`);
|
||
db.exec(`CREATE TABLE IF NOT EXISTS ticket_events (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
ticket_id INTEGER NOT NULL,
|
||
actor_id INTEGER,
|
||
event_type TEXT NOT NULL,
|
||
field_name TEXT DEFAULT '',
|
||
old_value TEXT DEFAULT '',
|
||
new_value TEXT DEFAULT '',
|
||
detail TEXT DEFAULT '',
|
||
created_at DATETIME DEFAULT (datetime('now')),
|
||
FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (actor_id) REFERENCES users(id) ON DELETE SET NULL
|
||
)`);
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_tickets_requester ON tickets(requester_id, updated_at DESC)');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_tickets_status_updated ON tickets(status, updated_at DESC)');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_tickets_assignee ON tickets(assignee_id, status, updated_at DESC)');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_tickets_category ON tickets(category, updated_at DESC)');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_ticket_messages_ticket ON ticket_messages(ticket_id, created_at ASC)');
|
||
db.exec('CREATE INDEX IF NOT EXISTS idx_ticket_events_ticket ON ticket_events(ticket_id, created_at ASC)');
|
||
} },
|
||
];
|
||
for (const m of migrations) {
|
||
if (current < m.version) { m.up(); db.exec('PRAGMA user_version = ' + m.version); }
|
||
}
|
||
}
|
||
|
||
function seedAdmin() {
|
||
if (!get('SELECT id FROM users WHERE username = ?', ['admin'])) {
|
||
const hash = bcrypt.hashSync('admin123', 10);
|
||
run('INSERT INTO users (username, password, role) VALUES (?, ?, ?)', ['admin', hash, 'admin']);
|
||
console.log('Admin: admin / admin123');
|
||
}
|
||
}
|
||
|
||
function seedDefaults() {
|
||
const defaults = {
|
||
site_name: 'RainWeb',
|
||
site_description: '个人云平台',
|
||
site_url: '',
|
||
primary_color: '#6750a4',
|
||
recaptcha_site_key: '',
|
||
recaptcha_secret_key: '',
|
||
turnstile_site_key: '',
|
||
turnstile_secret_key: '',
|
||
smtp_host: '',
|
||
smtp_port: '587',
|
||
smtp_user: '',
|
||
smtp_pass: '',
|
||
smtp_from_email: '',
|
||
smtp_from_name: 'RainWeb',
|
||
theme_wallpaper: '',
|
||
theme_wallpaper_scale: 'cover',
|
||
theme_wallpaper_enabled: '1',
|
||
nav_style: 'default',
|
||
card_style: 'default',
|
||
glass_blur: '20',
|
||
glass_opacity: '0.6',
|
||
captcha_login: '0',
|
||
captcha_register: '0',
|
||
captcha_forum: '0',
|
||
captcha_type: 'builtin',
|
||
rainid_enabled: '0',
|
||
rainid_client_id: '',
|
||
rainid_discovery_url: 'https://rainid.rainnya.asia/oauth',
|
||
rainid_register_redirect: '0',
|
||
site_favicon: 'data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🌧</text></svg>',
|
||
music_embed_enabled: '0',
|
||
music_embed_code: '',
|
||
music_embed_position: 'right',
|
||
music_embed_autohide: '0',
|
||
music_embed_idle_timeout: '10',
|
||
footer_style: 'classic',
|
||
footer_copyright: '<span class="copyright-glow">© 2026 <b>Rainnya Blog</b> All rights reserved</span>',
|
||
footer_powered: '<span class="powered-glow">由 <a href="https://git.rainnya.asia/miaomiao/rainblogweb" target="_blank" rel="noopener" style="color:#fff;text-decoration:underline;text-decoration-color:rgba(255,255,255,0.55);text-underline-offset:3px;">RainWeb Engine</a> 强力驱动</span>',
|
||
footer_desc: '',
|
||
// 页脚栏目自定义:JSON 字符串 [{title, links:[{label,url}]}];空串时前台回退现有硬编码栏目
|
||
footer_columns: '',
|
||
comment_moderate: '0',
|
||
comment_notify: '0',
|
||
// 评论等次要作者位是否显示 UID 后缀('1'=显示,'0'=隐藏)
|
||
show_uid_in_comments: '1',
|
||
// 论坛游客可见开关:'1'=游客可浏览论坛;'0'=游客需登录(读接口 401 + SSR 404,SEO 权衡)
|
||
forum_guest_visible: '1',
|
||
// RSS 订阅源:feed_forum_enabled='1' 才对外暴露论坛 feed(默认关,用户没开时不暴露);
|
||
// feed_show_full='1'=全文 / '0'=摘要;feed_max_items 为每源条数上限
|
||
feed_forum_enabled: '0',
|
||
feed_show_full: '0',
|
||
feed_max_items: '20',
|
||
};
|
||
for (const [k, v] of Object.entries(defaults)) {
|
||
if (!get('SELECT value FROM site_settings WHERE key = ?', [k])) {
|
||
run('INSERT INTO site_settings (key, value) VALUES (?, ?)', [k, v]);
|
||
}
|
||
}
|
||
}
|
||
|
||
function run(sql, params = []) {
|
||
try {
|
||
const info = db.prepare(sql).run(...params);
|
||
return Number(info.lastInsertRowid) || 0;
|
||
} catch (e) {
|
||
console.error('SQL run error:', e.message, 'SQL:', sql.substring(0, 80));
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
function get(sql, params = []) {
|
||
try {
|
||
const row = db.prepare(sql).get(...params);
|
||
return row || null;
|
||
} catch (e) {
|
||
console.error('SQL get error:', e.message, 'SQL:', sql.substring(0, 80));
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function all(sql, params = []) {
|
||
try {
|
||
return db.prepare(sql).all(...params);
|
||
} catch (e) {
|
||
console.error('SQL all error:', e.message, 'SQL:', sql.substring(0, 80));
|
||
return [];
|
||
}
|
||
}
|
||
|
||
function getSetting(key) {
|
||
const r = get('SELECT value FROM site_settings WHERE key = ?', [key]);
|
||
return r ? r.value : '';
|
||
}
|
||
|
||
function setSetting(key, value) {
|
||
const existing = get('SELECT key FROM site_settings WHERE key = ?', [key]);
|
||
if (existing) run('UPDATE site_settings SET value = ? WHERE key = ?', [value, key]);
|
||
else run('INSERT INTO site_settings (key, value) VALUES (?, ?)', [key, value]);
|
||
}
|
||
|
||
// 事务辅助:多步写操作原子执行(better-sqlite3 原生事务;失败自动 ROLLBACK)
|
||
// 供 L8 改 PIN 重加密、以及任何需要"要么全成要么全不"的批量写场景使用。
|
||
function transaction(fn) {
|
||
if (!db) throw new Error('Database not initialized');
|
||
return db.transaction(fn)();
|
||
}
|
||
|
||
function seedSampleData() {
|
||
const existing = get("SELECT COUNT(*) as c FROM forum_categories");
|
||
if (existing && existing.c > 0) return;
|
||
|
||
// Forum categories
|
||
run('INSERT INTO forum_categories (name, description, sort_order) VALUES (?, ?, ?)', ['技术讨论', '技术相关话题交流', 1]);
|
||
run('INSERT INTO forum_categories (name, description, sort_order) VALUES (?, ?, ?)', ['闲聊灌水', '自由讨论区域', 2]);
|
||
run('INSERT INTO forum_categories (name, description, sort_order) VALUES (?, ?, ?)', ['问题求助', '寻求帮助和解答', 3]);
|
||
|
||
const adminUser = get('SELECT id FROM users WHERE username = ?', ['admin']);
|
||
if (adminUser) {
|
||
const aid = adminUser.id;
|
||
// Forum posts
|
||
run('INSERT INTO forum_posts (category_id, title, content, author_id, sub_category) VALUES (?, ?, ?, ?, ?)', [1, '欢迎来到论坛', '这是论坛的第一篇帖子!欢迎大家交流讨论。', aid, '分享']);
|
||
run('INSERT INTO forum_posts (category_id, title, content, author_id, sub_category) VALUES (?, ?, ?, ?, ?)', [2, '今天天气真不错', '大家今天过得怎么样?来聊聊吧!', aid, '讨论']);
|
||
|
||
// Blog posts
|
||
const blogMarkdown = `## 欢迎使用 RainWeb\n\nRainWeb 是一个多功能的个人云平台,集成了 **博客、论坛、密码管理器** 等功能。\n\n- 🎨 Material Design 3 风格\n- 🌓 深色/浅色主题切换\n- 📧 邮箱验证注册\n- 🔒 密码管理器 (AES-256-GCM 加密)\n\n### 快速开始\n\n1. 点击右上角「登录」使用默认账号 \`admin / admin123\`\n2. 在管理后台配置 SMTP 邮件和 reCAPTCHA\n3. 在「面板链接」中添加你的各个管理后台\n4. 发布你的第一篇博客文章!`;
|
||
|
||
run('INSERT INTO blog_posts (title, content, excerpt, author_id, use_markdown) VALUES (?, ?, ?, ?, ?)',
|
||
['欢迎使用 RainWeb', blogMarkdown, 'RainWeb 功能简介与快速开始', aid, 1]);
|
||
|
||
const md2 = `## 自定义主题\n\n你可以在管理后台的「站点设置」中自定义:\n\n- **网站名称** - 显示在导航栏\n- **主题色** - 整个站点的主题颜色\n- **reCAPTCHA V2** - 注册验证\n- **SMTP 邮件** - 发送验证邮件`;
|
||
run('INSERT INTO blog_posts (title, content, excerpt, author_id, use_markdown) VALUES (?, ?, ?, ?, ?)',
|
||
['自定义主题与配置', md2, '站点设置与个性化配置说明', aid, 1]);
|
||
|
||
// Admin panel links
|
||
run('INSERT INTO admin_links (title, url, embed_url, description, icon, category, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||
['RainWeb 管理后台', '/admin.html', '', '系统管理配置', 'settings', '系统管理', 1]);
|
||
run('INSERT INTO admin_links (title, url, embed_url, description, icon, category, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||
['示例面板', 'https://example.com', 'https://example.com', '外部面板示例(可替换)', 'dashboard', '外部面板', 2]);
|
||
}
|
||
}
|
||
|
||
module.exports = { getDb, run, get, all, getSetting, setSetting, transaction };
|