- Implement complete authentication system with JWT token validation - Add auth provider with persistent login state across page refreshes - Create multilingual login/register forms with Material-UI components - Fix token validation using raw SQL queries to bypass Prisma sync issues - Add comprehensive error handling for expired/invalid tokens - Create profile and settings pages with full i18n support - Add proper user role management (admin/user) with database sync - Implement secure middleware with CSRF protection and auth checks - Add debug endpoints for troubleshooting authentication issues - Fix Zustand store persistence for authentication state 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
72 lines
4.2 KiB
TypeScript
72 lines
4.2 KiB
TypeScript
#!/usr/bin/env tsx
|
|
import fs from 'fs'
|
|
import path from 'path'
|
|
|
|
const ABBR = (process.env.EN_ABBR || 'BSB').toUpperCase()
|
|
const ROOT = process.env.INPUT_DIR || path.join('data', 'en_bible', ABBR)
|
|
|
|
const OT_ORDER = [
|
|
'Genesis','Exodus','Leviticus','Numbers','Deuteronomy','Joshua','Judges','Ruth','1 Samuel','2 Samuel','1 Kings','2 Kings','1 Chronicles','2 Chronicles','Ezra','Nehemiah','Esther','Job','Psalms','Proverbs','Ecclesiastes','Song of Songs','Isaiah','Jeremiah','Lamentations','Ezekiel','Daniel','Hosea','Joel','Amos','Obadiah','Jonah','Micah','Nahum','Habakkuk','Zephaniah','Haggai','Zechariah','Malachi'
|
|
]
|
|
const NT_ORDER = [
|
|
'Matthew','Mark','Luke','John','Acts','Romans','1 Corinthians','2 Corinthians','Galatians','Ephesians','Philippians','Colossians','1 Thessalonians','2 Thessalonians','1 Timothy','2 Timothy','Titus','Philemon','Hebrews','James','1 Peter','2 Peter','1 John','2 John','3 John','Jude','Revelation'
|
|
]
|
|
|
|
function titleFromAbbr(abbr: string): string {
|
|
const map: Record<string,string> = { GEN:'Genesis', EXO:'Exodus', LEV:'Leviticus', NUM:'Numbers', DEU:'Deuteronomy', JOS:'Joshua', JDG:'Judges', RUT:'Ruth', '1SA':'1 Samuel', '2SA':'2 Samuel', '1KI':'1 Kings', '2KI':'2 Kings', '1CH':'1 Chronicles', '2CH':'2 Chronicles', EZR:'Ezra', NEH:'Nehemiah', EST:'Esther', JOB:'Job', PSA:'Psalms', PRO:'Proverbs', ECC:'Ecclesiastes', SNG:'Song of Songs', ISA:'Isaiah', JER:'Jeremiah', LAM:'Lamentations', EZK:'Ezekiel', DAN:'Daniel', HOS:'Hosea', JOL:'Joel', AMO:'Amos', OBA:'Obadiah', JON:'Jonah', MIC:'Micah', NAM:'Nahum', HAB:'Habakkuk', ZEP:'Zephaniah', HAG:'Haggai', ZEC:'Zechariah', MAL:'Malachi', MAT:'Matthew', MRK:'Mark', LUK:'Luke', JHN:'John', ACT:'Acts', ROM:'Romans', '1CO':'1 Corinthians', '2CO':'2 Corinthians', GAL:'Galatians', EPH:'Ephesians', PHP:'Philippians', COL:'Colossians', '1TH':'1 Thessalonians', '2TH':'2 Thessalonians', '1TI':'1 Timothy', '2TI':'2 Timothy', TIT:'Titus', PHM:'Philemon', HEB:'Hebrews', JAS:'James', '1PE':'1 Peter', '2PE':'2 Peter', '1JN':'1 John', '2JN':'2 John', '3JN':'3 John', JUD:'Jude', REV:'Revelation' }
|
|
return map[abbr] || abbr
|
|
}
|
|
|
|
function detectBooks(dir: string): string[] {
|
|
if (!fs.existsSync(dir)) return []
|
|
return fs.readdirSync(dir).filter(d => fs.statSync(path.join(dir, d)).isDirectory())
|
|
}
|
|
|
|
function readChapters(bookDir: string) {
|
|
const files = fs.readdirSync(bookDir).filter(f => f.startsWith('chapter-') && f.endsWith('.json') && !f.includes('intro'))
|
|
const chapters: any[] = []
|
|
for (const f of files) {
|
|
const obj = JSON.parse(fs.readFileSync(path.join(bookDir, f), 'utf-8'))
|
|
chapters.push(obj)
|
|
}
|
|
chapters.sort((a,b) => a.chapterNum - b.chapterNum)
|
|
return chapters
|
|
}
|
|
|
|
function assemble() {
|
|
const bookDirs = detectBooks(ROOT)
|
|
const books: { name: string; chapters: any[] }[] = []
|
|
for (const d of bookDirs) {
|
|
// d is likely an abbreviation; try to infer common titles for GEN/EXO/LEV etc.
|
|
let name = d
|
|
if (d.toUpperCase() === d) name = titleFromAbbr(d.toUpperCase())
|
|
const chapters = readChapters(path.join(ROOT, d))
|
|
if (chapters.length > 0) books.push({ name, chapters })
|
|
}
|
|
|
|
const ot = books.filter(b => OT_ORDER.includes(b.name)).sort((a,b) => OT_ORDER.indexOf(a.name) - OT_ORDER.indexOf(b.name))
|
|
const nt = books.filter(b => NT_ORDER.includes(b.name)).sort((a,b) => NT_ORDER.indexOf(a.name) - NT_ORDER.indexOf(b.name))
|
|
|
|
// Verify completeness: all 66 books must be present
|
|
const have = new Set(books.map(b => b.name))
|
|
const missing = [...OT_ORDER, ...NT_ORDER].filter(n => !have.has(n))
|
|
if (missing.length > 0 && process.env.ALLOW_PARTIAL !== '1') {
|
|
console.error(`Missing ${missing.length} books. Full EN Bible not downloaded yet.`)
|
|
console.error('First few missing:', missing.slice(0, 10).join(', ') + (missing.length > 10 ? '...' : ''))
|
|
process.exit(1)
|
|
}
|
|
|
|
const otObj = { testament: 'Old Testament', books: ot }
|
|
const ntObj = { testament: 'New Testament', books: nt }
|
|
|
|
const otFile = path.join(ROOT, 'old_testament.json')
|
|
const ntFile = path.join(ROOT, 'new_testament.json')
|
|
fs.mkdirSync(ROOT, { recursive: true })
|
|
fs.writeFileSync(otFile, JSON.stringify(otObj, null, 2), 'utf-8')
|
|
fs.writeFileSync(ntFile, JSON.stringify(ntObj, null, 2), 'utf-8')
|
|
console.log('Assembled:', otFile)
|
|
console.log('Assembled:', ntFile)
|
|
}
|
|
|
|
assemble()
|