diff --git a/my-portfolio/.env b/my-portfolio/.env index d9cec4c..c0727b2 100644 --- a/my-portfolio/.env +++ b/my-portfolio/.env @@ -1,3 +1,4 @@ NEXT_PUBLIC_STORAGE_API_URL=/api/storage NEXT_PUBLIC_MEDIA_API_URL=/api/media NEXT_PUBLIC_ADMIN_API_URL=/api/admin +NEXTAUTH_SECRET=super_secret_jwt_key_12345 diff --git a/my-portfolio/app/admin/page.tsx b/my-portfolio/app/admin/page.tsx index 6076dd1..e3fd881 100644 --- a/my-portfolio/app/admin/page.tsx +++ b/my-portfolio/app/admin/page.tsx @@ -18,6 +18,7 @@ import { import Link from "next/link"; import { useState, useEffect } from "react"; import { useToast } from "@/components/ToastProvider"; +import { authFetch } from "@/lib/authFetch"; interface DockerContainer { id: string; @@ -51,7 +52,7 @@ export default function AdminPage() { try { setLoading(true); const baseUrl = process.env.NEXT_PUBLIC_ADMIN_API_URL || (typeof window !== "undefined" ? `http://${window.location.hostname}:4003` : "http://localhost:4003"); - const res = await fetch(`${baseUrl}/containers`); + const res = await authFetch(`${baseUrl}/containers`); const data = await res.json(); const mappedContainers = data.map((c: any) => ({ id: c.Id, @@ -81,7 +82,7 @@ export default function AdminPage() { try { addToast(`Requested container ${action}...`, "info"); const baseUrl = process.env.NEXT_PUBLIC_ADMIN_API_URL || (typeof window !== "undefined" ? `http://${window.location.hostname}:4003` : "http://localhost:4003"); - const res = await fetch(`${baseUrl}/containers/${id}/${action}`, { method: "POST" }); + const res = await authFetch(`${baseUrl}/containers/${id}/${action}`, { method: "POST" }); if (res.ok) { addToast(`Container ${action}ed successfully`, "success"); fetchContainers(); diff --git a/my-portfolio/app/api/auth/token/route.ts b/my-portfolio/app/api/auth/token/route.ts new file mode 100644 index 0000000..82bfe79 --- /dev/null +++ b/my-portfolio/app/api/auth/token/route.ts @@ -0,0 +1,39 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/lib/authOptions'; +import crypto from 'crypto'; + +export async function GET() { + const session = await getServerSession(authOptions); + + if (!session) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const secret = process.env.NEXTAUTH_SECRET; + if (!secret) { + return NextResponse.json({ error: 'Server misconfiguration' }, { status: 500 }); + } + + const header = { + alg: 'HS256', + typ: 'JWT' + }; + + const payload = { + user: session.user?.email, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + (5 * 60) // 5 minutes expiration + }; + + const b64Header = Buffer.from(JSON.stringify(header)).toString('base64url'); + const b64Payload = Buffer.from(JSON.stringify(payload)).toString('base64url'); + + const signature = crypto.createHmac('sha256', secret) + .update(b64Header + '.' + b64Payload) + .digest('base64url'); + + const token = `${b64Header}.${b64Payload}.${signature}`; + + return NextResponse.json({ token }); +} diff --git a/my-portfolio/app/cloud/page.tsx b/my-portfolio/app/cloud/page.tsx index 06702b4..517822a 100644 --- a/my-portfolio/app/cloud/page.tsx +++ b/my-portfolio/app/cloud/page.tsx @@ -31,6 +31,7 @@ import Link from "next/link"; import { useState, useEffect } from "react"; import { useToast } from "@/components/ToastProvider"; import SparkVideoPlayer from "@/components/SparkVideoPlayer"; +import { authFetch, getAuthToken } from "@/lib/authFetch"; const STORAGE_API_BASE = process.env.NEXT_PUBLIC_STORAGE_API_URL || (typeof window !== "undefined" ? `http://${window.location.hostname}:4001` : "http://localhost:4001"); @@ -80,6 +81,29 @@ export default function CloudStoragePage() { const [previewContent, setPreviewContent] = useState<{ text?: string; type: "text" | "image" | "video" | "audio" | "binary" } | null>(null); const [previewLoading, setPreviewLoading] = useState(false); const [sideNavMode, setSideNavMode] = useState<"files" | "favorites" | "recent" | "trash">("files"); + const [authToken, setAuthToken] = useState(""); + + useEffect(() => { + getAuthToken().then(setAuthToken); + const interval = setInterval(() => getAuthToken().then(setAuthToken), 3 * 60 * 1000); + return () => clearInterval(interval); + }, []); + + const getViewUrl = (filename: string, pathSegments: string[]) => { + const subPath = pathSegments.join("/"); + let url = `${STORAGE_API_BASE}/view/${encodeURIComponent(filename)}`; + if (subPath) url += `?path=${encodeURIComponent(subPath)}&token=${authToken}`; + else url += `?token=${authToken}`; + return url; + }; + + const getDownloadUrl = (filename: string, pathSegments: string[]) => { + const subPath = pathSegments.join("/"); + let url = `${STORAGE_API_BASE}/download/${encodeURIComponent(filename)}`; + if (subPath) url += `?path=${encodeURIComponent(subPath)}&token=${authToken}`; + else url += `?token=${authToken}`; + return url; + }; const fetchFiles = async (pathSegments: string[] = currentPath, forcedMode?: typeof sideNavMode) => { try { @@ -98,7 +122,7 @@ export default function CloudStoragePage() { ? `${STORAGE_API_BASE}/files?path=${encodeURIComponent(subPath)}` : `${STORAGE_API_BASE}/files`; } - const res = await fetch(url); + const res = await authFetch(url); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); const mappedFiles = data.map((f: any) => ({ @@ -173,7 +197,7 @@ export default function CloudStoragePage() { const url = subPath ? `${STORAGE_API_BASE}/content/${encodeURIComponent(file.name)}?path=${encodeURIComponent(subPath)}` : `${STORAGE_API_BASE}/content/${encodeURIComponent(file.name)}`; - const res = await fetch(url); + const res = await authFetch(url); if (!res.ok) { setPreviewContent({ type: "binary" }); return; } const data = await res.json(); if (data.encoding === "text") { @@ -205,7 +229,7 @@ export default function CloudStoragePage() { setNewFolderOpen(false); try { - const res = await fetch(`${STORAGE_API_BASE}/mkdir`, { + const res = await authFetch(`${STORAGE_API_BASE}/mkdir`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ folderName, parentPath: currentPath.join("/") }), @@ -240,7 +264,7 @@ export default function CloudStoragePage() { ? `${STORAGE_API_BASE}/upload?path=${encodeURIComponent(subPath)}` : `${STORAGE_API_BASE}/upload`; - fetch(uploadUrl, { method: "POST", body: formData }) + authFetch(uploadUrl, { method: "POST", body: formData }) .then(res => res.json()) .then(() => { clearInterval(interval); @@ -256,10 +280,7 @@ export default function CloudStoragePage() { }; const handleDownload = (filename: string) => { - const subPath = currentPath.join("/"); - const url = subPath - ? `${STORAGE_API_BASE}/download/${filename}?path=${encodeURIComponent(subPath)}` - : `${STORAGE_API_BASE}/download/${filename}`; + const url = getDownloadUrl(filename, currentPath); window.open(url, '_blank'); addToast(`Downloading ${filename}`, "success"); }; @@ -272,7 +293,7 @@ export default function CloudStoragePage() { ? `${STORAGE_API_BASE}/${endpoint}/${filename}?path=${encodeURIComponent(subPath)}` : `${STORAGE_API_BASE}/${endpoint}/${filename}`; - await fetch(url, { method: "DELETE" }); + await authFetch(url, { method: "DELETE" }); addToast(sideNavMode === "trash" ? "Permanently deleted" : "Moved to trash", "success"); fetchFiles(); } catch { @@ -283,7 +304,7 @@ export default function CloudStoragePage() { const handleToggleFavorite = async (file: FileItem) => { const filePath = file.path || file.name; try { - const res = await fetch(`${STORAGE_API_BASE}/favorites/toggle`, { + const res = await authFetch(`${STORAGE_API_BASE}/favorites/toggle`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: filePath }) @@ -299,18 +320,14 @@ export default function CloudStoragePage() { }; const handleShare = (file: FileItem) => { - const subPath = currentPath.join("/"); - const filename = encodeURIComponent(file.name); - const pathQuery = subPath ? `?path=${encodeURIComponent(subPath)}` : ""; - const publicLink = `${STORAGE_API_BASE}/download/${filename}${pathQuery}`; - + const publicLink = getDownloadUrl(file.name, currentPath); navigator.clipboard.writeText(publicLink); addToast("Share link copied to clipboard!", "success"); }; const handleRestore = async (filename: string) => { try { - const res = await fetch(`${STORAGE_API_BASE}/trash/restore/${encodeURIComponent(filename)}`, { + const res = await authFetch(`${STORAGE_API_BASE}/trash/restore/${encodeURIComponent(filename)}`, { method: "POST" }); if (res.ok) { @@ -450,7 +467,7 @@ export default function CloudStoragePage() {