Create Folder in FileNode needs more work
This commit is contained in:
parent
59444e9c7c
commit
e961f60b01
5 changed files with 225 additions and 133 deletions
|
|
@ -2,6 +2,12 @@ import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
|
experimental: {
|
||||||
|
serverActions: {
|
||||||
|
// Set this higher than your MAX_FILE_SIZE in upload-view.tsx
|
||||||
|
bodySizeLimit: '150mb',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
@ -50,7 +50,7 @@ model Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
model FileNode {
|
model FileNode {
|
||||||
id String @id @default(uuid())
|
id String @id // Removed @default(uuid()) to allow manual assignment
|
||||||
name String
|
name String
|
||||||
size BigInt?
|
size BigInt?
|
||||||
isFolder Boolean @default(false)
|
isFolder Boolean @default(false)
|
||||||
|
|
|
||||||
|
|
@ -5,99 +5,94 @@ import { getFreshAccessToken } from "@/lib/auth-utils";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a virtual folder in the database.
|
||||||
|
* No physical folder is created on OneDrive to keep storage flat and fast.
|
||||||
|
*/
|
||||||
|
export async function createFolderAction(name: string, parentId: string | null = null) {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session?.user?.id) throw new Error("Unauthorized");
|
||||||
|
|
||||||
|
const internalId = crypto.randomUUID();
|
||||||
|
|
||||||
|
await prisma.fileNode.create({
|
||||||
|
data: {
|
||||||
|
id: internalId,
|
||||||
|
name: name,
|
||||||
|
isFolder: true,
|
||||||
|
path: `/virtual/${name}`,
|
||||||
|
ownerId: session.user.id,
|
||||||
|
parentId: parentId || null,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
revalidatePath("/upload");
|
||||||
|
revalidatePath("/dashboard");
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a file to a unique UUID folder on OneDrive
|
||||||
|
* and links it to a virtual parent in the DB.
|
||||||
|
*/
|
||||||
export async function uploadFileAction(formData: FormData) {
|
export async function uploadFileAction(formData: FormData) {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
if (!session?.user?.id) throw new Error("Unauthorized");
|
if (!session?.user?.id) throw new Error("Unauthorized");
|
||||||
|
|
||||||
const file = formData.get("file") as File;
|
const file = formData.get("file") as File;
|
||||||
|
const description = formData.get("description") as string || "";
|
||||||
|
const parentId = formData.get("parentId") as string | null;
|
||||||
|
|
||||||
if (!file) throw new Error("No file selected");
|
if (!file) throw new Error("No file selected");
|
||||||
|
|
||||||
const accessToken = await getFreshAccessToken(session.user.id);
|
const accessToken = await getFreshAccessToken(session.user.id);
|
||||||
const folderName = "WebCalibre";
|
const rootFolder = "WebCalibre";
|
||||||
|
const internalId = crypto.randomUUID();
|
||||||
|
|
||||||
// --- 1. CHECK/CREATE THE WEBCALIBRE FOLDER ---
|
// 1. Ensure WebCalibre exists (Simplified for brevity)
|
||||||
// We check if the folder exists at the root of the user's OneDrive
|
// ... (Keep the root folder check logic from your previous version)
|
||||||
const folderCheckUrl = `https://graph.microsoft.com/v1.0/me/drive/root:/${folderName}`;
|
|
||||||
const folderCheck = await fetch(folderCheckUrl, {
|
// 2. Create the unique UUID folder on OneDrive
|
||||||
headers: { Authorization: `Bearer ${accessToken}` }
|
const createSubFolderRes = await fetch(`https://graph.microsoft.com/v1.0/me/drive/root:/${rootFolder}:/children`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ name: internalId, folder: {}, "@microsoft.graph.conflictBehavior": "fail" })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (folderCheck.status === 404) {
|
if (!createSubFolderRes.ok) throw new Error("Storage directory creation failed");
|
||||||
console.log(`📂 Folder '${folderName}' not found. Creating it...`);
|
const subFolderData = await createSubFolderRes.json();
|
||||||
const createFolderRes = await fetch(`https://graph.microsoft.com/v1.0/me/drive/root/children`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
name: folderName,
|
|
||||||
folder: {}, // Empty object tells Graph to create a folder
|
|
||||||
"@microsoft.graph.conflictBehavior": "fail"
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!createFolderRes.ok) {
|
|
||||||
const errorData = await createFolderRes.json();
|
|
||||||
console.error("❌ Folder Creation Error:", errorData);
|
|
||||||
throw new Error("Could not create WebCalibre folder on OneDrive.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 2. CREATE UPLOAD SESSION ---
|
// 3. Create Upload Session & Upload
|
||||||
// encodeURIComponent is vital for filenames with spaces or special characters
|
const sessionUrl = `https://graph.microsoft.com/v1.0/me/drive/items/${subFolderData.id}:/${encodeURIComponent(file.name)}:/createUploadSession`;
|
||||||
const sessionUrl = `https://graph.microsoft.com/v1.0/me/drive/root:/${folderName}/${encodeURIComponent(file.name)}:/createUploadSession`;
|
|
||||||
|
|
||||||
const sessionRes = await fetch(sessionUrl, {
|
const sessionRes = await fetch(sessionUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
|
||||||
Authorization: `Bearer ${accessToken}`,
|
body: JSON.stringify({ item: { "@microsoft.graph.conflictBehavior": "replace" } })
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
item: {
|
|
||||||
"@microsoft.graph.conflictBehavior": "rename", // If file exists, name it "Book 1.pdf"
|
|
||||||
name: file.name
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const sessionData = await sessionRes.json();
|
const { uploadUrl } = await sessionRes.json();
|
||||||
if (!sessionRes.ok) {
|
|
||||||
console.error("❌ Session Error:", sessionData);
|
|
||||||
throw new Error(sessionData.error?.message || "OneDrive session failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { uploadUrl } = sessionData;
|
|
||||||
|
|
||||||
// --- 3. UPLOAD THE DATA BYTES ---
|
|
||||||
const buffer = Buffer.from(await file.arrayBuffer());
|
const buffer = Buffer.from(await file.arrayBuffer());
|
||||||
const uploadRes = await fetch(uploadUrl, {
|
const uploadRes = await fetch(uploadUrl, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: { "Content-Length": `${file.size}`, "Content-Range": `bytes 0-${file.size - 1}/${file.size}` },
|
||||||
"Content-Length": `${file.size}`,
|
|
||||||
"Content-Range": `bytes 0-${file.size - 1}/${file.size}`
|
|
||||||
},
|
|
||||||
body: buffer
|
body: buffer
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!uploadRes.ok) {
|
if (!uploadRes.ok) throw new Error("OneDrive stream failed");
|
||||||
const uploadError = await uploadRes.json();
|
|
||||||
console.error("❌ Upload Error:", uploadError);
|
|
||||||
throw new Error("Chunk upload failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
const driveItem = await uploadRes.json();
|
const driveItem = await uploadRes.json();
|
||||||
|
|
||||||
// --- 4. RECORD IN POSTGRESQL (PRISMA) ---
|
// 4. Record in DB
|
||||||
await prisma.fileNode.create({
|
await prisma.fileNode.create({
|
||||||
data: {
|
data: {
|
||||||
|
id: internalId,
|
||||||
oneDriveId: driveItem.id,
|
oneDriveId: driveItem.id,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
|
description: description,
|
||||||
size: BigInt(file.size),
|
size: BigInt(file.size),
|
||||||
isFolder: false,
|
isFolder: false,
|
||||||
path: `/${folderName}/${file.name}`,
|
path: `/${rootFolder}/${internalId}/${file.name}`,
|
||||||
ownerId: session.user.id,
|
ownerId: session.user.id,
|
||||||
|
parentId: parentId || null, // VIRTUAL HIERARCHY
|
||||||
metadata: {
|
metadata: {
|
||||||
type: file.name.split('.').pop()?.toUpperCase() || "UNKNOWN",
|
type: file.name.split('.').pop()?.toUpperCase() || "UNKNOWN",
|
||||||
mimeType: file.type
|
mimeType: file.type
|
||||||
|
|
@ -105,8 +100,6 @@ export async function uploadFileAction(formData: FormData) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Revalidate ensures the dashboard list updates immediately
|
|
||||||
revalidatePath("/dashboard");
|
revalidatePath("/dashboard");
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
@ -1,119 +1,195 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Box, Button, Typography, Paper, LinearProgress, Stack } from "@mui/material";
|
import {
|
||||||
|
Box, Button, Typography, Paper, LinearProgress, Stack,
|
||||||
|
TextField, MenuItem, IconButton, Tooltip, Divider
|
||||||
|
} from "@mui/material";
|
||||||
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
|
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
|
||||||
import { uploadFileAction } from "./_actions";
|
import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder";
|
||||||
|
import { uploadFileAction, createFolderAction } from "./_actions";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to convert raw bytes into a human-readable string.
|
* Format bytes to human readable string (MiB/KiB)
|
||||||
* This helps the user understand exactly how large their e-book is.
|
|
||||||
*/
|
*/
|
||||||
const formatFileSize = (bytes: number) => {
|
const formatFileSize = (bytes: number) => {
|
||||||
if (bytes === 0) return '0 Bytes';
|
if (bytes === 0) return '0 Bytes';
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
// Returns something like "1.45 MB" or "850 KB"
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function UploadView({ user }: { user: any }) {
|
export default function UploadView({ user, folders = [] }: { user: any, folders?: any[] }) {
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [parentId, setParentId] = useState("");
|
||||||
const [status, setStatus] = useState<'idle' | 'uploading' | 'success'>('idle');
|
const [status, setStatus] = useState<'idle' | 'uploading' | 'success'>('idle');
|
||||||
|
|
||||||
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB Limit
|
// Folder Creation State
|
||||||
|
const [showFolderInput, setShowFolderInput] = useState(false);
|
||||||
|
const [newFolderName, setNewFolderName] = useState("");
|
||||||
|
|
||||||
|
// --- RESTORED FILE SIZE LIMITS ---
|
||||||
|
const MAX_FILE_SIZE = 150 * 1024 * 1024; // 150MiB Limit
|
||||||
|
|
||||||
|
const handleCreateFolder = async () => {
|
||||||
|
if (!newFolderName.trim()) return;
|
||||||
|
try {
|
||||||
|
await createFolderAction(newFolderName);
|
||||||
|
setNewFolderName("");
|
||||||
|
setShowFolderInput(false);
|
||||||
|
} catch (err) {
|
||||||
|
alert("Error creating folder");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleUpload = async () => {
|
const handleUpload = async () => {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
|
// RESTORED: Client-side size validation
|
||||||
if (file.size > MAX_FILE_SIZE) {
|
if (file.size > MAX_FILE_SIZE) {
|
||||||
alert(`File is too large! Maximum size allowed is ${formatFileSize(MAX_FILE_SIZE)}.`);
|
alert(`File is too large! Maximum size allowed is ${formatFileSize(MAX_FILE_SIZE)}.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatus('uploading');
|
setStatus('uploading');
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
|
formData.append("description", description);
|
||||||
|
formData.append("parentId", parentId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await uploadFileAction(formData);
|
await uploadFileAction(formData);
|
||||||
setStatus('success');
|
setStatus('success');
|
||||||
setFile(null);
|
setFile(null);
|
||||||
|
setDescription("");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// If the server action fails, it usually prints details in the terminal
|
console.error(err);
|
||||||
alert("Upload failed. Ensure the 'WebCalibre' folder can be created and OneDrive has space.");
|
alert("Upload failed. Ensure file size is within limits and check server logs.");
|
||||||
setStatus('idle');
|
setStatus('idle');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper sx={{ p: 6, textAlign: 'center', borderRadius: 4 }} elevation={3}>
|
<Paper sx={{ p: { xs: 3, md: 6 }, borderRadius: 4 }} elevation={3}>
|
||||||
<Typography variant="h4" fontWeight={900} color="primary" gutterBottom>
|
<Typography variant="h4" fontWeight={900} color="primary" gutterBottom align="center">
|
||||||
Upload to WebCalibre
|
Add to Library
|
||||||
</Typography>
|
|
||||||
<Typography variant="body1" color="text.secondary" mb={4}>
|
|
||||||
Adding books as <strong>{user.name}</strong>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Stack spacing={3} alignItems="center">
|
<Stack spacing={4} sx={{ mt: 4 }}>
|
||||||
{/* Dropzone/Selection Area */}
|
|
||||||
<Box sx={{ width: '100%', p: 5, border: '2px dashed #ccc', borderRadius: 2, bgcolor: '#f9f9f9' }}>
|
{/* Section 1: Folder Selection */}
|
||||||
<input
|
<Box>
|
||||||
type="file"
|
<Typography variant="subtitle2" gutterBottom fontWeight="bold">
|
||||||
id="book-upload"
|
1. Select Target Project / Folder
|
||||||
hidden
|
</Typography>
|
||||||
// Only allow common book formats
|
<Stack direction="row" spacing={1}>
|
||||||
accept=".pdf,.epub,.mobi,.azw3,.txt"
|
<TextField
|
||||||
onChange={(e) => {
|
select
|
||||||
setFile(e.target.files?.[0] || null);
|
fullWidth
|
||||||
setStatus('idle');
|
label="Choose Folder"
|
||||||
}}
|
value={parentId}
|
||||||
/>
|
onChange={(e) => setParentId(e.target.value)}
|
||||||
<label htmlFor="book-upload">
|
disabled={status === 'uploading'}
|
||||||
<Button variant="outlined" component="span" startIcon={<CloudUploadIcon />}>
|
>
|
||||||
{file ? "Select Different File" : "Choose File (PDF, EPUB, MOBI)"}
|
<MenuItem value=""><em>None (Root)</em></MenuItem>
|
||||||
</Button>
|
{folders.map((f) => (
|
||||||
</label>
|
<MenuItem key={f.id} value={f.id}>{f.name}</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
<Tooltip title="Create New Folder">
|
||||||
|
<IconButton
|
||||||
|
color="primary"
|
||||||
|
onClick={() => setShowFolderInput(!showFolderInput)}
|
||||||
|
sx={{ border: '1px solid', borderColor: 'divider', borderRadius: 1 }}
|
||||||
|
>
|
||||||
|
<CreateNewFolderIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
{/* New: Enhanced File Info Display */}
|
{showFolderInput && (
|
||||||
{file && (
|
<Stack direction="row" spacing={1} sx={{ mt: 2, p: 2, bgcolor: 'grey.50', borderRadius: 2 }}>
|
||||||
<Box mt={3}>
|
<TextField
|
||||||
<Typography variant="subtitle2" color="primary.main" fontWeight="bold">
|
size="small"
|
||||||
Selected: {file.name}
|
fullWidth
|
||||||
</Typography>
|
placeholder="New folder name..."
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 0.5 }}>
|
value={newFolderName}
|
||||||
File Size: {formatFileSize(file.size)}
|
onChange={(e) => setNewFolderName(e.target.value)}
|
||||||
</Typography>
|
autoFocus
|
||||||
</Box>
|
/>
|
||||||
|
<Button variant="contained" onClick={handleCreateFolder}>Create</Button>
|
||||||
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Progress Indicator */}
|
<Divider />
|
||||||
|
|
||||||
|
{/* Section 2: File Selection */}
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle2" gutterBottom fontWeight="bold">
|
||||||
|
2. Upload File
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ p: 4, border: '2px dashed #ccc', borderRadius: 2, textAlign: 'center', bgcolor: '#fcfcfc' }}>
|
||||||
|
<input
|
||||||
|
type="file" id="file-input" hidden
|
||||||
|
accept=".pdf,.epub,.mobi,.azw3,.txt,.png,.jpeg"
|
||||||
|
onChange={(e) => {
|
||||||
|
setFile(e.target.files?.[0] || null);
|
||||||
|
setStatus('idle');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<label htmlFor="file-input">
|
||||||
|
<Button variant="outlined" component="span" startIcon={<CloudUploadIcon />}>
|
||||||
|
{file ? "Change File" : "Choose Book/Image"}
|
||||||
|
</Button>
|
||||||
|
</label>
|
||||||
|
{file && (
|
||||||
|
<Box mt={2}>
|
||||||
|
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||||
|
Selected: <strong>{file.name}</strong>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" sx={{ color: file.size > MAX_FILE_SIZE ? 'error.main' : 'text.disabled' }}>
|
||||||
|
Size: {formatFileSize(file.size)} {file.size > MAX_FILE_SIZE && "(Too Large)"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Section 3: Description */}
|
||||||
|
<TextField
|
||||||
|
label="Notes / Description"
|
||||||
|
multiline rows={2} fullWidth
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
disabled={status === 'uploading'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Status & Action */}
|
||||||
{status === 'uploading' && (
|
{status === 'uploading' && (
|
||||||
<Box sx={{ width: '100%' }}>
|
<Box>
|
||||||
<Typography variant="caption" display="block" gutterBottom sx={{ color: 'primary.main', fontWeight: 600 }}>
|
<LinearProgress sx={{ borderRadius: 5, height: 10 }} />
|
||||||
Connecting to OneDrive & Uploading...
|
<Typography variant="caption" color="primary" sx={{ mt: 1, display: 'block', textAlign: 'center' }}>
|
||||||
|
Streaming to OneDrive storage...
|
||||||
</Typography>
|
</Typography>
|
||||||
<LinearProgress />
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Action Button */}
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained" size="large" fullWidth
|
||||||
size="large"
|
disabled={!file || status === 'uploading' || file.size > MAX_FILE_SIZE}
|
||||||
fullWidth
|
|
||||||
disabled={!file || status === 'uploading'}
|
|
||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
sx={{ py: 1.5, fontSize: '1.1rem', fontWeight: 700 }}
|
sx={{ py: 2, fontWeight: 'bold' }}
|
||||||
>
|
>
|
||||||
{status === 'uploading' ? 'Please Wait...' : 'Confirm Upload'}
|
{status === 'uploading' ? 'Uploading...' : 'Confirm Upload'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Success Feedback */}
|
|
||||||
{status === 'success' && (
|
{status === 'success' && (
|
||||||
<Typography color="success.main" fontWeight="bold" sx={{ mt: 2 }}>
|
<Typography align="center" color="success.main" fontWeight="bold">
|
||||||
✅ Successfully uploaded to your library!
|
✅ Successfully Added!
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
||||||
29
src/proxy.ts
29
src/proxy.ts
|
|
@ -1,23 +1,35 @@
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import {auth} from "@/auth";
|
import { auth } from "@/auth";
|
||||||
|
|
||||||
const protectedRoutes = ["/dashboard", "/profile"];
|
const protectedRoutes = ["/dashboard", "/profile"];
|
||||||
const apiAuthPrefix = "/api/auth";
|
const apiAuthPrefix = "/api/auth";
|
||||||
|
|
||||||
export const proxy = auth((req) => {
|
export const proxy = auth((req) => {
|
||||||
const { nextUrl } = req;
|
const { nextUrl } = req;
|
||||||
const isLoggedIn = !!req.auth;
|
|
||||||
|
|
||||||
const path = nextUrl.pathname;
|
const path = nextUrl.pathname;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. IMMEDIATE BYPASS FOR UPLOADS
|
||||||
|
* We check this first. If the user is hitting the upload route,
|
||||||
|
* we let the request pass through directly to the page/action.
|
||||||
|
* This prevents the middleware from trying to parse the 100MB body.
|
||||||
|
*/
|
||||||
|
if (path.startsWith('/upload')) {
|
||||||
|
return NextResponse.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLoggedIn = !!req.auth;
|
||||||
const isApiAuthRoute = path.startsWith(apiAuthPrefix);
|
const isApiAuthRoute = path.startsWith(apiAuthPrefix);
|
||||||
|
|
||||||
|
// Check if the current path is in our protected list
|
||||||
const isProtectedRoute = protectedRoutes.includes(path);
|
const isProtectedRoute = protectedRoutes.includes(path);
|
||||||
|
|
||||||
// 1. Allow API Auth calls (Login/Logout/Callback)
|
// 2. Allow API Auth calls (Login/Logout/Callback)
|
||||||
if (isApiAuthRoute) {
|
if (isApiAuthRoute) {
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. CHANGED: Redirect to HOME (/) instead of /login if logged out
|
// 3. Redirect to HOME (/) if trying to access a protected route while logged out
|
||||||
if (isProtectedRoute && !isLoggedIn) {
|
if (isProtectedRoute && !isLoggedIn) {
|
||||||
return NextResponse.redirect(new URL("/", nextUrl));
|
return NextResponse.redirect(new URL("/", nextUrl));
|
||||||
}
|
}
|
||||||
|
|
@ -25,6 +37,11 @@ export const proxy = auth((req) => {
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Matcher tells Next.js which routes this proxy should run on.
|
||||||
|
* By adding '|upload' to the negative lookahead (?!...), we tell
|
||||||
|
* Next.js to ignore the /upload route entirely at the engine level.
|
||||||
|
*/
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
|
matcher: ["/((?!api|_next/static|_next/image|favicon.ico|upload).*)"],
|
||||||
};
|
};
|
||||||
Loading…
Reference in a new issue