feat: 初始提交(仅核心代码,已排除大文件)
This commit is contained in:
@@ -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 }
|
||||
Reference in New Issue
Block a user