This commit is contained in:
Linrador 2025-06-09 13:21:06 +02:00
parent b66eec433b
commit 63becfbc3a
79 changed files with 1150 additions and 878 deletions

99
package-lock.json generated
View File

@ -14,7 +14,7 @@
"@fortawesome/react-fontawesome": "^0.2.2",
"@preline/dropdown": "^3.0.1",
"@preline/tooltip": "^3.0.0",
"@prisma/client": "^6.7.0",
"@prisma/client": "^6.9.0",
"csgo-sharecode": "^3.1.2",
"datatables.net": "^2.2.2",
"date-fns": "^4.1.0",
@ -51,7 +51,7 @@
"@types/ws": "^8.18.1",
"eslint": "^9",
"eslint-config-next": "15.3.0",
"prisma": "^6.7.0",
"prisma": "^6.9.0",
"tailwindcss": "^4.1.4",
"ts-node": "^10.9.2",
"tsx": "^4.19.4",
@ -1522,9 +1522,9 @@
"license": "Licensed under MIT and Preline UI Fair Use License"
},
"node_modules/@prisma/client": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.7.0.tgz",
"integrity": "sha512-+k61zZn1XHjbZul8q6TdQLpuI/cvyfil87zqK2zpreNIXyXtpUv3+H/oM69hcsFcZXaokHJIzPAt5Z8C8eK2QA==",
"version": "6.9.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.9.0.tgz",
"integrity": "sha512-Gg7j1hwy3SgF1KHrh0PZsYvAaykeR0PaxusnLXydehS96voYCGt1U5zVR31NIouYc63hWzidcrir1a7AIyCsNQ==",
"hasInstallScript": true,
"license": "Apache-2.0",
"engines": {
@ -1544,64 +1544,63 @@
}
},
"node_modules/@prisma/config": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.7.0.tgz",
"integrity": "sha512-di8QDdvSz7DLUi3OOcCHSwxRNeW7jtGRUD2+Z3SdNE3A+pPiNT8WgUJoUyOwJmUr5t+JA2W15P78C/N+8RXrOA==",
"version": "6.9.0",
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.9.0.tgz",
"integrity": "sha512-Wcfk8/lN3WRJd5w4jmNQkUwhUw0eksaU/+BlAJwPQKW10k0h0LC9PD/6TQFmqKVbHQL0vG2z266r0S1MPzzhbA==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"esbuild": ">=0.12 <1",
"esbuild-register": "3.6.0"
"jiti": "2.4.2"
}
},
"node_modules/@prisma/debug": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.7.0.tgz",
"integrity": "sha512-RabHn9emKoYFsv99RLxvfG2GHzWk2ZI1BuVzqYtmMSIcuGboHY5uFt3Q3boOREM9de6z5s3bQoyKeWnq8Fz22w==",
"version": "6.9.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.9.0.tgz",
"integrity": "sha512-bFeur/qi/Q+Mqk4JdQ3R38upSYPebv5aOyD1RKywVD+rAMLtRkmTFn28ZuTtVOnZHEdtxnNOCH+bPIeSGz1+Fg==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/engines": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.7.0.tgz",
"integrity": "sha512-3wDMesnOxPrOsq++e5oKV9LmIiEazFTRFZrlULDQ8fxdub5w4NgRBoxtWbvXmj2nJVCnzuz6eFix3OhIqsZ1jw==",
"version": "6.9.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.9.0.tgz",
"integrity": "sha512-im0X0bwDLA0244CDf8fuvnLuCQcBBdAGgr+ByvGfQY9wWl6EA+kRGwVk8ZIpG65rnlOwtaWIr/ZcEU5pNVvq9g==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.7.0",
"@prisma/engines-version": "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed",
"@prisma/fetch-engine": "6.7.0",
"@prisma/get-platform": "6.7.0"
"@prisma/debug": "6.9.0",
"@prisma/engines-version": "6.9.0-10.81e4af48011447c3cc503a190e86995b66d2a28e",
"@prisma/fetch-engine": "6.9.0",
"@prisma/get-platform": "6.9.0"
}
},
"node_modules/@prisma/engines-version": {
"version": "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed.tgz",
"integrity": "sha512-EvpOFEWf1KkJpDsBCrih0kg3HdHuaCnXmMn7XFPObpFTzagK1N0Q0FMnYPsEhvARfANP5Ok11QyoTIRA2hgJTA==",
"version": "6.9.0-10.81e4af48011447c3cc503a190e86995b66d2a28e",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.9.0-10.81e4af48011447c3cc503a190e86995b66d2a28e.tgz",
"integrity": "sha512-Qp9gMoBHgqhKlrvumZWujmuD7q4DV/gooEyPCLtbkc13EZdSz2RsGUJ5mHb3RJgAbk+dm6XenqG7obJEhXcJ6Q==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/fetch-engine": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.7.0.tgz",
"integrity": "sha512-zLlAGnrkmioPKJR4Yf7NfW3hftcvqeNNEHleMZK9yX7RZSkhmxacAYyfGsCcqRt47jiZ7RKdgE0Wh2fWnm7WsQ==",
"version": "6.9.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.9.0.tgz",
"integrity": "sha512-PMKhJdl4fOdeE3J3NkcWZ+tf3W6rx3ht/rLU8w4SXFRcLhd5+3VcqY4Kslpdm8osca4ej3gTfB3+cSk5pGxgFg==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.7.0",
"@prisma/engines-version": "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed",
"@prisma/get-platform": "6.7.0"
"@prisma/debug": "6.9.0",
"@prisma/engines-version": "6.9.0-10.81e4af48011447c3cc503a190e86995b66d2a28e",
"@prisma/get-platform": "6.9.0"
}
},
"node_modules/@prisma/get-platform": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.7.0.tgz",
"integrity": "sha512-i9IH5lO4fQwnMLvQLYNdgVh9TK3PuWBfQd7QLk/YurnAIg+VeADcZDbmhAi4XBBDD+hDif9hrKyASu0hbjwabw==",
"version": "6.9.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.9.0.tgz",
"integrity": "sha512-/B4n+5V1LI/1JQcHp+sUpyRT1bBgZVPHbsC4lt4/19Xp4jvNIVcq5KYNtQDk5e/ukTSjo9PZVAxxy9ieFtlpTQ==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.7.0"
"@prisma/debug": "6.9.0"
}
},
"node_modules/@rtsao/scc": {
@ -3159,7 +3158,7 @@
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@ -3477,7 +3476,7 @@
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz",
"integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==",
"devOptional": true,
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
@ -3514,19 +3513,6 @@
"@esbuild/win32-x64": "0.25.3"
}
},
"node_modules/esbuild-register": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz",
"integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"debug": "^4.3.4"
},
"peerDependencies": {
"esbuild": ">=0.12 <1"
}
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@ -4988,7 +4974,7 @@
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
"dev": true,
"devOptional": true,
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
@ -5553,7 +5539,7 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
@ -6223,15 +6209,15 @@
"peer": true
},
"node_modules/prisma": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.7.0.tgz",
"integrity": "sha512-vArg+4UqnQ13CVhc2WUosemwh6hr6cr6FY2uzDvCIFwH8pu8BXVv38PktoMLVjtX7sbYThxbnZF5YiR8sN2clw==",
"version": "6.9.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.9.0.tgz",
"integrity": "sha512-resJAwMyZREC/I40LF6FZ6rZTnlrlrYrb63oW37Gq+U+9xHwbyMSPJjKtM7VZf3gTO86t/Oyz+YeSXr3CmAY1Q==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/config": "6.7.0",
"@prisma/engines": "6.7.0"
"@prisma/config": "6.9.0",
"@prisma/engines": "6.9.0"
},
"bin": {
"prisma": "build/index.js"
@ -6239,9 +6225,6 @@
"engines": {
"node": ">=18.18"
},
"optionalDependencies": {
"fsevents": "2.3.3"
},
"peerDependencies": {
"typescript": ">=5.1.0"
},

View File

@ -17,7 +17,7 @@
"@fortawesome/react-fontawesome": "^0.2.2",
"@preline/dropdown": "^3.0.1",
"@preline/tooltip": "^3.0.0",
"@prisma/client": "^6.7.0",
"@prisma/client": "^6.9.0",
"csgo-sharecode": "^3.1.2",
"datatables.net": "^2.2.2",
"date-fns": "^4.1.0",
@ -54,7 +54,7 @@
"@types/ws": "^8.18.1",
"eslint": "^9",
"eslint-config-next": "15.3.0",
"prisma": "^6.7.0",
"prisma": "^6.9.0",
"tailwindcss": "^4.1.4",
"ts-node": "^10.9.2",
"tsx": "^4.19.4",

View File

@ -54,7 +54,7 @@ model Team {
}
model Match {
matchId BigInt @id @default(autoincrement())
id String @id @default(uuid())
teamAId String?
teamBId String?
matchDate DateTime
@ -78,11 +78,11 @@ model Match {
model MatchPlayer {
id String @id @default(cuid())
matchId BigInt
matchId String
steamId String
teamId String?
match Match @relation(fields: [matchId], references: [matchId])
match Match @relation(fields: [matchId], references: [id]) // 👈 id statt matchId
user User @relation(fields: [steamId], references: [steamId])
team Team? @relation(fields: [teamId], references: [id])
stats MatchPlayerStats?
@ -92,6 +92,7 @@ model MatchPlayer {
@@unique([matchId, steamId])
}
model MatchPlayerStats {
id String @id @default(cuid())
matchPlayerId String @unique
@ -123,17 +124,18 @@ model MatchPlayerStats {
model DemoFile {
id String @id @default(cuid())
matchId BigInt @unique
matchId String @unique
steamId String
fileName String @unique
filePath String
parsed Boolean @default(false)
createdAt DateTime @default(now())
match Match @relation(fields: [matchId], references: [matchId])
match Match @relation(fields: [matchId], references: [id]) // 👈 id statt matchId
user User @relation(fields: [steamId], references: [steamId])
}
model Invitation {
id String @id @default(cuid())
userId String
@ -163,10 +165,11 @@ model CS2MatchRequest {
id String @id @default(cuid())
userId String
steamId String
matchId BigInt
matchId String
reservationId BigInt
tvPort BigInt
processed Boolean @default(false)
failed Boolean @default(false)
createdAt DateTime @default(now())
user User @relation("MatchRequests", fields: [userId], references: [steamId])
@ -174,11 +177,12 @@ model CS2MatchRequest {
@@unique([steamId, matchId])
}
model PremierRankHistory {
id String @id @default(cuid())
userId String
steamId String
matchId BigInt?
matchId String? // optionaler String
rankOld Int
rankNew Int
@ -187,5 +191,5 @@ model PremierRankHistory {
createdAt DateTime @default(now())
user User @relation("UserRankHistory", fields: [userId], references: [steamId])
match Match? @relation("MatchRankHistory", fields: [matchId], references: [matchId])
}
match Match? @relation("MatchRankHistory", fields: [matchId], references: [id]) // 👈 id statt matchId
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1,36 +1,67 @@
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/app/lib/auth'
import { prisma } from '@/app/lib/prisma'
import { decrypt, encrypt } from '@/app/lib/crypto'
import { decodeMatchShareCode, MatchInformation } from 'csgo-sharecode';
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/app/lib/prisma';
import { decodeMatchShareCode } from 'csgo-sharecode';
import { decrypt } from '@/app/lib/crypto';
function delay(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
}
// Maximal 30 Tage gültig
const EXPIRY_DAYS = 30;
export async function GET(req: NextRequest) {
const session = await getServerSession(authOptions(req))
let steamId = session?.user?.steamId ?? req.headers.get('x-steamid') ?? undefined;
const steamId = req.headers.get('x-steamid') ?? undefined;
if (!steamId) {
return NextResponse.json({ valid: false, reason: 'Missing steamId' });
}
const user = await prisma.user.findUnique({
where: { steamId },
select: {
authCode: true,
lastKnownShareCode: true,
lastKnownShareCodeDate: true,
},
});
if (!user?.authCode || !user.lastKnownShareCode) {
return NextResponse.json({ valid: false, reason: 'missing-sharecode' });
}
const isExpired =
user.lastKnownShareCodeDate &&
new Date().getTime() - new Date(user.lastKnownShareCodeDate).getTime() >
EXPIRY_DAYS * 24 * 60 * 60 * 1000;
if (isExpired) {
return NextResponse.json({ valid: false, reason: 'expired' });
}
return handleShareCodeRequest(
steamId,
decrypt(user.authCode),
user.lastKnownShareCode
);
}
export async function POST(req: NextRequest) {
const body = await req.json();
const steamId: string = body.steamId;
const authCode: string = body.authCode;
const currentCode: string = body.currentCode;
if (!steamId || !authCode || !currentCode) {
return NextResponse.json({ valid: false, error: 'Missing parameters' });
}
return handleShareCodeRequest(steamId, authCode, currentCode);
}
async function handleShareCodeRequest(
steamId: string,
authCode: string,
knownCode: string
) {
try {
const user = await prisma.user.findUnique({
where: { steamId },
select: { authCode: true, lastKnownShareCode: true },
});
if (!user?.authCode || !user.lastKnownShareCode) {
return NextResponse.json({ valid: false });
}
const decryptedAuthCode = decrypt(user.authCode);
// Nur EINEN nächsten Code abrufen
const url = `https://api.steampowered.com/ICSGOPlayers_730/GetNextMatchSharingCode/v1?key=${process.env.STEAM_API_KEY}&steamid=${steamId}&steamidkey=${decryptedAuthCode}&knowncode=${user.lastKnownShareCode}`;
const url = `https://api.steampowered.com/ICSGOPlayers_730/GetNextMatchSharingCode/v1?key=${process.env.STEAM_API_KEY}&steamid=${steamId}&steamidkey=${authCode}&knowncode=${knownCode}`;
const res = await fetch(url);
const data = await res.json();
@ -41,42 +72,29 @@ export async function GET(req: NextRequest) {
return NextResponse.json({ valid: true, nextCode: null });
}
// MatchInfo extrahieren & speichern
const matchInfo = decodeMatchShareCode(nextCode);
await prisma.cS2MatchRequest.upsert({
where: {
steamId_matchId: {
steamId,
matchId: matchInfo.matchId,
matchId: matchInfo.matchId.toString(),
},
},
update: {},
create: {
userId: steamId,
steamId: steamId,
matchId: matchInfo.matchId,
matchId: matchInfo.matchId.toString(),
reservationId: matchInfo.reservationId,
tvPort: matchInfo.tvPort,
},
});
return NextResponse.json({
valid: true,
nextCode,
});
return NextResponse.json({ valid: true, nextCode });
} catch (err) {
if (err instanceof Error && err.message === 'INVALID_CODE') {
return NextResponse.json({
valid: false,
error: 'Invalid authCode or knownCode (veraltet oder ungültig)',
});
}
return NextResponse.json({
valid: false,
error: err instanceof Error ? err.message : String(err),
});
}
}

View File

@ -1,40 +1,13 @@
// /api/cs2/sharecode/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/app/lib/auth'
import { prisma } from '@/app/lib/prisma'
import { encrypt, decrypt } from '@/app/lib/crypto'
import { decrypt, encrypt } from '@/app/lib/crypto'
export async function PUT(req: NextRequest) {
const session = await getServerSession(authOptions(req))
const steamId = session?.user?.steamId
if (!steamId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { authCode, lastKnownShareCode } = await req.json()
if (!authCode || typeof authCode !== 'string') {
return NextResponse.json({ error: 'Ungültiger Auth Code' }, { status: 400 })
}
try {
await prisma.user.update({
where: { steamId },
data: {
authCode: encrypt(authCode),
lastKnownShareCode: lastKnownShareCode || undefined,
lastKnownShareCodeDate: lastKnownShareCode ? new Date() : undefined,
},
})
return new NextResponse(null, { status: 204 })
} catch (error) {
console.error('Fehler beim Speichern:', error)
return NextResponse.json({ error: 'Fehler beim Speichern der Codes' }, { status: 500 })
}
}
// Maximal 30 Tage gültig
const EXPIRY_DAYS = 30
export async function GET(req: NextRequest) {
const session = await getServerSession(authOptions(req))
@ -54,13 +27,62 @@ export async function GET(req: NextRequest) {
},
})
const authCode = user?.authCode ? decrypt(user.authCode) : null
const lastKnownShareCode = user?.lastKnownShareCode ?? null
const lastKnownShareCodeDate = user?.lastKnownShareCodeDate ?? null
let reason: 'expired' | null = null
if (
lastKnownShareCodeDate &&
new Date().getTime() - new Date(lastKnownShareCodeDate).getTime() > EXPIRY_DAYS * 24 * 60 * 60 * 1000
) {
reason = 'expired'
}
return NextResponse.json({
authCode: user?.authCode ? decrypt(user.authCode) : null,
lastKnownShareCode: user?.lastKnownShareCode ?? null,
lastKnownShareCodeDate: user?.lastKnownShareCodeDate ?? null,
authCode,
lastKnownShareCode,
lastKnownShareCodeDate,
reason,
})
} catch (error) {
console.error('Fehler beim Abrufen:', error)
return NextResponse.json({ error: 'Fehler beim Abrufen der Codes' }, { status: 500 })
console.error('[GET /api/cs2/sharecode]', error)
return NextResponse.json({ error: 'Fehler beim Abrufen' }, { status: 500 })
}
}
export async function PUT(req: NextRequest) {
const session = await getServerSession(authOptions(req))
const steamId = session?.user?.steamId
if (!steamId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { authCode, lastKnownShareCode } = await req.json()
// Optional: zusätzliche Validierung für authCode
const isValidAuthCode = !authCode || /^[A-Z0-9]{4}-[A-Z0-9]{5}-[A-Z0-9]{4}$/.test(authCode)
const isValidShareCode = !lastKnownShareCode || /^CSGO(-[a-zA-Z0-9]{5}){5}$/.test(lastKnownShareCode)
if (!isValidShareCode) {
return NextResponse.json({ error: 'expired-sharecode' }, { status: 400 })
}
try {
await prisma.user.update({
where: { steamId },
data: {
authCode: authCode && isValidAuthCode ? encrypt(authCode) : undefined,
lastKnownShareCode: lastKnownShareCode || undefined,
lastKnownShareCodeDate: lastKnownShareCode ? new Date() : undefined,
},
})
return new NextResponse(null, { status: 204 })
} catch (error) {
console.error('[PUT /api/cs2/sharecode]', error)
return NextResponse.json({ error: 'Fehler beim Speichern' }, { status: 500 })
}
}

View File

@ -14,7 +14,7 @@ export async function GET(_: Request, { params }: Params) {
try {
const match = await prisma.match.findUnique({
where: { matchId: BigInt(id) },
where: { id },
include: {
teamA: true,
teamB: true,
@ -58,7 +58,7 @@ export async function PUT(req: NextRequest, { params }: Params) {
const { title, description, matchDate, players } = body
const match = await prisma.match.findUnique({
where: { matchId: BigInt(id) },
where: { id },
include: {
teamA: { include: { leader: true } },
teamB: { include: { leader: true } },
@ -94,7 +94,7 @@ export async function PUT(req: NextRequest, { params }: Params) {
}
try {
await prisma.matchPlayer.deleteMany({ where: { matchId: BigInt(id) } })
await prisma.matchPlayer.deleteMany({ where: { id } })
await prisma.matchPlayer.createMany({
data: players.map((p: any) => ({
@ -105,7 +105,7 @@ export async function PUT(req: NextRequest, { params }: Params) {
})
const updated = await prisma.match.update({
where: { matchId: BigInt(id) },
where: { id },
data: {
title,
description,
@ -159,7 +159,7 @@ export async function DELETE(req: NextRequest, { params }: Params) {
const { id } = params
try {
await prisma.match.delete({ where: { matchId: BigInt(id) } })
await prisma.match.delete({ where: { id } })
return NextResponse.json({ success: true })
} catch (err) {
console.error(`DELETE /matches/${id} failed:`, err)

View File

@ -46,12 +46,12 @@ export async function POST (req: NextRequest) {
/* 2. Spieler-Datensätze vorbereiten */
const playersData = [
...teamA.activePlayers.map((steamId: string) => ({
matchId: newMatch.matchId,
matchId: newMatch.id,
steamId,
teamId : teamAId
})),
...teamB.activePlayers.map((steamId: string) => ({
matchId: newMatch.matchId,
matchId: newMatch.id,
steamId,
teamId : teamBId
}))

View File

@ -37,7 +37,7 @@ export async function POST(req: NextRequest) {
await prisma.notification.create({
data: {
userId: leader,
userId: leader.steamId,
title: 'Team erstellt',
message: `Du hast erfolgreich das Team "${teamname}" erstellt.`,
},

View File

@ -0,0 +1,58 @@
// /app/api/user/matches/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/lib/auth';
import { prisma } from '@/app/lib/prisma';
export async function GET(req: NextRequest) {
const session = await getServerSession(authOptions(req));
const steamId = session?.user?.steamId;
if (!steamId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const matchPlayers = await prisma.matchPlayer.findMany({
where: { steamId },
include: {
match: {
select: {
matchDate: true,
map: true,
scoreA: true,
scoreB: true,
matchType: true,
rankUpdates: true,
},
},
stats: true,
},
orderBy: {
match: {
matchDate: 'desc',
},
},
});
const data = matchPlayers.map((mp) => {
const isTeamA = mp.teamId === mp.match.teamAId;
const kills = mp.stats?.kills ?? 0;
const deaths = mp.stats?.deaths ?? 0;
const kd = deaths > 0 ? (kills / deaths).toFixed(2) : '∞';
return {
map: mp.match.map ?? 'Unknown',
date: mp.match.matchDate,
score: `${mp.match.scoreA ?? 0} : ${mp.match.scoreB ?? 0}`,
isTeamA,
rankNew: mp.stats?.rankNew ?? null,
rankOld: mp.stats?.rankOld ?? null,
rating: (mp.stats?.adr ?? 0).toFixed(2), // optional: hier besser eigener Rating-Alg.
kills,
deaths,
kd,
};
});
return NextResponse.json(data);
}

View File

@ -23,7 +23,7 @@ function getRoundedDate() {
}
function getTeamLogo(logo?: string | null) {
return logo ? `/assets/img/logos/${logo}` : '/default-logo.png'
return logo ? `/assets/img/logos/${logo}` : '/assets/img/logos/cs2.webp'
}
export default function MatchesAdminManager() {
@ -110,14 +110,14 @@ export default function MatchesAdminManager() {
<div className="flex items-center justify-between text-center">
<div className="flex flex-col items-center w-1/4">
<Image
src={getTeamLogo(match.teamA.logo)}
alt={match.teamA.teamname}
src={getTeamLogo(match.teamA?.logo)}
alt={match.teamA?.teamname || 'Team A'}
width={64}
height={64}
className="rounded-full border object-cover bg-white"
/>
<span className="mt-2 text-sm text-gray-700 dark:text-gray-200">
{match.teamA.teamname}
{match.teamA?.teamname || 'Team A'}
</span>
</div>
<div className="text-sm text-gray-600 dark:text-gray-300 w-1/2">
@ -129,14 +129,14 @@ export default function MatchesAdminManager() {
</div>
<div className="flex flex-col items-center w-1/4">
<Image
src={getTeamLogo(match.teamB.logo)}
alt={match.teamB.teamname}
src={getTeamLogo(match.teamB?.logo)}
alt={match.teamB?.teamname || 'Team B'}
width={64}
height={64}
className="rounded-full border object-cover bg-white"
/>
<span className="mt-2 text-sm text-gray-700 dark:text-gray-200">
{match.teamB.teamname}
{match.teamB?.teamname || 'Team B'}
</span>
</div>
</div>

View File

@ -72,7 +72,8 @@ export default function NotificationCenter() {
'team-left',
'team-member-left',
'team-leader-changed',
'team-join-request'
'team-join-request',
'expired-sharecode'
].includes(data.type)
if (!isNotificationType) return

View File

@ -12,11 +12,11 @@ function TableWrapper({ children }: TableProps) {
<div className="flex flex-col">
<div className="-m-1.5 overflow-x-auto">
<div className="p-1.5 min-w-full inline-block align-middle">
<div className="border border-t-0 border-gray-200 overflow-hidden dark:border-neutral-700">
<table className="min-w-full divide-y divide-gray-200 dark:divide-neutral-700">
{children}
</table>
</div>
<div className="overflow-hidden">
<table className="min-w-full divide-y divide-gray-200 dark:divide-neutral-700">
{children}
</table>
</div>
</div>
</div>
</div>
@ -24,7 +24,7 @@ function TableWrapper({ children }: TableProps) {
}
function Head({ children }: { children: ReactNode }) {
return <thead className="bg-gray-50 dark:bg-neutral-700">{children}</thead>
return <thead className="px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-neutral-500">{children}</thead>
}
function Body({ children }: { children: ReactNode }) {
@ -32,15 +32,16 @@ function Body({ children }: { children: ReactNode }) {
}
function Row({
children,
hoverable = false,
}: {
children: ReactNode
hoverable?: boolean
}) {
const className = hoverable ? 'hover:bg-gray-100 dark:hover:bg-neutral-700' : ''
return <tr className={className}>{children}</tr>
}
children,
hoverable = false,
...rest
}: {
children: ReactNode
hoverable?: boolean
} & React.HTMLAttributes<HTMLTableRowElement>) {
const className = `${hoverable ? 'hover:bg-gray-100 dark:hover:bg-neutral-700' : ''} ${rest.className ?? ''}`
return <tr {...rest} className={className}>{children}</tr>
}
function Cell({

View File

@ -0,0 +1,91 @@
'use client';
import { useEffect, useState } from 'react';
import Table from './Table';
import Link from 'next/link';
import { mapNameMap } from '../lib/mapNameMap';
import { useRouter } from 'next/navigation';
interface Match {
id: string;
map: string;
date: string;
score: string;
rating: string;
kills: number;
deaths: number;
kd: string;
rankNew: number | null;
rankOld: number | null;
}
export default function UserMatchesTable() {
const [matches, setMatches] = useState<Match[]>([]);
const router = useRouter();
useEffect(() => {
fetch('/api/user/matches')
.then((res) => res.json())
.then(setMatches)
.catch(console.error);
}, []);
return (
<Table>
<Table.Head>
<Table.Row>
<Table.Cell as="th">Map</Table.Cell>
<Table.Cell as="th">Date</Table.Cell>
<Table.Cell as="th">Score</Table.Cell>
<Table.Cell as="th">Rank</Table.Cell>
<Table.Cell as="th">Kills</Table.Cell>
<Table.Cell as="th">Deaths</Table.Cell>
<Table.Cell as="th">K/D</Table.Cell>
</Table.Row>
</Table.Head>
<Table.Body>
{matches.map((m) => {
const mapInfo = mapNameMap[m.map] ?? mapNameMap['lobby_mapveto'];
return (
<Table.Row
key={m.id}
hoverable
onClick={() => router.push(`/matches-details/${m.id}`)}
className="cursor-pointer"
>
<Table.Cell>
<div className="flex items-center gap-2">
<img
src={`/assets/img/maps/${m.map}.png`}
alt={mapInfo.name}
className="w-5 h-5 rounded"
/>
{mapInfo.name}
</div>
</Table.Cell>
<Table.Cell>{new Date(m.date).toLocaleString()}</Table.Cell>
<Table.Cell>{m.score}</Table.Cell>
<Table.Cell>
{m.rankNew !== null ? (
m.rankOld !== null ? (
<span className={m.rankNew > m.rankOld ? 'text-green-500' : m.rankNew < m.rankOld ? 'text-red-500' : ''}>
{m.rankNew > m.rankOld ? '+' : ''}
{m.rankNew - m.rankOld}
</span>
) : (
m.rankNew
)
) : (
'—'
)}
</Table.Cell>
<Table.Cell>{m.kills}</Table.Cell>
<Table.Cell>{m.deaths}</Table.Cell>
<Table.Cell>{m.kd}</Table.Cell>
</Table.Row>
);
})}
</Table.Body>
</Table>
);
}

View File

@ -1,6 +1,6 @@
'use client'
import { useEffect, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useSession } from 'next-auth/react'
import Link from 'next/link'
import Popover from '../../Popover'
@ -8,17 +8,24 @@ import LatestKnownCodeSettings from './LatestKnownCodeSettings'
export default function AuthCodeSettings() {
const { data: session } = useSession()
const [userId, setUserId] = useState<string | null>(null)
const [authCode, setAuthCode] = useState('')
const [authCodeValid, setAuthCodeValid] = useState(false)
const [lastKnownShareCode, setLastKnownShareCode] = useState('')
const [lastKnownShareCodeValid, setLastKnownShareCodeValid] = useState(false)
const [lastKnownShareCodeDate, setLastKnownShareCodeDate] = useState<Date | null>(null)
const [shareCodeManuallySet, setShareCodeManuallySet] = useState(false)
const [shareCodeSaved, setShareCodeSaved] = useState(false)
const [touched, setTouched] = useState(false)
const [isLoading, setIsLoading] = useState(true)
const shareCodeExpired = useMemo(() => {
if (!lastKnownShareCodeDate) return false
const daysSince = (Date.now() - new Date(lastKnownShareCodeDate).getTime()) / (1000 * 60 * 60 * 24)
return daysSince > 30
}, [lastKnownShareCodeDate])
const formatAuthCode = (value: string) => {
const raw = value.replace(/[^A-Za-z0-9]/g, '').toUpperCase()
const part1 = raw.slice(0, 4)
@ -37,7 +44,7 @@ export default function AuthCodeSettings() {
if (!validateAuthCode(updatedAuthCode) || !validateShareCode(updatedKnownCode)) return
try {
await fetch('/api/cs2/sharecode', {
const res = await fetch('/api/cs2/sharecode', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@ -45,11 +52,26 @@ export default function AuthCodeSettings() {
lastKnownShareCode: updatedKnownCode,
}),
})
if (res.ok) {
setLastKnownShareCodeDate(new Date())
setShareCodeSaved(true)
}
} catch (err) {
console.error('Fehler beim Speichern der Codes:', err)
}
}
const handleSetShareCode = (val: string) => {
setLastKnownShareCode(val)
setShareCodeManuallySet(true)
setShareCodeSaved(false)
if (validateShareCode(val) && authCodeValid) {
saveCodes(authCode, val)
}
}
const handleAuthCodeChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const formatted = formatAuthCode(e.target.value)
setAuthCode(formatted)
@ -63,10 +85,6 @@ export default function AuthCodeSettings() {
}
}
useEffect(() => {
if (session?.user?.id) setUserId(session.user.id)
}, [session])
useEffect(() => {
const fetchCodes = async () => {
try {
@ -78,9 +96,12 @@ export default function AuthCodeSettings() {
setAuthCodeValid(validateAuthCode(data.authCode))
}
if (data?.lastKnownShareCode) {
if (data?.lastKnownShareCode !== undefined) {
setLastKnownShareCode(data.lastKnownShareCode)
setLastKnownShareCodeValid(validateShareCode(data.lastKnownShareCode))
}
if (data?.lastKnownShareCodeDate) {
setLastKnownShareCodeDate(new Date(data.lastKnownShareCodeDate))
}
setIsLoading(false)
@ -92,6 +113,10 @@ export default function AuthCodeSettings() {
fetchCodes()
}, [])
const showShareCodeInput = !isLoading && (
!lastKnownShareCode || shareCodeExpired || shareCodeManuallySet
)
return (
<div className="py-6 sm:py-8 space-y-5 border-t border-gray-200 dark:border-neutral-700">
<div className="grid sm:grid-cols-12 gap-y-1.5 sm:gap-y-0 sm:gap-x-5">
@ -130,8 +155,7 @@ export default function AuthCodeSettings() {
onBlur={() => setTouched(true)}
className={`border py-2.5 sm:py-3 px-4 block w-full rounded-lg sm:text-sm
dark:bg-neutral-800 dark:text-neutral-200 dark:border-neutral-700
${touched ? (authCodeValid ? 'border-teal-500 focus:border-teal-500 focus:ring-teal-500' : 'border-red-500 focus:border-red-500 focus:ring-red-500') : 'border-gray-200'}
`}
${touched ? (authCodeValid ? 'border-teal-500 focus:border-teal-500 focus:ring-teal-500' : 'border-red-500 focus:border-red-500 focus:ring-red-500') : 'border-gray-200'}`}
placeholder="XXXX-XXXXX-XXXX"
required
/>
@ -154,17 +178,12 @@ export default function AuthCodeSettings() {
</div>
</div>
{!isLoading && (!lastKnownShareCodeValid || !authCodeValid) && (
{showShareCodeInput && (
<LatestKnownCodeSettings
lastKnownShareCode={lastKnownShareCode}
setLastKnownShareCode={(val: string) => {
setLastKnownShareCode(val)
const valid = validateShareCode(val)
setLastKnownShareCodeValid(valid)
if (valid && authCodeValid) {
saveCodes(authCode, val)
}
}}
setLastKnownShareCode={handleSetShareCode}
isInvalid={shareCodeExpired}
isSaved={shareCodeSaved}
/>
)}
</div>

View File

@ -6,9 +6,16 @@ import Popover from '../../Popover'
interface Props {
lastKnownShareCode: string
setLastKnownShareCode: (value: string) => void
isInvalid?: boolean
isSaved?: boolean
}
export default function LatestKnownCodeSettings({ lastKnownShareCode, setLastKnownShareCode }: Props) {
export default function LatestKnownCodeSettings({
lastKnownShareCode,
setLastKnownShareCode,
isInvalid = false,
isSaved = false,
}: Props) {
const formatLastKnownShareCode = (value: string) => {
const raw = value.replace(/[^a-zA-Z0-9]/g, '')
const part0 = raw.slice(0, 4)
@ -29,6 +36,7 @@ export default function LatestKnownCodeSettings({ lastKnownShareCode, setLastKno
}
const isValid = validate(lastKnownShareCode)
const showError = isInvalid || !isValid
return (
<div className="grid sm:grid-cols-12 gap-y-1.5 sm:gap-y-0 sm:gap-x-5">
@ -65,20 +73,28 @@ export default function LatestKnownCodeSettings({ lastKnownShareCode, setLastKno
onChange={handleChange}
className={`border py-2.5 sm:py-3 px-4 block w-full rounded-lg sm:text-sm
dark:bg-neutral-800 dark:text-neutral-200 dark:border-neutral-700
${isValid ? 'border-teal-500 focus:border-teal-500 focus:ring-teal-500' : 'border-red-500 focus:border-red-500 focus:ring-red-500'}
`}
${showError
? 'border-red-500 focus:border-red-500 focus:ring-red-500'
: 'border-teal-500 focus:border-teal-500 focus:ring-teal-500'}`}
placeholder="CSGO-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"
required
/>
{isValid && (
{!showError && (
<div className="absolute inset-y-0 end-0 flex items-center pe-3 pointer-events-none">
<svg className="shrink-0 size-4 text-teal-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="20 6 9 17 4 12" />
</svg>
</div>
)}
{!isValid && (
<p className="text-sm text-red-600 mt-2">Ungültiger Austauschcode</p>
{showError && (
<p className="text-sm text-red-600 mt-2">
Abgelaufener Austauschcode
</p>
)}
{isSaved && !showError && (
<p className="text-sm text-teal-600 mt-2">
Gespeichert!
</p>
)}
</div>
</div>

24
src/app/lib/mapNameMap.ts Normal file
View File

@ -0,0 +1,24 @@
// src/lib/mapNameMap.ts
export const mapNameMap: Record<string, { name: string }> = {
de_train: { name: 'Train' },
ar_baggage: { name: 'Baggage' },
ar_pool_day: { name: 'Pool Day' },
ar_shoots: { name: 'Shoots' },
cs_agency: { name: 'Agency' },
cs_italy: { name: 'Italy' },
cs_office: { name: 'Office' },
de_ancient: { name: 'Ancient' },
de_anubis: { name: 'Anubis' },
de_brewery: { name: 'Brewery' },
de_dogtown: { name: 'Dogtown' },
de_dust2: { name: 'Dust 2' },
de_grail: { name: 'Grail' },
de_inferno: { name: 'Inferno' },
de_jura: { name: 'Jura' },
de_mirage: { name: 'Mirage' },
de_nuke: { name: 'Nuke' },
de_overpass: { name: 'Overpass' },
de_vertigo: { name: 'Vertigo' },
lobby_mapveto: { name: 'Pick/Ban' },
};

View File

@ -1,4 +1,4 @@
const host = '10.0.1.25' // oder deine IP wie '10.0.1.25'
const host = 'localhost' // oder deine IP wie '10.0.1.25'
export async function sendServerWebSocketMessage(message: any) {
try {

View File

@ -16,7 +16,7 @@ export const useWS = create<WSState>((set, get) => {
return
}
const ws = new WebSocket(`ws://10.0.1.25:3001?steamId=${steamId}`)
const ws = new WebSocket(`ws://localhost:3001?steamId=${steamId}`)
ws.onopen = () => {
set({ socket: ws, isConnected: true })

View File

@ -6,6 +6,7 @@ import Card from '@/app/components/Card'
import CreateTeamButton from '@/app/components/CreateTeamButton'
import TeamCardComponent from '@/app/components/TeamCardComponent'
import AccountSettings from '@/app/components/settings/AccountSettings'
import UserMatchesTable from '@/app/components/UserMatchesTable'
export default function Page({ params }: { params: Promise<{ tab: string }> }) {
const { tab } = use(params)
@ -31,7 +32,7 @@ export default function Page({ params }: { params: Promise<{ tab: string }> }) {
case 'matches':
return (
<Card maxWidth='auto'>
<TeamCardComponent />
<UserMatchesTable />
</Card>
)
default:

File diff suppressed because one or more lines are too long

View File

@ -20,12 +20,12 @@ exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 6.7.0
* Query Engine version: 3cff47a7f5d65c3ea74883f1d736e41d68ce91ed
* Prisma Client JS version: 6.9.0
* Query Engine version: 81e4af48011447c3cc503a190e86995b66d2a28e
*/
Prisma.prismaVersion = {
client: "6.7.0",
engine: "3cff47a7f5d65c3ea74883f1d736e41d68ce91ed"
client: "6.9.0",
engine: "81e4af48011447c3cc503a190e86995b66d2a28e"
}
Prisma.PrismaClientKnownRequestError = () => {
@ -145,7 +145,7 @@ exports.Prisma.TeamScalarFieldEnum = {
};
exports.Prisma.MatchScalarFieldEnum = {
matchId: 'matchId',
id: 'id',
teamAId: 'teamAId',
teamBId: 'teamBId',
matchDate: 'matchDate',
@ -232,6 +232,7 @@ exports.Prisma.CS2MatchRequestScalarFieldEnum = {
reservationId: 'reservationId',
tvPort: 'tvPort',
processed: 'processed',
failed: 'failed',
createdAt: 'createdAt'
};

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
{
"name": "prisma-client-e738cc370bf095ce801f97ff86cc5e77ac9a34cd0d0b04f968607628b9755dee",
"name": "prisma-client-9a8c37bb270b506ae34f7883dcf8d6883cab59123439f5237b94576b020b37c3",
"main": "index.js",
"types": "index.d.ts",
"browser": "index-browser.js",
@ -97,11 +97,17 @@
"import": "./runtime/binary.mjs",
"default": "./runtime/binary.mjs"
},
"./runtime/wasm": {
"types": "./runtime/wasm.d.ts",
"require": "./runtime/wasm.js",
"import": "./runtime/wasm.mjs",
"default": "./runtime/wasm.mjs"
"./runtime/wasm-engine-edge": {
"types": "./runtime/wasm-engine-edge.d.ts",
"require": "./runtime/wasm-engine-edge.js",
"import": "./runtime/wasm-engine-edge.mjs",
"default": "./runtime/wasm-engine-edge.mjs"
},
"./runtime/wasm-compiler-edge": {
"types": "./runtime/wasm-compiler-edge.d.ts",
"require": "./runtime/wasm-compiler-edge.js",
"import": "./runtime/wasm-compiler-edge.mjs",
"default": "./runtime/wasm-compiler-edge.mjs"
},
"./runtime/edge": {
"types": "./runtime/edge.d.ts",
@ -135,6 +141,6 @@
},
"./*": "./*"
},
"version": "6.7.0",
"version": "6.9.0",
"sideEffects": false
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -73,7 +73,7 @@ export declare type Args_3<T, F extends Operation> = Args<T, F>;
* Query arguments marked with this type are sanitized before being sent to the database.
* Notice while a query argument may be `null`, `ArgType` is guaranteed to be defined.
*/
declare type ArgType = 'Int32' | 'Int64' | 'Float' | 'Double' | 'Text' | 'Enum' | 'EnumArray' | 'Bytes' | 'Boolean' | 'Char' | 'Array' | 'Numeric' | 'Json' | 'Xml' | 'Uuid' | 'DateTime' | 'Date' | 'Time';
declare type ArgType = 'Int32' | 'Int64' | 'Float' | 'Double' | 'Text' | 'Enum' | 'EnumArray' | 'Bytes' | 'Boolean' | 'Char' | 'Array' | 'Numeric' | 'Json' | 'Xml' | 'Uuid' | 'DateTime' | 'Date' | 'Time' | 'Unknown';
/**
* Attributes is a map from string to attribute values.
@ -98,7 +98,7 @@ export declare type BaseDMMF = {
declare type BatchArgs = {
queries: BatchQuery[];
transaction?: {
isolationLevel?: IsolationLevel;
isolationLevel?: IsolationLevel_2;
};
};
@ -126,7 +126,7 @@ declare type BatchQueryOptionsCbArgs = {
declare type BatchResponse = MultiBatchResponse | CompactedBatchResponse;
declare type BatchTransactionOptions = {
isolationLevel?: IsolationLevel;
isolationLevel?: Transaction_2.IsolationLevel;
};
declare interface BinaryTargetsEnvValue {
@ -1475,7 +1475,7 @@ export declare type GetAggregateResult<P extends OperationPayload, A> = {
};
};
declare function getBatchRequestPayload(batch: JsonQuery[], transaction?: TransactionOptions_3<unknown>): QueryEngineBatchRequest;
declare function getBatchRequestPayload(batch: JsonQuery[], transaction?: TransactionOptions_2<unknown>): QueryEngineBatchRequest;
export declare type GetBatchResult = {
count: number;
@ -1638,7 +1638,7 @@ export declare function getPrismaClient(config: GetPrismaClientConfig): {
*/
_transactionWithCallback({ callback, options, }: {
callback: (client: Client) => Promise<unknown>;
options?: TransactionOptions_2;
options?: Options;
}): Promise<unknown>;
_createItxClient(transaction: PrismaPromiseInteractiveTransaction): Client;
/**
@ -1963,6 +1963,8 @@ declare type InternalRequestParams = {
declare type IsolationLevel = 'READ UNCOMMITTED' | 'READ COMMITTED' | 'REPEATABLE READ' | 'SNAPSHOT' | 'SERIALIZABLE';
declare type IsolationLevel_2 = 'ReadUncommitted' | 'ReadCommitted' | 'RepeatableRead' | 'Snapshot' | 'Serializable';
declare function isSkip(value: unknown): value is Skip;
export declare function isTypedSql(value: unknown): value is UnknownTypedSql;
@ -2007,7 +2009,7 @@ export declare interface JsonArray extends Array<JsonValue> {
export declare type JsonBatchQuery = {
batch: JsonQuery[];
transaction?: {
isolationLevel?: IsolationLevel;
isolationLevel?: IsolationLevel_2;
};
};
@ -2422,6 +2424,15 @@ export declare type OptionalKeys<O> = {
}[keyof O];
declare type Options = {
/** Timeout for starting the transaction */
maxWait?: number;
/** Timeout for the transaction body */
timeout?: number;
/** Transaction isolation level */
isolationLevel?: IsolationLevel_2;
};
declare type Options_2 = {
clientVersion: string;
};
@ -2570,7 +2581,7 @@ export declare class PrismaClientUnknownRequestError extends Error implements Er
export declare class PrismaClientValidationError extends Error {
name: string;
clientVersion: string;
constructor(message: string, { clientVersion }: Options);
constructor(message: string, { clientVersion }: Options_2);
get [Symbol.toStringTag](): string;
}
@ -2622,7 +2633,7 @@ declare interface PrismaPromise_2<TResult, TSpec extends PrismaOperationSpec<unk
declare type PrismaPromiseBatchTransaction = {
kind: 'batch';
id: number;
isolationLevel?: IsolationLevel;
isolationLevel?: IsolationLevel_2;
index: number;
lock: PromiseLike<void>;
};
@ -2705,7 +2716,7 @@ declare type QueryCompilerOptions = {
declare type QueryEngineBatchGraphQLRequest = {
batch: QueryEngineRequest[];
transaction?: boolean;
isolationLevel?: IsolationLevel;
isolationLevel?: IsolationLevel_2;
};
declare type QueryEngineBatchRequest = QueryEngineBatchGraphQLRequest | JsonBatchQuery;
@ -2851,7 +2862,7 @@ export declare type RenameAndNestPayloadKeys<P> = {
};
declare type RequestBatchOptions<InteractiveTransactionPayload> = {
transaction?: TransactionOptions_3<InteractiveTransactionPayload>;
transaction?: TransactionOptions_2<InteractiveTransactionPayload>;
traceparent?: string;
numTry?: number;
containsWrite: boolean;
@ -3495,7 +3506,8 @@ declare interface Transaction extends AdapterInfo, SqlQueryable {
declare namespace Transaction_2 {
export {
TransactionOptions_2 as Options,
Options,
IsolationLevel_2 as IsolationLevel,
InteractiveTransactionInfo,
TransactionHeaders
}
@ -3509,13 +3521,7 @@ declare type TransactionOptions = {
usePhantomQuery: boolean;
};
declare type TransactionOptions_2 = {
maxWait?: number;
timeout?: number;
isolationLevel?: IsolationLevel;
};
declare type TransactionOptions_3<InteractiveTransactionPayload> = {
declare type TransactionOptions_2<InteractiveTransactionPayload> = {
kind: 'itx';
options: InteractiveTransactionOptions<InteractiveTransactionPayload>;
} | {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -54,7 +54,7 @@ model Team {
}
model Match {
matchId BigInt @id @default(autoincrement())
id String @id @default(uuid())
teamAId String?
teamBId String?
matchDate DateTime
@ -78,11 +78,11 @@ model Match {
model MatchPlayer {
id String @id @default(cuid())
matchId BigInt
matchId String
steamId String
teamId String?
match Match @relation(fields: [matchId], references: [matchId])
match Match @relation(fields: [matchId], references: [id]) // 👈 id statt matchId
user User @relation(fields: [steamId], references: [steamId])
team Team? @relation(fields: [teamId], references: [id])
stats MatchPlayerStats?
@ -123,14 +123,14 @@ model MatchPlayerStats {
model DemoFile {
id String @id @default(cuid())
matchId BigInt @unique
matchId String @unique
steamId String
fileName String @unique
filePath String
parsed Boolean @default(false)
createdAt DateTime @default(now())
match Match @relation(fields: [matchId], references: [matchId])
match Match @relation(fields: [matchId], references: [id]) // 👈 id statt matchId
user User @relation(fields: [steamId], references: [steamId])
}
@ -163,10 +163,11 @@ model CS2MatchRequest {
id String @id @default(cuid())
userId String
steamId String
matchId BigInt
matchId String
reservationId BigInt
tvPort BigInt
processed Boolean @default(false)
failed Boolean @default(false)
createdAt DateTime @default(now())
user User @relation("MatchRequests", fields: [userId], references: [steamId])
@ -178,7 +179,7 @@ model PremierRankHistory {
id String @id @default(cuid())
userId String
steamId String
matchId BigInt?
matchId String? // optionaler String
rankOld Int
rankNew Int
@ -187,5 +188,5 @@ model PremierRankHistory {
createdAt DateTime @default(now())
user User @relation("UserRankHistory", fields: [userId], references: [steamId])
match Match? @relation("MatchRankHistory", fields: [matchId], references: [matchId])
match Match? @relation("MatchRankHistory", fields: [matchId], references: [id]) // 👈 id statt matchId
}

View File

@ -20,12 +20,12 @@ exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 6.7.0
* Query Engine version: 3cff47a7f5d65c3ea74883f1d736e41d68ce91ed
* Prisma Client JS version: 6.9.0
* Query Engine version: 81e4af48011447c3cc503a190e86995b66d2a28e
*/
Prisma.prismaVersion = {
client: "6.7.0",
engine: "3cff47a7f5d65c3ea74883f1d736e41d68ce91ed"
client: "6.9.0",
engine: "81e4af48011447c3cc503a190e86995b66d2a28e"
}
Prisma.PrismaClientKnownRequestError = () => {
@ -145,7 +145,7 @@ exports.Prisma.TeamScalarFieldEnum = {
};
exports.Prisma.MatchScalarFieldEnum = {
matchId: 'matchId',
id: 'id',
teamAId: 'teamAId',
teamBId: 'teamBId',
matchDate: 'matchDate',
@ -232,6 +232,7 @@ exports.Prisma.CS2MatchRequestScalarFieldEnum = {
reservationId: 'reservationId',
tvPort: 'tvPort',
processed: 'processed',
failed: 'failed',
createdAt: 'createdAt'
};

View File

@ -12,7 +12,7 @@ export async function fetchSteamProfile(
if (!res.ok || !contentType.includes('application/json')) {
const text = await res.text();
console.warn(`[SteamAPI] ⚠️ Ungültige Antwort für ${steamId} (${res.status}):`, text.slice(0, 200));
console.warn(`[SteamAPI] ⚠️ Ungültige Antwort für ${steamId} (${res.status}):`, text.slice(0, 200));
return null;
}

View File

@ -1,16 +1,27 @@
export async function getNextShareCodeFromAPI(authCode: string, currentCode: string): Promise<string | null> {
try {
const res = await fetch('http://localhost:3000/api/cs2/getNextCode', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ authCode, currentCode }),
});
const data = await res.json();
return data?.nextCode || null;
} catch (err) {
console.error('❌ Fehler beim Abrufen des nächsten ShareCodes:', err);
// getNextShareCodeFromAPI.ts
export async function getNextShareCodeFromAPI(
steamId: string,
authCode: string,
currentCode: string
): Promise<string | null> {
try {
const res = await fetch('http://localhost:3000/api/cs2/getNextCode', {
method: 'GET',
headers: {
'x-steamid': steamId,
},
});
const data = await res.json();
if (!data?.valid || !data?.nextCode || data?.nextCode === 'n/a') {
return null;
}
return data.nextCode;
} catch (err) {
console.error(`❌ Fehler beim Abrufen des nächsten ShareCodes für ${steamId}:`, err);
return null;
}
}

View File

@ -31,7 +31,7 @@ interface PlayerStatsExtended {
}
interface DemoMatchData {
matchId: bigint;
matchId: string;
matchDate: Date;
map: string;
filePath: string;
@ -65,16 +65,16 @@ export async function parseAndStoreDemo(
const relativePath = path.relative(process.cwd(), actualDemoPath);
const existing = await prisma.match.findUnique({
where: { matchId: parsed.matchId },
where: { id: parsed.matchId },
});
if (existing) return null;
const match = await prisma.match.create({
data: {
id: parsed.matchId,
title: `CS2 Match vom ${parsed.matchDate.toLocaleDateString()}`,
matchDate: parsed.matchDate,
matchId: parsed.matchId,
map: parsed.map,
demoFilePath: relativePath,
matchType: demoPath.endsWith('_premier.dem')
@ -88,7 +88,7 @@ export async function parseAndStoreDemo(
await prisma.demoFile.create({
data: {
steamId,
matchId: match.matchId,
matchId: match.id,
fileName: path.basename(actualDemoPath),
filePath: relativePath,
parsed: true,
@ -103,7 +103,7 @@ export async function parseAndStoreDemo(
let steamProfile = null;
if (!playerUser?.name || !playerUser?.avatar) {
steamProfile = await fetchSteamProfile(player.steamId).catch(() => null);
await delay(500);
await delay(1000);
}
const isPremier = path.basename(actualDemoPath).toLowerCase().endsWith('_premier.dem');
@ -148,7 +148,7 @@ export async function parseAndStoreDemo(
const matchPlayer = await prisma.matchPlayer.create({
data: {
matchId: match.matchId,
matchId: match.id,
steamId: player.steamId,
...(playerUser?.teamId && { teamId: playerUser.teamId }),
},
@ -211,7 +211,7 @@ export async function parseAndStoreDemo(
async function parseDemoViaGo(filePath: string, shareCode: string): Promise<DemoMatchData | null> {
if (!shareCode) throw new Error('❌ Kein ShareCode für MatchId verfügbar');
return new Promise((resolve) => {
const parserPath = path.resolve(__dirname, '../../../cs2-parser/parser_cs2-win.exe');
const parserPath = path.resolve(__dirname, '../../../ironie-cs2-parser/parser_cs2-win.exe');
const decoded = decodeMatchShareCode(shareCode);
const matchId = decoded.matchId.toString();
@ -228,7 +228,7 @@ async function parseDemoViaGo(filePath: string, shareCode: string): Promise<Demo
try {
const parsed = JSON.parse(output);
resolve({
matchId: BigInt(parsed.matchId),
matchId,
matchDate: new Date(),
map: parsed.map,
filePath,

View File

@ -1,23 +1,23 @@
// processAllUsersCron.ts
import cron from 'node-cron';
import { prisma } from '../app/lib/prisma.js';
import { runDownloaderForUser } from './runDownloaderForUser.js';
import { sendServerWebSocketMessage } from '../app/lib/websocket-server-client.js';
import { decrypt } from '../app/lib/crypto.js';
import { encodeMatch } from 'csgo-sharecode';
import { encodeMatch, decodeMatchShareCode } from 'csgo-sharecode';
import { log } from '../../scripts/cs2-cron-runner.js';
import { getNextShareCodeFromAPI } from './getNextShareCodeFromAPI.js';
let isRunning = false;
export function startCS2MatchCron() {
log('🚀 CS2-CronJob Runner gestartet!')
log('🚀 CS2-CronJob Runner gestartet!');
const job = cron.schedule('* * * * * *', async () => {
await runMatchCheck();
});
runMatchCheck(); // direkt beim Start ausführen
runMatchCheck(); // direkt beim Start
return job;
}
@ -33,106 +33,126 @@ async function runMatchCheck() {
});
for (const user of users) {
let errorOccurred = false;
if (!user.authCode) {
errorOccurred = true;
log(`[${user.steamId}] ⚠️ Kein authCode vorhanden`, "error");
continue;
}
const decryptedAuthCode = decrypt(user.authCode);
const decryptedAuthCode = decrypt(user.authCode!);
const allNewMatches = [];
log(`[${user.steamId}] 🔍 Suche nach neuem Match...`);
//log(`[${user.steamId}] 🔍 Suche nach neuem Match...`);
// 1. ShareCode abrufen
const res = await fetch('http://localhost:3000/api/cs2/getNextCode', {
method: 'GET',
headers: {
'x-steamid': user.steamId,
},
});
let latestKnownCode = user.lastKnownShareCode!;
let nextShareCode = await getNextShareCodeFromAPI(user.steamId, decryptedAuthCode, latestKnownCode);
const data = await res.json();
if (nextShareCode === null) {
// log(`⚠️ Ungültiger lastKnownShareCode bei ${user.steamId}`)
if (!data.valid) {
errorOccurred = true;
log(`🛑 ${data.error}`, "error");
continue;
const alreadyNotified = await prisma.notification.findFirst({
where: {
userId: user.steamId,
actionType: 'expired-sharecode',
},
})
if (!alreadyNotified) {
const notification = await prisma.notification.create({
data: {
userId: user.steamId,
title: 'Share Code abgelaufen',
message: 'Dein gespeicherter Share Code ist abgelaufen.',
actionType: 'expired-sharecode',
},
})
// WebSocket senden
await sendServerWebSocketMessage({
type: 'expired-sharecode',
targetUserIds: [user.steamId],
message: notification.message,
id: notification.id,
actionType: notification.actionType ?? undefined,
actionData: notification.actionData ?? undefined,
createdAt: notification.createdAt.toISOString(),
})
}
continue // springe zum nächsten User
}
const nextCode = data.nextCode;
if (!nextCode) {
log(` Keine neuen ShareCodes für ${user.steamId}`);
continue;
}
while (nextShareCode) {
const matchInfo = decodeMatchShareCode(nextShareCode);
log(`[${user.steamId}] 🆕 Neuer ShareCode gefunden!`);
// 2. MatchRequest abrufen
const matchRequest = await prisma.cS2MatchRequest.findFirst({
where: {
steamId: user.steamId,
processed: false,
},
});
if (!matchRequest) {
log(` Kein unbearbeiteter MatchRequest für ${user.steamId}`, "warn");
continue;
}
// 3. Prüfen, ob Match bereits analysiert wurde
const existing = await prisma.match.findUnique({
where: {
matchId: matchRequest.matchId,
},
});
if (existing) {
log(`↪️ Match ${matchRequest.matchId} existiert bereits übersprungen`, "warn");
await prisma.cS2MatchRequest.update({
where: { id: matchRequest.id },
data: { processed: true },
const existingMatch = await prisma.match.findUnique({
where: { id: matchInfo.matchId.toString() },
});
continue;
}
if (existingMatch) {
log(`↪️ Match ${matchInfo.matchId} existiert bereits übersprungen`);
await prisma.user.update({
where: { steamId: user.steamId },
data: { lastKnownShareCode: nextShareCode },
});
latestKnownCode = nextShareCode;
nextShareCode = await getNextShareCodeFromAPI(user.steamId, decryptedAuthCode, latestKnownCode);
continue;
}
// 4. Match verarbeiten
const shareCode = encodeMatch({
matchId: matchRequest.matchId,
reservationId: matchRequest.reservationId,
tvPort: Number(matchRequest.tvPort),
});
const result = await runDownloaderForUser({
...user,
lastKnownShareCode: shareCode,
});
allNewMatches.push(...result.newMatches);
if (result.newMatches.length > 0) {
await prisma.user.update({
where: { steamId: user.steamId },
data: {
lastKnownShareCode: shareCode,
lastKnownShareCodeDate: new Date(),
// Eintragen in cS2MatchRequest (falls noch nicht geschehen)
await prisma.cS2MatchRequest.upsert({
where: {
steamId_matchId: {
steamId: user.steamId,
matchId: matchInfo.matchId.toString(),
},
},
update: {},
create: {
userId: user.steamId,
steamId: user.steamId,
matchId: matchInfo.matchId.toString(),
reservationId: matchInfo.reservationId,
tvPort: matchInfo.tvPort,
},
});
log(`[${user.steamId}] 🔁 Neuer lastKnownShareCode gesetzt`);
const shareCode = encodeMatch(matchInfo);
const result = await runDownloaderForUser({
...user,
lastKnownShareCode: shareCode,
});
if (result.newMatches.length > 0) {
allNewMatches.push(...result.newMatches);
await prisma.user.update({
where: { steamId: user.steamId },
data: {
lastKnownShareCode: shareCode,
lastKnownShareCodeDate: new Date(),
},
});
await prisma.cS2MatchRequest.updateMany({
where: { matchId: matchInfo.matchId.toString() },
data: { processed: true },
});
latestKnownCode = shareCode;
nextShareCode = await getNextShareCodeFromAPI(user.steamId, decryptedAuthCode, latestKnownCode);
} else {
log(`❌ Parsing fehlgeschlagen für Match ${matchInfo.matchId}`);
await prisma.cS2MatchRequest.updateMany({
where: { matchId: matchInfo.matchId.toString() },
data: { failed: true },
});
latestKnownCode = nextShareCode;
nextShareCode = await getNextShareCodeFromAPI(user.steamId, decryptedAuthCode, latestKnownCode);
}
// Kleines Delay zwischen Requests
await new Promise((r) => setTimeout(r, 1000));
}
await prisma.cS2MatchRequest.update({
where: { id: matchRequest.id },
data: { processed: true },
});
// 🧠 Notification & WebSocket
if (allNewMatches.length > 0) {
log(`${allNewMatches.length} neue Matches für ${user.steamId}`);
@ -141,15 +161,15 @@ async function runMatchCheck() {
userId: user.steamId,
title: 'Neue CS2-Matches geladen',
message: `${allNewMatches.length} neue Matches wurden analysiert.`,
actionType: 'cs2-match',
actionType: 'new-cs2-match',
actionData: JSON.stringify({
matchIds: allNewMatches.map(m => m.matchId.toString()),
matchIds: allNewMatches.map((m) => m.id), // ← m.id statt m.matchId
}),
},
});
await sendServerWebSocketMessage({
type: 'cs2-match',
type: 'new-cs2-match',
targetUserIds: [user.steamId],
message: notification.message,
id: notification.id,
@ -157,8 +177,8 @@ async function runMatchCheck() {
actionData: notification.actionData ?? undefined,
createdAt: notification.createdAt.toISOString(),
});
} else if (!errorOccurred) {
log(` Keine neuen Matches für ${user.steamId}`);
} else {
//log(` Keine neuen Matches für ${user.steamId}`);
}
}

View File

@ -46,7 +46,7 @@ export async function runDownloaderForUser(user: User): Promise<{
if (match) {
newMatches.push(match);
log(`[${steamId}] ✅ Match gespeichert: ${match.matchId}`);
log(`[${steamId}] ✅ Match gespeichert: ${match.id}`);
} else {
log(`[${steamId}] ⚠️ Match bereits vorhanden oder Analyse fehlgeschlagen`, 'warn');
}