Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions web/app/api/auth/nonce/route.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
import { getRedisClient } from "c:/Users/USER/Desktop/Roster-Rumble/web/lib/redis";
import { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";
import { getRedisClient } from "@/lib/redis";

const generateNonce = (): string => {
return crypto.randomBytes(32).toString('hex');
return crypto.randomBytes(32).toString("hex");
};

const NONCE_TTL = 300;

export async function GET(request: NextRequest) {
try {
const url = request.nextUrl || new URL(request.url);
const walletAddress = url.searchParams.get('walletAddress');
const walletAddress = url.searchParams.get("walletAddress");

if (!walletAddress) {
return NextResponse.json(
{ error: 'Missing required parameter: walletAddress' },
{ error: "Missing required parameter: walletAddress" },
{ status: 400 }
);
}
Expand All @@ -27,23 +27,23 @@ export async function GET(request: NextRequest) {
// Store the nonce in Redis with a 5-minute TTL
const redis = await getRedisClient();
const nonceKey = `auth:nonce:${walletAddress}`;

await redis.set(nonceKey, nonce, { EX: NONCE_TTL });

// Return the nonce as JSON
return NextResponse.json({ nonce });
} catch (redisError) {
console.error('Redis error:', redisError);
console.error("Redis error:", redisError);
return NextResponse.json(
{ error: 'Failed to store nonce' },
{ error: "Failed to store nonce" },
{ status: 500 }
);
}
} catch (error) {
console.error('Error generating nonce:', error);
console.error("Error generating nonce:", error);
return NextResponse.json(
{ error: 'Failed to generate nonce' },
{ error: "Failed to generate nonce" },
{ status: 500 }
);
}
}
}
95 changes: 58 additions & 37 deletions web/app/api/contests/routes.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import jwt from 'jsonwebtoken';
import { Pool } from 'pg';
import amqp from 'amqplib';
import { getRedisClient } from '@/lib/redis';
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import jwt from "jsonwebtoken";
import { Pool } from "pg";
import amqp from "amqplib";
import { getRedisClient } from "@/lib/redis";

const DATABASE_URL = process.env.DATABASE_URL!;
const JWT_SECRET = process.env.JWT_SECRET!;
const RABBITMQ_URL = process.env.RABBITMQ_URL!;

if (!DATABASE_URL || !JWT_SECRET || !RABBITMQ_URL) {
throw new Error('Missing required environment variables.');
throw new Error("Missing required environment variables.");
}

// PostgreSQL pool
Expand All @@ -22,7 +22,7 @@ const contestSchema = z.object({
sport: z.string(),
entryFee: z.number().nonnegative(),
startsAt: z.string().refine((date: string) => !isNaN(Date.parse(date)), {
message: 'Invalid ISO8601 date string',
message: "Invalid ISO8601 date string",
}),
maxPlayers: z.number().int().positive(),
});
Expand All @@ -42,12 +42,12 @@ type ContestQuery = z.infer<typeof contestQuerySchema>;
// Helper: verify JWT and check admin
export function verifyAdmin(token: string | undefined) {
if (!token) {
throw new Error('No token provided');
throw new Error("No token provided");
}
try {
const payload = jwt.verify(token, JWT_SECRET) as any;
if (payload.role !== 'admin') {
throw new Error('Forbidden');
if (payload.role !== "admin") {
throw new Error("Forbidden");
}
return payload;
} catch (err) {
Expand All @@ -61,7 +61,7 @@ export async function getMqChannel() {
if (mqChannel) return mqChannel;
const conn = await amqp.connect(RABBITMQ_URL);
const channel = await conn.createChannel();
await channel.assertExchange('contest.events', 'fanout', { durable: true });
await channel.assertExchange("contest.events", "fanout", { durable: true });
mqChannel = channel;
return mqChannel;
}
Expand All @@ -74,7 +74,7 @@ export async function GET(req: NextRequest) {

// Build query params object
const rawQuery: Record<string, string> = {};
for (const key of ['sport', 'minFee', 'maxFee', 'page', 'limit'] as const) {
for (const key of ["sport", "minFee", "maxFee", "page", "limit"] as const) {
const v = searchParams.get(key);
if (v !== null) rawQuery[key] = v;
}
Expand All @@ -86,7 +86,10 @@ export async function GET(req: NextRequest) {
} catch (validationError) {
if (validationError instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Invalid query parameters', details: validationError.errors },
{
error: "Invalid query parameters",
details: validationError.errors,
},
{ status: 400 }
);
}
Expand Down Expand Up @@ -115,7 +118,7 @@ export async function GET(req: NextRequest) {
return NextResponse.json(parsed, { status: 200 });
}
} catch (cacheErr) {
console.warn('Redis cache error, proceeding without cache:', cacheErr);
console.warn("Redis cache error, proceeding without cache:", cacheErr);
}

// Build SQL query with filters
Expand All @@ -140,15 +143,19 @@ export async function GET(req: NextRequest) {
paramIndex++;
}

const whereClause = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
const whereClause = conditions.length
? `WHERE ${conditions.join(" AND ")}`
: "";

// Get total count
const countQuery = `SELECT COUNT(*) AS total FROM contests ${whereClause}`;
const countResult = await client.query(countQuery, values);
if (!countResult.rows || !countResult.rows[0]) {
throw new Error('Failed to retrieve total count');
throw new Error("Failed to retrieve total count");
}
const total = countResult.rows[0] ? parseInt(countResult.rows[0].total || '0', 10) : 0;
const total = countResult.rows[0]
? parseInt(countResult.rows[0].total || "0", 10)
: 0;

// Get paginated data
const offset = (queryParams.page - 1) * queryParams.limit;
Expand All @@ -165,7 +172,11 @@ export async function GET(req: NextRequest) {
ORDER BY starts_at DESC
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
`;
const dataResult = await client.query(dataQuery, [...values, queryParams.limit, offset]);
const dataResult = await client.query(dataQuery, [
...values,
queryParams.limit,
offset,
]);

const response = {
contests: dataResult.rows || [],
Expand All @@ -180,19 +191,22 @@ export async function GET(req: NextRequest) {
const redis = await getRedisClient();
await redis.setEx(cacheKey, 30, JSON.stringify(response));
} catch (cacheErr) {
console.warn('Failed to cache result:', cacheErr);
console.warn("Failed to cache result:", cacheErr);
}

return NextResponse.json(response, { status: 200 });
} catch (err: any) {
console.error('GET /contests error:', err.message, err.stack);
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
console.error("GET /contests error:", err.message, err.stack);
return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 }
);
} finally {
if (client) {
try {
await client.release();
} catch (releaseErr) {
console.warn('Failed to release client:', releaseErr);
console.warn("Failed to release client:", releaseErr);
}
}
}
Expand All @@ -203,8 +217,10 @@ export async function POST(req: NextRequest) {
let client;
try {
// Admin JWT from Authorization header
const authHeader = req.headers.get('authorization') || '';
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : undefined;
const authHeader = req.headers.get("authorization") || "";
const token = authHeader.startsWith("Bearer ")
? authHeader.slice(7)
: undefined;
verifyAdmin(token);

const body = await req.json();
Expand All @@ -221,40 +237,45 @@ export async function POST(req: NextRequest) {

// Emit event
const channel = await getMqChannel();
const eventPayload = Buffer.from(JSON.stringify({ type: 'ContestCreated', data: contest }));
channel.publish('contest.events', '', eventPayload);
const eventPayload = Buffer.from(
JSON.stringify({ type: "ContestCreated", data: contest })
);
channel.publish("contest.events", "", eventPayload);

// Invalidate cache after creating new contest
try {
const redis = await getRedisClient();
const keys = await redis.keys('contests:*');
const keys = await redis.keys("contests:*");
if (keys.length > 0) {
await redis.del(keys);
}
} catch (cacheError) {
console.warn('Failed to invalidate cache:', cacheError);
console.warn("Failed to invalidate cache:", cacheError);
}

return NextResponse.json(contest, { status: 201 });
} catch (err: any) {
if (err.message === 'Forbidden') {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
if (err.message === "Forbidden") {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
if (err instanceof z.ZodError) {
return NextResponse.json({ errors: err.errors }, { status: 400 });
}
if (err.message === 'No token provided') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
if (err.message === "No token provided") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
console.error('POST /contests error:', err);
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
console.error("POST /contests error:", err);
return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 }
);
} finally {
if (client) {
try {
await client.release();
} catch (releaseErr) {
console.warn('Failed to release client:', releaseErr);
console.warn("Failed to release client:", releaseErr);
}
}
}
}
}
8 changes: 0 additions & 8 deletions web/app/components/ToasterProviderWrapper.tsx

This file was deleted.

53 changes: 34 additions & 19 deletions web/app/context/ToasterContext.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
'use client';
"use client";

import React, { createContext, useContext, ReactNode } from 'react';
import { useToastStore, ToastStatus } from '../../lib/toastStore';
import { Toaster } from '../components/Toaster';
import React, { createContext, useContext, ReactNode } from "react";
import { useToastStore, ToastStatus } from "../../lib/toastStore";
import { Toaster } from "@/components/Toaster";

interface ToasterContextValue {
showToast: (message: string, status: ToastStatus, options?: {
txHash?: string;
network?: string;
autoDismiss?: number;
}) => string;
showToast: (
message: string,
status: ToastStatus,
options?: {
txHash?: string;
network?: string;
autoDismiss?: number;
}
) => string;
updateToast: (id: string, message: string, status: ToastStatus) => void;
dismissToast: (id: string) => void;
pendingTransactions: Array<{
Expand All @@ -20,10 +24,19 @@ interface ToasterContextValue {
}>;
}

const ToasterContext = createContext<ToasterContextValue | undefined>(undefined);
const ToasterContext = createContext<ToasterContextValue | undefined>(
undefined
);

export const ToasterProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const { toasts, addToast, updateToast: updateStoreToast, removeToast } = useToastStore();
export const ToasterProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const {
toasts,
addToast,
updateToast: updateStoreToast,
removeToast,
} = useToastStore();

const showToast = (message: string, status: ToastStatus, options = {}) => {
return addToast(message, status, options);
Expand All @@ -38,16 +51,18 @@ export const ToasterProvider: React.FC<{ children: ReactNode }> = ({ children })
};

const pendingTransactions = toasts
.filter(toast => toast.status === 'pending' && toast.txHash)
.map(toast => ({
.filter((toast) => toast.status === "pending" && toast.txHash)
.map((toast) => ({
id: toast.id,
message: toast.message,
message: toast.message,
txHash: toast.txHash,
network: toast.network
network: toast.network,
}));

return (
<ToasterContext.Provider value={{ showToast, updateToast, dismissToast, pendingTransactions }}>
<ToasterContext.Provider
value={{ showToast, updateToast, dismissToast, pendingTransactions }}
>
{children}
<Toaster />
</ToasterContext.Provider>
Expand All @@ -57,7 +72,7 @@ export const ToasterProvider: React.FC<{ children: ReactNode }> = ({ children })
export const useToaster = (): ToasterContextValue => {
const context = useContext(ToasterContext);
if (context === undefined) {
throw new Error('useToaster must be used within a ToasterProvider');
throw new Error("useToaster must be used within a ToasterProvider");
}
return context;
};
};
Loading
Loading