feat: 初始提交(仅核心代码,已排除大文件)

This commit is contained in:
yangxiangyuan
2026-07-08 10:29:50 +08:00
commit be7c5842f1
623 changed files with 332897 additions and 0 deletions
+203
View File
@@ -0,0 +1,203 @@
const Database = require('better-sqlite3')
const path = require('path')
const fs = require('fs')
const crypto = require('crypto')
const dataDir = path.join(process.cwd(), 'data')
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true })
const dbPath = path.join(dataDir, 'language_behavior_lock.db')
const db = new Database(dbPath)
db.exec(`
CREATE TABLE IF NOT EXISTS lbl_users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS lbl_sessions (
token TEXT PRIMARY KEY,
username TEXT NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS lbl_user_data (
username TEXT PRIMARY KEY,
records_json TEXT NOT NULL DEFAULT '[]',
keywords_json TEXT NOT NULL DEFAULT '[]',
draft_json TEXT,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_lbl_sessions_user ON lbl_sessions(username);
CREATE INDEX IF NOT EXISTS idx_lbl_sessions_exp ON lbl_sessions(expires_at);
`)
const now = () => Date.now()
const SESSION_MAX_AGE = 1 * 24 * 3600 * 1000
const COOKIE_NAME = 'language_behavior_lock_sid'
const DEFAULT_USER = { username: 'admin_lbl', password: process.env.LOCK_DEFAULT_PASSWORD || '' }
const parseCookie = cookieStr => {
const out = {}
String(cookieStr || '').split(/;\s*/).forEach(p => {
const idx = p.indexOf('=')
if (idx > 0) out[p.slice(0, idx)] = decodeURIComponent(p.slice(idx + 1))
})
return out
}
const ensureDefaultUser = () => {
const row = db.prepare('SELECT username FROM lbl_users WHERE username = ?').get(DEFAULT_USER.username)
if (!row) {
db.prepare('INSERT INTO lbl_users (username, password, created_at) VALUES (?, ?, ?)').run(DEFAULT_USER.username, DEFAULT_USER.password, now())
}
}
const cleanupSessions = () => {
db.prepare('DELETE FROM lbl_sessions WHERE expires_at <= ?').run(now())
}
const readDataOf = username => {
const row = db.prepare('SELECT records_json, keywords_json, draft_json FROM lbl_user_data WHERE username = ?').get(username)
if (!row) return { db: { records: [], keywords: [] }, draft: null }
let records = []
let keywords = []
let draft = null
try { records = JSON.parse(row.records_json || '[]') } catch {}
try { keywords = JSON.parse(row.keywords_json || '[]') } catch {}
try { draft = row.draft_json ? JSON.parse(row.draft_json) : null } catch {}
return {
db: {
records: Array.isArray(records) ? records : [],
keywords: Array.isArray(keywords) ? keywords : []
},
draft: draft && typeof draft === 'object' ? draft : null
}
}
const writeDataOf = (username, data) => {
const dbData = data && data.db && typeof data.db === 'object' ? data.db : {}
const records = Array.isArray(dbData.records) ? dbData.records : []
const keywords = Array.isArray(dbData.keywords) ? dbData.keywords : []
const draft = data && data.draft && typeof data.draft === 'object' ? data.draft : null
db.prepare(`
INSERT INTO lbl_user_data (username, records_json, keywords_json, draft_json, updated_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(username) DO UPDATE SET
records_json = excluded.records_json,
keywords_json = excluded.keywords_json,
draft_json = excluded.draft_json,
updated_at = excluded.updated_at
`).run(
username,
JSON.stringify(records),
JSON.stringify(keywords),
draft ? JSON.stringify(draft) : null,
now()
)
}
const getSessionUser = req => {
cleanupSessions()
const cookies = parseCookie(req.headers.cookie || '')
const token = String(cookies[COOKIE_NAME] || '')
if (!token) return ''
const row = db.prepare('SELECT username, expires_at FROM lbl_sessions WHERE token = ?').get(token)
if (!row) return ''
if (Number(row.expires_at) <= now()) {
db.prepare('DELETE FROM lbl_sessions WHERE token = ?').run(token)
return ''
}
return String(row.username || '')
}
const requireAuth = (req, res, next) => {
const username = getSessionUser(req)
if (!username) return res.status(401).json({ ok: false, error: 'unauthorized' })
req.lblUser = username
next()
}
const bindRoutes = app => {
ensureDefaultUser()
app.get('/api/language_behavior_lock/health', (req, res) => {
res.json({ ok: true })
})
app.get('/api/language_behavior_lock/auth/local_creds', (req, res) => {
try {
const os = require('os')
const userProfile = process.env.USERPROFILE || process.env.HOME || os.homedir()
const candidates = [path.join(userProfile, 'Toolbox_local_creds.env.local')]
const usersRoot = path.join(path.parse(userProfile).root, 'Users')
try {
if (fs.existsSync(usersRoot)) {
fs.readdirSync(usersRoot).forEach(name => {
const dir = path.join(usersRoot, name)
if (dir !== userProfile && fs.existsSync(dir)) candidates.push(path.join(dir, 'Toolbox_local_creds.env.local'))
})
}
} catch {}
let content = null
for (const p of candidates) { if (fs.existsSync(p)) { content = fs.readFileSync(p, 'utf-8'); break } }
if (!content) return res.json({ ok: true, creds: null })
const creds = {}
content.split(/\r?\n/).forEach(line => {
const t = line.trim()
if (!t || t.startsWith('#')) return
const idx = t.indexOf('=')
if (idx > 0) creds[t.slice(0, idx).trim()] = t.slice(idx + 1).trim()
})
const serverToken = String(creds.TOOLBOX_LOCAL_CREDS_TOKEN || '')
if (serverToken) {
const clientToken = String(req.query.token || '')
if (clientToken !== serverToken) return res.json({ ok: true, creds: null })
}
const u = creds.LBL_USERNAME || ''
const p = creds.LBL_PASSWORD || ''
if (!u && !p) return res.json({ ok: true, creds: null })
res.json({ ok: true, creds: { username: u, password: p } })
} catch { res.json({ ok: true, creds: null }) }
})
app.post('/api/language_behavior_lock/auth/login', (req, res) => {
const username = String((req.body && req.body.username) || '').trim()
const password = String((req.body && req.body.password) || '')
if (!username || !password) return res.status(400).json({ ok: false, error: 'invalid_credentials' })
const row = db.prepare('SELECT username, password FROM lbl_users WHERE username = ?').get(username)
if (!row || String(row.password || '') !== password) return res.status(401).json({ ok: false, error: 'invalid_credentials' })
const token = crypto.randomBytes(24).toString('hex')
const createdAt = now()
const expiresAt = createdAt + SESSION_MAX_AGE
db.prepare('INSERT INTO lbl_sessions (token, username, created_at, expires_at) VALUES (?, ?, ?, ?)').run(token, username, createdAt, expiresAt)
res.cookie(COOKIE_NAME, token, { httpOnly: true, sameSite: 'lax', maxAge: SESSION_MAX_AGE })
res.json({ ok: true, username })
})
app.post('/api/language_behavior_lock/auth/logout', (req, res) => {
const cookies = parseCookie(req.headers.cookie || '')
const token = String(cookies[COOKIE_NAME] || '')
if (token) db.prepare('DELETE FROM lbl_sessions WHERE token = ?').run(token)
res.cookie(COOKIE_NAME, '', { httpOnly: true, sameSite: 'lax', maxAge: 0 })
res.json({ ok: true })
})
app.get('/api/language_behavior_lock/auth/me', (req, res) => {
const username = getSessionUser(req)
if (!username) return res.status(401).json({ ok: false, error: 'unauthorized' })
res.json({ ok: true, username })
})
app.get('/api/language_behavior_lock/data', requireAuth, (req, res) => {
const payload = readDataOf(req.lblUser)
res.json({ ok: true, ...payload })
})
app.put('/api/language_behavior_lock/data', requireAuth, (req, res) => {
writeDataOf(req.lblUser, req.body || {})
res.json({ ok: true })
})
}
module.exports = { bindRoutes }