瀏覽代碼

feat: add CORS headers and auto-generated OpenAPI docs

- Add Access-Control-Allow-Origin: * to all API responses
- Implement OPTIONS handlers for CORS preflight requests
- Introduce swagger-jsdoc & swagger-ui-react to generate /api-docs
- Auto-generate OpenAPI spec at /api/openapi.json
vtugulan 6 月之前
父節點
當前提交
b6bc11d52c
共有 8 個文件被更改,包括 1372 次插入78 次删除
  1. 243 0
      app/api-docs/page.tsx
  2. 18 3
      app/api/files/[id]/route.ts
  3. 20 2
      app/api/files/route.ts
  4. 226 0
      app/api/openapi.json/route.ts
  5. 20 3
      app/api/upload/route.ts
  6. 12 54
      app/page.tsx
  7. 829 15
      package-lock.json
  8. 4 1
      package.json

+ 243 - 0
app/api-docs/page.tsx

@@ -0,0 +1,243 @@
+'use client';
+
+import SwaggerUI from 'swagger-ui-react';
+import 'swagger-ui-react/swagger-ui.css';
+
+export default function ApiDocs() {
+  const spec = {
+    openapi: '3.0.0',
+    info: {
+      title: 'Vtorio API',
+      version: '1.0.0',
+      description: 'File upload and management API for Vtorio application',
+    },
+    servers: [
+      {
+        url: process.env.NODE_ENV === 'production' 
+          ? 'https://your-domain.com' 
+          : 'http://localhost:3000',
+        description: process.env.NODE_ENV === 'production' ? 'Production server' : 'Development server',
+      },
+    ],
+    paths: {
+      '/api/upload': {
+        post: {
+          summary: 'Upload a file',
+          description: 'Upload a file to the server and store it in the database',
+          tags: ['Files'],
+          requestBody: {
+            required: true,
+            content: {
+              'multipart/form-data': {
+                schema: {
+                  type: 'object',
+                  properties: {
+                    file: {
+                      type: 'string',
+                      format: 'binary',
+                      description: 'The file to upload',
+                    },
+                  },
+                  required: ['file'],
+                },
+              },
+            },
+          },
+          responses: {
+            '200': {
+              description: 'File uploaded successfully',
+              content: {
+                'application/json': {
+                  schema: {
+                    type: 'object',
+                    properties: {
+                      success: { type: 'boolean', example: true },
+                      file: {
+                        type: 'object',
+                        properties: {
+                          id: { type: 'string', example: 'clh123abc456' },
+                          filename: { type: 'string', example: 'document.pdf' },
+                          mimetype: { type: 'string', example: 'application/pdf' },
+                          size: { type: 'number', example: 1024000 },
+                          createdAt: { type: 'string', format: 'date-time' },
+                        },
+                      },
+                    },
+                  },
+                },
+              },
+            },
+            '400': {
+              description: 'Bad request - No file provided',
+              content: {
+                'application/json': {
+                  schema: {
+                    type: 'object',
+                    properties: {
+                      success: { type: 'boolean', example: false },
+                      error: { type: 'string', example: 'No file provided' },
+                    },
+                  },
+                },
+              },
+            },
+            '500': {
+              description: 'Internal server error',
+              content: {
+                'application/json': {
+                  schema: {
+                    type: 'object',
+                    properties: {
+                      success: { type: 'boolean', example: false },
+                      error: { type: 'string', example: 'Failed to upload file' },
+                    },
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+      '/api/files': {
+        get: {
+          summary: 'List all files',
+          description: 'Get a list of all uploaded files with metadata',
+          tags: ['Files'],
+          responses: {
+            '200': {
+              description: 'List of files retrieved successfully',
+              content: {
+                'application/json': {
+                  schema: {
+                    type: 'object',
+                    properties: {
+                      success: { type: 'boolean', example: true },
+                      files: {
+                        type: 'array',
+                        items: {
+                          type: 'object',
+                          properties: {
+                            id: { type: 'string', example: 'clh123abc456' },
+                            filename: { type: 'string', example: 'document.pdf' },
+                            mimetype: { type: 'string', example: 'application/pdf' },
+                            size: { type: 'number', example: 1024000 },
+                            createdAt: { type: 'string', format: 'date-time' },
+                            updatedAt: { type: 'string', format: 'date-time' },
+                          },
+                        },
+                      },
+                    },
+                  },
+                },
+              },
+            },
+            '500': {
+              description: 'Internal server error',
+              content: {
+                'application/json': {
+                  schema: {
+                    type: 'object',
+                    properties: {
+                      success: { type: 'boolean', example: false },
+                      error: { type: 'string', example: 'Failed to list files' },
+                    },
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+      '/api/files/{id}': {
+        get: {
+          summary: 'Download a file',
+          description: 'Download a specific file by its ID',
+          tags: ['Files'],
+          parameters: [
+            {
+              name: 'id',
+              in: 'path',
+              required: true,
+              description: 'The ID of the file to download',
+              schema: {
+                type: 'string',
+                example: 'clh123abc456',
+              },
+            },
+          ],
+          responses: {
+            '200': {
+              description: 'File downloaded successfully',
+              content: {
+                'application/octet-stream': {
+                  schema: {
+                    type: 'string',
+                    format: 'binary',
+                  },
+                },
+              },
+              headers: {
+                'Content-Type': {
+                  description: 'MIME type of the file',
+                  schema: { type: 'string' },
+                },
+                'Content-Disposition': {
+                  description: 'Attachment header with filename',
+                  schema: { type: 'string' },
+                },
+                'Content-Length': {
+                  description: 'Size of the file in bytes',
+                  schema: { type: 'integer' },
+                },
+              },
+            },
+            '404': {
+              description: 'File not found',
+              content: {
+                'application/json': {
+                  schema: {
+                    type: 'object',
+                    properties: {
+                      error: { type: 'string', example: 'File not found' },
+                    },
+                  },
+                },
+              },
+            },
+            '500': {
+              description: 'Internal server error',
+              content: {
+                'application/json': {
+                  schema: {
+                    type: 'object',
+                    properties: {
+                      error: { type: 'string', example: 'Failed to retrieve file' },
+                    },
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    },
+  };
+
+  return (
+    <div className="min-h-screen bg-gray-50">
+      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
+        <div className="bg-white shadow rounded-lg">
+          <div className="px-4 py-5 sm:p-6">
+            <h1 className="text-2xl font-bold text-gray-900 mb-4">
+              Vtorio API Documentation
+            </h1>
+            <p className="text-gray-600 mb-6">
+              Interactive API documentation for file upload and management endpoints.
+            </p>
+            <SwaggerUI spec={spec} />
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 18 - 3
app/api/files/[id]/route.ts

@@ -13,27 +13,42 @@ export const GET = async (
     });
 
     if (!file) {
-      return NextResponse.json(
+      const response = NextResponse.json(
         { error: "File not found" },
         { status: 404 }
       );
+      response.headers.set('Access-Control-Allow-Origin', '*');
+      return response;
     }
 
     // Return file as downloadable response
-    return new NextResponse(file.data, {
+    const response = new NextResponse(file.data, {
       headers: {
         "Content-Type": file.mimetype,
         "Content-Disposition": `attachment; filename="${file.filename}"`,
         "Content-Length": file.size.toString(),
+        "Access-Control-Allow-Origin": "*",
       },
     });
+    
+    return response;
   } catch (error) {
     console.error("Error retrieving file:", error);
-    return NextResponse.json(
+    const response = NextResponse.json(
       { error: "Failed to retrieve file" },
       { status: 500 }
     );
+    response.headers.set('Access-Control-Allow-Origin', '*');
+    return response;
   } finally {
     await prisma.$disconnect();
   }
 };
+
+export const OPTIONS = async () => {
+  const response = new NextResponse(null, { status: 200 });
+  response.headers.set('Access-Control-Allow-Origin', '*');
+  response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
+  response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
+  return response;
+};

+ 20 - 2
app/api/files/route.ts

@@ -19,17 +19,35 @@ export const GET = async (req: NextRequest) => {
       },
     });
 
-    return NextResponse.json({
+    const response = NextResponse.json({
       success: true,
       files,
     });
+    
+    // Add CORS headers
+    response.headers.set('Access-Control-Allow-Origin', '*');
+    response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
+    response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
+    
+    return response;
   } catch (error) {
     console.error("Error listing files:", error);
-    return NextResponse.json(
+    const response = NextResponse.json(
       { success: false, error: "Failed to list files" },
       { status: 500 }
     );
+    
+    response.headers.set('Access-Control-Allow-Origin', '*');
+    return response;
   } finally {
     await prisma.$disconnect();
   }
 };
+
+export const OPTIONS = async () => {
+  const response = new NextResponse(null, { status: 200 });
+  response.headers.set('Access-Control-Allow-Origin', '*');
+  response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
+  response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
+  return response;
+};

+ 226 - 0
app/api/openapi.json/route.ts

@@ -0,0 +1,226 @@
+import { NextResponse } from 'next/server';
+
+const openApiSpec = {
+  openapi: '3.0.0',
+  info: {
+    title: 'Vtorio API',
+    version: '1.0.0',
+    description: 'File upload and management API for Vtorio application',
+  },
+  servers: [
+    {
+      url: 'http://localhost:3000',
+      description: 'Development server',
+    },
+  ],
+  paths: {
+    '/api/upload': {
+      post: {
+        summary: 'Upload a file',
+        description: 'Upload a file to the server and store it in the database',
+        tags: ['Files'],
+        requestBody: {
+          required: true,
+          content: {
+            'multipart/form-data': {
+              schema: {
+                type: 'object',
+                properties: {
+                  file: {
+                    type: 'string',
+                    format: 'binary',
+                    description: 'The file to upload',
+                  },
+                },
+                required: ['file'],
+              },
+            },
+          },
+        },
+        responses: {
+          '200': {
+            description: 'File uploaded successfully',
+            content: {
+              'application/json': {
+                schema: {
+                  type: 'object',
+                  properties: {
+                    success: { type: 'boolean', example: true },
+                    file: {
+                      type: 'object',
+                      properties: {
+                        id: { type: 'string', example: 'clh123abc456' },
+                        filename: { type: 'string', example: 'document.pdf' },
+                        mimetype: { type: 'string', example: 'application/pdf' },
+                        size: { type: 'number', example: 1024000 },
+                        createdAt: { type: 'string', format: 'date-time' },
+                      },
+                    },
+                  },
+                },
+              },
+            },
+          },
+          '400': {
+            description: 'Bad request - No file provided',
+            content: {
+              'application/json': {
+                schema: {
+                  type: 'object',
+                  properties: {
+                    success: { type: 'boolean', example: false },
+                    error: { type: 'string', example: 'No file provided' },
+                  },
+                },
+              },
+            },
+          },
+          '500': {
+            description: 'Internal server error',
+            content: {
+              'application/json': {
+                schema: {
+                  type: 'object',
+                  properties: {
+                    success: { type: 'boolean', example: false },
+                    error: { type: 'string', example: 'Failed to upload file' },
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    },
+    '/api/files': {
+      get: {
+        summary: 'List all files',
+        description: 'Get a list of all uploaded files with metadata',
+        tags: ['Files'],
+        responses: {
+          '200': {
+            description: 'List of files retrieved successfully',
+            content: {
+              'application/json': {
+                schema: {
+                  type: 'object',
+                  properties: {
+                    success: { type: 'boolean', example: true },
+                    files: {
+                      type: 'array',
+                      items: {
+                        type: 'object',
+                        properties: {
+                          id: { type: 'string', example: 'clh123abc456' },
+                          filename: { type: 'string', example: 'document.pdf' },
+                          mimetype: { type: 'string', example: 'application/pdf' },
+                          size: { type: 'number', example: 1024000 },
+                          createdAt: { type: 'string', format: 'date-time' },
+                          updatedAt: { type: 'string', format: 'date-time' },
+                        },
+                      },
+                    },
+                  },
+                },
+              },
+            },
+          },
+          '500': {
+            description: 'Internal server error',
+            content: {
+              'application/json': {
+                schema: {
+                  type: 'object',
+                  properties: {
+                    success: { type: 'boolean', example: false },
+                    error: { type: 'string', example: 'Failed to list files' },
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    },
+    '/api/files/{id}': {
+      get: {
+        summary: 'Download a file',
+        description: 'Download a specific file by its ID',
+        tags: ['Files'],
+        parameters: [
+          {
+            name: 'id',
+            in: 'path',
+            required: true,
+            description: 'The ID of the file to download',
+            schema: {
+              type: 'string',
+              example: 'clh123abc456',
+            },
+          },
+        ],
+        responses: {
+          '200': {
+            description: 'File downloaded successfully',
+            content: {
+              'application/octet-stream': {
+                schema: {
+                  type: 'string',
+                  format: 'binary',
+                },
+              },
+            },
+            headers: {
+              'Content-Type': {
+                description: 'MIME type of the file',
+                schema: { type: 'string' },
+              },
+              'Content-Disposition': {
+                description: 'Attachment header with filename',
+                schema: { type: 'string' },
+              },
+              'Content-Length': {
+                description: 'Size of the file in bytes',
+                schema: { type: 'integer' },
+              },
+            },
+          },
+          '404': {
+            description: 'File not found',
+            content: {
+              'application/json': {
+                schema: {
+                  type: 'object',
+                  properties: {
+                    error: { type: 'string', example: 'File not found' },
+                  },
+                },
+              },
+            },
+          },
+          '500': {
+            description: 'Internal server error',
+            content: {
+              'application/json': {
+                schema: {
+                  type: 'object',
+                  properties: {
+                    error: { type: 'string', example: 'Failed to retrieve file' },
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    },
+  },
+};
+
+export const GET = async () => {
+  return NextResponse.json(openApiSpec, {
+    headers: {
+      'Content-Type': 'application/json',
+    },
+  });
+};

+ 20 - 3
app/api/upload/route.ts

@@ -10,10 +10,13 @@ export const POST = async (req: NextRequest) => {
     const file = (body.file as File) || null;
 
     if (!file) {
-      return NextResponse.json({
+      const response = NextResponse.json({
         success: false,
         error: "No file provided",
       }, { status: 400 });
+      
+      response.headers.set('Access-Control-Allow-Origin', '*');
+      return response;
     }
 
     // Convert file to buffer
@@ -29,7 +32,7 @@ export const POST = async (req: NextRequest) => {
       },
     });
 
-    return NextResponse.json({
+    const response = NextResponse.json({
       success: true,
       file: {
         id: savedFile.id,
@@ -39,13 +42,27 @@ export const POST = async (req: NextRequest) => {
         createdAt: savedFile.createdAt,
       },
     });
+    
+    response.headers.set('Access-Control-Allow-Origin', '*');
+    return response;
   } catch (error) {
     console.error("Upload error:", error);
-    return NextResponse.json({
+    const response = NextResponse.json({
       success: false,
       error: "Failed to upload file",
     }, { status: 500 });
+    
+    response.headers.set('Access-Control-Allow-Origin', '*');
+    return response;
   } finally {
     await prisma.$disconnect();
   }
 };
+
+export const OPTIONS = async () => {
+  const response = new NextResponse(null, { status: 200 });
+  response.headers.set('Access-Control-Allow-Origin', '*');
+  response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
+  response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
+  return response;
+};

+ 12 - 54
app/page.tsx

@@ -40,64 +40,22 @@ export default function Home() {
             />
             Go to VixFlixOnline
           </a>
-          {/* <a
-            className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
-            href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
-            target="_blank"
-            rel="noopener noreferrer"
+          <a
+            className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
+            href="/api-docs"
           >
-            Read our docs
-          </a> */}
+            <Image
+              className="dark:invert"
+              src="/file.svg"
+              alt="API docs icon"
+              width={20}
+              height={20}
+            />
+            API Documentation
+          </a>
           <UploadForm />
         </div>
       </main>
-      {/* <footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
-        <a
-          className="flex items-center gap-2 hover:underline hover:underline-offset-4"
-          href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          <Image
-            aria-hidden
-            src="/file.svg"
-            alt="File icon"
-            width={16}
-            height={16}
-          />
-          Learn
-        </a>
-        <a
-          className="flex items-center gap-2 hover:underline hover:underline-offset-4"
-          href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          <Image
-            aria-hidden
-            src="/window.svg"
-            alt="Window icon"
-            width={16}
-            height={16}
-          />
-          Examples
-        </a>
-        <a
-          className="flex items-center gap-2 hover:underline hover:underline-offset-4"
-          href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          <Image
-            aria-hidden
-            src="/globe.svg"
-            alt="Globe icon"
-            width={16}
-            height={16}
-          />
-          Go to nextjs.org →
-        </a>
-      </footer> */}
     </div>
   );
 }

文件差異過大導致無法顯示
+ 829 - 15
package-lock.json


+ 4 - 1
package.json

@@ -10,11 +10,14 @@
   },
   "dependencies": {
     "@prisma/client": "^6.11.1",
+    "@types/swagger-ui-react": "^5.18.0",
     "next": "15.1.6",
     "pg": "^8.16.3",
     "prisma": "^6.11.1",
     "react": "^19.0.0",
-    "react-dom": "^19.0.0"
+    "react-dom": "^19.0.0",
+    "swagger-jsdoc": "^6.2.8",
+    "swagger-ui-react": "^5.26.2"
   },
   "devDependencies": {
     "@eslint/eslintrc": "^3",

部分文件因文件數量過多而無法顯示