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

View File

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

View File

@ -54,7 +54,7 @@ model Team {
} }
model Match { model Match {
matchId BigInt @id @default(autoincrement()) id String @id @default(uuid())
teamAId String? teamAId String?
teamBId String? teamBId String?
matchDate DateTime matchDate DateTime
@ -78,11 +78,11 @@ model Match {
model MatchPlayer { model MatchPlayer {
id String @id @default(cuid()) id String @id @default(cuid())
matchId BigInt matchId String
steamId String steamId String
teamId 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]) user User @relation(fields: [steamId], references: [steamId])
team Team? @relation(fields: [teamId], references: [id]) team Team? @relation(fields: [teamId], references: [id])
stats MatchPlayerStats? stats MatchPlayerStats?
@ -92,6 +92,7 @@ model MatchPlayer {
@@unique([matchId, steamId]) @@unique([matchId, steamId])
} }
model MatchPlayerStats { model MatchPlayerStats {
id String @id @default(cuid()) id String @id @default(cuid())
matchPlayerId String @unique matchPlayerId String @unique
@ -123,17 +124,18 @@ model MatchPlayerStats {
model DemoFile { model DemoFile {
id String @id @default(cuid()) id String @id @default(cuid())
matchId BigInt @unique matchId String @unique
steamId String steamId String
fileName String @unique fileName String @unique
filePath String filePath String
parsed Boolean @default(false) parsed Boolean @default(false)
createdAt DateTime @default(now()) 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]) user User @relation(fields: [steamId], references: [steamId])
} }
model Invitation { model Invitation {
id String @id @default(cuid()) id String @id @default(cuid())
userId String userId String
@ -163,10 +165,11 @@ model CS2MatchRequest {
id String @id @default(cuid()) id String @id @default(cuid())
userId String userId String
steamId String steamId String
matchId BigInt matchId String
reservationId BigInt reservationId BigInt
tvPort BigInt tvPort BigInt
processed Boolean @default(false) processed Boolean @default(false)
failed Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
user User @relation("MatchRequests", fields: [userId], references: [steamId]) user User @relation("MatchRequests", fields: [userId], references: [steamId])
@ -174,11 +177,12 @@ model CS2MatchRequest {
@@unique([steamId, matchId]) @@unique([steamId, matchId])
} }
model PremierRankHistory { model PremierRankHistory {
id String @id @default(cuid()) id String @id @default(cuid())
userId String userId String
steamId String steamId String
matchId BigInt? matchId String? // optionaler String
rankOld Int rankOld Int
rankNew Int rankNew Int
@ -187,5 +191,5 @@ model PremierRankHistory {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
user User @relation("UserRankHistory", fields: [userId], references: [steamId]) 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 { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth' import { prisma } from '@/app/lib/prisma';
import { authOptions } from '@/app/lib/auth' import { decodeMatchShareCode } from 'csgo-sharecode';
import { prisma } from '@/app/lib/prisma' import { decrypt } from '@/app/lib/crypto';
import { decrypt, encrypt } from '@/app/lib/crypto'
import { decodeMatchShareCode, MatchInformation } from 'csgo-sharecode';
function delay(ms: number) { // Maximal 30 Tage gültig
return new Promise(resolve => setTimeout(resolve, ms)) const EXPIRY_DAYS = 30;
}
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
const session = await getServerSession(authOptions(req)) const steamId = req.headers.get('x-steamid') ?? undefined;
let steamId = session?.user?.steamId ?? req.headers.get('x-steamid') ?? undefined;
if (!steamId) { if (!steamId) {
return NextResponse.json({ valid: false, reason: 'Missing steamId' }); return NextResponse.json({ valid: false, reason: 'Missing steamId' });
} }
try {
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { steamId }, where: { steamId },
select: { authCode: true, lastKnownShareCode: true }, select: {
authCode: true,
lastKnownShareCode: true,
lastKnownShareCodeDate: true,
},
}); });
if (!user?.authCode || !user.lastKnownShareCode) { if (!user?.authCode || !user.lastKnownShareCode) {
return NextResponse.json({ valid: false }); return NextResponse.json({ valid: false, reason: 'missing-sharecode' });
} }
const decryptedAuthCode = decrypt(user.authCode); const isExpired =
user.lastKnownShareCodeDate &&
new Date().getTime() - new Date(user.lastKnownShareCodeDate).getTime() >
EXPIRY_DAYS * 24 * 60 * 60 * 1000;
// Nur EINEN nächsten Code abrufen if (isExpired) {
const url = `https://api.steampowered.com/ICSGOPlayers_730/GetNextMatchSharingCode/v1?key=${process.env.STEAM_API_KEY}&steamid=${steamId}&steamidkey=${decryptedAuthCode}&knowncode=${user.lastKnownShareCode}`; 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 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 res = await fetch(url);
const data = await res.json(); const data = await res.json();
@ -41,42 +72,29 @@ export async function GET(req: NextRequest) {
return NextResponse.json({ valid: true, nextCode: null }); return NextResponse.json({ valid: true, nextCode: null });
} }
// MatchInfo extrahieren & speichern
const matchInfo = decodeMatchShareCode(nextCode); const matchInfo = decodeMatchShareCode(nextCode);
await prisma.cS2MatchRequest.upsert({ await prisma.cS2MatchRequest.upsert({
where: { where: {
steamId_matchId: { steamId_matchId: {
steamId, steamId,
matchId: matchInfo.matchId, matchId: matchInfo.matchId.toString(),
}, },
}, },
update: {}, update: {},
create: { create: {
userId: steamId, userId: steamId,
steamId: steamId, steamId: steamId,
matchId: matchInfo.matchId, matchId: matchInfo.matchId.toString(),
reservationId: matchInfo.reservationId, reservationId: matchInfo.reservationId,
tvPort: matchInfo.tvPort, tvPort: matchInfo.tvPort,
}, },
}); });
return NextResponse.json({ return NextResponse.json({ valid: true, nextCode });
valid: true,
nextCode,
});
} catch (err) { } 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({ return NextResponse.json({
valid: false, valid: false,
error: err instanceof Error ? err.message : String(err), 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 { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth' import { getServerSession } from 'next-auth'
import { authOptions } from '@/app/lib/auth' import { authOptions } from '@/app/lib/auth'
import { prisma } from '@/app/lib/prisma' 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) { // Maximal 30 Tage gültig
const session = await getServerSession(authOptions(req)) const EXPIRY_DAYS = 30
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 })
}
}
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
const session = await getServerSession(authOptions(req)) 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({ return NextResponse.json({
authCode: user?.authCode ? decrypt(user.authCode) : null, authCode,
lastKnownShareCode: user?.lastKnownShareCode ?? null, lastKnownShareCode,
lastKnownShareCodeDate: user?.lastKnownShareCodeDate ?? null, lastKnownShareCodeDate,
reason,
}) })
} catch (error) { } catch (error) {
console.error('Fehler beim Abrufen:', error) console.error('[GET /api/cs2/sharecode]', error)
return NextResponse.json({ error: 'Fehler beim Abrufen der Codes' }, { status: 500 }) 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 { try {
const match = await prisma.match.findUnique({ const match = await prisma.match.findUnique({
where: { matchId: BigInt(id) }, where: { id },
include: { include: {
teamA: true, teamA: true,
teamB: true, teamB: true,
@ -58,7 +58,7 @@ export async function PUT(req: NextRequest, { params }: Params) {
const { title, description, matchDate, players } = body const { title, description, matchDate, players } = body
const match = await prisma.match.findUnique({ const match = await prisma.match.findUnique({
where: { matchId: BigInt(id) }, where: { id },
include: { include: {
teamA: { include: { leader: true } }, teamA: { include: { leader: true } },
teamB: { include: { leader: true } }, teamB: { include: { leader: true } },
@ -94,7 +94,7 @@ export async function PUT(req: NextRequest, { params }: Params) {
} }
try { try {
await prisma.matchPlayer.deleteMany({ where: { matchId: BigInt(id) } }) await prisma.matchPlayer.deleteMany({ where: { id } })
await prisma.matchPlayer.createMany({ await prisma.matchPlayer.createMany({
data: players.map((p: any) => ({ data: players.map((p: any) => ({
@ -105,7 +105,7 @@ export async function PUT(req: NextRequest, { params }: Params) {
}) })
const updated = await prisma.match.update({ const updated = await prisma.match.update({
where: { matchId: BigInt(id) }, where: { id },
data: { data: {
title, title,
description, description,
@ -159,7 +159,7 @@ export async function DELETE(req: NextRequest, { params }: Params) {
const { id } = params const { id } = params
try { try {
await prisma.match.delete({ where: { matchId: BigInt(id) } }) await prisma.match.delete({ where: { id } })
return NextResponse.json({ success: true }) return NextResponse.json({ success: true })
} catch (err) { } catch (err) {
console.error(`DELETE /matches/${id} failed:`, 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 */ /* 2. Spieler-Datensätze vorbereiten */
const playersData = [ const playersData = [
...teamA.activePlayers.map((steamId: string) => ({ ...teamA.activePlayers.map((steamId: string) => ({
matchId: newMatch.matchId, matchId: newMatch.id,
steamId, steamId,
teamId : teamAId teamId : teamAId
})), })),
...teamB.activePlayers.map((steamId: string) => ({ ...teamB.activePlayers.map((steamId: string) => ({
matchId: newMatch.matchId, matchId: newMatch.id,
steamId, steamId,
teamId : teamBId teamId : teamBId
})) }))

View File

@ -37,7 +37,7 @@ export async function POST(req: NextRequest) {
await prisma.notification.create({ await prisma.notification.create({
data: { data: {
userId: leader, userId: leader.steamId,
title: 'Team erstellt', title: 'Team erstellt',
message: `Du hast erfolgreich das Team "${teamname}" 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) { 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() { export default function MatchesAdminManager() {
@ -110,14 +110,14 @@ export default function MatchesAdminManager() {
<div className="flex items-center justify-between text-center"> <div className="flex items-center justify-between text-center">
<div className="flex flex-col items-center w-1/4"> <div className="flex flex-col items-center w-1/4">
<Image <Image
src={getTeamLogo(match.teamA.logo)} src={getTeamLogo(match.teamA?.logo)}
alt={match.teamA.teamname} alt={match.teamA?.teamname || 'Team A'}
width={64} width={64}
height={64} height={64}
className="rounded-full border object-cover bg-white" className="rounded-full border object-cover bg-white"
/> />
<span className="mt-2 text-sm text-gray-700 dark:text-gray-200"> <span className="mt-2 text-sm text-gray-700 dark:text-gray-200">
{match.teamA.teamname} {match.teamA?.teamname || 'Team A'}
</span> </span>
</div> </div>
<div className="text-sm text-gray-600 dark:text-gray-300 w-1/2"> <div className="text-sm text-gray-600 dark:text-gray-300 w-1/2">
@ -129,14 +129,14 @@ export default function MatchesAdminManager() {
</div> </div>
<div className="flex flex-col items-center w-1/4"> <div className="flex flex-col items-center w-1/4">
<Image <Image
src={getTeamLogo(match.teamB.logo)} src={getTeamLogo(match.teamB?.logo)}
alt={match.teamB.teamname} alt={match.teamB?.teamname || 'Team B'}
width={64} width={64}
height={64} height={64}
className="rounded-full border object-cover bg-white" className="rounded-full border object-cover bg-white"
/> />
<span className="mt-2 text-sm text-gray-700 dark:text-gray-200"> <span className="mt-2 text-sm text-gray-700 dark:text-gray-200">
{match.teamB.teamname} {match.teamB?.teamname || 'Team B'}
</span> </span>
</div> </div>
</div> </div>

View File

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

View File

@ -12,7 +12,7 @@ function TableWrapper({ children }: TableProps) {
<div className="flex flex-col"> <div className="flex flex-col">
<div className="-m-1.5 overflow-x-auto"> <div className="-m-1.5 overflow-x-auto">
<div className="p-1.5 min-w-full inline-block align-middle"> <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"> <div className="overflow-hidden">
<table className="min-w-full divide-y divide-gray-200 dark:divide-neutral-700"> <table className="min-w-full divide-y divide-gray-200 dark:divide-neutral-700">
{children} {children}
</table> </table>
@ -24,7 +24,7 @@ function TableWrapper({ children }: TableProps) {
} }
function Head({ children }: { children: ReactNode }) { 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 }) { function Body({ children }: { children: ReactNode }) {
@ -34,13 +34,14 @@ function Body({ children }: { children: ReactNode }) {
function Row({ function Row({
children, children,
hoverable = false, hoverable = false,
}: { ...rest
}: {
children: ReactNode children: ReactNode
hoverable?: boolean hoverable?: boolean
}) { } & React.HTMLAttributes<HTMLTableRowElement>) {
const className = hoverable ? 'hover:bg-gray-100 dark:hover:bg-neutral-700' : '' const className = `${hoverable ? 'hover:bg-gray-100 dark:hover:bg-neutral-700' : ''} ${rest.className ?? ''}`
return <tr className={className}>{children}</tr> return <tr {...rest} className={className}>{children}</tr>
} }
function Cell({ 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' 'use client'
import { useEffect, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { useSession } from 'next-auth/react' import { useSession } from 'next-auth/react'
import Link from 'next/link' import Link from 'next/link'
import Popover from '../../Popover' import Popover from '../../Popover'
@ -8,17 +8,24 @@ import LatestKnownCodeSettings from './LatestKnownCodeSettings'
export default function AuthCodeSettings() { export default function AuthCodeSettings() {
const { data: session } = useSession() const { data: session } = useSession()
const [userId, setUserId] = useState<string | null>(null)
const [authCode, setAuthCode] = useState('') const [authCode, setAuthCode] = useState('')
const [authCodeValid, setAuthCodeValid] = useState(false) const [authCodeValid, setAuthCodeValid] = useState(false)
const [lastKnownShareCode, setLastKnownShareCode] = useState('') 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 [touched, setTouched] = useState(false)
const [isLoading, setIsLoading] = useState(true) 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 formatAuthCode = (value: string) => {
const raw = value.replace(/[^A-Za-z0-9]/g, '').toUpperCase() const raw = value.replace(/[^A-Za-z0-9]/g, '').toUpperCase()
const part1 = raw.slice(0, 4) const part1 = raw.slice(0, 4)
@ -37,7 +44,7 @@ export default function AuthCodeSettings() {
if (!validateAuthCode(updatedAuthCode) || !validateShareCode(updatedKnownCode)) return if (!validateAuthCode(updatedAuthCode) || !validateShareCode(updatedKnownCode)) return
try { try {
await fetch('/api/cs2/sharecode', { const res = await fetch('/api/cs2/sharecode', {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
@ -45,11 +52,26 @@ export default function AuthCodeSettings() {
lastKnownShareCode: updatedKnownCode, lastKnownShareCode: updatedKnownCode,
}), }),
}) })
if (res.ok) {
setLastKnownShareCodeDate(new Date())
setShareCodeSaved(true)
}
} catch (err) { } catch (err) {
console.error('Fehler beim Speichern der Codes:', 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 handleAuthCodeChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const formatted = formatAuthCode(e.target.value) const formatted = formatAuthCode(e.target.value)
setAuthCode(formatted) setAuthCode(formatted)
@ -63,10 +85,6 @@ export default function AuthCodeSettings() {
} }
} }
useEffect(() => {
if (session?.user?.id) setUserId(session.user.id)
}, [session])
useEffect(() => { useEffect(() => {
const fetchCodes = async () => { const fetchCodes = async () => {
try { try {
@ -78,9 +96,12 @@ export default function AuthCodeSettings() {
setAuthCodeValid(validateAuthCode(data.authCode)) setAuthCodeValid(validateAuthCode(data.authCode))
} }
if (data?.lastKnownShareCode) { if (data?.lastKnownShareCode !== undefined) {
setLastKnownShareCode(data.lastKnownShareCode) setLastKnownShareCode(data.lastKnownShareCode)
setLastKnownShareCodeValid(validateShareCode(data.lastKnownShareCode)) }
if (data?.lastKnownShareCodeDate) {
setLastKnownShareCodeDate(new Date(data.lastKnownShareCodeDate))
} }
setIsLoading(false) setIsLoading(false)
@ -92,6 +113,10 @@ export default function AuthCodeSettings() {
fetchCodes() fetchCodes()
}, []) }, [])
const showShareCodeInput = !isLoading && (
!lastKnownShareCode || shareCodeExpired || shareCodeManuallySet
)
return ( return (
<div className="py-6 sm:py-8 space-y-5 border-t border-gray-200 dark:border-neutral-700"> <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"> <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)} onBlur={() => setTouched(true)}
className={`border py-2.5 sm:py-3 px-4 block w-full rounded-lg sm:text-sm 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 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" placeholder="XXXX-XXXXX-XXXX"
required required
/> />
@ -154,17 +178,12 @@ export default function AuthCodeSettings() {
</div> </div>
</div> </div>
{!isLoading && (!lastKnownShareCodeValid || !authCodeValid) && ( {showShareCodeInput && (
<LatestKnownCodeSettings <LatestKnownCodeSettings
lastKnownShareCode={lastKnownShareCode} lastKnownShareCode={lastKnownShareCode}
setLastKnownShareCode={(val: string) => { setLastKnownShareCode={handleSetShareCode}
setLastKnownShareCode(val) isInvalid={shareCodeExpired}
const valid = validateShareCode(val) isSaved={shareCodeSaved}
setLastKnownShareCodeValid(valid)
if (valid && authCodeValid) {
saveCodes(authCode, val)
}
}}
/> />
)} )}
</div> </div>

View File

@ -6,9 +6,16 @@ import Popover from '../../Popover'
interface Props { interface Props {
lastKnownShareCode: string lastKnownShareCode: string
setLastKnownShareCode: (value: string) => void 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 formatLastKnownShareCode = (value: string) => {
const raw = value.replace(/[^a-zA-Z0-9]/g, '') const raw = value.replace(/[^a-zA-Z0-9]/g, '')
const part0 = raw.slice(0, 4) const part0 = raw.slice(0, 4)
@ -29,6 +36,7 @@ export default function LatestKnownCodeSettings({ lastKnownShareCode, setLastKno
} }
const isValid = validate(lastKnownShareCode) const isValid = validate(lastKnownShareCode)
const showError = isInvalid || !isValid
return ( return (
<div className="grid sm:grid-cols-12 gap-y-1.5 sm:gap-y-0 sm:gap-x-5"> <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} onChange={handleChange}
className={`border py-2.5 sm:py-3 px-4 block w-full rounded-lg sm:text-sm 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 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" placeholder="CSGO-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"
required required
/> />
{isValid && ( {!showError && (
<div className="absolute inset-y-0 end-0 flex items-center pe-3 pointer-events-none"> <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"> <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" /> <polyline points="20 6 9 17 4 12" />
</svg> </svg>
</div> </div>
)} )}
{!isValid && ( {showError && (
<p className="text-sm text-red-600 mt-2">Ungültiger Austauschcode</p> <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>
</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) { export async function sendServerWebSocketMessage(message: any) {
try { try {

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@ -20,12 +20,12 @@ exports.Prisma = Prisma
exports.$Enums = {} exports.$Enums = {}
/** /**
* Prisma Client JS version: 6.7.0 * Prisma Client JS version: 6.9.0
* Query Engine version: 3cff47a7f5d65c3ea74883f1d736e41d68ce91ed * Query Engine version: 81e4af48011447c3cc503a190e86995b66d2a28e
*/ */
Prisma.prismaVersion = { Prisma.prismaVersion = {
client: "6.7.0", client: "6.9.0",
engine: "3cff47a7f5d65c3ea74883f1d736e41d68ce91ed" engine: "81e4af48011447c3cc503a190e86995b66d2a28e"
} }
Prisma.PrismaClientKnownRequestError = () => { Prisma.PrismaClientKnownRequestError = () => {
@ -145,7 +145,7 @@ exports.Prisma.TeamScalarFieldEnum = {
}; };
exports.Prisma.MatchScalarFieldEnum = { exports.Prisma.MatchScalarFieldEnum = {
matchId: 'matchId', id: 'id',
teamAId: 'teamAId', teamAId: 'teamAId',
teamBId: 'teamBId', teamBId: 'teamBId',
matchDate: 'matchDate', matchDate: 'matchDate',
@ -232,6 +232,7 @@ exports.Prisma.CS2MatchRequestScalarFieldEnum = {
reservationId: 'reservationId', reservationId: 'reservationId',
tvPort: 'tvPort', tvPort: 'tvPort',
processed: 'processed', processed: 'processed',
failed: 'failed',
createdAt: 'createdAt' 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", "main": "index.js",
"types": "index.d.ts", "types": "index.d.ts",
"browser": "index-browser.js", "browser": "index-browser.js",
@ -97,11 +97,17 @@
"import": "./runtime/binary.mjs", "import": "./runtime/binary.mjs",
"default": "./runtime/binary.mjs" "default": "./runtime/binary.mjs"
}, },
"./runtime/wasm": { "./runtime/wasm-engine-edge": {
"types": "./runtime/wasm.d.ts", "types": "./runtime/wasm-engine-edge.d.ts",
"require": "./runtime/wasm.js", "require": "./runtime/wasm-engine-edge.js",
"import": "./runtime/wasm.mjs", "import": "./runtime/wasm-engine-edge.mjs",
"default": "./runtime/wasm.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": { "./runtime/edge": {
"types": "./runtime/edge.d.ts", "types": "./runtime/edge.d.ts",
@ -135,6 +141,6 @@
}, },
"./*": "./*" "./*": "./*"
}, },
"version": "6.7.0", "version": "6.9.0",
"sideEffects": false "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. * 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. * 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. * Attributes is a map from string to attribute values.
@ -98,7 +98,7 @@ export declare type BaseDMMF = {
declare type BatchArgs = { declare type BatchArgs = {
queries: BatchQuery[]; queries: BatchQuery[];
transaction?: { transaction?: {
isolationLevel?: IsolationLevel; isolationLevel?: IsolationLevel_2;
}; };
}; };
@ -126,7 +126,7 @@ declare type BatchQueryOptionsCbArgs = {
declare type BatchResponse = MultiBatchResponse | CompactedBatchResponse; declare type BatchResponse = MultiBatchResponse | CompactedBatchResponse;
declare type BatchTransactionOptions = { declare type BatchTransactionOptions = {
isolationLevel?: IsolationLevel; isolationLevel?: Transaction_2.IsolationLevel;
}; };
declare interface BinaryTargetsEnvValue { 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 = { export declare type GetBatchResult = {
count: number; count: number;
@ -1638,7 +1638,7 @@ export declare function getPrismaClient(config: GetPrismaClientConfig): {
*/ */
_transactionWithCallback({ callback, options, }: { _transactionWithCallback({ callback, options, }: {
callback: (client: Client) => Promise<unknown>; callback: (client: Client) => Promise<unknown>;
options?: TransactionOptions_2; options?: Options;
}): Promise<unknown>; }): Promise<unknown>;
_createItxClient(transaction: PrismaPromiseInteractiveTransaction): Client; _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 = '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; declare function isSkip(value: unknown): value is Skip;
export declare function isTypedSql(value: unknown): value is UnknownTypedSql; export declare function isTypedSql(value: unknown): value is UnknownTypedSql;
@ -2007,7 +2009,7 @@ export declare interface JsonArray extends Array<JsonValue> {
export declare type JsonBatchQuery = { export declare type JsonBatchQuery = {
batch: JsonQuery[]; batch: JsonQuery[];
transaction?: { transaction?: {
isolationLevel?: IsolationLevel; isolationLevel?: IsolationLevel_2;
}; };
}; };
@ -2422,6 +2424,15 @@ export declare type OptionalKeys<O> = {
}[keyof O]; }[keyof O];
declare type Options = { 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; clientVersion: string;
}; };
@ -2570,7 +2581,7 @@ export declare class PrismaClientUnknownRequestError extends Error implements Er
export declare class PrismaClientValidationError extends Error { export declare class PrismaClientValidationError extends Error {
name: string; name: string;
clientVersion: string; clientVersion: string;
constructor(message: string, { clientVersion }: Options); constructor(message: string, { clientVersion }: Options_2);
get [Symbol.toStringTag](): string; get [Symbol.toStringTag](): string;
} }
@ -2622,7 +2633,7 @@ declare interface PrismaPromise_2<TResult, TSpec extends PrismaOperationSpec<unk
declare type PrismaPromiseBatchTransaction = { declare type PrismaPromiseBatchTransaction = {
kind: 'batch'; kind: 'batch';
id: number; id: number;
isolationLevel?: IsolationLevel; isolationLevel?: IsolationLevel_2;
index: number; index: number;
lock: PromiseLike<void>; lock: PromiseLike<void>;
}; };
@ -2705,7 +2716,7 @@ declare type QueryCompilerOptions = {
declare type QueryEngineBatchGraphQLRequest = { declare type QueryEngineBatchGraphQLRequest = {
batch: QueryEngineRequest[]; batch: QueryEngineRequest[];
transaction?: boolean; transaction?: boolean;
isolationLevel?: IsolationLevel; isolationLevel?: IsolationLevel_2;
}; };
declare type QueryEngineBatchRequest = QueryEngineBatchGraphQLRequest | JsonBatchQuery; declare type QueryEngineBatchRequest = QueryEngineBatchGraphQLRequest | JsonBatchQuery;
@ -2851,7 +2862,7 @@ export declare type RenameAndNestPayloadKeys<P> = {
}; };
declare type RequestBatchOptions<InteractiveTransactionPayload> = { declare type RequestBatchOptions<InteractiveTransactionPayload> = {
transaction?: TransactionOptions_3<InteractiveTransactionPayload>; transaction?: TransactionOptions_2<InteractiveTransactionPayload>;
traceparent?: string; traceparent?: string;
numTry?: number; numTry?: number;
containsWrite: boolean; containsWrite: boolean;
@ -3495,7 +3506,8 @@ declare interface Transaction extends AdapterInfo, SqlQueryable {
declare namespace Transaction_2 { declare namespace Transaction_2 {
export { export {
TransactionOptions_2 as Options, Options,
IsolationLevel_2 as IsolationLevel,
InteractiveTransactionInfo, InteractiveTransactionInfo,
TransactionHeaders TransactionHeaders
} }
@ -3509,13 +3521,7 @@ declare type TransactionOptions = {
usePhantomQuery: boolean; usePhantomQuery: boolean;
}; };
declare type TransactionOptions_2 = { declare type TransactionOptions_2<InteractiveTransactionPayload> = {
maxWait?: number;
timeout?: number;
isolationLevel?: IsolationLevel;
};
declare type TransactionOptions_3<InteractiveTransactionPayload> = {
kind: 'itx'; kind: 'itx';
options: InteractiveTransactionOptions<InteractiveTransactionPayload>; 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 { model Match {
matchId BigInt @id @default(autoincrement()) id String @id @default(uuid())
teamAId String? teamAId String?
teamBId String? teamBId String?
matchDate DateTime matchDate DateTime
@ -78,11 +78,11 @@ model Match {
model MatchPlayer { model MatchPlayer {
id String @id @default(cuid()) id String @id @default(cuid())
matchId BigInt matchId String
steamId String steamId String
teamId 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]) user User @relation(fields: [steamId], references: [steamId])
team Team? @relation(fields: [teamId], references: [id]) team Team? @relation(fields: [teamId], references: [id])
stats MatchPlayerStats? stats MatchPlayerStats?
@ -123,14 +123,14 @@ model MatchPlayerStats {
model DemoFile { model DemoFile {
id String @id @default(cuid()) id String @id @default(cuid())
matchId BigInt @unique matchId String @unique
steamId String steamId String
fileName String @unique fileName String @unique
filePath String filePath String
parsed Boolean @default(false) parsed Boolean @default(false)
createdAt DateTime @default(now()) 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]) user User @relation(fields: [steamId], references: [steamId])
} }
@ -163,10 +163,11 @@ model CS2MatchRequest {
id String @id @default(cuid()) id String @id @default(cuid())
userId String userId String
steamId String steamId String
matchId BigInt matchId String
reservationId BigInt reservationId BigInt
tvPort BigInt tvPort BigInt
processed Boolean @default(false) processed Boolean @default(false)
failed Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
user User @relation("MatchRequests", fields: [userId], references: [steamId]) user User @relation("MatchRequests", fields: [userId], references: [steamId])
@ -178,7 +179,7 @@ model PremierRankHistory {
id String @id @default(cuid()) id String @id @default(cuid())
userId String userId String
steamId String steamId String
matchId BigInt? matchId String? // optionaler String
rankOld Int rankOld Int
rankNew Int rankNew Int
@ -187,5 +188,5 @@ model PremierRankHistory {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
user User @relation("UserRankHistory", fields: [userId], references: [steamId]) 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 = {} exports.$Enums = {}
/** /**
* Prisma Client JS version: 6.7.0 * Prisma Client JS version: 6.9.0
* Query Engine version: 3cff47a7f5d65c3ea74883f1d736e41d68ce91ed * Query Engine version: 81e4af48011447c3cc503a190e86995b66d2a28e
*/ */
Prisma.prismaVersion = { Prisma.prismaVersion = {
client: "6.7.0", client: "6.9.0",
engine: "3cff47a7f5d65c3ea74883f1d736e41d68ce91ed" engine: "81e4af48011447c3cc503a190e86995b66d2a28e"
} }
Prisma.PrismaClientKnownRequestError = () => { Prisma.PrismaClientKnownRequestError = () => {
@ -145,7 +145,7 @@ exports.Prisma.TeamScalarFieldEnum = {
}; };
exports.Prisma.MatchScalarFieldEnum = { exports.Prisma.MatchScalarFieldEnum = {
matchId: 'matchId', id: 'id',
teamAId: 'teamAId', teamAId: 'teamAId',
teamBId: 'teamBId', teamBId: 'teamBId',
matchDate: 'matchDate', matchDate: 'matchDate',
@ -232,6 +232,7 @@ exports.Prisma.CS2MatchRequestScalarFieldEnum = {
reservationId: 'reservationId', reservationId: 'reservationId',
tvPort: 'tvPort', tvPort: 'tvPort',
processed: 'processed', processed: 'processed',
failed: 'failed',
createdAt: 'createdAt' createdAt: 'createdAt'
}; };

View File

@ -1,16 +1,27 @@
export async function getNextShareCodeFromAPI(authCode: string, currentCode: string): Promise<string | null> { // getNextShareCodeFromAPI.ts
export async function getNextShareCodeFromAPI(
steamId: string,
authCode: string,
currentCode: string
): Promise<string | null> {
try { try {
const res = await fetch('http://localhost:3000/api/cs2/getNextCode', { const res = await fetch('http://localhost:3000/api/cs2/getNextCode', {
method: 'POST', method: 'GET',
headers: { 'Content-Type': 'application/json' }, headers: {
body: JSON.stringify({ authCode, currentCode }), 'x-steamid': steamId,
},
}); });
const data = await res.json(); const data = await res.json();
return data?.nextCode || null;
} catch (err) { if (!data?.valid || !data?.nextCode || data?.nextCode === 'n/a') {
console.error('❌ Fehler beim Abrufen des nächsten ShareCodes:', err);
return null; 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 { interface DemoMatchData {
matchId: bigint; matchId: string;
matchDate: Date; matchDate: Date;
map: string; map: string;
filePath: string; filePath: string;
@ -65,16 +65,16 @@ export async function parseAndStoreDemo(
const relativePath = path.relative(process.cwd(), actualDemoPath); const relativePath = path.relative(process.cwd(), actualDemoPath);
const existing = await prisma.match.findUnique({ const existing = await prisma.match.findUnique({
where: { matchId: parsed.matchId }, where: { id: parsed.matchId },
}); });
if (existing) return null; if (existing) return null;
const match = await prisma.match.create({ const match = await prisma.match.create({
data: { data: {
id: parsed.matchId,
title: `CS2 Match vom ${parsed.matchDate.toLocaleDateString()}`, title: `CS2 Match vom ${parsed.matchDate.toLocaleDateString()}`,
matchDate: parsed.matchDate, matchDate: parsed.matchDate,
matchId: parsed.matchId,
map: parsed.map, map: parsed.map,
demoFilePath: relativePath, demoFilePath: relativePath,
matchType: demoPath.endsWith('_premier.dem') matchType: demoPath.endsWith('_premier.dem')
@ -88,7 +88,7 @@ export async function parseAndStoreDemo(
await prisma.demoFile.create({ await prisma.demoFile.create({
data: { data: {
steamId, steamId,
matchId: match.matchId, matchId: match.id,
fileName: path.basename(actualDemoPath), fileName: path.basename(actualDemoPath),
filePath: relativePath, filePath: relativePath,
parsed: true, parsed: true,
@ -103,7 +103,7 @@ export async function parseAndStoreDemo(
let steamProfile = null; let steamProfile = null;
if (!playerUser?.name || !playerUser?.avatar) { if (!playerUser?.name || !playerUser?.avatar) {
steamProfile = await fetchSteamProfile(player.steamId).catch(() => null); steamProfile = await fetchSteamProfile(player.steamId).catch(() => null);
await delay(500); await delay(1000);
} }
const isPremier = path.basename(actualDemoPath).toLowerCase().endsWith('_premier.dem'); const isPremier = path.basename(actualDemoPath).toLowerCase().endsWith('_premier.dem');
@ -148,7 +148,7 @@ export async function parseAndStoreDemo(
const matchPlayer = await prisma.matchPlayer.create({ const matchPlayer = await prisma.matchPlayer.create({
data: { data: {
matchId: match.matchId, matchId: match.id,
steamId: player.steamId, steamId: player.steamId,
...(playerUser?.teamId && { teamId: playerUser.teamId }), ...(playerUser?.teamId && { teamId: playerUser.teamId }),
}, },
@ -211,7 +211,7 @@ export async function parseAndStoreDemo(
async function parseDemoViaGo(filePath: string, shareCode: string): Promise<DemoMatchData | null> { async function parseDemoViaGo(filePath: string, shareCode: string): Promise<DemoMatchData | null> {
if (!shareCode) throw new Error('❌ Kein ShareCode für MatchId verfügbar'); if (!shareCode) throw new Error('❌ Kein ShareCode für MatchId verfügbar');
return new Promise((resolve) => { 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 decoded = decodeMatchShareCode(shareCode);
const matchId = decoded.matchId.toString(); const matchId = decoded.matchId.toString();
@ -228,7 +228,7 @@ async function parseDemoViaGo(filePath: string, shareCode: string): Promise<Demo
try { try {
const parsed = JSON.parse(output); const parsed = JSON.parse(output);
resolve({ resolve({
matchId: BigInt(parsed.matchId), matchId,
matchDate: new Date(), matchDate: new Date(),
map: parsed.map, map: parsed.map,
filePath, filePath,

View File

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

View File

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