mirror of
https://github.com/ershisan99/www.git
synced 2025-12-17 12:34:17 +00:00
persist logs
This commit is contained in:
145
src/app/(home)/admin/logs/logs-client.tsx
Normal file
145
src/app/(home)/admin/logs/logs-client.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card'
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/table'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Trash2 } from 'lucide-react'
|
||||||
|
|
||||||
|
type LogFile = {
|
||||||
|
id: number
|
||||||
|
fileName: string
|
||||||
|
fileUrl: string
|
||||||
|
createdAt: string
|
||||||
|
userId: string | null
|
||||||
|
userName: string | null
|
||||||
|
userEmail: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogsClient() {
|
||||||
|
const [logs, setLogs] = useState<LogFile[]>([])
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false)
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const fetchLogs = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/logs')
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch logs')
|
||||||
|
}
|
||||||
|
const data = await response.json()
|
||||||
|
setLogs(data)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'An error occurred')
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchLogs()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleViewInParser = (id: number) => {
|
||||||
|
// Navigate to the log parser page with the log ID as a query parameter
|
||||||
|
router.push(`/log-parser?logId=${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (id: number) => {
|
||||||
|
if (confirm('Are you sure you want to delete this log file?')) {
|
||||||
|
setIsDeleting(true)
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/logs?id=${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to delete log file')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the logs list
|
||||||
|
await fetchLogs()
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'An error occurred while deleting')
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Log Files</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
View and manage uploaded log files
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{isLoading ? (
|
||||||
|
<p>Loading logs...</p>
|
||||||
|
) : error ? (
|
||||||
|
<p className="text-red-500">{error}</p>
|
||||||
|
) : logs.length === 0 ? (
|
||||||
|
<p>No logs found</p>
|
||||||
|
) : (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>File Name</TableHead>
|
||||||
|
<TableHead>Uploaded By</TableHead>
|
||||||
|
<TableHead>Date</TableHead>
|
||||||
|
<TableHead>Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{logs.map((log) => (
|
||||||
|
<TableRow key={log.id}>
|
||||||
|
<TableCell>{log.fileName}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{log.userName || log.userEmail || 'Anonymous'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{new Date(log.createdAt).toLocaleString()}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleViewInParser(log.id)}
|
||||||
|
>
|
||||||
|
View in Parser
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => handleDelete(log.id)}
|
||||||
|
disabled={isDeleting}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
30
src/app/(home)/admin/logs/page.tsx
Normal file
30
src/app/(home)/admin/logs/page.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { LogsClient } from '@/app/(home)/admin/logs/logs-client'
|
||||||
|
import { auth } from '@/server/auth'
|
||||||
|
import { Suspense } from 'react'
|
||||||
|
|
||||||
|
export default async function LogsPage() {
|
||||||
|
const session = await auth()
|
||||||
|
const isAdmin = session?.user.role === 'admin'
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
return (
|
||||||
|
<div className={'container mx-auto pt-8'}>
|
||||||
|
<div className={'prose'}>
|
||||||
|
<h1>Forbidden</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'mx-auto flex w-[calc(100%-1rem)] max-w-fd-container flex-col gap-4 pt-16'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<LogsClient />
|
||||||
|
</div>
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -36,7 +36,8 @@ import {
|
|||||||
import { jokers } from '@/shared/jokers'
|
import { jokers } from '@/shared/jokers'
|
||||||
import { useFormatter } from 'next-intl'
|
import { useFormatter } from 'next-intl'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { Fragment, useState } from 'react'
|
import { useSearchParams } from 'next/navigation'
|
||||||
|
import { Fragment, useEffect, useState } from 'react'
|
||||||
import { type PvpBlind, PvpBlindsCard } from './_components/pvp-blinds'
|
import { type PvpBlind, PvpBlindsCard } from './_components/pvp-blinds'
|
||||||
// Define the structure for individual log events within a game
|
// Define the structure for individual log events within a game
|
||||||
type LogEvent = {
|
type LogEvent = {
|
||||||
@@ -171,19 +172,143 @@ function boolStrToText(str: string | boolean | undefined | null): string {
|
|||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to convert date strings to Date objects recursively
|
||||||
|
function convertDates<T>(obj: T): T {
|
||||||
|
if (!obj || typeof obj !== 'object') {
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.map((item) => convertDates(item)) as unknown as T
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = { ...obj } as any
|
||||||
|
|
||||||
|
// Process each property
|
||||||
|
for (const key in result) {
|
||||||
|
const value = result[key]
|
||||||
|
|
||||||
|
// Check if the value is a date string (ISO format)
|
||||||
|
if (
|
||||||
|
typeof value === 'string' &&
|
||||||
|
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)
|
||||||
|
) {
|
||||||
|
result[key] = new Date(value)
|
||||||
|
}
|
||||||
|
// Also handle date strings in the format used in the logs (YYYY-MM-DD HH:MM:SS)
|
||||||
|
else if (
|
||||||
|
typeof value === 'string' &&
|
||||||
|
/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/.test(value)
|
||||||
|
) {
|
||||||
|
result[key] = new Date(value)
|
||||||
|
}
|
||||||
|
// Recursively process nested objects and arrays
|
||||||
|
else if (value && typeof value === 'object') {
|
||||||
|
result[key] = convertDates(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// Main component
|
// Main component
|
||||||
export default function LogParser() {
|
export default function LogParser() {
|
||||||
const formatter = useFormatter()
|
const formatter = useFormatter()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
const [parsedGames, setParsedGames] = useState<Game[]>([])
|
const [parsedGames, setParsedGames] = useState<Game[]>([])
|
||||||
|
console.log(parsedGames)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
const parseLogFile = async (file: File) => {
|
// Check for logId query parameter and load the parsed data if it exists
|
||||||
|
useEffect(() => {
|
||||||
|
const logId = searchParams.get('logId')
|
||||||
|
const fileUrl = searchParams.get('fileUrl')
|
||||||
|
|
||||||
|
if (logId) {
|
||||||
|
// If logId is provided, fetch the parsed data from the database
|
||||||
|
setIsLoading(true)
|
||||||
|
setError(null)
|
||||||
|
setParsedGames([])
|
||||||
|
|
||||||
|
fetch(`/api/logs?id=${logId}`)
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch log file')
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
// Use the parsed JSON data directly from the database
|
||||||
|
if (data.parsedJson && Array.isArray(data.parsedJson)) {
|
||||||
|
const parsedGamesWithDates = convertDates(data.parsedJson)
|
||||||
|
setParsedGames(parsedGamesWithDates)
|
||||||
|
} else {
|
||||||
|
setError('No parsed games found in the log file.')
|
||||||
|
}
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Error loading log file:', err)
|
||||||
|
setError(`Failed to load log file: ${err.message}`)
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
} else if (fileUrl) {
|
||||||
|
// For backward compatibility, still support fileUrl
|
||||||
|
// But this should be deprecated in favor of logId
|
||||||
|
setIsLoading(true)
|
||||||
|
setError(null)
|
||||||
|
setParsedGames([])
|
||||||
|
|
||||||
|
fetch(fileUrl)
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch log file')
|
||||||
|
}
|
||||||
|
return response.text()
|
||||||
|
})
|
||||||
|
.then((content) => {
|
||||||
|
// Create a File object from the content
|
||||||
|
const file = new File([content], 'log.txt', { type: 'text/plain' })
|
||||||
|
// Parse the file
|
||||||
|
parseLogFile(file, true)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Error loading log file:', err)
|
||||||
|
setError(`Failed to load log file: ${err.message}`)
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [searchParams])
|
||||||
|
|
||||||
|
const parseLogFile = async (file: File, skipUpload?: boolean) => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
setParsedGames([])
|
setParsedGames([])
|
||||||
|
let logFileId = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Create a FormData object to send the file
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
|
if (!skipUpload) {
|
||||||
|
const response = await fetch('/api/logs/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json()
|
||||||
|
throw new Error(errorData.error || 'Failed to upload log file')
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseData = await response.json()
|
||||||
|
logFileId = responseData.id
|
||||||
|
}
|
||||||
|
// Upload the file to the server
|
||||||
|
|
||||||
|
// Get the file content
|
||||||
const content = await file.text()
|
const content = await file.text()
|
||||||
const logLines = content.split('\n')
|
const logLines = content.split('\n')
|
||||||
|
|
||||||
@@ -727,8 +852,10 @@ export default function LogParser() {
|
|||||||
lastEventTime ?? lastProcessedTimestamp ?? currentGame.startDate // Fallback chain
|
lastEventTime ?? lastProcessedTimestamp ?? currentGame.startDate // Fallback chain
|
||||||
}
|
}
|
||||||
currentGame.durationSeconds = currentGame.endDate
|
currentGame.durationSeconds = currentGame.endDate
|
||||||
? (currentGame.endDate.getTime() - currentGame.startDate.getTime()) /
|
? (currentGame.endDate instanceof Date
|
||||||
1000
|
? currentGame.endDate.getTime()
|
||||||
|
: new Date(currentGame.endDate).getTime() -
|
||||||
|
currentGame.startDate.getTime()) / 1000
|
||||||
: null
|
: null
|
||||||
games.push(currentGame)
|
games.push(currentGame)
|
||||||
}
|
}
|
||||||
@@ -737,7 +864,26 @@ export default function LogParser() {
|
|||||||
setError('No games found in the log file.')
|
setError('No games found in the log file.')
|
||||||
}
|
}
|
||||||
|
|
||||||
setParsedGames(games)
|
// Send the parsed games to the server
|
||||||
|
if (!skipUpload) {
|
||||||
|
console.log('Sending parsed games to server...')
|
||||||
|
const uploadResponse = await fetch('/api/logs/upload', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
logFileId,
|
||||||
|
parsedGames: games,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!uploadResponse.ok) {
|
||||||
|
console.error('Failed to save parsed games')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setParsedGames(convertDates(games))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error parsing log:', err)
|
console.error('Error parsing log:', err)
|
||||||
setError(
|
setError(
|
||||||
@@ -842,16 +988,26 @@ export default function LogParser() {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Started:{' '}
|
Started:{' '}
|
||||||
{formatter.dateTime(game.startDate, {
|
{formatter.dateTime(
|
||||||
dateStyle: 'short',
|
game.startDate instanceof Date
|
||||||
timeStyle: 'short',
|
? game.startDate
|
||||||
})}{' '}
|
: new Date(game.startDate),
|
||||||
|
{
|
||||||
|
dateStyle: 'short',
|
||||||
|
timeStyle: 'short',
|
||||||
|
}
|
||||||
|
)}{' '}
|
||||||
| Ended:{' '}
|
| Ended:{' '}
|
||||||
{game.endDate
|
{game.endDate
|
||||||
? formatter.dateTime(game.endDate, {
|
? formatter.dateTime(
|
||||||
dateStyle: 'short',
|
game.endDate instanceof Date
|
||||||
timeStyle: 'short',
|
? game.endDate
|
||||||
})
|
: new Date(game.endDate),
|
||||||
|
{
|
||||||
|
dateStyle: 'short',
|
||||||
|
timeStyle: 'short',
|
||||||
|
}
|
||||||
|
)
|
||||||
: 'N/A'}{' '}
|
: 'N/A'}{' '}
|
||||||
| Duration: {formatDuration(game.durationSeconds)}
|
| Duration: {formatDuration(game.durationSeconds)}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
@@ -966,9 +1122,14 @@ export default function LogParser() {
|
|||||||
<span
|
<span
|
||||||
className={`${event.text.includes('Opponent') ? 'ml-2' : 'mr-2'} font-mono`}
|
className={`${event.text.includes('Opponent') ? 'ml-2' : 'mr-2'} font-mono`}
|
||||||
>
|
>
|
||||||
{formatter.dateTime(event.timestamp, {
|
{formatter.dateTime(
|
||||||
timeStyle: 'medium',
|
event.timestamp instanceof Date
|
||||||
})}
|
? event.timestamp
|
||||||
|
: new Date(event.timestamp),
|
||||||
|
{
|
||||||
|
timeStyle: 'medium',
|
||||||
|
}
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span>{event.text}</span>
|
<span>{event.text}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
111
src/app/api/logs/route.ts
Normal file
111
src/app/api/logs/route.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { auth } from '@/server/auth'
|
||||||
|
import { db } from '@/server/db'
|
||||||
|
import { logFiles, users } from '@/server/db/schema'
|
||||||
|
import { eq } from 'drizzle-orm'
|
||||||
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
// Get the log file ID from the request
|
||||||
|
const { searchParams } = new URL(req.url)
|
||||||
|
const id = searchParams.get('id')
|
||||||
|
|
||||||
|
// Check if user is authenticated
|
||||||
|
const session = await auth()
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
// Fetching a specific log file by ID
|
||||||
|
// For specific log files, we allow access to the owner or admins
|
||||||
|
const logFile = await db
|
||||||
|
.select({
|
||||||
|
id: logFiles.id,
|
||||||
|
fileName: logFiles.fileName,
|
||||||
|
fileUrl: logFiles.fileUrl,
|
||||||
|
parsedJson: logFiles.parsedJson,
|
||||||
|
createdAt: logFiles.createdAt,
|
||||||
|
userId: logFiles.userId,
|
||||||
|
})
|
||||||
|
.from(logFiles)
|
||||||
|
.where(eq(logFiles.id, Number.parseInt(id)))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (logFile.length === 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Log file not found' },
|
||||||
|
{ status: 404 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is authorized to access this log file
|
||||||
|
// Allow access if user is admin or the owner of the log file
|
||||||
|
if (
|
||||||
|
!session ||
|
||||||
|
(session.user.role !== 'admin' &&
|
||||||
|
logFile?.[0]?.userId !== session.user.id)
|
||||||
|
) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(logFile[0])
|
||||||
|
}
|
||||||
|
// Fetching all log files (admin only)
|
||||||
|
if (!session || session.user.role !== 'admin') {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all log files with user information
|
||||||
|
const logs = await db
|
||||||
|
.select({
|
||||||
|
id: logFiles.id,
|
||||||
|
fileName: logFiles.fileName,
|
||||||
|
fileUrl: logFiles.fileUrl,
|
||||||
|
createdAt: logFiles.createdAt,
|
||||||
|
userId: logFiles.userId,
|
||||||
|
userName: users.name,
|
||||||
|
userEmail: users.email,
|
||||||
|
})
|
||||||
|
.from(logFiles)
|
||||||
|
.leftJoin(users, eq(logFiles.userId, users.id))
|
||||||
|
.orderBy(logFiles.createdAt)
|
||||||
|
|
||||||
|
return NextResponse.json(logs)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching log files:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to fetch log files' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
// Check if user is authenticated and is an admin
|
||||||
|
const session = await auth()
|
||||||
|
if (!session || session.user.role !== 'admin') {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the log file ID from the request
|
||||||
|
const { searchParams } = new URL(req.url)
|
||||||
|
const id = searchParams.get('id')
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Log file ID is required' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the log file from the database
|
||||||
|
await db.delete(logFiles).where(eq(logFiles.id, Number.parseInt(id)))
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting log file:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to delete log file' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
101
src/app/api/logs/upload/route.ts
Normal file
101
src/app/api/logs/upload/route.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { auth } from '@/server/auth'
|
||||||
|
import { db } from '@/server/db'
|
||||||
|
import { logFiles } from '@/server/db/schema'
|
||||||
|
import { uploadFile } from '@/server/minio'
|
||||||
|
import { eq } from 'drizzle-orm'
|
||||||
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
// Check if user is authenticated (optional)
|
||||||
|
const session = await auth()
|
||||||
|
const userId = session?.user?.id
|
||||||
|
|
||||||
|
// Parse the multipart form data
|
||||||
|
const formData = await req.formData()
|
||||||
|
const file = formData.get('file') as File | null
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return NextResponse.json({ error: 'No file provided' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the file to a buffer and text
|
||||||
|
const buffer = Buffer.from(await file.arrayBuffer())
|
||||||
|
const fileContent = await file.text()
|
||||||
|
|
||||||
|
// Upload the file to MinIO
|
||||||
|
const fileUrl = await uploadFile(buffer, file.name, file.type)
|
||||||
|
|
||||||
|
// Store the information in the database with an empty JSON object for now
|
||||||
|
// The actual parsed games will be updated via PUT request
|
||||||
|
const [logFile] = await db
|
||||||
|
.insert(logFiles)
|
||||||
|
.values({
|
||||||
|
userId,
|
||||||
|
fileName: file.name,
|
||||||
|
fileUrl,
|
||||||
|
parsedJson: {},
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
if (!logFile) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'This should never happen, hopefully' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Return the log file information
|
||||||
|
return NextResponse.json({
|
||||||
|
id: logFile.id,
|
||||||
|
fileName: logFile.fileName,
|
||||||
|
fileUrl: logFile.fileUrl,
|
||||||
|
createdAt: logFile.createdAt,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error uploading log file:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to upload log file' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PUT(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
// Check if user is authenticated (optional)
|
||||||
|
const session = await auth()
|
||||||
|
|
||||||
|
// Parse the JSON data
|
||||||
|
const data = await req.json()
|
||||||
|
const { logFileId, parsedGames } = data
|
||||||
|
|
||||||
|
if (!logFileId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'No log file ID provided' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsedGames || !Array.isArray(parsedGames)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Invalid parsed games data' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the log file record with the parsed games
|
||||||
|
await db
|
||||||
|
.update(logFiles)
|
||||||
|
.set({
|
||||||
|
parsedJson: parsedGames,
|
||||||
|
})
|
||||||
|
.where(eq(logFiles.id, logFileId))
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating parsed games:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to update parsed games' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -163,3 +163,19 @@ export const releasesRelations = relations(releases, ({ one }) => ({
|
|||||||
references: [branches.id],
|
references: [branches.id],
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
export const logFiles = pgTable('log_files', {
|
||||||
|
id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
|
||||||
|
userId: text('user_id').references(() => users.id),
|
||||||
|
fileName: text('file_name').notNull(),
|
||||||
|
fileUrl: text('file_url').notNull(),
|
||||||
|
parsedJson: json('parsed_json').notNull(),
|
||||||
|
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const logFilesRelations = relations(logFiles, ({ one }) => ({
|
||||||
|
user: one(users, {
|
||||||
|
fields: [logFiles.userId],
|
||||||
|
references: [users.id],
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|||||||
Reference in New Issue
Block a user