Finish JSON export feature and simplify JWT config
This commit is contained in:
2
TODO.md
2
TODO.md
@@ -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
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user