tournament logo upload via api
This commit is contained in:
@@ -24,8 +24,10 @@ import { Route as AuthedTeamsTeamIdRouteImport } from './routes/_authed/teams.$t
|
||||
import { Route as AuthedProfilePlayerIdRouteImport } from './routes/_authed/profile.$playerId'
|
||||
import { Route as AuthedAdminPreviewRouteImport } from './routes/_authed/admin/preview'
|
||||
import { ServerRoute as ApiTestServerRouteImport } from './routes/api/test'
|
||||
import { ServerRoute as ApiTournamentsUploadLogoServerRouteImport } from './routes/api/tournaments/upload-logo'
|
||||
import { ServerRoute as ApiEventsSplatServerRouteImport } from './routes/api/events.$'
|
||||
import { ServerRoute as ApiAuthSplatServerRouteImport } from './routes/api/auth.$'
|
||||
import { ServerRoute as ApiFilesCollectionRecordIdFileServerRouteImport } from './routes/api/files/$collection/$recordId/$file'
|
||||
|
||||
const rootServerRouteImport = createServerRootRoute()
|
||||
|
||||
@@ -94,6 +96,12 @@ const ApiTestServerRoute = ApiTestServerRouteImport.update({
|
||||
path: '/api/test',
|
||||
getParentRoute: () => rootServerRouteImport,
|
||||
} as any)
|
||||
const ApiTournamentsUploadLogoServerRoute =
|
||||
ApiTournamentsUploadLogoServerRouteImport.update({
|
||||
id: '/api/tournaments/upload-logo',
|
||||
path: '/api/tournaments/upload-logo',
|
||||
getParentRoute: () => rootServerRouteImport,
|
||||
} as any)
|
||||
const ApiEventsSplatServerRoute = ApiEventsSplatServerRouteImport.update({
|
||||
id: '/api/events/$',
|
||||
path: '/api/events/$',
|
||||
@@ -104,6 +112,12 @@ const ApiAuthSplatServerRoute = ApiAuthSplatServerRouteImport.update({
|
||||
path: '/api/auth/$',
|
||||
getParentRoute: () => rootServerRouteImport,
|
||||
} as any)
|
||||
const ApiFilesCollectionRecordIdFileServerRoute =
|
||||
ApiFilesCollectionRecordIdFileServerRouteImport.update({
|
||||
id: '/api/files/$collection/$recordId/$file',
|
||||
path: '/api/files/$collection/$recordId/$file',
|
||||
getParentRoute: () => rootServerRouteImport,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/login': typeof LoginRoute
|
||||
@@ -196,30 +210,54 @@ export interface FileServerRoutesByFullPath {
|
||||
'/api/test': typeof ApiTestServerRoute
|
||||
'/api/auth/$': typeof ApiAuthSplatServerRoute
|
||||
'/api/events/$': typeof ApiEventsSplatServerRoute
|
||||
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoServerRoute
|
||||
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileServerRoute
|
||||
}
|
||||
export interface FileServerRoutesByTo {
|
||||
'/api/test': typeof ApiTestServerRoute
|
||||
'/api/auth/$': typeof ApiAuthSplatServerRoute
|
||||
'/api/events/$': typeof ApiEventsSplatServerRoute
|
||||
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoServerRoute
|
||||
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileServerRoute
|
||||
}
|
||||
export interface FileServerRoutesById {
|
||||
__root__: typeof rootServerRouteImport
|
||||
'/api/test': typeof ApiTestServerRoute
|
||||
'/api/auth/$': typeof ApiAuthSplatServerRoute
|
||||
'/api/events/$': typeof ApiEventsSplatServerRoute
|
||||
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoServerRoute
|
||||
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileServerRoute
|
||||
}
|
||||
export interface FileServerRouteTypes {
|
||||
fileServerRoutesByFullPath: FileServerRoutesByFullPath
|
||||
fullPaths: '/api/test' | '/api/auth/$' | '/api/events/$'
|
||||
fullPaths:
|
||||
| '/api/test'
|
||||
| '/api/auth/$'
|
||||
| '/api/events/$'
|
||||
| '/api/tournaments/upload-logo'
|
||||
| '/api/files/$collection/$recordId/$file'
|
||||
fileServerRoutesByTo: FileServerRoutesByTo
|
||||
to: '/api/test' | '/api/auth/$' | '/api/events/$'
|
||||
id: '__root__' | '/api/test' | '/api/auth/$' | '/api/events/$'
|
||||
to:
|
||||
| '/api/test'
|
||||
| '/api/auth/$'
|
||||
| '/api/events/$'
|
||||
| '/api/tournaments/upload-logo'
|
||||
| '/api/files/$collection/$recordId/$file'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/api/test'
|
||||
| '/api/auth/$'
|
||||
| '/api/events/$'
|
||||
| '/api/tournaments/upload-logo'
|
||||
| '/api/files/$collection/$recordId/$file'
|
||||
fileServerRoutesById: FileServerRoutesById
|
||||
}
|
||||
export interface RootServerRouteChildren {
|
||||
ApiTestServerRoute: typeof ApiTestServerRoute
|
||||
ApiAuthSplatServerRoute: typeof ApiAuthSplatServerRoute
|
||||
ApiEventsSplatServerRoute: typeof ApiEventsSplatServerRoute
|
||||
ApiTournamentsUploadLogoServerRoute: typeof ApiTournamentsUploadLogoServerRoute
|
||||
ApiFilesCollectionRecordIdFileServerRoute: typeof ApiFilesCollectionRecordIdFileServerRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
@@ -319,6 +357,13 @@ declare module '@tanstack/react-start/server' {
|
||||
preLoaderRoute: typeof ApiTestServerRouteImport
|
||||
parentRoute: typeof rootServerRouteImport
|
||||
}
|
||||
'/api/tournaments/upload-logo': {
|
||||
id: '/api/tournaments/upload-logo'
|
||||
path: '/api/tournaments/upload-logo'
|
||||
fullPath: '/api/tournaments/upload-logo'
|
||||
preLoaderRoute: typeof ApiTournamentsUploadLogoServerRouteImport
|
||||
parentRoute: typeof rootServerRouteImport
|
||||
}
|
||||
'/api/events/$': {
|
||||
id: '/api/events/$'
|
||||
path: '/api/events/$'
|
||||
@@ -333,6 +378,13 @@ declare module '@tanstack/react-start/server' {
|
||||
preLoaderRoute: typeof ApiAuthSplatServerRouteImport
|
||||
parentRoute: typeof rootServerRouteImport
|
||||
}
|
||||
'/api/files/$collection/$recordId/$file': {
|
||||
id: '/api/files/$collection/$recordId/$file'
|
||||
path: '/api/files/$collection/$recordId/$file'
|
||||
fullPath: '/api/files/$collection/$recordId/$file'
|
||||
preLoaderRoute: typeof ApiFilesCollectionRecordIdFileServerRouteImport
|
||||
parentRoute: typeof rootServerRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,6 +437,9 @@ const rootServerRouteChildren: RootServerRouteChildren = {
|
||||
ApiTestServerRoute: ApiTestServerRoute,
|
||||
ApiAuthSplatServerRoute: ApiAuthSplatServerRoute,
|
||||
ApiEventsSplatServerRoute: ApiEventsSplatServerRoute,
|
||||
ApiTournamentsUploadLogoServerRoute: ApiTournamentsUploadLogoServerRoute,
|
||||
ApiFilesCollectionRecordIdFileServerRoute:
|
||||
ApiFilesCollectionRecordIdFileServerRoute,
|
||||
}
|
||||
export const serverRouteTree = rootServerRouteImport
|
||||
._addFileChildren(rootServerRouteChildren)
|
||||
|
||||
95
src/app/routes/api/files/$collection/$recordId/$file.ts
Normal file
95
src/app/routes/api/files/$collection/$recordId/$file.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { createServerFileRoute } from "@tanstack/react-start/server";
|
||||
import { logger } from "@/lib/logger";
|
||||
|
||||
export const ServerRoute = createServerFileRoute("/api/files/$collection/$recordId/$file").methods({
|
||||
GET: async ({ params, request }) => {
|
||||
try {
|
||||
const { collection, recordId, file } = params;
|
||||
const pocketbaseUrl = process.env.VITE_POCKETBASE_URL || 'http://127.0.0.1:8090';
|
||||
const fileUrl = `${pocketbaseUrl}/api/files/${collection}/${recordId}/${file}`;
|
||||
|
||||
logger.info('File proxy', {
|
||||
collection,
|
||||
recordId,
|
||||
file,
|
||||
targetUrl: fileUrl
|
||||
});
|
||||
|
||||
const response = await fetch(fileUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
...(request.headers.get('range') && { 'Range': request.headers.get('range')! }),
|
||||
...(request.headers.get('if-none-match') && { 'If-None-Match': request.headers.get('if-none-match')! }),
|
||||
...(request.headers.get('if-modified-since') && { 'If-Modified-Since': request.headers.get('if-modified-since')! }),
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
logger.error('PocketBase file request failed', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
url: fileUrl
|
||||
});
|
||||
|
||||
if (response.status === 404) {
|
||||
return new Response('File not found', { status: 404 });
|
||||
}
|
||||
|
||||
return new Response(`PocketBase error: ${response.statusText}`, {
|
||||
status: response.status
|
||||
});
|
||||
}
|
||||
|
||||
const body = response.body;
|
||||
const responseHeaders = new Headers();
|
||||
const headers = [
|
||||
'content-type',
|
||||
'content-length',
|
||||
'content-disposition',
|
||||
'etag',
|
||||
'last-modified',
|
||||
'cache-control',
|
||||
'accept-ranges',
|
||||
'content-range'
|
||||
];
|
||||
|
||||
headers.forEach(header => {
|
||||
const value = response.headers.get(header);
|
||||
if (value) {
|
||||
responseHeaders.set(header, value);
|
||||
}
|
||||
});
|
||||
|
||||
responseHeaders.set('Access-Control-Allow-Origin', '*');
|
||||
responseHeaders.set('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS');
|
||||
responseHeaders.set('Access-Control-Allow-Headers', 'Range, If-None-Match, If-Modified-Since');
|
||||
|
||||
logger.info('File proxy response', {
|
||||
status: response.status,
|
||||
contentType: response.headers.get('content-type'),
|
||||
contentLength: response.headers.get('content-length')
|
||||
});
|
||||
|
||||
return new Response(body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: responseHeaders
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('File proxy error', error);
|
||||
return new Response('Internal server error', { status: 500 });
|
||||
}
|
||||
},
|
||||
|
||||
OPTIONS: () => {
|
||||
return new Response(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||
'Access-Control-Max-Age': '86400',
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
115
src/app/routes/api/tournaments/upload-logo.ts
Normal file
115
src/app/routes/api/tournaments/upload-logo.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { createServerFileRoute } from '@tanstack/react-start/server';
|
||||
import { superTokensRequestMiddleware } from '@/utils/supertokens';
|
||||
import { pbAdmin } from '@/lib/pocketbase/client';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { z } from 'zod';
|
||||
|
||||
const uploadSchema = z.object({
|
||||
tournamentId: z.string().min(1, 'Tournament ID is required'),
|
||||
});
|
||||
|
||||
export const ServerRoute = createServerFileRoute('/api/tournaments/upload-logo')
|
||||
.middleware([superTokensRequestMiddleware])
|
||||
.methods({
|
||||
POST: async ({ request, context }) => {
|
||||
try {
|
||||
const userId = context.userAuthId;
|
||||
const isAdmin = context.roles.includes("Admin");
|
||||
|
||||
if (!userId) return new Response('Unauthenticated', { status: 401 });
|
||||
if (!isAdmin) return new Response('Unauthorized', { status: 403 });
|
||||
|
||||
const formData = await request.formData();
|
||||
const tournamentId = formData.get('tournamentId') as string;
|
||||
const logoFile = formData.get('logo') as File;
|
||||
|
||||
const validationResult = uploadSchema.safeParse({ tournamentId });
|
||||
if (!validationResult.success) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Invalid input',
|
||||
details: validationResult.error.issues
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
if (!logoFile || logoFile.size === 0) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Logo file is required'
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'];
|
||||
if (!allowedTypes.includes(logoFile.type)) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Invalid file type. Only JPEG, PNG and GIF are allowed.'
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const maxSize = 10 * 1024 * 1024;
|
||||
if (logoFile.size > maxSize) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'File too large. Maximum size is 10MB.'
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const tournament = await pbAdmin.getTournament(tournamentId);
|
||||
if (!tournament) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Tournament not found'
|
||||
}), {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
logger.info('Uploading tournament logo', {
|
||||
tournamentId,
|
||||
fileName: logoFile.name,
|
||||
fileSize: logoFile.size,
|
||||
userId
|
||||
});
|
||||
|
||||
const pbFormData = new FormData();
|
||||
pbFormData.append('logo', logoFile);
|
||||
|
||||
const updatedTournament = await pbAdmin.updateTournament(tournamentId, pbFormData as any);
|
||||
|
||||
logger.info('Tournament logo uploaded successfully', {
|
||||
tournamentId,
|
||||
logo: updatedTournament.logo
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
tournament: updatedTournament,
|
||||
message: 'Logo uploaded successfully'
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
logger.error('Error uploading tournament logo:', error);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Failed to upload logo',
|
||||
message: error.message || 'Unknown error occurred'
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user