v0.4
This commit is contained in:
@@ -6,6 +6,26 @@ const path = require('path')
|
||||
|
||||
const app = express()
|
||||
const PORT = process.env.PORT || 3000
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || ''
|
||||
|
||||
// Stateless admin token: hmac(timestamp, secret). Valid 24h.
|
||||
function makeAdminToken() {
|
||||
const ts = Date.now().toString()
|
||||
const sig = crypto.createHmac('sha256', 'colony-admin-' + ADMIN_PASSWORD).update(ts).digest('hex')
|
||||
return ts + '.' + sig
|
||||
}
|
||||
function validAdminToken(token) {
|
||||
if (!ADMIN_PASSWORD || !token) return !ADMIN_PASSWORD // if no password set, allow all
|
||||
const [ts, sig] = (token || '').split('.')
|
||||
if (!ts || !sig) return false
|
||||
if (Date.now() - Number(ts) > 86_400_000) return false // expired
|
||||
const expected = crypto.createHmac('sha256', 'colony-admin-' + ADMIN_PASSWORD).update(ts).digest('hex')
|
||||
return sig === expected
|
||||
}
|
||||
function requireAdmin(req, res, next) {
|
||||
if (validAdminToken(req.headers['x-admin-token'])) return next()
|
||||
res.status(401).json({ error: 'Admin authentication required' })
|
||||
}
|
||||
|
||||
// SSE clients: Map<slug, Set<res>>
|
||||
const sseClients = new Map()
|
||||
@@ -67,10 +87,18 @@ function publicGame(g) {
|
||||
timeline_start: g.timeline_start, timeline_end: g.timeline_end,
|
||||
start_title: g.start_title || '', start_description: g.start_description || '', start_mood: g.start_mood || '',
|
||||
end_title: g.end_title || '', end_description: g.end_description || '', end_mood: g.end_mood || '',
|
||||
deck_id: g.deck_id || '',
|
||||
created_at: g.created_at
|
||||
}
|
||||
}
|
||||
|
||||
// --- Admin auth ---
|
||||
app.post('/api/admin/login', (req, res) => {
|
||||
if (!ADMIN_PASSWORD) return res.json({ token: 'no-auth-required' })
|
||||
if (req.body.password !== ADMIN_PASSWORD) return res.status(401).json({ error: 'Wrong password' })
|
||||
res.json({ token: makeAdminToken() })
|
||||
})
|
||||
|
||||
// --- Live updates (SSE) ---
|
||||
app.get('/api/games/:slug/live', (req, res) => {
|
||||
res.writeHead(200, {
|
||||
@@ -150,6 +178,7 @@ app.put('/api/games/:slug', requirePassword, (req, res) => {
|
||||
game.end_title = end_title || ''
|
||||
game.end_description = end_description || ''
|
||||
game.end_mood = end_mood || ''
|
||||
game.deck_id = req.body.deck_id || ''
|
||||
saveDB(req.db); broadcast(req.params.slug)
|
||||
res.json(publicGame(game))
|
||||
})
|
||||
@@ -258,4 +287,167 @@ app.delete('/api/games/:slug/keywords/:id', requirePassword, (req, res) => {
|
||||
res.json({ ok: true })
|
||||
})
|
||||
|
||||
// --- Decks (image libraries) ---
|
||||
function loadDecks() {
|
||||
try { return JSON.parse(fs.readFileSync(path.join(__dirname, 'decks.json'), 'utf8')) }
|
||||
catch { return { decks: [] } }
|
||||
}
|
||||
function saveDecks(data) {
|
||||
const tmp = path.join(__dirname, 'decks.json') + '.tmp'
|
||||
fs.writeFileSync(tmp, JSON.stringify(data, null, 2))
|
||||
fs.renameSync(tmp, path.join(__dirname, 'decks.json'))
|
||||
}
|
||||
|
||||
app.get('/api/decks', (req, res) => {
|
||||
res.json(loadDecks().decks)
|
||||
})
|
||||
|
||||
app.post('/api/decks', requireAdmin, (req, res) => {
|
||||
const { name, description } = req.body
|
||||
if (!name) return res.status(400).json({ error: 'name required' })
|
||||
const db = loadDecks()
|
||||
const deck = { id: newId(), name, description: description || '', images: [], created_at: now() }
|
||||
db.decks.push(deck)
|
||||
saveDecks(db)
|
||||
res.status(201).json(deck)
|
||||
})
|
||||
|
||||
app.put('/api/decks/:id', requireAdmin, (req, res) => {
|
||||
const db = loadDecks()
|
||||
const deck = db.decks.find(d => d.id === req.params.id)
|
||||
if (!deck) return res.status(404).json({ error: 'Deck not found' })
|
||||
deck.name = req.body.name || deck.name
|
||||
deck.description = req.body.description ?? deck.description
|
||||
saveDecks(db)
|
||||
res.json(deck)
|
||||
})
|
||||
|
||||
app.delete('/api/decks/:id', requireAdmin, (req, res) => {
|
||||
const db = loadDecks()
|
||||
const idx = db.decks.findIndex(d => d.id === req.params.id)
|
||||
if (idx === -1) return res.status(404).json({ error: 'Deck not found' })
|
||||
// Delete image files
|
||||
for (const img of db.decks[idx].images || []) {
|
||||
try { fs.unlinkSync(path.join(__dirname, 'assets', 'deck-images', img.filename)) } catch {}
|
||||
}
|
||||
db.decks.splice(idx, 1)
|
||||
saveDecks(db)
|
||||
res.json({ ok: true })
|
||||
})
|
||||
|
||||
// --- Cards within a deck ---
|
||||
app.post('/api/decks/:id/cards', requireAdmin, (req, res) => {
|
||||
const db = loadDecks()
|
||||
const deck = db.decks.find(d => d.id === req.params.id)
|
||||
if (!deck) return res.status(404).json({ error: 'Deck not found' })
|
||||
const { title, type, question, a, b, requires } = req.body
|
||||
if (!title || !a || !b) return res.status(400).json({ error: 'title, a, b required' })
|
||||
const card = { id: newId(), title, type: type || 'action', question: question || '', a, b, requires: requires || [], images: [], created_at: now() }
|
||||
deck.cards = deck.cards || []
|
||||
deck.cards.push(card)
|
||||
saveDecks(db)
|
||||
res.status(201).json(card)
|
||||
})
|
||||
|
||||
app.put('/api/decks/:deckId/cards/:cardId', requireAdmin, (req, res) => {
|
||||
const db = loadDecks()
|
||||
const deck = db.decks.find(d => d.id === req.params.deckId)
|
||||
if (!deck) return res.status(404).json({ error: 'Deck not found' })
|
||||
const card = (deck.cards || []).find(c => c.id === req.params.cardId)
|
||||
if (!card) return res.status(404).json({ error: 'Card not found' })
|
||||
const { title, type, question, a, b, requires } = req.body
|
||||
Object.assign(card, { title: title ?? card.title, type: type ?? card.type, question: question ?? card.question, a: a ?? card.a, b: b ?? card.b, requires: requires ?? card.requires })
|
||||
saveDecks(db)
|
||||
res.json(card)
|
||||
})
|
||||
|
||||
app.delete('/api/decks/:deckId/cards/:cardId', requireAdmin, (req, res) => {
|
||||
const db = loadDecks()
|
||||
const deck = db.decks.find(d => d.id === req.params.deckId)
|
||||
if (!deck) return res.status(404).json({ error: 'Deck not found' })
|
||||
const idx = (deck.cards || []).findIndex(c => c.id === req.params.cardId)
|
||||
if (idx === -1) return res.status(404).json({ error: 'Card not found' })
|
||||
for (const img of deck.cards[idx].images || []) {
|
||||
try { fs.unlinkSync(path.join(__dirname, 'assets', 'deck-images', img.filename)) } catch {}
|
||||
}
|
||||
deck.cards.splice(idx, 1)
|
||||
saveDecks(db)
|
||||
res.json({ ok: true })
|
||||
})
|
||||
|
||||
app.post('/api/decks/:deckId/cards/:cardId/images', requireAdmin, (req, res) => {
|
||||
const db = loadDecks()
|
||||
const deck = db.decks.find(d => d.id === req.params.deckId)
|
||||
if (!deck) return res.status(404).json({ error: 'Deck not found' })
|
||||
const card = (deck.cards || []).find(c => c.id === req.params.cardId)
|
||||
if (!card) return res.status(404).json({ error: 'Card not found' })
|
||||
const { filename, data, keywords } = req.body
|
||||
if (!data) return res.status(400).json({ error: 'data required' })
|
||||
const ext = path.extname(filename || '.jpg') || '.jpg'
|
||||
const savedName = `deck-${newId()}${ext}`
|
||||
fs.mkdirSync(path.join(__dirname, 'assets', 'deck-images'), { recursive: true })
|
||||
fs.writeFileSync(path.join(__dirname, 'assets', 'deck-images', savedName), Buffer.from(data, 'base64'))
|
||||
const img = { id: newId(), filename: savedName, keywords: keywords || [], created_at: now() }
|
||||
card.images = card.images || []
|
||||
card.images.push(img)
|
||||
saveDecks(db)
|
||||
res.status(201).json(img)
|
||||
})
|
||||
|
||||
app.delete('/api/decks/:deckId/cards/:cardId/images/:imgId', requireAdmin, (req, res) => {
|
||||
const db = loadDecks()
|
||||
const deck = db.decks.find(d => d.id === req.params.deckId)
|
||||
if (!deck) return res.status(404).json({ error: 'Deck not found' })
|
||||
const card = (deck.cards || []).find(c => c.id === req.params.cardId)
|
||||
if (!card) return res.status(404).json({ error: 'Card not found' })
|
||||
const idx = (card.images || []).findIndex(i => i.id === req.params.imgId)
|
||||
if (idx === -1) return res.status(404).json({ error: 'Image not found' })
|
||||
try { fs.unlinkSync(path.join(__dirname, 'assets', 'deck-images', card.images[idx].filename)) } catch {}
|
||||
card.images.splice(idx, 1)
|
||||
saveDecks(db)
|
||||
res.json({ ok: true })
|
||||
})
|
||||
|
||||
// --- Pool images (for event auto-matching) ---
|
||||
app.post('/api/decks/:id/images', requireAdmin, (req, res) => {
|
||||
const db = loadDecks()
|
||||
const deck = db.decks.find(d => d.id === req.params.id)
|
||||
if (!deck) return res.status(404).json({ error: 'Deck not found' })
|
||||
const { filename, data, keywords } = req.body
|
||||
if (!data) return res.status(400).json({ error: 'data required' })
|
||||
const ext = path.extname(filename || '.jpg') || '.jpg'
|
||||
const savedName = `deck-${newId()}${ext}`
|
||||
const imgPath = path.join(__dirname, 'assets', 'deck-images', savedName)
|
||||
fs.mkdirSync(path.dirname(imgPath), { recursive: true })
|
||||
fs.writeFileSync(imgPath, Buffer.from(data, 'base64'))
|
||||
const img = { id: newId(), filename: savedName, keywords: keywords || [], created_at: now() }
|
||||
deck.images = deck.images || []
|
||||
deck.images.push(img)
|
||||
saveDecks(db)
|
||||
res.status(201).json(img)
|
||||
})
|
||||
|
||||
app.put('/api/decks/:deckId/images/:imgId', requireAdmin, (req, res) => {
|
||||
const db = loadDecks()
|
||||
const deck = db.decks.find(d => d.id === req.params.deckId)
|
||||
if (!deck) return res.status(404).json({ error: 'Deck not found' })
|
||||
const img = deck.images.find(i => i.id === req.params.imgId)
|
||||
if (!img) return res.status(404).json({ error: 'Image not found' })
|
||||
img.keywords = req.body.keywords ?? img.keywords
|
||||
saveDecks(db)
|
||||
res.json(img)
|
||||
})
|
||||
|
||||
app.delete('/api/decks/:deckId/images/:imgId', requireAdmin, (req, res) => {
|
||||
const db = loadDecks()
|
||||
const deck = db.decks.find(d => d.id === req.params.deckId)
|
||||
if (!deck) return res.status(404).json({ error: 'Deck not found' })
|
||||
const idx = deck.images.findIndex(i => i.id === req.params.imgId)
|
||||
if (idx === -1) return res.status(404).json({ error: 'Image not found' })
|
||||
try { fs.unlinkSync(path.join(__dirname, 'assets', 'deck-images', deck.images[idx].filename)) } catch {}
|
||||
deck.images.splice(idx, 1)
|
||||
saveDecks(db)
|
||||
res.json({ ok: true })
|
||||
})
|
||||
|
||||
app.listen(PORT, () => console.log(`Colony Archive running on http://localhost:${PORT}`))
|
||||
|
||||
Reference in New Issue
Block a user