REFACTOR(repo): simplify project structure

- Move services/nextjs to nextjs/
- Move deploy/docker/Dockerfile.prod to Dockerfile
- Add GitHub Actions workflows (ci.yml, build.yml)
- Remove deploy/, services/, scripts/ folders
This commit is contained in:
2026-01-05 02:29:10 +09:00
commit e82ea71c22
205 changed files with 21304 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
import { NextRequest, NextResponse } from 'next/server'
import { authenticateUser, generateToken } from '@/shared/lib/auth'
export async function POST(req: NextRequest) {
try {
const { email, password } = await req.json()
// Validation
if (!email || !password) {
return NextResponse.json(
{ error: 'Email and password are required' },
{ status: 400 }
)
}
// Authenticate user
const user = await authenticateUser(email, password)
if (!user) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
)
}
// Generate JWT token
const token = generateToken({
userId: user.id,
email: user.email,
})
// Create response with user data
const response = NextResponse.json({
message: 'Login successful',
token,
user: {
id: user.id,
email: user.email,
name: user.name,
image: user.image,
},
})
// Set auth token in cookie (HttpOnly for security)
response.cookies.set('auth-token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 days
path: '/',
})
return response
} catch (error) {
console.error('Login error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from 'next/server'
export async function POST(req: NextRequest) {
try {
// Create response
const response = NextResponse.json({
message: 'Logout successful',
})
// Clear auth token cookie
response.cookies.delete('auth-token')
return response
} catch (error) {
console.error('Logout error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,54 @@
import { NextRequest, NextResponse } from 'next/server'
import { verifyToken } from '@/shared/lib/auth'
import { db } from '@/shared/lib/db'
export async function GET(req: NextRequest) {
try {
// Get token from cookie
const token = req.cookies.get('auth-token')?.value
if (!token) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
// Verify token
const payload = verifyToken(token)
if (!payload) {
return NextResponse.json(
{ error: 'Invalid token' },
{ status: 401 }
)
}
// Get user from database
const user = await db.user.findUnique({
where: { id: payload.userId },
select: {
id: true,
email: true,
name: true,
image: true,
},
})
if (!user) {
return NextResponse.json(
{ error: 'User not found' },
{ status: 404 }
)
}
return NextResponse.json(user)
} catch (error) {
console.error('Get user error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,76 @@
import { NextRequest, NextResponse } from 'next/server'
import { createUser, generateToken } from '@/shared/lib/auth'
import { db } from '@/shared/lib/db'
export async function POST(req: NextRequest) {
try {
const { email, password, name } = await req.json()
// Validation
if (!email || !password) {
return NextResponse.json(
{ error: 'Email and password are required' },
{ status: 400 }
)
}
if (password.length < 6) {
return NextResponse.json(
{ error: 'Password must be at least 6 characters' },
{ status: 400 }
)
}
// Check if user already exists
const existingUser = await db.user.findUnique({
where: { email },
})
if (existingUser) {
return NextResponse.json(
{ error: 'User already exists' },
{ status: 400 }
)
}
// Create user
const user = await createUser(email, password, name)
// Generate JWT token
const token = generateToken({
userId: user.id,
email: user.email,
})
// Create response with user data
const response = NextResponse.json(
{
message: 'User created successfully',
token,
user: {
id: user.id,
email: user.email,
name: user.name,
}
},
{ status: 201 }
)
// Set auth token in cookie (HttpOnly for security)
response.cookies.set('auth-token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 days
path: '/',
})
return response
} catch (error) {
console.error('Registration error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,49 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/shared/lib/db'
// GET /api/documents/[id]/public - Get a public document (no auth required)
export async function GET(req: NextRequest) {
try {
const url = new URL(req.url)
const id = url.pathname.split('/')[3] // Extract ID from /api/documents/[id]/public
if (!id) {
return NextResponse.json(
{ error: 'Document ID is required' },
{ status: 400 }
)
}
// 공개된 문서만 조회 (인증 없이)
const document = await db.document.findFirst({
where: {
id,
isPublished: true, // 공개된 문서만
isArchived: false,
},
include: {
user: {
select: {
name: true,
email: true,
}
}
}
})
if (!document) {
return NextResponse.json(
{ error: 'Document not found or not published' },
{ status: 404 }
)
}
return NextResponse.json(document)
} catch (error) {
console.error('Error fetching public document:', error)
return NextResponse.json(
{ error: 'Failed to fetch document' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,202 @@
import { NextRequest, NextResponse } from 'next/server'
import { withAuth } from '@/shared/lib/middleware'
import { db } from '@/shared/lib/db'
import { deleteFile } from '@/shared/lib/s3'
// GET /api/documents/[id] - Get a specific document
export const GET = withAuth(async (req: NextRequest, userId: string) => {
try {
const url = new URL(req.url)
const id = url.pathname.split('/').pop()
if (!id) {
return NextResponse.json(
{ error: 'Document ID is required' },
{ status: 400 }
)
}
// Allow viewing both archived and non-archived documents
const document = await db.document.findFirst({
where: {
id,
userId,
},
})
if (!document) {
return NextResponse.json(
{ error: 'Document not found' },
{ status: 404 }
)
}
return NextResponse.json(document)
} catch (error) {
console.error('Error fetching document:', error)
return NextResponse.json(
{ error: 'Failed to fetch document' },
{ status: 500 }
)
}
})
// PUT /api/documents/[id] - Update a specific document
export const PUT = withAuth(async (req: NextRequest, userId: string) => {
try {
const url = new URL(req.url)
const id = url.pathname.split('/').pop()
const { title, content, icon, cover, isPublished } = await req.json()
if (!id) {
return NextResponse.json(
{ error: 'Document ID is required' },
{ status: 400 }
)
}
// Check if document exists and belongs to user
const existingDocument = await db.document.findFirst({
where: {
id,
userId,
isArchived: false,
},
})
if (!existingDocument) {
return NextResponse.json(
{ error: 'Document not found' },
{ status: 404 }
)
}
const document = await db.document.update({
where: { id },
data: {
...(title && { title }),
...(content && { content }),
...(icon !== undefined && { icon }),
...(cover !== undefined && { cover }),
...(isPublished !== undefined && { isPublished }),
updatedAt: new Date(),
},
})
return NextResponse.json(document)
} catch (error) {
console.error('Error updating document:', error)
return NextResponse.json(
{ error: 'Failed to update document' },
{ status: 500 }
)
}
})
// Helper function to extract image paths from document content
async function extractImagePaths(content: any): Promise<string[]> {
if (!content) return [];
const paths: string[] = [];
try {
const parsed = typeof content === 'string' ? JSON.parse(content) : content;
// Recursive function to traverse the document structure
const traverse = (node: any) => {
if (!node) return;
// Check if this is an imageUpload node with a path
if (node.type === 'imageUpload' && node.attrs?.path) {
paths.push(node.attrs.path);
}
// Recursively check content and children
if (node.content && Array.isArray(node.content)) {
node.content.forEach(traverse);
}
if (node.children && Array.isArray(node.children)) {
node.children.forEach(traverse);
}
};
// Check if parsed is an object with content
if (parsed.content) {
traverse(parsed);
} else if (Array.isArray(parsed)) {
parsed.forEach(traverse);
} else if (parsed.type) {
traverse(parsed);
}
} catch (error) {
console.error('Error parsing document content:', error);
}
return paths;
}
// DELETE /api/documents/[id] - Archive a specific document
export const DELETE = withAuth(async (req: NextRequest, userId: string) => {
try {
const url = new URL(req.url)
const id = url.pathname.split('/').pop()
if (!id) {
return NextResponse.json(
{ error: 'Document ID is required' },
{ status: 400 }
)
}
// Check if document exists and belongs to user
const existingDocument = await db.document.findFirst({
where: {
id,
userId,
isArchived: false,
},
})
if (!existingDocument) {
return NextResponse.json(
{ error: 'Document not found' },
{ status: 404 }
)
}
// Extract and delete all image files from the document
const imagePaths = await extractImagePaths(existingDocument.content);
if (imagePaths.length > 0) {
console.log(`Deleting ${imagePaths.length} image files from document ${id}`);
// Delete all image files
await Promise.allSettled(
imagePaths.map(path => {
try {
return deleteFile(path);
} catch (error) {
console.error(`Failed to delete image file ${path}:`, error);
return Promise.resolve();
}
})
);
}
// Archive the document instead of deleting it
const document = await db.document.update({
where: { id },
data: {
isArchived: true,
updatedAt: new Date(),
},
})
return NextResponse.json({ message: 'Document archived successfully' })
} catch (error) {
console.error('Error archiving document:', error)
return NextResponse.json(
{ error: 'Failed to archive document' },
{ status: 500 }
)
}
})

View File

@@ -0,0 +1,84 @@
import { NextRequest, NextResponse } from 'next/server'
import { withAuth } from '@/shared/lib/middleware'
import { db } from '@/shared/lib/db'
// GET /api/documents - Get all documents for the authenticated user
export const GET = withAuth(async (req: NextRequest, userId: string) => {
try {
const url = new URL(req.url)
const folderId = url.searchParams.get('folderId')
const documents = await db.document.findMany({
where: {
userId,
isArchived: false,
...(folderId && { folderId }),
},
include: {
folder: {
select: {
id: true,
name: true,
icon: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
})
return NextResponse.json(documents)
} catch (error) {
console.error('Error fetching documents:', error)
return NextResponse.json(
{ error: 'Failed to fetch documents' },
{ status: 500 }
)
}
})
// POST /api/documents - Create a new document
export const POST = withAuth(async (req: NextRequest, userId: string) => {
try {
const { title, parentId, folderId } = await req.json()
if (!title) {
return NextResponse.json(
{ error: 'Title is required' },
{ status: 400 }
)
}
const document = await db.document.create({
data: {
title,
userId,
parentId: parentId || null,
folderId: folderId || null,
content: {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Start writing...',
},
],
},
],
},
},
})
return NextResponse.json(document, { status: 201 })
} catch (error) {
console.error('Error creating document:', error)
return NextResponse.json(
{ error: 'Failed to create document' },
{ status: 500 }
)
}
})

View File

@@ -0,0 +1,46 @@
import { NextRequest, NextResponse } from 'next/server';
import { GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { S3_CONFIG, s3Client } from '@/shared/config/s3';
// 파일 다운로드 URL 생성
const generateDownloadUrl = async (fileKey: string): Promise<string> => {
try {
const command = new GetObjectCommand({
Bucket: S3_CONFIG.BUCKET_NAME,
Key: fileKey,
});
const downloadUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600 });
return downloadUrl;
} catch (error) {
throw new Error('다운로드 URL 생성에 실패했습니다.');
}
};
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { fileKey } = body;
if (!fileKey) {
return NextResponse.json(
{ error: 'fileKey가 필요합니다.' },
{ status: 400 }
);
}
const downloadUrl = await generateDownloadUrl(fileKey);
return NextResponse.json({
downloadUrl,
});
} catch (error: any) {
return NextResponse.json(
{ error: error.message || '다운로드 URL 생성에 실패했습니다.' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,264 @@
import { NextRequest, NextResponse } from 'next/server'
import { withAuth } from '@/shared/lib/middleware'
import { db } from '@/shared/lib/db'
// GET /api/folders/[id] - Get a specific folder
export const GET = withAuth(async (req: NextRequest, userId: string) => {
try {
const url = new URL(req.url)
const id = url.pathname.split('/').pop()
if (!id) {
return NextResponse.json(
{ error: 'Folder ID is required' },
{ status: 400 }
)
}
const folder = await db.folder.findFirst({
where: {
id,
userId,
isArchived: false,
},
include: {
documents: {
where: {
isArchived: false,
},
select: {
id: true,
title: true,
icon: true,
updatedAt: true,
},
orderBy: {
updatedAt: 'desc',
},
},
children: {
where: {
isArchived: false,
},
orderBy: {
createdAt: 'desc',
},
},
parent: {
select: {
id: true,
name: true,
},
},
_count: {
select: {
documents: {
where: {
isArchived: false,
},
},
children: {
where: {
isArchived: false,
},
},
},
},
},
})
if (!folder) {
return NextResponse.json(
{ error: 'Folder not found' },
{ status: 404 }
)
}
return NextResponse.json(folder)
} catch (error) {
console.error('Error fetching folder:', error)
return NextResponse.json(
{ error: 'Failed to fetch folder' },
{ status: 500 }
)
}
})
// PUT /api/folders/[id] - Update a specific folder
export const PUT = withAuth(async (req: NextRequest, userId: string) => {
try {
const url = new URL(req.url)
const id = url.pathname.split('/').pop()
const { name, parentId, icon, color } = await req.json()
if (!id) {
return NextResponse.json(
{ error: 'Folder ID is required' },
{ status: 400 }
)
}
// Check if folder exists and belongs to user
const existingFolder = await db.folder.findFirst({
where: {
id,
userId,
isArchived: false,
},
})
if (!existingFolder) {
return NextResponse.json(
{ error: 'Folder not found' },
{ status: 404 }
)
}
const folder = await db.folder.update({
where: { id },
data: {
...(name && { name }),
...(parentId !== undefined && { parentId: parentId || null }),
...(icon !== undefined && { icon }),
...(color !== undefined && { color }),
updatedAt: new Date(),
},
include: {
documents: {
where: {
isArchived: false,
},
select: {
id: true,
title: true,
icon: true,
updatedAt: true,
},
orderBy: {
updatedAt: 'desc',
},
},
children: {
where: {
isArchived: false,
},
orderBy: {
createdAt: 'desc',
},
},
parent: {
select: {
id: true,
name: true,
},
},
_count: {
select: {
documents: {
where: {
isArchived: false,
},
},
children: {
where: {
isArchived: false,
},
},
},
},
},
})
return NextResponse.json(folder)
} catch (error) {
console.error('Error updating folder:', error)
return NextResponse.json(
{ error: 'Failed to update folder' },
{ status: 500 }
)
}
})
// DELETE /api/folders/[id] - Archive a specific folder
export const DELETE = withAuth(async (req: NextRequest, userId: string) => {
try {
const url = new URL(req.url)
const id = url.pathname.split('/').pop()
if (!id) {
return NextResponse.json(
{ error: 'Folder ID is required' },
{ status: 400 }
)
}
// Check if folder exists and belongs to user
const existingFolder = await db.folder.findFirst({
where: {
id,
userId,
isArchived: false,
},
})
if (!existingFolder) {
return NextResponse.json(
{ error: 'Folder not found' },
{ status: 404 }
)
}
// Recursively archive the folder and all its contents
const archiveFolderRecursively = async (folderId: string, tx: any) => {
// Archive all documents in this folder
await tx.document.updateMany({
where: {
folderId: folderId,
isArchived: false,
},
data: {
isArchived: true,
updatedAt: new Date(),
},
})
// Get all child folders
const childFolders = await tx.folder.findMany({
where: {
parentId: folderId,
isArchived: false,
},
select: {
id: true,
},
})
// Recursively archive all child folders
for (const childFolder of childFolders) {
await archiveFolderRecursively(childFolder.id, tx)
}
// Archive this folder
await tx.folder.update({
where: { id: folderId },
data: {
isArchived: true,
updatedAt: new Date(),
},
})
}
// Archive the folder and all its contents
await db.$transaction(async (tx) => {
await archiveFolderRecursively(id, tx)
})
return NextResponse.json({ message: 'Folder archived successfully' })
} catch (error) {
console.error('Error archiving folder:', error)
return NextResponse.json(
{ error: 'Failed to archive folder' },
{ status: 500 }
)
}
})

View File

@@ -0,0 +1,154 @@
import { NextRequest, NextResponse } from 'next/server'
import { withAuth } from '@/shared/lib/middleware'
import { db } from '@/shared/lib/db'
// GET /api/folders - Get all folders for the authenticated user
export const GET = withAuth(async (req: NextRequest, userId: string) => {
try {
const folders = await db.folder.findMany({
where: {
userId,
isArchived: false,
},
include: {
documents: {
where: {
isArchived: false,
},
select: {
id: true,
title: true,
icon: true,
updatedAt: true,
},
},
children: {
where: {
isArchived: false,
},
include: {
_count: {
select: {
documents: {
where: {
isArchived: false,
},
},
children: {
where: {
isArchived: false,
},
},
},
},
},
},
_count: {
select: {
documents: {
where: {
isArchived: false,
},
},
children: {
where: {
isArchived: false,
},
},
},
},
},
orderBy: {
createdAt: 'desc',
},
})
return NextResponse.json(folders)
} catch (error) {
console.error('Error fetching folders:', error)
return NextResponse.json(
{ error: 'Failed to fetch folders' },
{ status: 500 }
)
}
})
// POST /api/folders - Create a new folder
export const POST = withAuth(async (req: NextRequest, userId: string) => {
try {
const { name, parentId, icon, color } = await req.json()
if (!name) {
return NextResponse.json(
{ error: 'Folder name is required' },
{ status: 400 }
)
}
const folder = await db.folder.create({
data: {
name,
userId,
parentId: parentId || null,
icon: icon || '📁',
color: color || null,
},
include: {
documents: {
where: {
isArchived: false,
},
select: {
id: true,
title: true,
icon: true,
updatedAt: true,
},
},
children: {
where: {
isArchived: false,
},
include: {
_count: {
select: {
documents: {
where: {
isArchived: false,
},
},
children: {
where: {
isArchived: false,
},
},
},
},
},
},
_count: {
select: {
documents: {
where: {
isArchived: false,
},
},
children: {
where: {
isArchived: false,
},
},
},
},
},
})
return NextResponse.json(folder, { status: 201 })
} catch (error) {
console.error('Error creating folder:', error)
return NextResponse.json(
{ error: 'Failed to create folder' },
{ status: 500 }
)
}
})

View File

@@ -0,0 +1,27 @@
import { NextResponse } from 'next/server';
import { db } from '@/shared/lib/db';
export async function GET() {
try {
// Check database connectivity
await db.$queryRaw`SELECT 1`;
return NextResponse.json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
environment: process.env.NODE_ENV,
version: process.env.npm_package_version || '1.0.0',
database: 'connected'
});
} catch (error) {
return NextResponse.json(
{
status: 'unhealthy',
timestamp: new Date().toISOString(),
error: 'Database connection failed'
},
{ status: 503 }
);
}
}

View File

@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from 'next/server';
import { extractBookmarkMetadata } from '@/shared/lib/metadata-extractor';
export async function POST(request: NextRequest) {
try {
const { url } = await request.json();
if (!url) {
return NextResponse.json({ error: 'URL is required' }, { status: 400 });
}
const metadata = await extractBookmarkMetadata(url);
return NextResponse.json(metadata);
} catch (error) {
console.error('Error in metadata API:', error);
return NextResponse.json(
{ error: 'Failed to fetch metadata' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,105 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/shared/lib/prisma';
import { withAuth } from '@/shared/lib/middleware';
async function createTemplate(request: NextRequest, userId: string) {
try {
const { name, description, category, title, content } = await request.json();
if (!name || !title || !content) {
return NextResponse.json(
{ error: 'Template name, title, and content are required' },
{ status: 400 }
);
}
// Verify user exists in database
const user = await prisma.user.findUnique({
where: { id: userId }
});
if (!user) {
return NextResponse.json(
{ error: 'User not found' },
{ status: 404 }
);
}
// Save template to database (always private to the user)
const template = await prisma.template.create({
data: {
name,
description: description || '',
category: category || 'General',
title,
content,
isPublic: false, // Always private
userId,
},
});
console.log('Template created:', template);
return NextResponse.json({
success: true,
template,
message: 'Template created successfully'
});
} catch (error) {
console.error('Error creating template:', error);
return NextResponse.json(
{ error: 'Failed to create template' },
{ status: 500 }
);
}
}
export const POST = withAuth(createTemplate);
async function getTemplates(request: NextRequest, userId: string) {
try {
// Verify user exists in database
const user = await prisma.user.findUnique({
where: { id: userId }
});
if (!user) {
return NextResponse.json(
{ error: 'User not found' },
{ status: 404 }
);
}
// Fetch only user's own templates
const templates = await prisma.template.findMany({
where: {
userId // Only user's own templates
},
orderBy: {
createdAt: 'desc'
},
select: {
id: true,
name: true,
description: true,
category: true,
title: true,
content: true,
isPublic: true,
createdAt: true,
updatedAt: true,
userId: true,
}
});
return NextResponse.json({ templates });
} catch (error) {
console.error('Error fetching templates:', error);
return NextResponse.json(
{ error: 'Failed to fetch templates' },
{ status: 500 }
);
}
}
export const GET = withAuth(getTemplates);

View File

@@ -0,0 +1,57 @@
import { NextRequest, NextResponse } from 'next/server';
import { PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { S3_CONFIG, s3Client } from '@/shared/config/s3';
// 파일 업로드 URL 생성
const generateUploadUrl = async (
fileName: string,
fileType: string,
folder?: string
): Promise<{ uploadUrl: string; fileKey: string }> => {
try {
const fileKey = folder ? `${folder}/${Date.now()}-${fileName}` : `${Date.now()}-${fileName}`;
const command = new PutObjectCommand({
Bucket: S3_CONFIG.BUCKET_NAME,
Key: fileKey,
ContentType: fileType,
});
const uploadUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600 });
return {
uploadUrl,
fileKey,
};
} catch (error: any) {
throw new Error(`업로드 URL 생성에 실패했습니다: ${error.message}`);
}
};
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { fileName, fileType, folder } = body;
if (!fileName || !fileType) {
return NextResponse.json(
{ error: 'fileName과 fileType이 필요합니다.' },
{ status: 400 }
);
}
const { uploadUrl, fileKey } = await generateUploadUrl(fileName, fileType, folder || 'jotion-uploads');
return NextResponse.json({
uploadUrl,
fileKey,
});
} catch (error: any) {
return NextResponse.json(
{ error: error.message || '업로드 URL 생성에 실패했습니다.' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,177 @@
import { NextRequest, NextResponse } from 'next/server';
import { uploadFileServer, deleteFile } from '@/shared/lib/s3';
import { cookies } from 'next/headers';
import { verifyToken } from '@/shared/lib/auth';
// File size limits (in bytes)
const MAX_FILE_SIZE = {
image: 10 * 1024 * 1024, // 10MB
audio: 50 * 1024 * 1024, // 50MB
document: 20 * 1024 * 1024, // 20MB
};
// Allowed file types
const ALLOWED_TYPES = {
image: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'],
audio: ['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/mp4', 'audio/webm'],
document: [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/plain',
'text/csv',
],
};
export async function POST(request: NextRequest) {
try {
// Verify authentication - check cookie
const cookieStore = await cookies();
const cookieToken = cookieStore.get('auth-token')?.value;
const token = cookieToken;
if (!token) {
console.error('[Upload] No token found in Authorization header or cookie');
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
const decoded = verifyToken(token);
if (!decoded) {
console.error('[Upload] Token verification failed');
return NextResponse.json(
{ error: 'Invalid token' },
{ status: 401 }
);
}
console.log('[Upload] Token verified successfully for user:', decoded.userId);
// Parse form data
const formData = await request.formData();
const file = formData.get('file') as File;
const fileType = formData.get('type') as 'images' | 'audio' | 'documents';
if (!file) {
return NextResponse.json(
{ error: 'No file provided' },
{ status: 400 }
);
}
// Determine folder based on file type
let folder: 'images' | 'audio' | 'documents' = 'documents';
let maxSize = MAX_FILE_SIZE.document;
let allowedTypes = ALLOWED_TYPES.document;
if (fileType === 'images') {
folder = 'images';
maxSize = MAX_FILE_SIZE.image;
allowedTypes = ALLOWED_TYPES.image;
} else if (fileType === 'audio') {
folder = 'audio';
maxSize = MAX_FILE_SIZE.audio;
allowedTypes = ALLOWED_TYPES.audio;
} else if (file.type.startsWith('image/')) {
folder = 'images';
maxSize = MAX_FILE_SIZE.image;
allowedTypes = ALLOWED_TYPES.image;
} else if (file.type.startsWith('audio/')) {
folder = 'audio';
maxSize = MAX_FILE_SIZE.audio;
allowedTypes = ALLOWED_TYPES.audio;
}
// Validate file size
if (file.size > maxSize) {
return NextResponse.json(
{ error: `File size exceeds maximum limit of ${maxSize / 1024 / 1024}MB` },
{ status: 400 }
);
}
// Validate file type
if (!allowedTypes.includes(file.type)) {
return NextResponse.json(
{ error: `File type ${file.type} is not allowed` },
{ status: 400 }
);
}
// Upload file to S3
const { url, path } = await uploadFileServer(file, file.name, file.type, folder);
return NextResponse.json({
success: true,
url,
path,
fileName: file.name,
fileSize: file.size,
fileType: file.type,
});
} catch (error) {
console.error('Upload error:', error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to upload file' },
{ status: 500 }
);
}
}
// DELETE /api/upload - Delete a file from storage
export async function DELETE(request: NextRequest) {
try {
// Verify authentication
const authHeader = request.headers.get('Authorization');
const cookieStore = await cookies();
const cookieToken = cookieStore.get('auth-token')?.value;
const token = authHeader?.startsWith('Bearer ')
? authHeader.substring(7)
: cookieToken;
if (!token) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
const decoded = verifyToken(token);
if (!decoded) {
return NextResponse.json(
{ error: 'Invalid token' },
{ status: 401 }
);
}
const { searchParams } = new URL(request.url);
const filePath = searchParams.get('path');
if (!filePath) {
return NextResponse.json(
{ error: 'File path is required' },
{ status: 400 }
);
}
// Delete file from S3
await deleteFile(filePath);
return NextResponse.json({
success: true,
message: 'File deleted successfully',
});
} catch (error) {
console.error('Delete file error:', error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to delete file' },
{ status: 500 }
);
}
}