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) {
const blobNames = [];
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({
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;
+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 { BlobServiceClient, ContainerClient } from "@azure/storage-blob";
import { v4 as uuidv4 } from "uuid";
@@ -7,6 +10,7 @@ import {
listBlobHierarchical,
listContainersForUser,
} from "../actions/azurestorage";
import Head from "next/head";
import Link from "next/link";
import { PrismaClient } from "@prisma/client";
import {
@@ -19,8 +23,10 @@ import {
getIP,
sendRepCompleteMessage,
deleteMyRepresentationsFromBlob,
deleteAwaitingSubmissionsFromBlob,
} from "../actions";
import CryptoJS from "crypto-js";
import { useState } from "react";
function getUpToLastSlash(url) {
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) => {
deleteMyRepresentationsFromBlob(containerID, caseID, repfile).then(
(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 }) {
console.log(data);
const [showRepJson, setShowRepJson] = useState(true);
const [showTmpFile, setShowTmpFile] = useState(true);
const toggleAll = () => {
const newState = !(showRepJson || showTmpFile);
setShowRepJson(newState);
setShowTmpFile(newState);
};
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 && (
<div>
<Head>
<title>Storage Account Browser</title>
</Head>
<div id="page_wrapper">
<CookieBanner />
<Header />
<div className="govuk-width-container">
<div className="govuk-main-wrapper govuk-main-wrapper--auto-spacing">
<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) => (
<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>
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
);
{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 && (
<div>
{" "}
<button
title={
"Do you want to delete rep"
}
}}
>
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
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-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>
)}
</ul>
</div>
))}
</div>
))}
</div>
</div>
))}
</div>
))}
</div>
))}
</div>
<Footer />
</div>
</div>
);
@@ -299,112 +474,6 @@ export async function getServerSideProps({ req }) {
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(
@@ -429,7 +498,16 @@ export async function getServerSideProps({ req }) {
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(
(f) => f.folderPath === folderPath
@@ -440,13 +518,18 @@ export async function getServerSideProps({ req }) {
blobs: [],
hasRepJson: false,
repJsonBlob: null,
repComplete: false, // new flag
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;