Files
pedwfrontend/pages/storageAdmin.js
T
2025-08-13 09:56:50 +01:00

509 lines
25 KiB
JavaScript

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 Link from "next/link";
import { PrismaClient } from "@prisma/client";
import {
bytesToSize,
getThumbnailIconByExtension,
updateLinks,
} from "../components/utils";
import {
consoleLogger,
getIP,
sendRepCompleteMessage,
deleteMyRepresentationsFromBlob,
} from "../actions";
import CryptoJS from "crypto-js";
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);
});
}
const deleteRepItem = (containerID, caseID, repfile) => {
deleteMyRepresentationsFromBlob(containerID, caseID, repfile).then(
(data) => {
console.log("======== rep deleted =========");
return data;
}
);
};
export default function StoragePage({ data }) {
console.log(data);
return (
<div className="govuk-width-container">
<div className="govuk-main-wrapper govuk-main-wrapper--auto-spacing">
<h1>Storage Account Contents</h1>
{data.map((user) => (
<div key={user.id} className="govuk-grid-row">
{user.containers.map((c) => (
<div key={c.container} className="card">
<div className="card-body">
<h3>
{c.container} - {user.email}
</h3>
{c.folders.map((folder) => (
<div key={folder.folderPath}>
<h4>
{folder.folderPath || "Root"}
</h4>
{/* {folder.hasRepJson && (
<p>
rep.json file found:{" "}
{folder.repJsonBlob.name}
</p>
)} */}
{folder.hasRepJson && (
<div>
{" "}
<button
title={
"Do you want to delete rep"
}
className=" cardModuleRemoveCase govuk-link govuk-link--no-underline govuk-link--inverse "
onClick={() => {
confirm(
"Do you want tyo delete rep " +
folder.repJsonBlob.name.split(
"/"
)[0] +
"?\n"
) &&
(deleteRepItem(
c.container,
folder.repJsonBlob.name.split(
"/"
)[0],
folder.repJsonBlob.name.split(
"/"
)[1]
),
alert(
"Rep Deleted!"
),
window.location.reload());
}}
>
Delete Rep
</button>
{folder.repComplete && (
<button
onClick={async () => {
const response =
await fetch(
"/api/file/editRepJson",
{
method: "POST",
headers:
{
"Content-Type":
"application/json",
},
body: JSON.stringify(
{
container:
c.container,
blobName:
folder
.repJsonBlob
.name,
}
),
}
);
const result =
await response.json();
if (
response.ok
) {
alert(
"repComplete removed!"
),
window.location.reload();
// Optionally refresh the page or update UI state here
} else {
alert(
"Error: " +
result.error
);
}
}}
>
Remove repComplete
</button>
)}
{folder.repComplete && (
<button
onClick={async () => {
confirm(
"Do you want resend the rep queue message " +
folder.repJsonBlob.name.split(
"/"
)[0] +
" --- " +
folder.repJsonBlob.name.split(
"/"
)[1]
) &&
(sendRepCompleteMessage(
c.container,
folder.repJsonBlob.name.split(
"/"
)[0],
folder.repJsonBlob.name.split(
"/"
)[1]
),
alert(
"rep queue message sent!"
),
window.location.reload());
}}
>
Recreate Queue
Message
</button>
)}
</div>
)}
<ul>
{folder.blobs.map((blob) => (
<li key={blob.name}>
<Link
scroll={false}
href={
"/api/file/downloadblob?container=" +
c.container +
"&casefolderID=" +
folder.folderPath +
"&blobname=" +
blob.name
.substring(
blob.name.lastIndexOf(
"/"
) + 1
)
.trim() +
"&hash=" +
blob.hashedfilepath
}
className="govuk-body govuk-!-font-size-14 govuk-!-padding-left-5 govuk-link"
>
{blob.name} (
{bytesToSize(
blob.contentLength ||
blob.size
)}
)
</Link>
</li>
))}
</ul>
</div>
))}
</div>
</div>
))}
</div>
))}
</div>
</div>
);
}
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
);
// Add hashedName for each blob
// const containersWithHashes = containers.map((container) => {
// console.log("eee ");
// const groupedByFolder = container.blobs.reduce((acc, blob) => {
// // Get folder path - everything before last slash, or "" if no slash
// 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 blobWithHash = { ...blob, hashedfilepath };
// // Find or create folder group
// let folderGroup = acc.find(
// (f) => f.folderPath === folderPath
// );
// if (!folderGroup) {
// folderGroup = { folderPath, blobs: [] };
// acc.push(folderGroup);
// }
// folderGroup.blobs.push(blobWithHash);
// return acc;
// }, []);
// console.log(groupedByFolder);
// return {
// ...container,
// blobs: container.blobs.map((blob) => ({
// ...blob,
// hashedfilepath: hashAPIPath(
// "/api/file/downloadblob?container=" +
// container.container +
// "&casefolderID=" +
// getUpToLastSlash(blob.name.trim()) +
// "&blobname=" +
// blob.name
// .substring(blob.name.lastIndexOf("/") + 1)
// .trim(),
// process.env.HASHKEY
// ),
// })),
// };
// });
// const containersWithHashes = containers.map((container) => {
// const groupedByFolder = container.blobs.reduce((acc, blob) => {
// 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 blobWithHash = { ...blob, hashedfilepath };
// let folderGroup = acc.find(
// (f) => f.folderPath === folderPath
// );
// if (!folderGroup) {
// folderGroup = {
// folderPath,
// blobs: [],
// hasRepJson: false, // flag
// repJsonBlob: null, // save the rep.json blob if found
// };
// acc.push(folderGroup);
// }
// folderGroup.blobs.push(blobWithHash);
// // Check if this blob ends with "rep.json"
// if (blob.name.toLowerCase().endsWith("rep.json")) {
// folderGroup.hasRepJson = true;
// folderGroup.repJsonBlob = blobWithHash;
// // You can also do some processing here if you want
// }
// return acc;
// }, []);
// return {
// ...container,
// folders: groupedByFolder,
// // Optionally remove flat blobs if you don't want them
// // blobs: undefined,
// };
// });
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 blobWithHash = { ...blob, hashedfilepath };
let folderGroup = acc.find(
(f) => f.folderPath === folderPath
);
if (!folderGroup) {
folderGroup = {
folderPath,
blobs: [],
hasRepJson: false,
repJsonBlob: null,
repComplete: false, // new flag
};
acc.push(folderGroup);
}
folderGroup.blobs.push(blobWithHash);
// 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;
})
);
return { props: { data: data.filter(Boolean) } };
}