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;
+355 -272
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,185 +104,312 @@ 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 className="govuk-width-container"> <div>
<div className="govuk-main-wrapper govuk-main-wrapper--auto-spacing"> <Head>
<h1>Storage Account Contents</h1> <title>Storage Account Browser</title>
{data.map((user) => ( </Head>
<div key={user.id} className="govuk-grid-row"> <div id="page_wrapper">
{user.containers.map((c) => ( <CookieBanner />
<div key={c.container} className="card"> <Header />
<div className="card-body"> <div className="govuk-width-container">
<h3> <div className="govuk-main-wrapper govuk-main-wrapper--auto-spacing">
{c.container} - {user.email} <h1>Storage Account Contents</h1>
</h3>
{c.folders.map((folder) => ( {/* Toggle buttons */}
<div key={folder.folderPath}> <div style={{ marginBottom: "0.2rem" }}>
<h4> <button
{folder.folderPath || "Root"} className="govuk-button govuk-button--secondary"
</h4> onClick={() => setShowRepJson(!showRepJson)}
{/* {folder.hasRepJson && ( >
{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) => (
<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}
style={
showRepJson &&
folder.hasRepJson
? {
display:
"block",
}
: showTmpFile &&
folder.hasTmpFile
? {
display:
"block",
}
: {
display:
"none",
}
}
>
<h4>
{folder.folderPath ||
"Root"}
</h4>
{/* {folder.hasRepJson && (
<p> <p>
rep.json file found:{" "} rep.json file found:{" "}
{folder.repJsonBlob.name} {folder.repJsonBlob.name}
</p> </p>
)} */} )} */}
{folder.hasRepJson && ( {folder.hasTmpFile && (
<div> <>
{" "} <button
<button onClick={() => {
title={ confirm(
"Do you want to delete rep" "Do you want to remove this unsubmitted appeal " +
} showTopThreeArr[
className=" cardModuleRemoveCase govuk-link govuk-link--no-underline govuk-link--inverse " key
onClick={() => { ]
confirm( .ticketnumber +
"Do you want tyo delete rep " + "?\n" +
folder.repJsonBlob.name.split( t(
"/" "case:do-you-want-to-delete-case-disclaimer-label"
)[0] + )
"?\n" ) &&
) && (deleteCaseItem(
(deleteRepItem( c.container,
c.container, folder.folderPath
folder.repJsonBlob.name.split( ),
"/" alert(
)[0], "Temp case Deleted!"
folder.repJsonBlob.name.split( ),
"/" window.location.reload());
)[1] }}
), >
alert( Delete this TMP
"Rep Deleted!" appeal
), </button>
window.location.reload()); </>
}} )}
> {folder.hasRepJson && (
Delete Rep <div>
</button> {" "}
{folder.repComplete && ( <button
<button title={
onClick={async () => { "Do you want to delete rep"
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
);
} }
}} className=" cardModuleRemoveCase govuk-link govuk-link--no-underline govuk-link--inverse "
> onClick={() => {
Remove repComplete confirm(
</button> "Do you want tyo delete rep " +
)} folder.repJsonBlob.name.split(
{folder.repComplete && ( "/"
<button )[0] +
onClick={async () => { "?\n"
confirm( ) &&
"Do you want resend the rep queue message " + (deleteRepItem(
folder.repJsonBlob.name.split( c.container,
"/" folder.repJsonBlob.name.split(
)[0] + "/"
" --- " + )[0],
folder.repJsonBlob.name.split( folder.repJsonBlob.name.split(
"/" "/"
)[1] )[1]
) && ),
(sendRepCompleteMessage( alert(
c.container, "Rep Deleted!"
folder.repJsonBlob.name.split( ),
"/" window.location.reload());
)[0], }}
folder.repJsonBlob.name.split( >
"/" Delete Rep
)[1] </button>
), {folder.repComplete && (
alert( <button
"rep queue message sent!" onClick={async () => {
), const response =
window.location.reload()); await fetch(
}} "/api/file/editRepJson",
> {
Recreate Queue method: "POST",
Message headers:
</button> {
)} "Content-Type":
</div> "application/json",
)} },
body: JSON.stringify(
<ul> {
{folder.blobs.map((blob) => ( container:
<li key={blob.name}> c.container,
<Link blobName:
scroll={false} folder
href={ .repJsonBlob
"/api/file/downloadblob?container=" + .name,
c.container + }
"&casefolderID=" + ),
folder.folderPath + }
"&blobname=" + );
blob.name const result =
.substring( await response.json();
blob.name.lastIndexOf( if (
"/" response.ok
) + 1 ) {
) alert(
.trim() + "repComplete removed!"
"&hash=" + ),
blob.hashedfilepath window.location.reload();
} // Optionally refresh the page or update UI state here
className="govuk-body govuk-!-font-size-14 govuk-!-padding-left-5 govuk-link" } else {
> alert(
{blob.name} ( "Error: " +
{bytesToSize( result.error
blob.contentLength || );
blob.size }
}}
>
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-link"
>
{
blob.name
}{" "}
(
{bytesToSize(
blob.contentLength ||
blob.size
)}
)
</Link>{" "}
{blob.lastModified && (
<span className="govuk-!-font-size-14">
{/* {
blob.displayDate
} */}
{formatBlobDates(
blob
)}
</span>
)}
</li>
) )
</Link> )}
</li> </ul>
))} </div>
</ul> ))}
</div> </div>
))} </div>
</div> ))}
</div> </div>
))} ))}
</div> </div>
))} </div>
<Footer />
</div> </div>
</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;