Finish JSON export feature and simplify JWT config

This commit is contained in:
2026-01-16 14:55:25 +01:00
parent e61673fc0e
commit 2f0485d7c5
6 changed files with 195 additions and 12 deletions

View File

@@ -2,7 +2,7 @@
## frontend/public/games.html
[ ] export logs to JSON via a button in the top right corner, the file should have the current date in the name i.e: edh_games_16_01_2026.json
[x] export logs to JSON via a button in the top right corner, the file should have the current date in the name i.e: edh_games_16_01_2026.json
## frontend/public/round-counter.html

View File

@@ -10,12 +10,7 @@ const rootDir = resolve(__dirname, '../../..')
config({ path: resolve(rootDir, '.env') })
export const jwtConfig = {
secret: process.env.JWT_SECRET || 'fallback-secret-for-development',
algorithm: 'HS512',
expiresIn: '24h',
refreshExpiresIn: '7d',
issuer: 'edh-stats',
audience: 'edh-stats-users'
secret: process.env.JWT_SECRET || 'fallback-secret-for-development'
}
export const corsConfig = {

View File

@@ -94,25 +94,35 @@ class Game {
ORDER BY g.date DESC
`
const params = [userId]
if (filters.commander) {
params.push(`%${filters.commander}%`)
}
if (filters.playerCount) {
query += ` AND g.player_count = ?`
params.push(filters.playerCount)
}
if (filters.commanderId) {
query += ` AND g.commander_id = ?`
params.push(filters.commanderId)
}
if (filters.dateFrom) {
query += ` AND g.date >= ?`
params.push(filters.dateFrom)
}
if (filters.dateTo) {
query += ` AND g.date <= ?`
params.push(filters.dateTo)
}
query += ` LIMIT ? OFFSET ?`
params.push(limit, offset)
const games = db.prepare(query).all([userId, limit, offset])
const games = db.prepare(query).all(params)
// Parse dates for frontend and transform to camelCase
return games.map((game) => ({
@@ -135,6 +145,81 @@ class Game {
}
}
static async exportByUserId(userId, filters = {}) {
const db = await dbManager.initialize()
try {
let query = `
SELECT
g.id,
g.date,
g.player_count,
g.commander_id,
g.won,
g.rounds,
g.starting_player_won,
g.sol_ring_turn_one_won,
g.notes,
cmdr.name as commander_name,
cmdr.colors as commander_colors,
g.created_at,
g.updated_at
FROM games g
LEFT JOIN commanders cmdr ON g.commander_id = cmdr.id
WHERE g.user_id = ?
${filters.commander ? `AND cmdr.name LIKE ?` : ''}
`
const params = [userId]
if (filters.commander) {
params.push(`%${filters.commander}%`)
}
if (filters.playerCount) {
query += ` AND g.player_count = ?`
params.push(filters.playerCount)
}
if (filters.commanderId) {
query += ` AND g.commander_id = ?`
params.push(filters.commanderId)
}
if (filters.dateFrom) {
query += ` AND g.date >= ?`
params.push(filters.dateFrom)
}
if (filters.dateTo) {
query += ` AND g.date <= ?`
params.push(filters.dateTo)
}
query += ` ORDER BY g.date DESC`
const games = db.prepare(query).all(params)
// Return data for export (minimal transformation)
return games.map(game => ({
id: game.id,
date: game.date,
playerCount: game.player_count,
commanderId: game.commander_id,
commanderName: game.commander_name,
commanderColors: JSON.parse(game.commander_colors || '[]'),
won: Boolean(game.won),
rounds: game.rounds || 0,
startingPlayerWon: Boolean(game.starting_player_won),
solRingTurnOneWon: Boolean(game.sol_ring_turn_one_won),
notes: game.notes,
createdAt: game.created_at,
updatedAt: game.updated_at
}))
} catch (error) {
throw new Error('Failed to export games')
}
}
static async update(id, updateData, userId) {
const db = await dbManager.initialize()

View File

@@ -347,6 +347,66 @@ export default async function gameRoutes(fastify, options) {
reply.code(500).send({
error: 'Failed to delete game'
})
}
}
)
// Export games as JSON
fastify.get(
'/export',
{
config: { rateLimit: { max: 5, timeWindow: '1 minute' } },
preHandler: [
async (request, reply) => {
try {
await request.jwtVerify()
} catch (err) {
reply.code(401).send({
error: 'Unauthorized',
message: 'Invalid or expired token'
})
}
}
]
},
async (request, reply) => {
try {
const userId = request.user.id
const filters = {}
// Parse optional query filters
if (request.query.commander) filters.commander = request.query.commander
if (request.query.playerCount) filters.playerCount = parseInt(request.query.playerCount)
if (request.query.commanderId) filters.commanderId = parseInt(request.query.commanderId)
if (request.query.dateFrom) filters.dateFrom = request.query.dateFrom
if (request.query.dateTo) filters.dateTo = request.query.dateTo
const games = await Game.exportByUserId(userId, filters)
// Generate filename with current date
const today = new Date().toLocaleDateString('en-US').replace(/\//g, '_')
const filename = `edh_games_${today}.json`
// Set appropriate headers for file download
reply.header('Content-Type', 'application/json')
reply.header('Content-Disposition', `attachment; filename="${filename}"`)
const exportData = {
metadata: {
exportDate: new Date().toISOString(),
totalGames: games.length,
userId: userId
},
games: games
}
reply.send(exportData)
} catch (error) {
fastify.log.error('Export games error:', error)
reply.code(500).send({
error: 'Internal Server Error',
message: 'Failed to export games'
})
}
}
)

View File

@@ -35,10 +35,15 @@
<!-- Header & Action -->
<div class="mb-8 flex justify-between items-center">
<h2 class="text-2xl font-semibold text-gray-900">Recent Games</h2>
<button @click="showLogForm = !showLogForm" class="btn btn-primary">
<span x-show="!showLogForm">Log New Game</span>
<span x-show="showLogForm">Cancel</span>
</button>
<div class="flex space-x-4">
<button @click="exportGames()" class="btn btn-secondary">
Export JSON
</button>
<button @click="showLogForm = !showLogForm" class="btn btn-primary">
<span x-show="!showLogForm">Log New Game</span>
<span x-show="showLogForm">Cancel</span>
</button>
</div>
</div>
<!-- Log Game Form -->

View File

@@ -377,6 +377,44 @@ function gameManager() {
day: 'numeric',
year: 'numeric'
})
},
async exportGames() {
try {
const token = localStorage.getItem('edh-stats-token') || sessionStorage.getItem('edh-stats-token')
const response = await fetch('/api/games/export', {
headers: {
Authorization: `Bearer ${token}`
}
})
if (!response.ok) {
throw new Error('Export failed')
}
// Generate filename with current date
const today = new Date().toLocaleDateString('en-US').replace(/\//g, '_')
const filename = `edh_games_${today}.json`
// Create blob and download
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.style.display = 'none'
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
} catch (error) {
console.error('Export failed:', error)
// Show error message to user
this.serverError = 'Failed to export games. Please try again.'
setTimeout(() => {
this.serverError = ''
}, 5000)
}
}
}
}