updated storage admin

This commit is contained in:
2025-08-13 15:57:18 +01:00
parent ad2dddc9a4
commit ec3a591126
2 changed files with 434 additions and 274 deletions
+79 -2
View File
@@ -1693,14 +1693,91 @@ export const listBlobHierarchical = async (
} }
} }
}; };
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 formatDateDisplay(date) {
return date
? `${date.toLocaleString("en-GB")} (${formatTimeSince(date)})`
: "Unknown";
}
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)})`;
}
export async function listBlobHierarchicalForUser(containerClient) { export async function listBlobHierarchicalForUser(containerClient) {
const blobNames = []; const blobNames = [];
for await (const blob of containerClient.listBlobsFlat()) { for await (const blob of containerClient.listBlobsFlat()) {
// blobNames.push(blob.name); const lastModifiedDate = blob.properties?.lastModified || null;
const createdOnDate = blob.properties?.createdOn || null;
let displayDate = "";
if (createdOnDate && lastModifiedDate) {
if (
createdOnDate.toDateString() === lastModifiedDate.toDateString()
) {
// Same day — show just one
displayDate = `Date: ${formatDateDisplay(lastModifiedDate)}`;
} else {
// Different days — show both
displayDate = `Created: ${formatDateDisplay(
createdOnDate
)}, Modified: ${formatDateDisplay(lastModifiedDate)}`;
}
} else if (lastModifiedDate) {
displayDate = `Modified: ${formatDateDisplay(lastModifiedDate)}`;
} else if (createdOnDate) {
displayDate = `Created: ${formatDateDisplay(createdOnDate)}`;
} else {
displayDate = "Unknown date";
}
blobNames.push({ blobNames.push({
name: blob.name, name: blob.name,
size: blob.properties.contentLength || 0, // in bytes size: blob.properties.contentLength || 0,
lastModified: lastModifiedDate
? lastModifiedDate.toISOString()
: null,
createdOn: createdOnDate ? createdOnDate.toISOString() : null,
displayDate,
}); });
} }
return blobNames; return blobNames;
+203 -120
View File
@@ -1,3 +1,6 @@
import CookieBanner from "../components/cookieBanner";
import Footer from "../components/footer";
import Header from "../components/header";
import { DefaultAzureCredential } from "@azure/identity"; import { DefaultAzureCredential } from "@azure/identity";
import { BlobServiceClient, ContainerClient } from "@azure/storage-blob"; import { BlobServiceClient, ContainerClient } from "@azure/storage-blob";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
@@ -7,6 +10,7 @@ import {
listBlobHierarchical, listBlobHierarchical,
listContainersForUser, listContainersForUser,
} from "../actions/azurestorage"; } from "../actions/azurestorage";
import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
import { import {
@@ -19,8 +23,10 @@ import {
getIP, getIP,
sendRepCompleteMessage, sendRepCompleteMessage,
deleteMyRepresentationsFromBlob, deleteMyRepresentationsFromBlob,
deleteAwaitingSubmissionsFromBlob,
} from "../actions"; } from "../actions";
import CryptoJS from "crypto-js"; import CryptoJS from "crypto-js";
import { useState } from "react";
function getUpToLastSlash(url) { function getUpToLastSlash(url) {
let lastSlash = url.lastIndexOf("/"); let lastSlash = url.lastIndexOf("/");
@@ -47,6 +53,48 @@ async function streamToString(readableStream) {
}); });
} }
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) => { const deleteRepItem = (containerID, caseID, repfile) => {
deleteMyRepresentationsFromBlob(containerID, caseID, repfile).then( deleteMyRepresentationsFromBlob(containerID, caseID, repfile).then(
(data) => { (data) => {
@@ -56,12 +104,61 @@ const deleteRepItem = (containerID, caseID, repfile) => {
); );
}; };
const deleteCaseItem = (containerID, caseID) => {
deleteAwaitingSubmissionsFromBlob(containerID, caseID).then((data) => {
return data;
});
};
export default function StoragePage({ data }) { export default function StoragePage({ data }) {
console.log(data); console.log(data);
const [showRepJson, setShowRepJson] = useState(true);
const [showTmpFile, setShowTmpFile] = useState(true);
const toggleAll = () => {
const newState = !(showRepJson || showTmpFile);
setShowRepJson(newState);
setShowTmpFile(newState);
};
return ( return (
<div>
<Head>
<title>Storage Account Browser</title>
</Head>
<div id="page_wrapper">
<CookieBanner />
<Header />
<div className="govuk-width-container"> <div className="govuk-width-container">
<div className="govuk-main-wrapper govuk-main-wrapper--auto-spacing"> <div className="govuk-main-wrapper govuk-main-wrapper--auto-spacing">
<h1>Storage Account Contents</h1> <h1>Storage Account Contents</h1>
{/* Toggle buttons */}
<div style={{ marginBottom: "0.2rem" }}>
<button
className="govuk-button govuk-button--secondary"
onClick={() => setShowRepJson(!showRepJson)}
>
{showRepJson ? "Hide Reps" : "Show Reps"}
</button>
<button
className="govuk-button govuk-button--secondary"
onClick={() => setShowTmpFile(!showTmpFile)}
style={{ marginLeft: "0.5rem" }}
>
{showTmpFile
? "Hide Temp Appeal"
: "Show Temp Appeal"}
</button>
<button
className="govuk-button govuk-button--secondary"
onClick={toggleAll}
style={{ marginLeft: "0.5rem" }}
>
Show/Hide All
</button>
</div>
{data.map((user) => ( {data.map((user) => (
<div key={user.id} className="govuk-grid-row"> <div key={user.id} className="govuk-grid-row">
{user.containers.map((c) => ( {user.containers.map((c) => (
@@ -71,9 +168,30 @@ export default function StoragePage({ data }) {
{c.container} - {user.email} {c.container} - {user.email}
</h3> </h3>
{c.folders.map((folder) => ( {c.folders.map((folder) => (
<div key={folder.folderPath}> <div
key={folder.folderPath}
style={
showRepJson &&
folder.hasRepJson
? {
display:
"block",
}
: showTmpFile &&
folder.hasTmpFile
? {
display:
"block",
}
: {
display:
"none",
}
}
>
<h4> <h4>
{folder.folderPath || "Root"} {folder.folderPath ||
"Root"}
</h4> </h4>
{/* {folder.hasRepJson && ( {/* {folder.hasRepJson && (
<p> <p>
@@ -81,6 +199,36 @@ export default function StoragePage({ data }) {
{folder.repJsonBlob.name} {folder.repJsonBlob.name}
</p> </p>
)} */} )} */}
{folder.hasTmpFile && (
<>
<button
onClick={() => {
confirm(
"Do you want to remove this unsubmitted appeal " +
showTopThreeArr[
key
]
.ticketnumber +
"?\n" +
t(
"case:do-you-want-to-delete-case-disclaimer-label"
)
) &&
(deleteCaseItem(
c.container,
folder.folderPath
),
alert(
"Temp case Deleted!"
),
window.location.reload());
}}
>
Delete this TMP
appeal
</button>
</>
)}
{folder.hasRepJson && ( {folder.hasRepJson && (
<div> <div>
{" "} {" "}
@@ -157,7 +305,8 @@ export default function StoragePage({ data }) {
} }
}} }}
> >
Remove repComplete Remove
repComplete
</button> </button>
)} )}
{folder.repComplete && ( {folder.repComplete && (
@@ -188,7 +337,8 @@ export default function StoragePage({ data }) {
window.location.reload()); window.location.reload());
}} }}
> >
Recreate Queue Recreate
Queue
Message Message
</button> </button>
)} )}
@@ -196,10 +346,17 @@ export default function StoragePage({ data }) {
)} )}
<ul> <ul>
{folder.blobs.map((blob) => ( {folder.blobs.map(
<li key={blob.name}> (blob) => (
<li
key={
blob.name
}
>
<Link <Link
scroll={false} scroll={
false
}
href={ href={
"/api/file/downloadblob?container=" + "/api/file/downloadblob?container=" +
c.container + c.container +
@@ -210,23 +367,38 @@ export default function StoragePage({ data }) {
.substring( .substring(
blob.name.lastIndexOf( blob.name.lastIndexOf(
"/" "/"
) + 1 ) +
1
) )
.trim() + .trim() +
"&hash=" + "&hash=" +
blob.hashedfilepath blob.hashedfilepath
} }
className="govuk-body govuk-!-font-size-14 govuk-!-padding-left-5 govuk-link" className="govuk-body govuk-!-font-size-14 govuk-link"
> >
{blob.name} ( {
blob.name
}{" "}
(
{bytesToSize( {bytesToSize(
blob.contentLength || blob.contentLength ||
blob.size blob.size
)} )}
) )
</Link> </Link>{" "}
{blob.lastModified && (
<span className="govuk-!-font-size-14">
{/* {
blob.displayDate
} */}
{formatBlobDates(
blob
)}
</span>
)}
</li> </li>
))} )
)}
</ul> </ul>
</div> </div>
))} ))}
@@ -237,6 +409,9 @@ export default function StoragePage({ data }) {
))} ))}
</div> </div>
</div> </div>
<Footer />
</div>
</div>
); );
} }
@@ -299,112 +474,6 @@ export async function getServerSideProps({ req }) {
user.email 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( const containersWithHashes = await Promise.all(
containers.map(async (container) => { containers.map(async (container) => {
const groupedByFolder = await container.blobs.reduce( const groupedByFolder = await container.blobs.reduce(
@@ -429,7 +498,16 @@ export async function getServerSideProps({ req }) {
process.env.HASHKEY process.env.HASHKEY
); );
const blobWithHash = { ...blob, hashedfilepath }; const fileNameOnly = blob.name.substring(
blob.name.lastIndexOf("/") + 1
);
const isTmpFile = fileNameOnly.startsWith("TMP-");
const blobWithHash = {
...blob,
hashedfilepath,
isTmpFile,
};
let folderGroup = acc.find( let folderGroup = acc.find(
(f) => f.folderPath === folderPath (f) => f.folderPath === folderPath
@@ -440,13 +518,18 @@ export async function getServerSideProps({ req }) {
blobs: [], blobs: [],
hasRepJson: false, hasRepJson: false,
repJsonBlob: null, repJsonBlob: null,
repComplete: false, // new flag repComplete: false,
hasTmpFile: false,
}; };
acc.push(folderGroup); acc.push(folderGroup);
} }
folderGroup.blobs.push(blobWithHash); folderGroup.blobs.push(blobWithHash);
if (isTmpFile) {
folderGroup.hasTmpFile = true;
}
// If this blob ends with rep.json, fetch and parse JSON // If this blob ends with rep.json, fetch and parse JSON
if (blob.name.toLowerCase().endsWith("rep.json")) { if (blob.name.toLowerCase().endsWith("rep.json")) {
folderGroup.hasRepJson = true; folderGroup.hasRepJson = true;