import CookieBanner from "../components/cookieBanner"; import Footer from "../components/footer"; import Header from "../components/header"; import { DefaultAzureCredential } from "@azure/identity"; import { BlobServiceClient, ContainerClient } from "@azure/storage-blob"; import { v4 as uuidv4 } from "uuid"; import { createContainerSas, listContainers, listBlobHierarchical, listContainersForUser, } from "../actions/azurestorage"; import Head from "next/head"; import Link from "next/link"; import { PrismaClient } from "@prisma/client"; import { bytesToSize, getThumbnailIconByExtension, updateLinks, } from "../components/utils"; import { consoleLogger, getIP, sendRepCompleteMessage, deleteMyRepresentationsFromBlob, deleteAwaitingSubmissionsFromBlob, } from "../actions"; import CryptoJS from "crypto-js"; import { useState } from "react"; function getUpToLastSlash(url) { let lastSlash = url.lastIndexOf("/"); url = url.substring(0, lastSlash); url = url.lastIndexOf("/files") > 0 ? url.substring(0, url.lastIndexOf("/files")) : url; return url; } async function streamToString(readableStream) { return new Promise((resolve, reject) => { const chunks = []; readableStream.on("data", (data) => { chunks.push(data.toString()); }); readableStream.on("end", () => { resolve(chunks.join("")); }); readableStream.on("error", reject); }); } function formatTimeSince(date) { if (!date) return "Unknown date"; const now = new Date(); const diffMs = now - date; const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffDays < 7) { return `${diffDays} day${diffDays !== 1 ? "s" : ""} ago`; } else { const diffWeeks = Math.floor(diffDays / 7); return `${diffWeeks} week${diffWeeks !== 1 ? "s" : ""} ago`; } } function formatBlobDates(blob) { const created = blob.createdOn ? new Date(blob.createdOn) : null; const modified = blob.lastModified ? new Date(blob.lastModified) : null; if (!created && !modified) return "Unknown"; // If both exist and are the same if (created && modified && created.getTime() === modified.getTime()) { return `${created.toLocaleString("en-GB")} (${formatTimeSince( created )})`; } // If modified exists and is newer than created if (created && modified && modified.getTime() > created.getTime()) { return `Created: ${created.toLocaleString("en-GB")} (${formatTimeSince( created )}), Modified: ${modified.toLocaleString("en-GB")} (${formatTimeSince( modified )})`; } // Only one date exists const date = modified || created; return `${date.toLocaleString("en-GB")} (${formatTimeSince(date)})`; } const deleteRepItem = (containerID, caseID, repfile) => { deleteMyRepresentationsFromBlob(containerID, caseID, repfile).then( (data) => { console.log("======== rep deleted ========="); return data; } ); }; const deleteCaseItem = (containerID, caseID) => { deleteAwaitingSubmissionsFromBlob(containerID, caseID).then((data) => { return data; }); }; export default function StoragePage(props) { const { data, userData, repCompleteCount } = props; const [showRepJson, setShowRepJson] = useState(true); const [showTmpFile, setShowTmpFile] = useState(true); const [whichTab, setWhichTab] = useState("storage"); const toggleAll = () => { const newState = !(showRepJson || showTmpFile); setShowRepJson(newState); setShowTmpFile(newState); }; return (
Storage Account Browser

Contents

Storage Account Contents

{repCompleteCount > 0 && (

Number of Orphaned Reps:{" "} {repCompleteCount}

)} {/* Toggle buttons */}
{data.map((user) => (
{user.containers.map((c) => (

{c.container} -{" "}

{folder.folderPath || "Root"}

{/* {folder.hasRepJson && (

rep.json file found:{" "} {folder.repJsonBlob.name}

)} */} {folder.hasTmpFile && ( <> )} {folder.hasRepJson && (
{" "} {folder.repComplete && ( )} {folder.repComplete && ( )}
)}
    {folder.blobs.map( (blob) => (
  • { blob.name }{" "} ( {bytesToSize( blob.contentLength || blob.size )} ) {" "} {blob.lastModified && ( {/* { blob.displayDate } */} {formatBlobDates( blob )} )}
  • ) )}
))}
))}
))}

User Accounts

Number of accounts created:{" "} {userData.length}

Name
Last accessed
{userData.map((user) => (
Last accessed: {user.created}{" "}
))}
); } export async function getServerSideProps({ req }) { // Load allowed IPs from env and trim spaces const ALLOWED_IPS = process.env.ALLOWED_IPS ? process.env.ALLOWED_IPS.split(",").map((ip) => ip.trim()) : []; // Always allow localhost addresses const LOCALHOST_IPS = ["127.0.0.1", "::1"]; // Get IP address from headers or socket const forwarded = req.headers["x-forwarded-for"]; const ip = typeof forwarded === "string" ? forwarded.split(",")[0] : req.socket.remoteAddress; console.log("Visitor IP:", ip); // Check whitelist + localhost if (![...ALLOWED_IPS, ...LOCALHOST_IPS].includes(ip)) { return { redirect: { destination: "/403", // custom "Access Denied" page permanent: false, }, }; } const accountName = process.env.AZURE_STORAGE_ACCOUNT_NAME; const creds = new DefaultAzureCredential(); const globalForPrisma = global; const prisma = globalForPrisma.prisma || new PrismaClient(); const blobServiceClient = new BlobServiceClient( `https://${accountName}.blob.core.windows.net`, creds ); const users = await prisma.user.findMany(); const hashAPIPath = (queryPath, WORDKEY) => { var hashlink = CryptoJS.HmacSHA256( queryPath, CryptoJS.enc.Hex.parse(WORDKEY) ); hashlink = hashlink.toString(CryptoJS.enc.Hex); return hashlink; }; let data = await Promise.all( users.map(async (user) => { const containers = await listContainersForUser( blobServiceClient, `${user.id}`, user.email ); const containersWithHashes = await Promise.all( containers.map(async (container) => { const groupedByFolder = await container.blobs.reduce( async (accP, blob) => { // Because we'll do async read, reduce needs to be async, so accP is a Promise const acc = await accP; const folderPath = getUpToLastSlash(blob.name.trim()) || ""; const hashedfilepath = hashAPIPath( "/api/file/downloadblob?container=" + container.container + "&casefolderID=" + folderPath + "&blobname=" + blob.name .substring( blob.name.lastIndexOf("/") + 1 ) .trim(), process.env.HASHKEY ); const fileNameOnly = blob.name.substring( blob.name.lastIndexOf("/") + 1 ); const isTmpFile = fileNameOnly.startsWith("TMP-"); const blobWithHash = { ...blob, hashedfilepath, isTmpFile, }; let folderGroup = acc.find( (f) => f.folderPath === folderPath ); if (!folderGroup) { folderGroup = { folderPath, blobs: [], hasRepJson: false, repJsonBlob: null, repComplete: false, hasTmpFile: false, }; acc.push(folderGroup); } folderGroup.blobs.push(blobWithHash); if (isTmpFile) { folderGroup.hasTmpFile = true; } // If this blob ends with rep.json, fetch and parse JSON if (blob.name.toLowerCase().endsWith("rep.json")) { folderGroup.hasRepJson = true; folderGroup.repJsonBlob = blobWithHash; // Read blob content from Azure Storage const containerClient = blobServiceClient.getContainerClient( container.container ); const blockBlobClient = containerClient.getBlockBlobClient( blob.name ); try { const downloadResponse = await blockBlobClient.download(0); const downloaded = await streamToString( downloadResponse.readableStreamBody ); const json = JSON.parse(downloaded); folderGroup.repComplete = Boolean( json.repComplete ); } catch (err) { console.error( "Error reading or parsing rep.json", err ); folderGroup.repComplete = false; } } return acc; }, Promise.resolve([]) ); return { ...container, folders: groupedByFolder, }; }) ); return containersWithHashes.length > 0 ? { id: user.id, email: user.email, containers: containersWithHashes, } : null; }) ); let userData = await Promise.all( users .filter((user) => user.email && user.email.trim() !== "") .map(async (user) => ({ id: user.id, email: user.email, created: user.emailVerified?.toLocaleString("en-GB") || "", })) ); let repCompleteCount = 0; data = data.filter(Boolean); data.forEach((user) => { user.containers.forEach((container) => { container.folders.forEach((folder) => { if (folder.repComplete === true) { repCompleteCount++; } }); }); }); return { props: { data: data, userData: userData, repCompleteCount, }, }; }