storage admin update

This commit is contained in:
2025-08-14 08:25:20 +01:00
parent 8e9d89a2af
commit b86712efb8
3 changed files with 416 additions and 293 deletions
-27
View File
@@ -1715,33 +1715,6 @@ function formatDateDisplay(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()) {
+29
View File
@@ -207,6 +207,35 @@ const SignIn = (props) => {
};
export async function getServerSideProps(context) {
const { query, req, res } = context;
// 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,
},
};
}
if (process.env.SHOWLOGIN == false) {
return {
redirect: {
+387 -266
View File
@@ -110,11 +110,11 @@ const deleteCaseItem = (containerID, caseID) => {
});
};
export default function StoragePage({ data }) {
console.log(data);
export default function StoragePage(props) {
const { data, userData } = props;
const [showRepJson, setShowRepJson] = useState(true);
const [showTmpFile, setShowTmpFile] = useState(true);
const [whichTab, setWhichTab] = useState("storage");
const toggleAll = () => {
const newState = !(showRepJson || showTmpFile);
@@ -130,283 +130,394 @@ export default function StoragePage({ data }) {
<CookieBanner />
<Header />
<div className="govuk-width-container">
<div className="govuk-main-wrapper govuk-main-wrapper--auto-spacing">
<h1>Storage Account Contents</h1>
<div className="js-enabled govuk-template__body govuk-frontend-supported">
<div className="govuk-tabs" data-module="govuk-tabs">
<h2 className="govuk-tabs__title">Contents</h2>
<ul className="govuk-tabs__list">
<li
className={
whichTab == "storage"
? "govuk-tabs__list-item govuk-tabs__list-item--selected"
: "govuk-tabs__list-item "
}
>
<a
className="govuk-tabs__tab"
href="#storage"
onClick={() => setWhichTab("storage")}
>
Storage Account
</a>
</li>
<li
className={
whichTab == "accounts"
? "govuk-tabs__list-item govuk-tabs__list-item--selected"
: "govuk-tabs__list-item "
}
>
<a
className="govuk-tabs__tab"
href="#accounts"
onClick={() => setWhichTab("accounts")}
>
User Accounts
</a>
</li>
</ul>
<div
className={
whichTab == "storage"
? "govuk-tabs__panel "
: "govuk-tabs__panel govuk-tabs__panel--hidden"
}
id="storage"
>
<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>
{/* 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 && (
{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} -{" "}
<Link
href={
"/auth/signin2?email=" +
user.email
}
>
{user.email}
</Link>
</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.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"
}
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"
{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());
}}
>
{
blob.name
}{" "}
(
{bytesToSize(
blob.contentLength ||
blob.size
)}
)
</Link>{" "}
{blob.lastModified && (
<span className="govuk-!-font-size-14">
{/* {
Delete
this TMP
appeal
</button>
</>
)}
{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-link"
>
{
blob.name
}{" "}
(
{bytesToSize(
blob.contentLength ||
blob.size
)}
)
</Link>{" "}
{blob.lastModified && (
<span className="govuk-!-font-size-14">
{/* {
blob.displayDate
} */}
{formatBlobDates(
blob
{formatBlobDates(
blob
)}
</span>
)}
</span>
)}
</li>
)
)}
</ul>
</li>
)
)}
</ul>
</div>
))}
</div>
))}
</div>
</div>
))}
</div>
))}
</div>
))}
<div
className={
whichTab == "accounts"
? "govuk-tabs__panel "
: "govuk-tabs__panel govuk-tabs__panel--hidden"
}
id="accounts"
>
<h1>User Accounts </h1>
<dl className=" govuk-summary-list govuk-!-margin-bottom-9 results">
<div class="govuk-summary-list__row results-headings">
<dd className="govuk-summary-list__key govuk-!-font-size-16 ">
Name
</dd>
<dd className="govuk-summary-list__key govuk-!-font-size-16">
Last accessed
</dd>
</div>
{userData.map((user) => (
<div
key={user.id}
className=" govuk-summary-list__row"
>
<dd className="govuk-summary-list__value govuk-!-font-size-16 ">
<Link
target="_blank"
href={
"/auth/signin2?email=" +
user.email
}
>
{user.email}
</Link>
</dd>
<dd className="govuk-summary-list__value govuk-!-font-size-16 ">
Last accessed: {user.created}{" "}
</dd>
</div>
))}
</dl>
</div>
</div>
</div>
</div>
<Footer />
@@ -587,5 +698,15 @@ export async function getServerSideProps({ req }) {
})
);
return { props: { data: data.filter(Boolean) } };
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") || "",
}))
);
return { props: { data: data.filter(Boolean), userData: userData } };
}