Merge ITHC recovery into mitigation baseline
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { getJson, requestJson } from "./endpointClient";
|
||||
import { getSignedJson, postSignedJson } from "./signedRequestClient";
|
||||
|
||||
export const getFileJson = (url) => {
|
||||
return getJson(url);
|
||||
export const getFileJson = (url, config) => {
|
||||
return getJson(url, config);
|
||||
};
|
||||
|
||||
export const getSignedFileJson = async (queryUrl) => {
|
||||
|
||||
@@ -11,31 +11,6 @@ export const getPersonalAccount = (contactid) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const getLogin = (emailAddress, pwd) => {
|
||||
return getJson(
|
||||
"/api/endpoint/getlogin_api?emailAddress=" +
|
||||
emailAddress +
|
||||
"&pwd=" +
|
||||
pwd
|
||||
).catch((error) => {
|
||||
consoleLogger(error);
|
||||
});
|
||||
};
|
||||
|
||||
export const updatePassword = (contactId, newpassword) => {
|
||||
var queryUrl = "/api/endpoint/updateaccount_api?contactId=" + contactId;
|
||||
var data = { "pinswg_custom_password": newpassword };
|
||||
var config = {
|
||||
method: "post",
|
||||
url: queryUrl,
|
||||
data: data
|
||||
};
|
||||
|
||||
return requestJson(config).catch((error) => {
|
||||
consoleLogger(error);
|
||||
});
|
||||
};
|
||||
|
||||
export const updateAccount = async (contactId, updateBody, ssr) => {
|
||||
var queryUrl =
|
||||
(ssr ? BASE_URL : "") +
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
getPersonalAccount,
|
||||
getLogin,
|
||||
updatePassword,
|
||||
updateAccount,
|
||||
createAccount,
|
||||
getEmailAccountCheck,
|
||||
@@ -12,8 +10,6 @@ import {
|
||||
|
||||
export {
|
||||
getPersonalAccount,
|
||||
getLogin,
|
||||
updatePassword,
|
||||
updateAccount,
|
||||
createAccount,
|
||||
getEmailAccountCheck,
|
||||
|
||||
@@ -76,35 +76,6 @@ export const getAppealID = (
|
||||
});
|
||||
};
|
||||
|
||||
export const createNewCase = (
|
||||
appealTypeId,
|
||||
lpaID,
|
||||
contactid,
|
||||
createBody,
|
||||
containerName
|
||||
) => {
|
||||
appealTypeId = parseInt(appealTypeId);
|
||||
|
||||
var data = createBody;
|
||||
|
||||
var queryUrl = buildFileQuery("/api/endpoint/createcase_api", {
|
||||
appealTypeId,
|
||||
lpaID,
|
||||
contactid,
|
||||
containername: containerName
|
||||
});
|
||||
|
||||
var config = {
|
||||
method: "post",
|
||||
url: withBaseUrl(BASE_URL, queryUrl),
|
||||
data: data
|
||||
};
|
||||
|
||||
return requestJson(config).catch((error) => {
|
||||
consoleLogger(error);
|
||||
});
|
||||
};
|
||||
|
||||
export const createNewCaseBlob = (
|
||||
appealTypeId,
|
||||
lpaID,
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
getSIPSEvents,
|
||||
getSIPSMedia,
|
||||
getAppealID,
|
||||
createNewCase,
|
||||
createNewCaseBlob,
|
||||
updateCase,
|
||||
updateCaseBlob,
|
||||
@@ -26,7 +25,6 @@ export {
|
||||
getSIPSEvents,
|
||||
getSIPSMedia,
|
||||
getAppealID,
|
||||
createNewCase,
|
||||
createNewCaseBlob,
|
||||
updateCase,
|
||||
updateCaseBlob,
|
||||
|
||||
@@ -28,13 +28,48 @@ export const getAwaitingSubmissionFromBlob = (containerName) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const getRepsFromBlob = (containerName) => {
|
||||
export const getRepsFromBlob = (containerName, config) => {
|
||||
const route = buildFileQuery("/api/file/getrepsblob", {
|
||||
container: containerName
|
||||
});
|
||||
|
||||
const url = withBaseUrl(BASE_URL, appendHashSuffix(route, hashAPIPath));
|
||||
|
||||
const hasForwardedCookie =
|
||||
typeof config?.headers?.Cookie === "string" &&
|
||||
config.headers.Cookie.length > 0;
|
||||
|
||||
if (hasForwardedCookie) {
|
||||
let trustedDestination = false;
|
||||
|
||||
try {
|
||||
const destination = new URL(url);
|
||||
const expectedOrigin = new URL(process.env.NEXTAUTH_URL);
|
||||
|
||||
trustedDestination =
|
||||
destination.origin === expectedOrigin.origin &&
|
||||
["http:", "https:"].includes(destination.protocol) &&
|
||||
!destination.username &&
|
||||
!destination.password;
|
||||
} catch {
|
||||
trustedDestination = false;
|
||||
}
|
||||
|
||||
if (!trustedDestination) {
|
||||
return Promise.reject(
|
||||
new Error("Untrusted representation request destination")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return getFileJson(
|
||||
withBaseUrl(BASE_URL, appendHashSuffix(route, hashAPIPath))
|
||||
url,
|
||||
hasForwardedCookie
|
||||
? {
|
||||
...config,
|
||||
maxRedirects: 0
|
||||
}
|
||||
: config
|
||||
).catch((error) => {
|
||||
consoleLogger(error);
|
||||
});
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
import useTranslation from "next-translate/useTranslation";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Field, formValueSelector, reduxForm } from "redux-form";
|
||||
import { updatePassword } from "../../actions/services/accountService";
|
||||
|
||||
const RenderTextfield = ({
|
||||
id,
|
||||
className,
|
||||
rows,
|
||||
datafieldname,
|
||||
name,
|
||||
label,
|
||||
input,
|
||||
type,
|
||||
errorMsg,
|
||||
meta: { touched, error },
|
||||
...custom
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={
|
||||
touched && error
|
||||
? "govuk-form-group govuk-form-group--error"
|
||||
: "govuk-form-group "
|
||||
}
|
||||
>
|
||||
<label className="govuk-label " htmlFor={id} id={id + "_label"}>
|
||||
{label}
|
||||
</label>
|
||||
{touched && error && (
|
||||
<span id={id + "-error"} className="govuk-error-message">
|
||||
<span className="govuk-visually-hidden">Error:</span>{" "}
|
||||
{touched &&
|
||||
((error && (
|
||||
<span>{errorMsg ? errorMsg : error}</span>
|
||||
)) ||
|
||||
(warning && <span>{warning}</span>))}
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
{...input}
|
||||
className={className}
|
||||
name={name}
|
||||
id={id}
|
||||
type={type}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ChangePassword = (props) => {
|
||||
const {
|
||||
footerLinks,
|
||||
pages,
|
||||
handleSubmit,
|
||||
value,
|
||||
setPasswordUpdated,
|
||||
passwordUpdated
|
||||
} = props;
|
||||
let { t, lang } = useTranslation();
|
||||
|
||||
const router = useRouter();
|
||||
const { locale } = router;
|
||||
|
||||
let errorsobj = {};
|
||||
|
||||
const onHandleSubmit = (values) => {
|
||||
let contactid = props.props.accountDetails.loggedinUserId;
|
||||
let updateBody = props.props.form.changepasswordForm.values.newpassword;
|
||||
console.log(contactid, updateBody);
|
||||
updatePassword(contactid, updateBody);
|
||||
setPasswordUpdated(true);
|
||||
};
|
||||
|
||||
const required = (value) => (value ? undefined : "Is required");
|
||||
|
||||
const passwordsMustMatch = (value, allValues) =>
|
||||
value !== allValues.newpassword ? "Passwords do not match" : undefined;
|
||||
|
||||
const minLength = (min) => (value) =>
|
||||
value && value.length < min
|
||||
? `Must be at least ${min} characters`
|
||||
: undefined;
|
||||
|
||||
const [hasErrors, setHasErrors] = useState("");
|
||||
|
||||
return (
|
||||
<>
|
||||
{passwordUpdated != true ? (
|
||||
<form onSubmit={handleSubmit(onHandleSubmit)}>
|
||||
<fieldset className="govuk-fieldset">
|
||||
<Field
|
||||
name="newpassword"
|
||||
id="newpassword"
|
||||
component={RenderTextfield}
|
||||
type="password"
|
||||
className="govuk-input govuk-input--width-20"
|
||||
validate={[required, minLength(6)]}
|
||||
label="New password"
|
||||
/>
|
||||
<Field
|
||||
name="confirmpassword"
|
||||
id="confirmpassword"
|
||||
component={RenderTextfield}
|
||||
type="password"
|
||||
className="govuk-input govuk-input--width-20"
|
||||
validate={[required, passwordsMustMatch]}
|
||||
label="Confirm new password"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<div className="govuk-button-group">
|
||||
<button
|
||||
type="submit"
|
||||
className="govuk-button"
|
||||
data-module="govuk-button"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
<Link
|
||||
href={
|
||||
"/myportal?q=" +
|
||||
props.accountDetails.loggedinUserId
|
||||
}
|
||||
className="govuk-link"
|
||||
>
|
||||
Cancel
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
<p className="govuk-body">
|
||||
<Link
|
||||
href={
|
||||
"/myportal?q=" +
|
||||
props.accountDetails.loggedinUserId
|
||||
}
|
||||
className="govuk-link"
|
||||
>
|
||||
Go to My Portal
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
search: state.search,
|
||||
searchResultsObj: state.searchResultsObj,
|
||||
formData: state.formData,
|
||||
appealType: state.appealType,
|
||||
accountDetails: state.accountDetails
|
||||
//form: state.form,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
setCurrentSection: (currentSection) => {
|
||||
dispatch(setCurrentSection(currentSection));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const selector = formValueSelector("changepasswordForm"); // <-- same as form name
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(
|
||||
reduxForm({
|
||||
form: "changepasswordForm",
|
||||
destroyOnUnmount: false
|
||||
})(ChangePassword)
|
||||
);
|
||||
@@ -92,11 +92,6 @@ const RegisterForm = (props) => {
|
||||
? "Invalid phone number"
|
||||
: undefined;
|
||||
|
||||
const passwordsMatch = (value, allValues) =>
|
||||
value !== allValues.pinswg_custom_password
|
||||
? "Passwords don't match"
|
||||
: undefined;
|
||||
|
||||
const minLength = (min) => (value) =>
|
||||
value && value.length < min
|
||||
? `Must be ${min} characters or more`
|
||||
|
||||
@@ -27,24 +27,6 @@ export default function YourAccount() {
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className="card-body">
|
||||
<h3 className="heading-small card-heading">
|
||||
{t("myportal:changepassword-card-title")}
|
||||
</h3>
|
||||
<p className="govuk-body-s">
|
||||
{" "}
|
||||
{t("myportal:changepassword-card-paragraph-one")}
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<Link href="/account/changepassword">
|
||||
<a className="govuk-button-secondary">
|
||||
{t("myportal:changepassword-card-link")}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -393,13 +393,6 @@ let CreateCase = (props) => {
|
||||
|
||||
delete values.confirmHASCAS;
|
||||
|
||||
// createNewCase(
|
||||
// appTypeCollection.appealTypeID,
|
||||
// values.lpaTypes,
|
||||
// props.loggedInUser,
|
||||
// values,
|
||||
// props.containerID
|
||||
// )
|
||||
const selectedLpaName = props.LPAData.LPAData.value.filter((obj) => {
|
||||
return obj.accountid === values.lpaTypes;
|
||||
})[0].name;
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { azureHeadersPaged } from "../../actions/core/headers";
|
||||
import { escapeODataString } from "../../actions/core/guards";
|
||||
import { consoleLogger } from "../../actions/core/logger";
|
||||
import { relayGetData } from "../../pages/api/middleware/relayForwarding";
|
||||
import { RELAY_POLICY_STRICT_LOGIN } from "../../pages/api/middleware/relayPolicyPresets";
|
||||
|
||||
const WELSH_LANGUAGE_CODE = 846040000;
|
||||
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
export const resolveCrmLocale = async (email) => {
|
||||
if (typeof email !== "string") return null;
|
||||
|
||||
const normalizedEmail = email.trim().toLowerCase();
|
||||
|
||||
if (!EMAIL_PATTERN.test(normalizedEmail)) return null;
|
||||
|
||||
const queryUrl =
|
||||
"contacts?$filter=emailaddress1 eq '" +
|
||||
escapeODataString(normalizedEmail) +
|
||||
"' and statuscode eq 1&$count=true&$select=pinswg_preferredlanguage";
|
||||
|
||||
try {
|
||||
const { data } = await relayGetData({
|
||||
queryUrl,
|
||||
requestOptionsBuilder: (accessToken) =>
|
||||
azureHeadersPaged(accessToken),
|
||||
relayPolicy: RELAY_POLICY_STRICT_LOGIN
|
||||
});
|
||||
const preferredLanguage = data?.value?.[0]?.pinswg_preferredlanguage;
|
||||
|
||||
if (preferredLanguage === WELSH_LANGUAGE_CODE) return "cy";
|
||||
if (preferredLanguage != null) return "en";
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
consoleLogger({
|
||||
name: "ResolveCrmLocale",
|
||||
message: "CRM locale lookup failed"
|
||||
});
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -120,7 +120,9 @@ export const loadRepresentationBootstrap = async ({ ctx }) => {
|
||||
|
||||
const [accountDetails, myRepresentations] = await Promise.all([
|
||||
getPersonalAccount(loggedInUser),
|
||||
getRepsFromBlob(thisSession.user.id)
|
||||
getRepsFromBlob(thisSession.user.id, {
|
||||
headers: { Cookie: ctx.req?.headers?.cookie || "" }
|
||||
})
|
||||
]);
|
||||
|
||||
const myRepresentationsDetails = await getDetails(
|
||||
|
||||
@@ -472,15 +472,6 @@ export const createDetailAndAccountRouteRenderers = ({
|
||||
{t("account:account-title")}
|
||||
</li>
|
||||
</>
|
||||
),
|
||||
|
||||
"/account/changepassword": () => (
|
||||
<>
|
||||
{renderLinkCrumb(cyMyPortalHref, t("common:breadcrumb-my-portal"))}
|
||||
<li className="govuk-breadcrumbs__list-item">
|
||||
Update your password
|
||||
</li>
|
||||
</>
|
||||
)
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { relayGetData } from "../../pages/api/middleware/relayForwarding";
|
||||
import { RELAY_POLICY_LOOKUP } from "../../pages/api/middleware/relayPolicyPresets";
|
||||
|
||||
export const getCrmPersonalAccount = async (contactId) => {
|
||||
const queryUrl =
|
||||
"contacts(" +
|
||||
contactId +
|
||||
")?$select=firstname, lastname, emailaddress1, telephone1, pinswg_companyname, address1_line1,address1_line2,address1_city, address1_county,address1_postalcode,pinswg_typeofinvolvement,pinswg_contact_associatedlpa,pinswg_preferredlanguage&$count=true";
|
||||
|
||||
const { data } = await relayGetData({
|
||||
queryUrl,
|
||||
relayPolicy: RELAY_POLICY_LOOKUP
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import axios from "axios";
|
||||
import { getToken } from "../../actions/core/token";
|
||||
import { hashAPIPath } from "../../actions/core/hash";
|
||||
|
||||
const WEBAPI_URL =
|
||||
process.env.RELAY_ROOT ||
|
||||
"https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/";
|
||||
|
||||
export const updateCrmContact = async (contactId, updatePayload) => {
|
||||
const token = await getToken();
|
||||
const queryUrl = "contacts(" + contactId + ")";
|
||||
|
||||
const config = {
|
||||
method: "patch",
|
||||
url: WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
|
||||
headers: {
|
||||
"OData-MaxVersion": "4.0",
|
||||
"OData-Version": "4.0",
|
||||
Accept: "application/json",
|
||||
Prefer: 'odata.include-annotations="*",return=representation',
|
||||
Authorization: "Bearer " + token.access_token,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
data: JSON.stringify(updatePayload)
|
||||
};
|
||||
|
||||
const response = await axios(config);
|
||||
return response.data;
|
||||
};
|
||||
@@ -1,120 +0,0 @@
|
||||
import Link from "next/link";
|
||||
import _ from "lodash";
|
||||
import Head from "next/head";
|
||||
import Header from "../../components/header";
|
||||
import Banner from "../../components/banner";
|
||||
import CookieBanner from "../../components/cookieBanner";
|
||||
import Breadcrumbs from "../../components/breadcrumbs";
|
||||
import Footer from "../../components/footer";
|
||||
|
||||
import { useSelector, shallowEqual, connect } from "react-redux";
|
||||
import { wrapper } from "../../store/store";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import useTranslation from "next-translate/useTranslation";
|
||||
import { JSONPath as jsonpath } from "jsonpath-plus";
|
||||
import { reduxForm, formValueSelector } from "redux-form";
|
||||
|
||||
import ChangePassword from "../../components/account/changepassword";
|
||||
import xpath from "xpath";
|
||||
import {
|
||||
setAppealType,
|
||||
setAppealTypeTitle,
|
||||
setAppealTypeID,
|
||||
setAppealLPA,
|
||||
getAppealTypeObj
|
||||
} from "../../store/appealType/action";
|
||||
import {
|
||||
getAppealsTypes,
|
||||
getLPA
|
||||
} from "../../actions/services/referenceDataService";
|
||||
import { setLPA } from "../../store/lpa/action";
|
||||
|
||||
import { getSession, useSession } from "next-auth/react";
|
||||
import NoSessionWarning from "../../components/nosession";
|
||||
|
||||
const Home = (props) => {
|
||||
const { footerLinks, pages, formData } = props;
|
||||
let { t, lang } = useTranslation();
|
||||
|
||||
const router = useRouter();
|
||||
const { locale } = router;
|
||||
const { appealtypes } = router.query;
|
||||
|
||||
const { appealType, LPAData } = props;
|
||||
const [passwordUpdated, setPasswordUpdated] = useState(false);
|
||||
|
||||
const { data: session, status } = useSession();
|
||||
|
||||
if (status === "loading") {
|
||||
return <NoSessionWarning loading={true} />;
|
||||
}
|
||||
|
||||
if (status === "unauthenticated") {
|
||||
router.push("/auth/signin");
|
||||
return <NoSessionWarning />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>
|
||||
{t("search:page-title")} - {t("common:service-name")}
|
||||
</title>
|
||||
</Head>
|
||||
<div id="page_wrapper">
|
||||
<CookieBanner />
|
||||
<Header />
|
||||
|
||||
<div className="govuk-grid-row">
|
||||
<div className="govuk-grid-column-full">
|
||||
<div className="govuk-width-container">
|
||||
<Breadcrumbs slug={appealtypes} props={props} />
|
||||
<main
|
||||
className="govuk-main-wrapper--auto-spacing"
|
||||
id="main-content"
|
||||
role="main"
|
||||
>
|
||||
<div className="govuk-grid-row">
|
||||
<div className="govuk-grid-column-two-thirds">
|
||||
{passwordUpdated == true ? (
|
||||
<h1>
|
||||
Your password has been changed
|
||||
</h1>
|
||||
) : (
|
||||
<h1>Change password</h1>
|
||||
)}
|
||||
<ChangePassword
|
||||
props={props}
|
||||
setPasswordUpdated={
|
||||
setPasswordUpdated
|
||||
}
|
||||
passwordUpdated={passwordUpdated}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer footerLinks={footerLinks} ticketnumber={props} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
search: state.search,
|
||||
searchResultsObj: state.searchResultsObj,
|
||||
formData: state.formData,
|
||||
appealType: state.appealType,
|
||||
LPAData: state.LPAData,
|
||||
form: state.form,
|
||||
accountDetails: state.accountDetails
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(Home);
|
||||
@@ -1,9 +1,11 @@
|
||||
import { BASE_URL } from "../../../actions/core/env";
|
||||
import { getToken } from "../../../actions/core/token";
|
||||
import { azureHeaders } from "../../../actions/core/headers";
|
||||
import { consoleLogger } from "../../../actions/core/logger";
|
||||
import { hashAPIPath } from "../../../actions/core/hash";
|
||||
import axios from "axios";
|
||||
import {
|
||||
ADMIN_SESSION_COOKIE,
|
||||
hasTrustedSameOrigin,
|
||||
readAdminSessionCookie,
|
||||
setAdminAuthNoStore,
|
||||
@@ -11,7 +13,16 @@ import {
|
||||
} from "../../../lib/admin/adminAuth";
|
||||
import { respondError, respondSuccess } from "../middleware/apiResponse";
|
||||
|
||||
const BASE_URL = process.env.API_ROOT || `http://localhost:${port}`;
|
||||
const isTrustedInternalDestination = () => {
|
||||
try {
|
||||
return (
|
||||
new URL(BASE_URL).origin ===
|
||||
new URL(process.env.NEXTAUTH_URL).origin
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const forbidden = (res) =>
|
||||
respondError(res, {
|
||||
@@ -39,10 +50,7 @@ export default async function handler(req, res) {
|
||||
}
|
||||
|
||||
const adminSession = readAdminSessionCookie(req);
|
||||
if (
|
||||
!validateAdminSession(adminSession) ||
|
||||
!hasTrustedSameOrigin(req)
|
||||
) {
|
||||
if (!validateAdminSession(adminSession) || !hasTrustedSameOrigin(req)) {
|
||||
return forbidden(res);
|
||||
}
|
||||
|
||||
@@ -55,14 +63,27 @@ export default async function handler(req, res) {
|
||||
});
|
||||
}
|
||||
|
||||
if (!isTrustedInternalDestination()) {
|
||||
return forbidden(res);
|
||||
}
|
||||
|
||||
const token = await getToken();
|
||||
const queryUrl =
|
||||
"/api/file/getrepsblob?container=" + encodeURIComponent(container);
|
||||
|
||||
try {
|
||||
const config = azureHeaders(token.access_token);
|
||||
|
||||
const { data } = await axios.get(
|
||||
BASE_URL + queryUrl + hashAPIPath(queryUrl),
|
||||
azureHeaders(token.access_token)
|
||||
{
|
||||
...config,
|
||||
headers: {
|
||||
...config.headers,
|
||||
Cookie: `${ADMIN_SESSION_COOKIE}=${encodeURIComponent(adminSession)}`
|
||||
},
|
||||
maxRedirects: 0
|
||||
}
|
||||
);
|
||||
return respondSuccess(res, data);
|
||||
} catch (error) {
|
||||
@@ -73,4 +94,4 @@ export default async function handler(req, res) {
|
||||
message: "Failed to fetch representation blobs"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ import { PrismaAdapter } from "@next-auth/prisma-adapter";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import NextAuth from "next-auth";
|
||||
import EmailProvider from "../../../lib/auth/email-provider";
|
||||
import { resolveCrmLocale } from "../../../lib/auth/resolveCrmLocale";
|
||||
import { consoleLogger, redactSensitive } from "../../../actions/core/logger";
|
||||
import { getPortalLogin } from "../../../actions/services/accountService";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
@@ -53,6 +53,8 @@ const createVerificationTokenSynchronizationAdapter = (
|
||||
};
|
||||
};
|
||||
|
||||
const WELSH_LANGUAGE_CODE = 846040000;
|
||||
|
||||
const authDiagnostic = (event, metadata = {}) => {
|
||||
try {
|
||||
const safeMeta = JSON.parse(JSON.stringify(metadata));
|
||||
@@ -82,6 +84,29 @@ const appendParamsAndPathToNewUrl = (fromUrl, toUrl) => {
|
||||
}
|
||||
};
|
||||
|
||||
const resolveCrmLocale = async (email) => {
|
||||
if (!email) return null;
|
||||
|
||||
try {
|
||||
const portalUserObj = await getPortalLogin(email);
|
||||
const preferredLanguage =
|
||||
portalUserObj?.value?.[0]?.pinswg_preferredlanguage;
|
||||
|
||||
if (preferredLanguage === WELSH_LANGUAGE_CODE) {
|
||||
return "cy";
|
||||
}
|
||||
|
||||
if (preferredLanguage != null) {
|
||||
return "en";
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
consoleLogger(error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const resolveEffectiveLocale = async (req, email) => {
|
||||
const crmLocale = await resolveCrmLocale(email);
|
||||
if (crmLocale) {
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import { getPortalLogin } from "../../../actions/services/accountService";
|
||||
import { consoleLogger } from "../../../actions/core/logger";
|
||||
|
||||
const WELSH_LANGUAGE_CODE = 846040000;
|
||||
|
||||
const resolveRequestLocale = (req) =>
|
||||
req?.query?.locale ||
|
||||
req?.body?.locale ||
|
||||
@@ -9,8 +14,30 @@ export default async function handler(req, res) {
|
||||
return res.status(405).json({ message: "Method not allowed" });
|
||||
}
|
||||
|
||||
const requestedLocale = resolveRequestLocale(req);
|
||||
const locale = requestedLocale === "cy" ? "cy" : "en";
|
||||
const email = String(req.body?.email || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const sessionLocale = resolveRequestLocale(req);
|
||||
|
||||
return res.status(200).json({ locale });
|
||||
if (!email) {
|
||||
return res.status(200).json({ locale: sessionLocale });
|
||||
}
|
||||
|
||||
try {
|
||||
const portalUserObj = await getPortalLogin(email);
|
||||
const preferredLanguage =
|
||||
portalUserObj?.value?.[0]?.pinswg_preferredlanguage;
|
||||
|
||||
const locale =
|
||||
preferredLanguage === WELSH_LANGUAGE_CODE
|
||||
? "cy"
|
||||
: preferredLanguage != null
|
||||
? "en"
|
||||
: sessionLocale;
|
||||
|
||||
return res.status(200).json({ locale });
|
||||
} catch (error) {
|
||||
consoleLogger(error);
|
||||
return res.status(200).json({ locale: sessionLocale });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
/**
|
||||
* @swagger
|
||||
* /api/endpoint/createcase_api:
|
||||
* post:
|
||||
* tags: [Case]
|
||||
* description: Create case
|
||||
* summary: Phase 2
|
||||
* parameters:
|
||||
* - name: body
|
||||
* in: requestbody
|
||||
* required: true
|
||||
* example: {}
|
||||
* schema:
|
||||
* type: object
|
||||
* - name: contactid
|
||||
* in: query
|
||||
* required: true
|
||||
* example: pinswg_planningappeals78s
|
||||
* schema:
|
||||
* type: string
|
||||
* - name: appealTypeId
|
||||
* in: query
|
||||
* required: true
|
||||
* example: pinswg_planningappeals78id
|
||||
* schema:
|
||||
* type: string
|
||||
* - name: containername
|
||||
* in: query
|
||||
* required: true
|
||||
* example: pinswg_planningappeals78id
|
||||
* schema:
|
||||
* type: string
|
||||
* - name: lpaID
|
||||
* in: query
|
||||
* required: true
|
||||
* example: pinswg_planningappeals78id
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Success
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import { consoleLogger } from "../../../actions/core/logger";
|
||||
import { getToken } from "../../../actions/core/token";
|
||||
import { getCaseBlob } from "../../../actions/azurestorage";
|
||||
import { hashAPIPath } from "../../../actions/core/hash";
|
||||
import { respondError, respondSuccess } from "../middleware/apiResponse";
|
||||
|
||||
const WEBAPI_URL =
|
||||
process.env.RELAY_ROOT ||
|
||||
"https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/";
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
const createCaseBody = req.body;
|
||||
const contactid = req.query.contactid;
|
||||
const appealTypeId = req.query.appealTypeId;
|
||||
const containerName = req.query.containername;
|
||||
const lpaID = req.query.lpaID;
|
||||
const queryUrl = "incidents";
|
||||
|
||||
if (!createCaseBody || typeof createCaseBody !== "object") {
|
||||
return respondError(res, {
|
||||
status: 400,
|
||||
code: "CASE_PAYLOAD_REQUIRED",
|
||||
message: "Case payload is required"
|
||||
});
|
||||
}
|
||||
if (typeof contactid !== "string" || contactid.trim().length === 0) {
|
||||
return respondError(res, {
|
||||
status: 400,
|
||||
code: "CONTACT_ID_REQUIRED",
|
||||
message: "contactid is required"
|
||||
});
|
||||
}
|
||||
if (typeof appealTypeId !== "string" || appealTypeId.trim().length === 0) {
|
||||
return respondError(res, {
|
||||
status: 400,
|
||||
code: "APPEAL_TYPE_ID_REQUIRED",
|
||||
message: "appealTypeId is required"
|
||||
});
|
||||
}
|
||||
if (
|
||||
typeof containerName !== "string" ||
|
||||
containerName.trim().length === 0
|
||||
) {
|
||||
return respondError(res, {
|
||||
status: 400,
|
||||
code: "CONTAINER_NAME_REQUIRED",
|
||||
message: "containername is required"
|
||||
});
|
||||
}
|
||||
if (typeof lpaID !== "string" || lpaID.trim().length === 0) {
|
||||
return respondError(res, {
|
||||
status: 400,
|
||||
code: "LPA_ID_REQUIRED",
|
||||
message: "lpaID is required"
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const token = await getToken();
|
||||
|
||||
const data = {
|
||||
"title": "insertion test case",
|
||||
"caseorigincode": 3,
|
||||
"servicestage": 1,
|
||||
"customerid_contact@odata.bind": "/contacts(" + contactid + ")",
|
||||
"pinswg_appealcasetype": appealTypeId,
|
||||
"pinswg_AssociatedLPA@odata.bind": "/accounts(" + lpaID + ")"
|
||||
};
|
||||
const newData = Object.assign(data, createCaseBody);
|
||||
|
||||
delete newData.lpaTypes;
|
||||
delete newData.appealTypes;
|
||||
|
||||
console.log("/////Create Case:\n", newData, "\n//////////////");
|
||||
|
||||
const config = {
|
||||
method: "post",
|
||||
url: WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
|
||||
headers: {
|
||||
"OData-MaxVersion": "4.0",
|
||||
"OData-Version": "4.0",
|
||||
"Accept": "application/json",
|
||||
"Prefer": 'odata.include-annotations="*",return=representation',
|
||||
"Authorization": "Bearer " + token.access_token,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
data: JSON.stringify(newData)
|
||||
};
|
||||
|
||||
const { data: responseData } = await axios(config);
|
||||
await getCaseBlob(
|
||||
containerName,
|
||||
responseData.ticketnumber,
|
||||
createCaseBody
|
||||
);
|
||||
|
||||
return respondSuccess(res, responseData);
|
||||
} catch (error) {
|
||||
consoleLogger(error);
|
||||
return respondError(res, {
|
||||
status: 400,
|
||||
code: "CASE_CREATE_FAILED",
|
||||
message: "Failed to create case"
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,3 @@
|
||||
/**
|
||||
* @swagger
|
||||
* /api/endpoint/createcase_api:
|
||||
* post:
|
||||
* tags: [Case]
|
||||
* description: Create case
|
||||
* summary: Phase 2
|
||||
* parameters:
|
||||
* - name: body
|
||||
* in: requestbody
|
||||
* required: true
|
||||
* example: {}
|
||||
* schema:
|
||||
* type: object
|
||||
* - name: contactid
|
||||
* in: query
|
||||
* required: true
|
||||
* example: pinswg_planningappeals78s
|
||||
* schema:
|
||||
* type: string
|
||||
* - name: appealTypeId
|
||||
* in: query
|
||||
* required: true
|
||||
* example: pinswg_planningappeals78id
|
||||
* schema:
|
||||
* type: string
|
||||
* - name: containername
|
||||
* in: query
|
||||
* required: true
|
||||
* example: pinswg_planningappeals78id
|
||||
* schema:
|
||||
* type: string
|
||||
* - name: lpaID
|
||||
* in: query
|
||||
* required: true
|
||||
* example: pinswg_planningappeals78id
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Success
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import { isNonEmptyString, sanitizeString } from "../../../actions/core/guards";
|
||||
import { consoleLogger } from "../../../actions/core/logger";
|
||||
|
||||
@@ -16,38 +16,73 @@ import { getToken } from "../../../actions/core/token";
|
||||
import { hashAPIPath } from "../../../actions/core/hash";
|
||||
import { respondError, respondSuccess } from "../middleware/apiResponse";
|
||||
import { relayGetData } from "../middleware/relayForwarding";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "../auth/[...nextauth]";
|
||||
import { azureHeaders } from "../../../actions/core/headers";
|
||||
import { escapeODataString } from "../../../actions/core/guards";
|
||||
|
||||
const WEBAPI_URL =
|
||||
process.env.RELAY_ROOT ||
|
||||
"https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/";
|
||||
|
||||
const GUID_PATTERN =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
const recordExists = async (incidentId, contactId, accessToken) => {
|
||||
const filter = `$filter=pinswg_WatchedCase/incidentid eq ${incidentId} and pinswg_Contact/contactid eq ${contactId}`;
|
||||
const queryUrl = `pinswg_watchlists?${filter}`;
|
||||
const queryUrl =
|
||||
`pinswg_watchlists?${filter}` +
|
||||
"&$select=pinswg_watchlistid,_pinswg_contact_value,_pinswg_watchedcase_value,pinswg_representationsubmitted";
|
||||
|
||||
try {
|
||||
const response = await relayGetData({
|
||||
queryUrl,
|
||||
accessToken,
|
||||
requestOptionsBuilder: (token) => ({
|
||||
headers: {
|
||||
Authorization: "Bearer " + token,
|
||||
Accept: "application/json"
|
||||
}
|
||||
})
|
||||
});
|
||||
const response = await relayGetData({
|
||||
queryUrl,
|
||||
accessToken,
|
||||
requestOptionsBuilder: (token) => ({
|
||||
headers: {
|
||||
Authorization: "Bearer " + token,
|
||||
Accept: "application/json"
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (response.data.value && response.data.value.length > 0) {
|
||||
return response.data.value[0]; // return the existing record
|
||||
}
|
||||
const records = response?.data?.value;
|
||||
|
||||
return null;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
if (!Array.isArray(records)) {
|
||||
return {
|
||||
status: "invalid",
|
||||
records: []
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: "found",
|
||||
records
|
||||
};
|
||||
};
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
if (req.method !== "POST") {
|
||||
return respondError(res, {
|
||||
status: 405,
|
||||
code: "METHOD_NOT_ALLOWED",
|
||||
message: "Only POST is supported"
|
||||
});
|
||||
}
|
||||
|
||||
const session = await getServerSession(req, res, authOptions(req, res));
|
||||
const authenticatedEmail = session?.user?.email;
|
||||
|
||||
if (
|
||||
typeof authenticatedEmail !== "string" ||
|
||||
authenticatedEmail.trim().length === 0
|
||||
) {
|
||||
return respondError(res, {
|
||||
status: 401,
|
||||
code: "UNAUTHENTICATED",
|
||||
message: "Authentication required"
|
||||
});
|
||||
}
|
||||
|
||||
const data = req.body;
|
||||
const watchedCaseBind = data?.["pinswg_WatchedCase@odata.bind"];
|
||||
const contactBind = data?.["pinswg_Contact@odata.bind"];
|
||||
@@ -66,8 +101,13 @@ export default async function ApiProxy(req, res) {
|
||||
});
|
||||
}
|
||||
|
||||
const watchedCaseMatch = watchedCaseBind.match(/\(([^)]+)\)/);
|
||||
const contactMatch = contactBind.match(/\(([^)]+)\)/);
|
||||
const watchedCaseMatch = watchedCaseBind.match(
|
||||
/^\/incidents\(([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\)$/i
|
||||
);
|
||||
|
||||
const contactMatch = contactBind.match(
|
||||
/^\/contacts\(([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\)$/i
|
||||
);
|
||||
|
||||
if (!watchedCaseMatch || !contactMatch) {
|
||||
return respondError(res, {
|
||||
@@ -79,26 +119,118 @@ export default async function ApiProxy(req, res) {
|
||||
|
||||
try {
|
||||
const token = await getToken();
|
||||
|
||||
const incidentId = watchedCaseMatch[1];
|
||||
const contactId = contactMatch[1];
|
||||
|
||||
const existingRecord = await recordExists(
|
||||
const escapedEmail = escapeODataString(authenticatedEmail);
|
||||
|
||||
const contactQueryUrl =
|
||||
"contacts?$filter=emailaddress1 eq '" +
|
||||
escapedEmail +
|
||||
"' and statuscode eq 1&$select=contactid";
|
||||
|
||||
const contactResponse = await relayGetData({
|
||||
queryUrl: contactQueryUrl,
|
||||
accessToken: token.access_token,
|
||||
requestOptionsBuilder: (accessToken) => azureHeaders(accessToken)
|
||||
});
|
||||
|
||||
const contacts = contactResponse?.data?.value;
|
||||
|
||||
if (
|
||||
!Array.isArray(contacts) ||
|
||||
contacts.length !== 1 ||
|
||||
typeof contacts[0]?.contactid !== "string" ||
|
||||
!GUID_PATTERN.test(contacts[0].contactid.trim())
|
||||
) {
|
||||
return respondError(res, {
|
||||
status: 403,
|
||||
code: "CONTACT_IDENTITY_NOT_RESOLVED",
|
||||
message: "Request not permitted"
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
contactId.trim().toLowerCase() !==
|
||||
contacts[0].contactid.trim().toLowerCase()
|
||||
) {
|
||||
return respondError(res, {
|
||||
status: 403,
|
||||
code: "CONTACT_OWNERSHIP_MISMATCH",
|
||||
message: "Request not permitted"
|
||||
});
|
||||
}
|
||||
|
||||
const lookup = await recordExists(
|
||||
incidentId,
|
||||
contactId,
|
||||
token.access_token
|
||||
);
|
||||
|
||||
let method, queryUrl;
|
||||
|
||||
if (existingRecord) {
|
||||
method = "patch";
|
||||
queryUrl = `pinswg_watchlists(${existingRecord.pinswg_watchlistid})`;
|
||||
} else {
|
||||
method = "post";
|
||||
queryUrl = "pinswg_watchlists";
|
||||
if (lookup.status === "invalid") {
|
||||
return respondError(res, {
|
||||
status: 500,
|
||||
code: "WATCHLIST_LOOKUP_FAILED",
|
||||
message: "Unable to verify existing watched-case records"
|
||||
});
|
||||
}
|
||||
|
||||
const records = lookup.records;
|
||||
|
||||
const isRepresentationSubmission =
|
||||
data?.pinswg_representationsubmitted != null;
|
||||
|
||||
// Validate every returned record before considering it for an update.
|
||||
const validRecords = records.every((record) => {
|
||||
const recordID = record?.pinswg_watchlistid;
|
||||
const recordContactID = record?._pinswg_contact_value;
|
||||
const recordCaseID = record?._pinswg_watchedcase_value;
|
||||
|
||||
return (
|
||||
typeof recordID === "string" &&
|
||||
GUID_PATTERN.test(recordID) &&
|
||||
typeof recordContactID === "string" &&
|
||||
GUID_PATTERN.test(recordContactID) &&
|
||||
typeof recordCaseID === "string" &&
|
||||
GUID_PATTERN.test(recordCaseID) &&
|
||||
recordContactID.toLowerCase() === contactId.toLowerCase() &&
|
||||
recordCaseID.toLowerCase() === incidentId.toLowerCase()
|
||||
);
|
||||
});
|
||||
|
||||
if (!validRecords) {
|
||||
return respondError(res, {
|
||||
status: 403,
|
||||
code: "WATCHLIST_OWNERSHIP_MISMATCH",
|
||||
message: "Request not permitted"
|
||||
});
|
||||
}
|
||||
|
||||
const watchingOnlyRecords = records.filter(
|
||||
(record) => record.pinswg_representationsubmitted == null
|
||||
);
|
||||
|
||||
let existingRecord;
|
||||
|
||||
if (isRepresentationSubmission) {
|
||||
// Reuse a watching-only record for the first representation.
|
||||
// Never overwrite an earlier representation submission.
|
||||
existingRecord =
|
||||
watchingOnlyRecords.length === 1
|
||||
? watchingOnlyRecords[0]
|
||||
: null;
|
||||
} else {
|
||||
// Watching or subscribing: prefer a watching-only record.
|
||||
// Otherwise reuse an existing representation-associated record.
|
||||
existingRecord = watchingOnlyRecords[0] ?? records[0] ?? null;
|
||||
}
|
||||
|
||||
const method = existingRecord ? "patch" : "post";
|
||||
|
||||
const queryUrl = existingRecord
|
||||
? `pinswg_watchlists(${existingRecord.pinswg_watchlistid})`
|
||||
: "pinswg_watchlists";
|
||||
|
||||
const config = {
|
||||
method: method,
|
||||
url: WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
|
||||
@@ -110,7 +242,10 @@ export default async function ApiProxy(req, res) {
|
||||
Authorization: "Bearer " + token.access_token,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
data: JSON.stringify(data)
|
||||
data: JSON.stringify({
|
||||
...data,
|
||||
"pinswg_Contact@odata.bind": `/contacts(${contacts[0].contactid.trim()})`
|
||||
})
|
||||
};
|
||||
|
||||
const result = await axios(config);
|
||||
|
||||
@@ -39,6 +39,7 @@ ApiProxy.get(async (req, res) => {
|
||||
const allowedPrefix = [
|
||||
"/api/endpoint/getportallogin_api",
|
||||
"/api/endpoint/deletewatchedcases_api",
|
||||
"/api/subscriptions/delete-watched-case",
|
||||
"/api/file/upload",
|
||||
"/api/file/uploadsinglefile",
|
||||
"/api/file/deleteblobcase",
|
||||
@@ -53,7 +54,7 @@ ApiProxy.get(async (req, res) => {
|
||||
typeof queryPath !== "string" ||
|
||||
queryPath.length === 0 ||
|
||||
!queryPath.startsWith("/api/") ||
|
||||
!allowedPrefix.some((prefix) => queryPath.startsWith(prefix)) ||
|
||||
!allowedPrefix.includes(queryPath) ||
|
||||
queryPath.includes("/api/endpoint/gethash_api")
|
||||
) {
|
||||
return respondError(res, {
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
/**
|
||||
* @swagger
|
||||
* /api/endpoint/getlogin_api:
|
||||
* get:
|
||||
* tags: [Login]
|
||||
* description: Returns the hello world
|
||||
* summary: Phase 2
|
||||
* parameters:
|
||||
* - name: emailaddress
|
||||
* in: query
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* - name: pwd
|
||||
* in: query
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: hello world
|
||||
*/
|
||||
|
||||
import { azureHeadersPaged } from "../../../actions/core/headers";
|
||||
import { respondError } from "../middleware/apiResponse";
|
||||
import { relayGet } from "../middleware/relayForwarding";
|
||||
import { RELAY_POLICY_STRICT_LOGIN } from "../middleware/relayPolicyPresets";
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
const emailAddress = req.query.emailAddress;
|
||||
const pwd = req.query.pwd;
|
||||
|
||||
if (typeof emailAddress !== "string" || emailAddress.trim().length === 0) {
|
||||
return respondError(res, {
|
||||
status: 400,
|
||||
code: "EMAIL_ADDRESS_REQUIRED",
|
||||
message: "emailAddress is required"
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof pwd !== "string" || pwd.trim().length === 0) {
|
||||
return respondError(res, {
|
||||
status: 400,
|
||||
code: "PASSWORD_REQUIRED",
|
||||
message: "pwd is required"
|
||||
});
|
||||
}
|
||||
|
||||
const queryUrl =
|
||||
"contacts?$filter=emailaddress1 eq '" +
|
||||
emailAddress +
|
||||
"' and pinswg_custom_password eq '" +
|
||||
pwd +
|
||||
"'&$count=true&$select=emailaddress1,contactid,pinswg_custom_password,yomifullname,firstname,lastname";
|
||||
|
||||
const relayPolicy = RELAY_POLICY_STRICT_LOGIN;
|
||||
|
||||
return relayGet({
|
||||
queryUrl,
|
||||
res,
|
||||
requestOptionsBuilder: (accessToken) => azureHeadersPaged(accessToken),
|
||||
relayPolicy,
|
||||
errorResponse: {
|
||||
status: 400,
|
||||
code: "LOGIN_FETCH_FAILED",
|
||||
message: "Failed to fetch login details"
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -17,61 +17,20 @@
|
||||
*/
|
||||
|
||||
import { azureHeadersPaged } from "../../../actions/core/headers";
|
||||
import { escapeODataString } from "../../../actions/core/guards";
|
||||
import { hashAPIPath } from "../../../actions/core/hash";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "../auth/[...nextauth]";
|
||||
import { respondError } from "../middleware/apiResponse";
|
||||
import { relayGet } from "../middleware/relayForwarding";
|
||||
import { RELAY_POLICY_STRICT_LOGIN } from "../middleware/relayPolicyPresets";
|
||||
|
||||
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+$/;
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
if (req.method !== "GET") {
|
||||
return respondError(res, {
|
||||
status: 405,
|
||||
code: "METHOD_NOT_ALLOWED",
|
||||
message: "Only GET is supported"
|
||||
});
|
||||
}
|
||||
|
||||
const session = await getServerSession(req, res, authOptions(req, res));
|
||||
const authenticatedEmail = session?.user?.email;
|
||||
|
||||
if (
|
||||
typeof authenticatedEmail !== "string" ||
|
||||
!EMAIL_PATTERN.test(authenticatedEmail.trim())
|
||||
) {
|
||||
return respondError(res, {
|
||||
status: 401,
|
||||
code: "UNAUTHENTICATED",
|
||||
message: "Authentication required"
|
||||
});
|
||||
}
|
||||
|
||||
const emailAddress = req.query.emailAddress;
|
||||
const checkHash = req.query.hash;
|
||||
|
||||
if (
|
||||
typeof emailAddress !== "string" ||
|
||||
!EMAIL_PATTERN.test(emailAddress.trim())
|
||||
) {
|
||||
if (typeof emailAddress !== "string" || emailAddress.trim().length === 0) {
|
||||
return respondError(res, {
|
||||
status: 400,
|
||||
code: "INVALID_EMAIL_ADDRESS",
|
||||
message: "emailAddress must be a valid email address"
|
||||
});
|
||||
}
|
||||
|
||||
const normalizedAuthenticatedEmail = authenticatedEmail.trim().toLowerCase();
|
||||
const normalizedRequestedEmail = emailAddress.trim().toLowerCase();
|
||||
|
||||
if (normalizedRequestedEmail !== normalizedAuthenticatedEmail) {
|
||||
return respondError(res, {
|
||||
status: 403,
|
||||
code: "FORBIDDEN",
|
||||
message: "Request not permitted"
|
||||
code: "EMAIL_ADDRESS_REQUIRED",
|
||||
message: "emailAddress is required"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -104,7 +63,7 @@ export default async function ApiProxy(req, res) {
|
||||
|
||||
const queryUrl =
|
||||
"contacts?$filter=emailaddress1 eq '" +
|
||||
escapeODataString(normalizedAuthenticatedEmail) +
|
||||
emailAddress +
|
||||
"' and statuscode eq 1&$count=true&$select=pinswg_preferredlanguage,emailaddress1,contactid,yomifullname,firstname,lastname";
|
||||
|
||||
const relayPolicy = RELAY_POLICY_STRICT_LOGIN;
|
||||
|
||||
@@ -1,37 +1,68 @@
|
||||
/**
|
||||
* @swagger
|
||||
* /api/endpoint/updateaccount_api:
|
||||
* patch:
|
||||
* tags: [Account]
|
||||
* description: Update account
|
||||
* summary: Phase 2
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Success
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import { consoleLogger } from "../../../actions/core/logger";
|
||||
import { getToken } from "../../../actions/core/token";
|
||||
import { hashAPIPath } from "../../../actions/core/hash";
|
||||
import { respondError, respondSuccess } from "../middleware/apiResponse";
|
||||
import { updateCrmContact } from "../../../lib/server/updateCrmContact";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "../auth/[...nextauth]";
|
||||
import { getToken } from "../../../actions/core/token";
|
||||
import { relayGetData } from "../middleware/relayForwarding";
|
||||
import { azureHeaders } from "../../../actions/core/headers";
|
||||
import { escapeODataString } from "../../../actions/core/guards";
|
||||
|
||||
const WEBAPI_URL =
|
||||
process.env.RELAY_ROOT ||
|
||||
"https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/";
|
||||
const GUID_PATTERN =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
const ALLOWED_ACCOUNT_FIELDS = new Set([
|
||||
"pinswg_preferredlanguage",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"telephone1",
|
||||
"pinswg_companyname",
|
||||
"address1_line1",
|
||||
"address1_line2",
|
||||
"address1_city",
|
||||
"address1_county",
|
||||
"address1_postalcode"
|
||||
]);
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
const contactId = req.query.contactId;
|
||||
|
||||
if (typeof contactId !== "string" || contactId.trim().length === 0) {
|
||||
if (req.method !== "POST") {
|
||||
res.setHeader("Allow", "POST");
|
||||
return respondError(res, {
|
||||
status: 400,
|
||||
code: "CONTACT_ID_REQUIRED",
|
||||
message: "contactId is required"
|
||||
status: 405,
|
||||
code: "METHOD_NOT_ALLOWED",
|
||||
message: "Method not allowed"
|
||||
});
|
||||
}
|
||||
|
||||
if (!req.body || Object.keys(req.body).length === 0) {
|
||||
const session = await getServerSession(req, res, authOptions(req, res));
|
||||
|
||||
if (
|
||||
typeof session?.user?.email !== "string" ||
|
||||
session.user.email.trim().length === 0
|
||||
) {
|
||||
return respondError(res, {
|
||||
status: 401,
|
||||
code: "UNAUTHENTICATED",
|
||||
message: "Authentication required"
|
||||
});
|
||||
}
|
||||
|
||||
const contactId = req.query.contactId;
|
||||
|
||||
if (typeof contactId !== "string" || !GUID_PATTERN.test(contactId.trim())) {
|
||||
return respondError(res, {
|
||||
status: 400,
|
||||
code: "INVALID_CONTACT_ID",
|
||||
message: "A valid contactId is required"
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!req.body ||
|
||||
typeof req.body !== "object" ||
|
||||
Array.isArray(req.body) ||
|
||||
Object.keys(req.body).length === 0
|
||||
) {
|
||||
return respondError(res, {
|
||||
status: 400,
|
||||
code: "ACCOUNT_UPDATE_PAYLOAD_REQUIRED",
|
||||
@@ -41,25 +72,62 @@ export default async function ApiProxy(req, res) {
|
||||
|
||||
try {
|
||||
const token = await getToken();
|
||||
const data = JSON.stringify(req.body);
|
||||
const queryUrl = "contacts(" + contactId + ")";
|
||||
|
||||
const config = {
|
||||
method: "patch",
|
||||
url: WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
|
||||
headers: {
|
||||
"OData-MaxVersion": "4.0",
|
||||
"OData-Version": "4.0",
|
||||
Accept: "application/json",
|
||||
Prefer: 'odata.include-annotations="*",return=representation',
|
||||
Authorization: "Bearer " + token.access_token,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
data
|
||||
};
|
||||
const escapedEmail = escapeODataString(session.user.email);
|
||||
|
||||
const response = await axios(config);
|
||||
return respondSuccess(res, response.data);
|
||||
const contactQueryUrl =
|
||||
"contacts?$filter=emailaddress1 eq '" +
|
||||
escapedEmail +
|
||||
"' and statuscode eq 1&$select=contactid";
|
||||
|
||||
const contactResponse = await relayGetData({
|
||||
queryUrl: contactQueryUrl,
|
||||
accessToken: token.access_token,
|
||||
requestOptionsBuilder: (accessToken) => azureHeaders(accessToken)
|
||||
});
|
||||
|
||||
const contacts = contactResponse?.data?.value;
|
||||
|
||||
if (
|
||||
!Array.isArray(contacts) ||
|
||||
contacts.length !== 1 ||
|
||||
typeof contacts[0]?.contactid !== "string" ||
|
||||
!GUID_PATTERN.test(contacts[0].contactid.trim())
|
||||
) {
|
||||
return respondError(res, {
|
||||
status: 403,
|
||||
code: "CONTACT_IDENTITY_NOT_RESOLVED",
|
||||
message: "Request not permitted"
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
contactId.trim().toLowerCase() !==
|
||||
contacts[0].contactid.trim().toLowerCase()
|
||||
) {
|
||||
return respondError(res, {
|
||||
status: 403,
|
||||
code: "CONTACT_OWNERSHIP_MISMATCH",
|
||||
message: "Request not permitted"
|
||||
});
|
||||
}
|
||||
|
||||
const updatePayload = Object.fromEntries(
|
||||
Object.entries(req.body).filter(([field]) =>
|
||||
ALLOWED_ACCOUNT_FIELDS.has(field)
|
||||
)
|
||||
);
|
||||
|
||||
if (Object.keys(updatePayload).length === 0) {
|
||||
return respondError(res, {
|
||||
status: 400,
|
||||
code: "NO_ALLOWED_ACCOUNT_FIELDS",
|
||||
message: "No permitted account fields supplied"
|
||||
});
|
||||
}
|
||||
|
||||
const data = await updateCrmContact(contactId.trim(), updatePayload);
|
||||
return respondSuccess(res, data);
|
||||
} catch (error) {
|
||||
consoleLogger(error);
|
||||
return respondError(res, {
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* @swagger
|
||||
* /api/endpoint/updatepassword_api:
|
||||
* patch:
|
||||
* tags: [Account]
|
||||
* description: Update password
|
||||
* summary: Phase 2
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Success
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import { consoleLogger } from "../../../actions/core/logger";
|
||||
import { getToken } from "../../../actions/core/token";
|
||||
import { hashAPIPath } from "../../../actions/core/hash";
|
||||
import { respondError, respondSuccess } from "../middleware/apiResponse";
|
||||
|
||||
const WEBAPI_URL =
|
||||
process.env.RELAY_ROOT ||
|
||||
"https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/";
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
const contactId = req.query.contactId;
|
||||
|
||||
if (typeof contactId !== "string" || contactId.trim().length === 0) {
|
||||
return respondError(res, {
|
||||
status: 400,
|
||||
code: "CONTACT_ID_REQUIRED",
|
||||
message: "contactId is required"
|
||||
});
|
||||
}
|
||||
|
||||
if (!req.body || Object.keys(req.body).length === 0) {
|
||||
return respondError(res, {
|
||||
status: 400,
|
||||
code: "PASSWORD_UPDATE_PAYLOAD_REQUIRED",
|
||||
message: "Password update payload is required"
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const token = await getToken();
|
||||
const data = JSON.stringify(req.body);
|
||||
const queryUrl = "contacts(" + contactId + ")";
|
||||
|
||||
const config = {
|
||||
method: "patch",
|
||||
url: WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
|
||||
headers: {
|
||||
"OData-MaxVersion": "4.0",
|
||||
"OData-Version": "4.0",
|
||||
Accept: "application/json",
|
||||
Prefer: 'odata.include-annotations="*",return=representation',
|
||||
Authorization: "Bearer " + token.access_token,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
data
|
||||
};
|
||||
|
||||
const response = await axios(config);
|
||||
return respondSuccess(res, response.data);
|
||||
} catch (error) {
|
||||
consoleLogger(error);
|
||||
return respondError(res, {
|
||||
status: 400,
|
||||
code: "PASSWORD_UPDATE_FAILED",
|
||||
message: "Failed to update password"
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { updateAccount } from "../../../actions/services/accountService";
|
||||
import { updateCrmContact } from "../../../lib/server/updateCrmContact";
|
||||
import { hashAPIPath } from "../../../actions/core/hash";
|
||||
import { consoleLogger } from "../../../actions/core/logger";
|
||||
import {
|
||||
@@ -59,10 +59,7 @@ ApiProxy.get(async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!isValidContainer(containerName) ||
|
||||
!isValidTempCaseRef(tempCaseRef)
|
||||
) {
|
||||
if (!isValidContainer(containerName) || !isValidTempCaseRef(tempCaseRef)) {
|
||||
return respondError(res, {
|
||||
status: 400,
|
||||
code: "INVALID_REQUEST",
|
||||
@@ -158,13 +155,9 @@ ApiProxy.get(async (req, res) => {
|
||||
if (typeofinvolvement != 846040000) {
|
||||
void (async () => {
|
||||
try {
|
||||
await updateAccount(
|
||||
contactId[1],
|
||||
{
|
||||
pinswg_typeofinvolvement: 846040001
|
||||
},
|
||||
true
|
||||
);
|
||||
await updateCrmContact(contactId[1], {
|
||||
pinswg_typeofinvolvement: 846040001
|
||||
});
|
||||
} catch (error) {
|
||||
consoleLogger(error);
|
||||
}
|
||||
|
||||
@@ -222,9 +222,11 @@ export default async function handler(req, res) {
|
||||
if (
|
||||
!isValidContainer(containerID) ||
|
||||
!isValidDraftFolder(caseRef) ||
|
||||
(Object.prototype.hasOwnProperty.call(reqBodyobj, "ticketnumber") &&
|
||||
Object.prototype.hasOwnProperty.call(reqBodyobj, "caseRef") &&
|
||||
reqBodyobj.ticketnumber !== reqBodyobj.caseRef) ||
|
||||
["ticketnumber", "caseRef"].some(
|
||||
(key) =>
|
||||
Object.prototype.hasOwnProperty.call(reqBodyobj, key) &&
|
||||
!isValidDraftFolder(reqBodyobj[key])
|
||||
) ||
|
||||
(typeof req.query.download !== "undefined" &&
|
||||
typeof req.query.download !== "string") ||
|
||||
[
|
||||
|
||||
@@ -38,10 +38,23 @@ import middleware from "../middleware/middleware";
|
||||
import { hashAPIPath } from "../../../actions/core/hash";
|
||||
import { consoleLogger } from "../../../actions/core/logger";
|
||||
import { respondError, respondSuccess } from "../middleware/apiResponse";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "../auth/[...nextauth]";
|
||||
import {
|
||||
readAdminSessionCookie,
|
||||
validateAdminSession
|
||||
} from "../../../lib/admin/adminAuth";
|
||||
|
||||
const ApiProxy = nextConnect();
|
||||
ApiProxy.use(middleware);
|
||||
|
||||
const forbidden = (res) =>
|
||||
respondError(res, {
|
||||
status: 403,
|
||||
code: "FORBIDDEN",
|
||||
message: "Request not permitted"
|
||||
});
|
||||
|
||||
ApiProxy.get(async (req, res) => {
|
||||
const containerName = req.query.container;
|
||||
const checkHash = req.query.hash;
|
||||
@@ -59,6 +72,17 @@ ApiProxy.get(async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
const session = await getServerSession(req, res, authOptions(req, res));
|
||||
const isPortalOwner =
|
||||
typeof session?.user?.id === "string" &&
|
||||
session.user.id.length > 0 &&
|
||||
session.user.id === containerName;
|
||||
const isAdministrator = validateAdminSession(readAdminSessionCookie(req));
|
||||
|
||||
if (!isPortalOwner && !isAdministrator) {
|
||||
return forbidden(res);
|
||||
}
|
||||
|
||||
const hashCandidatePaths = [
|
||||
"/api/file/getrepsblob?container=" + containerName,
|
||||
"/api/file/getrepsblob?container=" + encodeURIComponent(containerName)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { BASE_URL } from "../../../actions/core/env";
|
||||
import { getToken } from "../../../actions/core/token";
|
||||
import { azureHeaders } from "../../../actions/core/headers";
|
||||
import { consoleLogger } from "../../../actions/core/logger";
|
||||
@@ -7,7 +8,16 @@ import { respondError, respondSuccess } from "../middleware/apiResponse";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "../auth/[...nextauth]";
|
||||
|
||||
const BASE_URL = process.env.API_ROOT || `http://localhost:${port}`;
|
||||
const isTrustedInternalDestination = () => {
|
||||
try {
|
||||
return (
|
||||
new URL(BASE_URL).origin ===
|
||||
new URL(process.env.NEXTAUTH_URL).origin
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const forbidden = (res) =>
|
||||
respondError(res, {
|
||||
@@ -60,15 +70,28 @@ export default async function ApiProxy(req, res) {
|
||||
return forbidden(res);
|
||||
}
|
||||
|
||||
if (!isTrustedInternalDestination()) {
|
||||
return forbidden(res);
|
||||
}
|
||||
|
||||
const token = await getToken();
|
||||
|
||||
const queryUrl =
|
||||
"/api/file/getrepsblob?container=" + encodeURIComponent(containerName);
|
||||
|
||||
try {
|
||||
const config = azureHeaders(token.access_token);
|
||||
|
||||
const { data } = await axios.get(
|
||||
BASE_URL + queryUrl + hashAPIPath(queryUrl),
|
||||
azureHeaders(token.access_token)
|
||||
{
|
||||
...config,
|
||||
headers: {
|
||||
...config.headers,
|
||||
Cookie: req.headers.cookie || ""
|
||||
},
|
||||
maxRedirects: 0
|
||||
}
|
||||
);
|
||||
return respondSuccess(res, data);
|
||||
} catch (error) {
|
||||
|
||||
@@ -304,7 +304,8 @@ export const getServerSideProps = wrapper.getServerSideProps(
|
||||
break;
|
||||
case "myRepresentations":
|
||||
const myRepresentations = await getRepsFromBlob(
|
||||
thisSession.user.id
|
||||
thisSession.user.id,
|
||||
{ headers: { Cookie: req.headers.cookie || "" } }
|
||||
);
|
||||
|
||||
const myRepresentationsDetails = await getDetails(
|
||||
|
||||
+23
-7
@@ -7,10 +7,8 @@ import pLimit from "p-limit";
|
||||
|
||||
import { getIP } from "../../actions/core/logger";
|
||||
import { consoleLogger } from "../../actions/core/logger";
|
||||
import {
|
||||
getPersonalAccount,
|
||||
getPortalLogin
|
||||
} from "../../actions/services/accountService";
|
||||
import { getPortalLogin } from "../../actions/services/accountService";
|
||||
import { getCrmPersonalAccount } from "../../lib/server/getCrmPersonalAccount";
|
||||
import {
|
||||
getCase,
|
||||
getPortalModuleDetails
|
||||
@@ -257,9 +255,25 @@ export const getServerSideProps = wrapper.getServerSideProps(
|
||||
}
|
||||
|
||||
await createContainerProxy(thisSession.user.id);
|
||||
const accountDetails = await getPersonalAccount(loggedInUser);
|
||||
|
||||
if (accountDetails?.errorCode) {
|
||||
let accountDetails;
|
||||
|
||||
try {
|
||||
accountDetails = await getCrmPersonalAccount(loggedInUser);
|
||||
} catch (error) {
|
||||
consoleLogger({
|
||||
name: "MyPortalIndexAccountLookup",
|
||||
reasonCode: "UPSTREAM_FAILURE",
|
||||
message: "myportal index personal account lookup failed",
|
||||
error: error?.message
|
||||
});
|
||||
|
||||
return {
|
||||
redirect: { destination: "/myportal/error", permanent: false }
|
||||
};
|
||||
}
|
||||
|
||||
if (!accountDetails || accountDetails.errorCode) {
|
||||
return {
|
||||
redirect: { destination: "/myportal/error", permanent: false }
|
||||
};
|
||||
@@ -279,7 +293,9 @@ export const getServerSideProps = wrapper.getServerSideProps(
|
||||
awaitingSubmissionFromBlob
|
||||
] = await Promise.all([
|
||||
isLPA ? getMyLPACases(lpaId) : getMyCases(loggedInUser),
|
||||
getRepsFromBlob(thisSession.user.id),
|
||||
getRepsFromBlob(thisSession.user.id, {
|
||||
headers: { Cookie: req.headers.cookie || "" }
|
||||
}),
|
||||
getWatchedCases(loggedInUser),
|
||||
getAwaitingSubmissionFromBlob(thisSession.user.id)
|
||||
]);
|
||||
|
||||
@@ -14,10 +14,9 @@ import ServiceBanner from "../../components/myportal/servicebanner";
|
||||
import { getSession } from "next-auth/react";
|
||||
import pLimit from "p-limit";
|
||||
|
||||
import {
|
||||
getPersonalAccount,
|
||||
getPortalLogin
|
||||
} from "../../actions/services/accountService";
|
||||
import { getPortalLogin } from "../../actions/services/accountService";
|
||||
import { getCrmPersonalAccount } from "../../lib/server/getCrmPersonalAccount";
|
||||
import { consoleLogger } from "../../actions/core/logger";
|
||||
|
||||
import { getPortalModuleDetails } from "../../actions/services/caseService";
|
||||
|
||||
@@ -189,7 +188,25 @@ export const getServerSideProps = wrapper.getServerSideProps(
|
||||
};
|
||||
}
|
||||
|
||||
const accountDetails = await getPersonalAccount(loggedInUser);
|
||||
let accountDetails;
|
||||
|
||||
try {
|
||||
accountDetails = await getCrmPersonalAccount(loggedInUser);
|
||||
} catch (error) {
|
||||
consoleLogger({
|
||||
name: "MyPortalViewAllAccountLookup",
|
||||
reasonCode: "UPSTREAM_FAILURE",
|
||||
message: "myportal viewall personal account lookup failed",
|
||||
error: error?.message
|
||||
});
|
||||
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/myportal/error",
|
||||
permanent: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!accountDetails || accountDetails.errorCode) {
|
||||
return {
|
||||
@@ -354,7 +371,8 @@ export const getServerSideProps = wrapper.getServerSideProps(
|
||||
|
||||
case "myRepresentations": {
|
||||
const myRepresentations = await getRepsFromBlob(
|
||||
thisSession.user.id
|
||||
thisSession.user.id,
|
||||
{ headers: { Cookie: ctx.req?.headers?.cookie || "" } }
|
||||
);
|
||||
|
||||
// The existing View All component reads
|
||||
|
||||
@@ -155,7 +155,7 @@ test("createappealcompletemessage_api rejects missing hash with 400", async () =
|
||||
createBlob: async () => ({}),
|
||||
getCaseBlob: async () => ({}),
|
||||
createCaseCompleteMessage: async () => ({}),
|
||||
updateAccount: async () => ({}),
|
||||
updateCrmContact: async () => ({}),
|
||||
consoleLogger: () => {},
|
||||
_: { isEmpty: (value) => !value },
|
||||
nextConnect,
|
||||
|
||||
@@ -34,6 +34,8 @@ const loadModule = (relativePath, injected = {}) => {
|
||||
source += `\nif (typeof ${name} !== "undefined" && !module.exports.default) module.exports.default = ${name};\n`;
|
||||
});
|
||||
|
||||
source +=
|
||||
'\nif (typeof getCrmPersonalAccount !== "undefined") module.exports.getCrmPersonalAccount = getCrmPersonalAccount;\n';
|
||||
source +=
|
||||
'\nif (typeof respondSuccess !== "undefined") module.exports.respondSuccess = respondSuccess;\n';
|
||||
source +=
|
||||
@@ -48,6 +50,7 @@ const loadModule = (relativePath, injected = {}) => {
|
||||
exports: {},
|
||||
require,
|
||||
process,
|
||||
URL,
|
||||
console: {
|
||||
log: () => {},
|
||||
info: () => {},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -149,7 +149,9 @@ const assertNoUploadWrites = (calls) => {
|
||||
};
|
||||
|
||||
test("upload rejects a missing NextAuth session before HMAC or Azure write", async () => {
|
||||
const loaded = loadUploadAuthorization({ getServerSession: async () => null });
|
||||
const loaded = loadUploadAuthorization({
|
||||
getServerSession: async () => null
|
||||
});
|
||||
const res = createRes();
|
||||
|
||||
await loaded.mod.default.handler(uploadAuthorizationRequest(), res);
|
||||
@@ -906,9 +908,15 @@ for (const inputCase of [
|
||||
{ name: "missing container", body: { container: undefined } },
|
||||
{ name: "malformed container", body: { container: "Invalid_Container" } },
|
||||
{ name: "missing case identifier", body: { casefolderID: undefined } },
|
||||
{ name: "case identifier path separator", body: { casefolderID: "TMP/CASE-1" } },
|
||||
{
|
||||
name: "case identifier path separator",
|
||||
body: { casefolderID: "TMP/CASE-1" }
|
||||
},
|
||||
{ name: "case identifier traversal segment", body: { casefolderID: ".." } },
|
||||
{ name: "case identifier control character", body: { casefolderID: "TMP\u0000CASE" } }
|
||||
{
|
||||
name: "case identifier control character",
|
||||
body: { casefolderID: "TMP\u0000CASE" }
|
||||
}
|
||||
]) {
|
||||
test(`admin case deletion rejects ${inputCase.name} without deletion`, async () => {
|
||||
const loaded = loadAdminCaseDeletion();
|
||||
@@ -917,7 +925,8 @@ for (const inputCase of [
|
||||
|
||||
request.body = { ...request.body, ...inputCase.body };
|
||||
Object.keys(request.body).forEach((key) => {
|
||||
if (typeof request.body[key] === "undefined") delete request.body[key];
|
||||
if (typeof request.body[key] === "undefined")
|
||||
delete request.body[key];
|
||||
});
|
||||
await loaded.mod.default(request, res);
|
||||
|
||||
@@ -938,10 +947,9 @@ test("valid administrator can delete a cross-container case prefix with the exis
|
||||
assert.deepStrictEqual(loaded.calls.deletion, [
|
||||
[request.body.container, request.body.casefolderID]
|
||||
]);
|
||||
assert.deepStrictEqual(
|
||||
JSON.parse(JSON.stringify(res.state.jsonBody)),
|
||||
{ data: loaded.payload }
|
||||
);
|
||||
assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), {
|
||||
data: loaded.payload
|
||||
});
|
||||
});
|
||||
|
||||
test("admin case deletion dependency failure returns DELETE_BLOB_CASE_FAILED", async () => {
|
||||
@@ -1166,7 +1174,10 @@ test("valid administrative representation JSON edit preserves the existing mutat
|
||||
const updatedJson = JSON.parse(loaded.uploaded.content);
|
||||
assert.strictEqual(Object.hasOwn(updatedJson, "repComplete"), false);
|
||||
assert.strictEqual(updatedJson.containerID, loaded.sourceJson.containerID);
|
||||
assert.strictEqual(updatedJson.ticketnumber, loaded.sourceJson.ticketnumber);
|
||||
assert.strictEqual(
|
||||
updatedJson.ticketnumber,
|
||||
loaded.sourceJson.ticketnumber
|
||||
);
|
||||
assert.strictEqual(
|
||||
updatedJson.representationType,
|
||||
loaded.sourceJson.representationType
|
||||
@@ -1234,9 +1245,7 @@ test("deleteawaitingsubmissionfromblob accepts legacy deleteblob hash variant",
|
||||
getServerSession: async () => ({ user: { id: "container-1" } }),
|
||||
authOptions: () => ({}),
|
||||
hashAPIPath: (queryPath) =>
|
||||
queryPath.includes(
|
||||
"/api/file/deleteblob?container=container-1"
|
||||
)
|
||||
queryPath.includes("/api/file/deleteblob?container=container-1")
|
||||
? "&hash=expected"
|
||||
: "&hash=other",
|
||||
respondError: respondErrorMock,
|
||||
@@ -1545,6 +1554,10 @@ test("getrepsblob handler success returns representations payload", async () =>
|
||||
respondError: respondErrorMock,
|
||||
respondSuccess: respondSuccessMock,
|
||||
consoleLogger: () => {},
|
||||
getServerSession: async () => ({ user: { id: "c1" } }),
|
||||
authOptions: () => ({}),
|
||||
readAdminSessionCookie: () => null,
|
||||
validateAdminSession: () => false,
|
||||
getRepsBlobs: async () => [
|
||||
{ path: "c/ref_rep.json", name: "ref_rep.json" }
|
||||
],
|
||||
@@ -1603,6 +1616,10 @@ test("getrepsblob handler rejects invalid HMAC before Azure access", async () =>
|
||||
respondError: respondErrorMock,
|
||||
respondSuccess: respondSuccessMock,
|
||||
consoleLogger: () => {},
|
||||
getServerSession: async () => ({ user: { id: "c1" } }),
|
||||
authOptions: () => ({}),
|
||||
readAdminSessionCookie: () => null,
|
||||
validateAdminSession: () => false,
|
||||
getRepsBlobs: async () => {
|
||||
azureCalls += 1;
|
||||
return [];
|
||||
@@ -1692,7 +1709,12 @@ const appealCompletionSigningRequest = (pathValue) => ({
|
||||
});
|
||||
|
||||
for (const sessionCase of [
|
||||
{ name: "missing session", session: null, status: 401, code: "UNAUTHENTICATED" },
|
||||
{
|
||||
name: "missing session",
|
||||
session: null,
|
||||
status: 401,
|
||||
code: "UNAUTHENTICATED"
|
||||
},
|
||||
{ name: "session without user id", session: { user: {} } },
|
||||
{ name: "session with empty user id", session: { user: { id: "" } } },
|
||||
{
|
||||
@@ -1705,7 +1727,9 @@ for (const sessionCase of [
|
||||
}
|
||||
]) {
|
||||
test(`gethash rejects appeal completion signing for ${sessionCase.name} before HMAC generation`, async () => {
|
||||
const loaded = loadAppealCompletionSigning({ session: sessionCase.session });
|
||||
const loaded = loadAppealCompletionSigning({
|
||||
session: sessionCase.session
|
||||
});
|
||||
const res = createRes();
|
||||
|
||||
await loaded.mod.default.handler(appealCompletionSigningRequest(), res);
|
||||
@@ -1801,6 +1825,10 @@ test("getrepsblob handler dependency failure returns GET_REPS_BLOB_FAILED", asyn
|
||||
respondError: respondErrorMock,
|
||||
respondSuccess: respondSuccessMock,
|
||||
consoleLogger: () => {},
|
||||
getServerSession: async () => ({ user: { id: "c1" } }),
|
||||
authOptions: () => ({}),
|
||||
readAdminSessionCookie: () => null,
|
||||
validateAdminSession: () => false,
|
||||
getRepsBlobs: async () => {
|
||||
throw new Error("storage failure");
|
||||
},
|
||||
@@ -1828,6 +1856,10 @@ test("getrepsblob handler accepts encoded container hash variant", async () => {
|
||||
respondError: respondErrorMock,
|
||||
respondSuccess: respondSuccessMock,
|
||||
consoleLogger: () => {},
|
||||
getServerSession: async () => ({ user: { id: "cont/1" } }),
|
||||
authOptions: () => ({}),
|
||||
readAdminSessionCookie: () => null,
|
||||
validateAdminSession: () => false,
|
||||
getRepsBlobs: async () => [{ path: "cont/rep1.json" }],
|
||||
downloadAllRepsFiles: async () => ({ value: [{ id: "r-encoded" }] })
|
||||
});
|
||||
@@ -2256,14 +2288,29 @@ test("createrepcompletemessage rejects unsupported methods before authorization
|
||||
|
||||
for (const inputCase of [
|
||||
{ name: "missing container", query: { container: undefined } },
|
||||
{ name: "invalid container type", query: { container: ["user-container"] } },
|
||||
{
|
||||
name: "invalid container type",
|
||||
query: { container: ["user-container"] }
|
||||
},
|
||||
{ name: "malformed container", query: { container: "Invalid_Container" } },
|
||||
{ name: "missing case reference", query: { tempcaseref: undefined } },
|
||||
{ name: "unsafe case-reference path separator", query: { tempcaseref: "CAS/123" } },
|
||||
{ name: "unsafe case-reference traversal segment", query: { tempcaseref: ".." } },
|
||||
{
|
||||
name: "unsafe case-reference path separator",
|
||||
query: { tempcaseref: "CAS/123" }
|
||||
},
|
||||
{
|
||||
name: "unsafe case-reference traversal segment",
|
||||
query: { tempcaseref: ".." }
|
||||
},
|
||||
{ name: "missing representation identifier", query: { repid: undefined } },
|
||||
{ name: "unsafe representation path separator", query: { repid: "rep\\file" } },
|
||||
{ name: "unsafe representation control character", query: { repid: "rep\u0000file" } }
|
||||
{
|
||||
name: "unsafe representation path separator",
|
||||
query: { repid: "rep\\file" }
|
||||
},
|
||||
{
|
||||
name: "unsafe representation control character",
|
||||
query: { repid: "rep\u0000file" }
|
||||
}
|
||||
]) {
|
||||
test(`createrepcompletemessage rejects ${inputCase.name} before authorization work`, async () => {
|
||||
const loaded = loadOrdinaryRepCompletion();
|
||||
@@ -2272,7 +2319,8 @@ for (const inputCase of [
|
||||
|
||||
request.query = { ...request.query, ...inputCase.query };
|
||||
Object.keys(request.query).forEach((key) => {
|
||||
if (typeof request.query[key] === "undefined") delete request.query[key];
|
||||
if (typeof request.query[key] === "undefined")
|
||||
delete request.query[key];
|
||||
});
|
||||
await loaded.mod.default(request, res);
|
||||
|
||||
@@ -2416,7 +2464,13 @@ test("createrepcompletemessage dependency failure returns CREATE_REP_COMPLETE_ME
|
||||
});
|
||||
|
||||
const loadAdminRepCompletion = (overrides = {}) => {
|
||||
const calls = { noStore: 0, readSession: 0, validate: 0, origin: 0, queue: [] };
|
||||
const calls = {
|
||||
noStore: 0,
|
||||
readSession: 0,
|
||||
validate: 0,
|
||||
origin: 0,
|
||||
queue: []
|
||||
};
|
||||
const payload = { messageId: "admin-message" };
|
||||
const mod = loadModule("pages/api/admin/createrepcompletemessage.js", {
|
||||
respondError: respondErrorMock,
|
||||
@@ -2495,7 +2549,10 @@ for (const sessionCase of [
|
||||
}
|
||||
|
||||
for (const originCase of [
|
||||
{ name: "missing Origin", headers: { cookie: "pedw_admin_session=admin-token" } },
|
||||
{
|
||||
name: "missing Origin",
|
||||
headers: { cookie: "pedw_admin_session=admin-token" }
|
||||
},
|
||||
{
|
||||
name: "cross-origin request",
|
||||
headers: {
|
||||
@@ -2505,7 +2562,9 @@ for (const originCase of [
|
||||
}
|
||||
]) {
|
||||
test(`admin representation completion rejects ${originCase.name}`, async () => {
|
||||
const loaded = loadAdminRepCompletion({ hasTrustedSameOrigin: () => false });
|
||||
const loaded = loadAdminRepCompletion({
|
||||
hasTrustedSameOrigin: () => false
|
||||
});
|
||||
const res = createRes();
|
||||
|
||||
await loaded.mod.default(
|
||||
@@ -2533,7 +2592,8 @@ for (const inputCase of [
|
||||
|
||||
request.body = { ...request.body, ...inputCase.body };
|
||||
Object.keys(request.body).forEach((key) => {
|
||||
if (typeof request.body[key] === "undefined") delete request.body[key];
|
||||
if (typeof request.body[key] === "undefined")
|
||||
delete request.body[key];
|
||||
});
|
||||
await loaded.mod.default(request, res);
|
||||
|
||||
@@ -2825,7 +2885,13 @@ const loadOrdinaryRepsProxy = (overrides = {}) => {
|
||||
return "&hash=abc";
|
||||
},
|
||||
consoleLogger: () => {},
|
||||
process: { env: { API_ROOT: "http://example.com" } },
|
||||
BASE_URL: "http://example.com",
|
||||
process: {
|
||||
env: {
|
||||
API_ROOT: "http://example.com",
|
||||
NEXTAUTH_URL: "http://example.com"
|
||||
}
|
||||
},
|
||||
axios: {
|
||||
get: async (...args) => {
|
||||
calls.axios.push(args);
|
||||
@@ -2935,7 +3001,9 @@ for (const containerCase of [
|
||||
|
||||
test("getrepsblobproxy preserves signed path and response for owning user", async () => {
|
||||
const loaded = loadOrdinaryRepsProxy();
|
||||
const req = ordinaryRepsRequest();
|
||||
const req = ordinaryRepsRequest({
|
||||
headers: { cookie: "next-auth.session-token=test-session" }
|
||||
});
|
||||
const res = createRes();
|
||||
|
||||
await loaded.mod.default(req, res);
|
||||
@@ -2951,6 +3019,16 @@ test("getrepsblobproxy preserves signed path and response for owning user", asyn
|
||||
loaded.calls.axios[0][0],
|
||||
"http://example.com/api/file/getrepsblob?container=user-container&hash=abc"
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
JSON.parse(JSON.stringify(loaded.calls.axios[0][1])),
|
||||
{
|
||||
headers: {
|
||||
Authorization: "Bearer tok",
|
||||
Cookie: "next-auth.session-token=test-session"
|
||||
},
|
||||
maxRedirects: 0
|
||||
}
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
JSON.parse(JSON.stringify(res.state.jsonBody)),
|
||||
loaded.payload
|
||||
@@ -2986,6 +3064,7 @@ const loadAdminRepsProxy = (overrides = {}) => {
|
||||
const mod = loadModule("pages/api/admin/getrepsblobproxy.js", {
|
||||
respondError: respondErrorMock,
|
||||
respondSuccess: respondSuccessMock,
|
||||
ADMIN_SESSION_COOKIE: "pedw_admin_session",
|
||||
readAdminSessionCookie: () => "admin-token",
|
||||
validateAdminSession: () => true,
|
||||
hasTrustedSameOrigin: () => true,
|
||||
@@ -3000,7 +3079,13 @@ const loadAdminRepsProxy = (overrides = {}) => {
|
||||
return "&hash=abc";
|
||||
},
|
||||
consoleLogger: () => {},
|
||||
process: { env: { API_ROOT: "http://example.com" } },
|
||||
BASE_URL: "http://example.com",
|
||||
process: {
|
||||
env: {
|
||||
API_ROOT: "http://example.com",
|
||||
NEXTAUTH_URL: "http://example.com"
|
||||
}
|
||||
},
|
||||
axios: {
|
||||
get: async (...args) => {
|
||||
calls.axios.push(args);
|
||||
@@ -3051,7 +3136,9 @@ for (const adminSessionCase of [
|
||||
|
||||
for (const originCase of ["missing Origin", "cross-origin request"]) {
|
||||
test(`admin getrepsblobproxy rejects ${originCase} before proxy work`, async () => {
|
||||
const loaded = loadAdminRepsProxy({ hasTrustedSameOrigin: () => false });
|
||||
const loaded = loadAdminRepsProxy({
|
||||
hasTrustedSameOrigin: () => false
|
||||
});
|
||||
const req = adminRepsRequest();
|
||||
if (originCase === "missing Origin") delete req.headers.origin;
|
||||
if (originCase === "cross-origin request") {
|
||||
@@ -3125,6 +3212,16 @@ test("admin getrepsblobproxy preserves cross-container signed path and response"
|
||||
loaded.calls.axios[0][0],
|
||||
"http://example.com/api/file/getrepsblob?container=selected-user-container&hash=abc"
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
JSON.parse(JSON.stringify(loaded.calls.axios[0][1])),
|
||||
{
|
||||
headers: {
|
||||
Authorization: "Bearer tok",
|
||||
Cookie: "pedw_admin_session=admin-token"
|
||||
},
|
||||
maxRedirects: 0
|
||||
}
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
JSON.parse(JSON.stringify(res.state.jsonBody)),
|
||||
loaded.payload
|
||||
@@ -3156,6 +3253,19 @@ test("admin storage refresh uses dedicated POST proxy while ordinary callers rem
|
||||
assert.match(viewAllSource, /getRepsFromBlobProxy\(containerID\)/);
|
||||
});
|
||||
|
||||
test("admin getrepsblobproxy rejects untrusted internal destination before proxy work", async () => {
|
||||
const loaded = loadAdminRepsProxy({
|
||||
BASE_URL: "https://attacker.example"
|
||||
});
|
||||
const res = createRes();
|
||||
|
||||
await loaded.mod.default(adminRepsRequest(), res);
|
||||
|
||||
assert.strictEqual(res.state.statusCode, 403);
|
||||
assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN");
|
||||
assertNoProxyWork(loaded.calls);
|
||||
});
|
||||
|
||||
test("admin storage completion uses dedicated POST route while ordinary completion remains unchanged", () => {
|
||||
const rootDir = path.resolve(__dirname, "..", "..");
|
||||
const adminSource = fs.readFileSync(
|
||||
@@ -3563,53 +3673,59 @@ const loadAppealCompletionDirect = ({ session, ...overrides } = {}) => {
|
||||
"customerid_contact@odata.bind":
|
||||
"/contacts(00000000-0000-0000-0000-000000000001)"
|
||||
};
|
||||
const mod = loadModule("pages/api/file/createappealcompletemessage_api.js", {
|
||||
nextConnect: createNextConnectMock(),
|
||||
middleware: () => {},
|
||||
respondError: respondErrorMock,
|
||||
respondSuccess: respondSuccessMock,
|
||||
authOptions: (req, res) => {
|
||||
calls.options.push([req, res]);
|
||||
return options;
|
||||
},
|
||||
getServerSession: async (...args) => {
|
||||
calls.session.push(args);
|
||||
return resolvedSession;
|
||||
},
|
||||
hashAPIPath: (queryPath) => {
|
||||
calls.hash.push(queryPath);
|
||||
return "&hash=expected";
|
||||
},
|
||||
getProgressBlobs: async (...args) => {
|
||||
calls.progress.push(args);
|
||||
return { path: "TMP-CASE-1/progress.json" };
|
||||
},
|
||||
downloadProgressFile: async (...args) => {
|
||||
calls.download.push(args);
|
||||
return args[1].endsWith("_case.json")
|
||||
? { ...caseObject }
|
||||
: { ...progress };
|
||||
},
|
||||
createBlob: async (...args) => {
|
||||
calls.createBlob.push(args);
|
||||
return {};
|
||||
},
|
||||
getCaseBlob: async (...args) => {
|
||||
calls.caseBlob.push(args);
|
||||
return {};
|
||||
},
|
||||
updateAccount: async (...args) => {
|
||||
calls.account.push(args);
|
||||
return {};
|
||||
},
|
||||
createCaseCompleteMessage: async (...args) => {
|
||||
calls.queue.push(args);
|
||||
return {};
|
||||
},
|
||||
consoleLogger: () => {},
|
||||
_: { isEmpty: (value) => value === undefined || value === null || value === "" },
|
||||
...overrides
|
||||
});
|
||||
const mod = loadModule(
|
||||
"pages/api/file/createappealcompletemessage_api.js",
|
||||
{
|
||||
nextConnect: createNextConnectMock(),
|
||||
middleware: () => {},
|
||||
respondError: respondErrorMock,
|
||||
respondSuccess: respondSuccessMock,
|
||||
authOptions: (req, res) => {
|
||||
calls.options.push([req, res]);
|
||||
return options;
|
||||
},
|
||||
getServerSession: async (...args) => {
|
||||
calls.session.push(args);
|
||||
return resolvedSession;
|
||||
},
|
||||
hashAPIPath: (queryPath) => {
|
||||
calls.hash.push(queryPath);
|
||||
return "&hash=expected";
|
||||
},
|
||||
getProgressBlobs: async (...args) => {
|
||||
calls.progress.push(args);
|
||||
return { path: "TMP-CASE-1/progress.json" };
|
||||
},
|
||||
downloadProgressFile: async (...args) => {
|
||||
calls.download.push(args);
|
||||
return args[1].endsWith("_case.json")
|
||||
? { ...caseObject }
|
||||
: { ...progress };
|
||||
},
|
||||
createBlob: async (...args) => {
|
||||
calls.createBlob.push(args);
|
||||
return {};
|
||||
},
|
||||
getCaseBlob: async (...args) => {
|
||||
calls.caseBlob.push(args);
|
||||
return {};
|
||||
},
|
||||
updateCrmContact: async (...args) => {
|
||||
calls.account.push(args);
|
||||
return {};
|
||||
},
|
||||
createCaseCompleteMessage: async (...args) => {
|
||||
calls.queue.push(args);
|
||||
return {};
|
||||
},
|
||||
consoleLogger: () => {},
|
||||
_: {
|
||||
isEmpty: (value) =>
|
||||
value === undefined || value === null || value === ""
|
||||
},
|
||||
...overrides
|
||||
}
|
||||
);
|
||||
|
||||
return { mod, calls, options };
|
||||
};
|
||||
@@ -3647,7 +3763,9 @@ for (const sessionCase of [
|
||||
}
|
||||
]) {
|
||||
test(`createappealcompletemessage_api blocks direct bypass with ${sessionCase.name} before HMAC or side effects`, async () => {
|
||||
const loaded = loadAppealCompletionDirect({ session: sessionCase.session });
|
||||
const loaded = loadAppealCompletionDirect({
|
||||
session: sessionCase.session
|
||||
});
|
||||
const res = createRes();
|
||||
|
||||
await loaded.mod.default.handler(appealCompletionDirectRequest(), res);
|
||||
@@ -3799,11 +3917,7 @@ test("createappealcompletemessage_api preserves owner payloads and downstream co
|
||||
]);
|
||||
assert.deepStrictEqual(loaded.calls.download, [
|
||||
["owner-container", "TMP-CASE-1/progress.json", "TMP-CASE-1"],
|
||||
[
|
||||
"owner-container",
|
||||
"TMP-CASE-1/TMP-CASE-1_case.json",
|
||||
"TMP-CASE-1"
|
||||
]
|
||||
["owner-container", "TMP-CASE-1/TMP-CASE-1_case.json", "TMP-CASE-1"]
|
||||
]);
|
||||
const savedProgress = JSON.parse(loaded.calls.createBlob[0][0]);
|
||||
assert.strictEqual(savedProgress.appealComplete, "TMP-CASE-1");
|
||||
@@ -3813,16 +3927,12 @@ test("createappealcompletemessage_api preserves owner payloads and downstream co
|
||||
"TMP-CASE-1"
|
||||
]);
|
||||
assert.strictEqual(loaded.calls.caseBlob.length, 1);
|
||||
assert.deepStrictEqual(
|
||||
JSON.parse(JSON.stringify(loaded.calls.account)),
|
||||
assert.deepStrictEqual(JSON.parse(JSON.stringify(loaded.calls.account)), [
|
||||
[
|
||||
[
|
||||
"00000000-0000-0000-0000-000000000001",
|
||||
{ pinswg_typeofinvolvement: 846040001 },
|
||||
true
|
||||
]
|
||||
"00000000-0000-0000-0000-000000000001",
|
||||
{ pinswg_typeofinvolvement: 846040001 }
|
||||
]
|
||||
);
|
||||
]);
|
||||
assert.deepStrictEqual(loaded.calls.queue, [
|
||||
["owner-container", "TMP-CASE-1"]
|
||||
]);
|
||||
@@ -3905,7 +4015,7 @@ test("createappealcompletemessage_api accepts encoded hash variant and returns s
|
||||
createBlob: async () => ({}),
|
||||
getCaseBlob: async () => ({}),
|
||||
createCaseCompleteMessage: async () => ({}),
|
||||
updateAccount: async () => ({}),
|
||||
updateCrmContact: async () => ({}),
|
||||
consoleLogger: () => {},
|
||||
_: { isEmpty: (v) => v === undefined || v === null || v === "" }
|
||||
}
|
||||
@@ -4987,7 +5097,9 @@ for (const endpoint of [
|
||||
|
||||
test(`${endpoint.name} rejects another user's container before privileged work`, async () => {
|
||||
const loaded = endpoint.load({
|
||||
getServerSession: async () => ({ user: { id: "different-container" } })
|
||||
getServerSession: async () => ({
|
||||
user: { id: "different-container" }
|
||||
})
|
||||
});
|
||||
const res = createRes();
|
||||
|
||||
@@ -5000,7 +5112,9 @@ for (const endpoint of [
|
||||
|
||||
test(`${endpoint.name} ignores a client-provided user identity`, async () => {
|
||||
const loaded = endpoint.load({
|
||||
getServerSession: async () => ({ user: { id: "different-container" } })
|
||||
getServerSession: async () => ({
|
||||
user: { id: "different-container" }
|
||||
})
|
||||
});
|
||||
const req = endpoint.request();
|
||||
req.body.userID = PDF_OWNER_CONTAINER;
|
||||
@@ -5064,27 +5178,23 @@ for (const containerCase of [
|
||||
});
|
||||
}
|
||||
|
||||
test("generatepdf rejects missing and conflicting draft-folder identifiers", async () => {
|
||||
for (const body of [
|
||||
{
|
||||
representationType: "Other",
|
||||
repfile_name: "rep-file",
|
||||
containerID: PDF_OWNER_CONTAINER
|
||||
},
|
||||
{
|
||||
representationType: "Other",
|
||||
repfile_name: "rep-file",
|
||||
containerID: PDF_OWNER_CONTAINER,
|
||||
ticketnumber: PDF_CASE_FOLDER,
|
||||
caseRef: "CASE-2"
|
||||
}
|
||||
]) {
|
||||
const loaded = loadGeneratePdf();
|
||||
const res = createRes();
|
||||
await loaded.mod.default(generatePdfRequest({ body }), res);
|
||||
assert.strictEqual(res.state.statusCode, 400);
|
||||
assertNoGeneratePdfWork(loaded.calls);
|
||||
}
|
||||
test("generatepdf rejects missing draft-folder identifiers", async () => {
|
||||
const loaded = loadGeneratePdf();
|
||||
const res = createRes();
|
||||
|
||||
await loaded.mod.default(
|
||||
generatePdfRequest({
|
||||
body: {
|
||||
representationType: "Other",
|
||||
repfile_name: "rep-file",
|
||||
containerID: PDF_OWNER_CONTAINER
|
||||
}
|
||||
}),
|
||||
res
|
||||
);
|
||||
|
||||
assert.strictEqual(res.state.statusCode, 400);
|
||||
assertNoGeneratePdfWork(loaded.calls);
|
||||
});
|
||||
|
||||
test("generateappealpdf rejects missing or malformed draft-folder identifiers", async () => {
|
||||
@@ -5150,7 +5260,9 @@ for (const endpoint of [
|
||||
|
||||
test(`${endpoint.name} rejects a valid HMAC for an unauthorised container`, async () => {
|
||||
const loaded = endpoint.load({
|
||||
getServerSession: async () => ({ user: { id: "different-container" } })
|
||||
getServerSession: async () => ({
|
||||
user: { id: "different-container" }
|
||||
})
|
||||
});
|
||||
const res = createRes();
|
||||
await loaded.mod.default(endpoint.request(), res);
|
||||
@@ -5174,6 +5286,51 @@ test("generatepdf owner success writes the PDF to the authorised container", asy
|
||||
assert.strictEqual(res.state.jsonBody.status, "success");
|
||||
});
|
||||
|
||||
test("generatepdf preserves ticketnumber precedence when references differ", async () => {
|
||||
const loaded = loadGeneratePdf();
|
||||
const res = createRes();
|
||||
|
||||
await loaded.mod.default(
|
||||
generatePdfRequest({
|
||||
body: {
|
||||
representationType: "Other",
|
||||
repfile_name: "rep-file",
|
||||
containerID: PDF_OWNER_CONTAINER,
|
||||
ticketnumber: PDF_CASE_FOLDER,
|
||||
caseRef: "CASE-2"
|
||||
}
|
||||
}),
|
||||
res
|
||||
);
|
||||
|
||||
assert.strictEqual(res.state.statusCode, 200);
|
||||
assert.strictEqual(loaded.calls.create.length, 1);
|
||||
assert.strictEqual(loaded.calls.create[0][1], PDF_OWNER_CONTAINER);
|
||||
assert.strictEqual(loaded.calls.create[0][2], PDF_CASE_FOLDER);
|
||||
assert.strictEqual(res.state.jsonBody.status, "success");
|
||||
});
|
||||
|
||||
test("generatepdf rejects malformed secondary case references", async () => {
|
||||
const loaded = loadGeneratePdf();
|
||||
const res = createRes();
|
||||
|
||||
await loaded.mod.default(
|
||||
generatePdfRequest({
|
||||
body: {
|
||||
representationType: "Other",
|
||||
repfile_name: "rep-file",
|
||||
containerID: PDF_OWNER_CONTAINER,
|
||||
ticketnumber: PDF_CASE_FOLDER,
|
||||
caseRef: "../other-case"
|
||||
}
|
||||
}),
|
||||
res
|
||||
);
|
||||
|
||||
assert.strictEqual(res.state.statusCode, 400);
|
||||
assertNoGeneratePdfWork(loaded.calls);
|
||||
});
|
||||
|
||||
test("generateappealpdf owner success reads and writes only the authorised container", async () => {
|
||||
const loaded = loadGenerateAppealPdf();
|
||||
const res = createRes();
|
||||
@@ -5457,10 +5614,7 @@ test("createcase_api creates both draft JSON blobs in the authenticated owner's
|
||||
assert.strictEqual(loaded.calls.caseBlob.length, 1);
|
||||
assert.strictEqual(loaded.calls.appealBlob.length, 1);
|
||||
assert.strictEqual(loaded.calls.caseBlob[0][0], CASE_OWNER_CONTAINER);
|
||||
assert.strictEqual(
|
||||
loaded.calls.caseBlob[0][1],
|
||||
GENERATED_CASE_REFERENCE
|
||||
);
|
||||
assert.strictEqual(loaded.calls.caseBlob[0][1], GENERATED_CASE_REFERENCE);
|
||||
assert.strictEqual(loaded.calls.appealBlob[0][1], CASE_OWNER_CONTAINER);
|
||||
assert.deepStrictEqual(
|
||||
JSON.parse(JSON.stringify(loaded.calls.caseBlob[0][2])),
|
||||
@@ -5469,10 +5623,12 @@ test("createcase_api creates both draft JSON blobs in the authenticated owner's
|
||||
assert.deepStrictEqual(JSON.parse(loaded.calls.appealBlob[0][0]), {
|
||||
pinswg_name: GENERATED_CASE_REFERENCE,
|
||||
pinswg_appealcasetype: "846040000",
|
||||
_pinswg_associatedlpa_value:
|
||||
"00000000-0000-4000-8000-000000000002"
|
||||
_pinswg_associatedlpa_value: "00000000-0000-4000-8000-000000000002"
|
||||
});
|
||||
assert.strictEqual(res.state.jsonBody.ticketnumber, GENERATED_CASE_REFERENCE);
|
||||
assert.strictEqual(
|
||||
res.state.jsonBody.ticketnumber,
|
||||
GENERATED_CASE_REFERENCE
|
||||
);
|
||||
assert.strictEqual(res.state.jsonBody.title, GENERATED_CASE_REFERENCE);
|
||||
assert.strictEqual(
|
||||
res.state.jsonBody.incidentid,
|
||||
@@ -5499,6 +5655,75 @@ test("createcase_api preserves the existing propagated token dependency failure
|
||||
assert.strictEqual(loaded.calls.appealBlob.length, 0);
|
||||
});
|
||||
|
||||
const loadRepresentationDeletion = (route, operationName, session) => {
|
||||
const calls = [];
|
||||
const mod = loadModule(route, {
|
||||
nextConnect: createNextConnectMock(),
|
||||
middleware: () => {},
|
||||
getServerSession: async () => session,
|
||||
authOptions: () => ({}),
|
||||
hashAPIPath: () => "&hash=expected",
|
||||
respondError: respondErrorMock,
|
||||
respondSuccess: respondSuccessMock,
|
||||
[operationName]: async (...args) => {
|
||||
calls.push(args);
|
||||
return { deleted: true };
|
||||
}
|
||||
});
|
||||
|
||||
return { mod, calls };
|
||||
};
|
||||
|
||||
test("whole-representation deletion remains cross-user protected with zero Azure operations", async () => {
|
||||
const loaded = loadRepresentationDeletion(
|
||||
"pages/api/file/deleteblobrep.js",
|
||||
"deleteBlobRep",
|
||||
{ user: { id: "different-container" } }
|
||||
);
|
||||
const res = createRes();
|
||||
|
||||
await loaded.mod.default.handler(
|
||||
{
|
||||
query: {
|
||||
container: "organisation-container",
|
||||
casefolderID: "CASE-1",
|
||||
repfile: "REP-1",
|
||||
hash: "expected"
|
||||
}
|
||||
},
|
||||
res
|
||||
);
|
||||
|
||||
assert.strictEqual(res.state.statusCode, 403);
|
||||
assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN");
|
||||
assert.strictEqual(loaded.calls.length, 0);
|
||||
});
|
||||
|
||||
test("representation supporting-file deletion remains cross-user protected with zero Azure operations", async () => {
|
||||
const loaded = loadRepresentationDeletion(
|
||||
"pages/api/file/deleteblob.js",
|
||||
"deleteBlob",
|
||||
{ user: { id: "different-container" } }
|
||||
);
|
||||
const res = createRes();
|
||||
|
||||
await loaded.mod.default.handler(
|
||||
{
|
||||
query: {
|
||||
container: "organisation-container",
|
||||
casefolderID: "CASE-1/REP-1",
|
||||
blobname: "evidence.pdf",
|
||||
hash: "expected"
|
||||
}
|
||||
},
|
||||
res
|
||||
);
|
||||
|
||||
assert.strictEqual(res.state.statusCode, 403);
|
||||
assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN");
|
||||
assert.strictEqual(loaded.calls.length, 0);
|
||||
});
|
||||
|
||||
const run = async () => {
|
||||
let passed = 0;
|
||||
for (const currentTest of tests) {
|
||||
|
||||
@@ -129,7 +129,7 @@ test("excluded route families remain outside this myportal slice", async () => {
|
||||
const pageSource = read("pages/myportal/index.js");
|
||||
|
||||
assert.match(portalSource, /\/api\/subscriptions\/get-watched-cases/);
|
||||
assert.match(pageSource, /getRepsFromBlob\(thisSession.user.id\)/);
|
||||
assert.match(pageSource, /getRepsFromBlob\(thisSession.user.id,\s*\{/);
|
||||
assert.match(
|
||||
pageSource,
|
||||
/getAwaitingSubmissionFromBlob\(thisSession.user.id\)/
|
||||
|
||||
@@ -6,10 +6,7 @@ const vm = require("vm");
|
||||
const tests = [];
|
||||
const test = (name, fn) => tests.push({ name, fn });
|
||||
|
||||
const loadAuthInternals = ({
|
||||
adapterFactory = () => ({}),
|
||||
resolveCrmLocale = async () => null
|
||||
} = {}) => {
|
||||
const loadAuthInternals = ({ adapterFactory = () => ({}) } = {}) => {
|
||||
const filePath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
@@ -59,7 +56,7 @@ const loadAuthInternals = ({
|
||||
},
|
||||
NextAuth: () => ({}),
|
||||
EmailProvider: () => ({}),
|
||||
resolveCrmLocale,
|
||||
getPortalLogin: async () => ({ value: [] }),
|
||||
redactSensitive: (value) => value,
|
||||
consoleLogger: () => {},
|
||||
console: {
|
||||
|
||||
@@ -203,12 +203,6 @@ test("breadcrumbs/factory module includes expected mapped route definitions", as
|
||||
true,
|
||||
"Expected /account/personaldetails to be mapped in createDetailAndAccountRouteRenderers"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
source.includes('"/account/changepassword": () =>'),
|
||||
true,
|
||||
"Expected /account/changepassword to be mapped in createDetailAndAccountRouteRenderers"
|
||||
);
|
||||
});
|
||||
|
||||
test("breadcrumbs/component composes mapped routes via imported factories and shared resolver", async () => {
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const vm = require("vm");
|
||||
|
||||
const tests = [];
|
||||
const test = (name, fn) => tests.push({ name, fn });
|
||||
|
||||
const loadIndexLoader = (overrides = {}) => {
|
||||
const filePath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"pages",
|
||||
"myportal",
|
||||
"index.js"
|
||||
);
|
||||
|
||||
let source = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
// Execute only the server-side loader, not the React component.
|
||||
source = source.slice(source.indexOf("export const getServerSideProps"));
|
||||
source = source.slice(0, source.indexOf("const mapStateToProps"));
|
||||
|
||||
source = source.replace(
|
||||
/export\s+const\s+getServerSideProps\s*=/,
|
||||
"const getServerSideProps ="
|
||||
);
|
||||
source += "\nmodule.exports = { getServerSideProps };\n";
|
||||
|
||||
const dispatches = [];
|
||||
const store = {
|
||||
dispatch: (action) => dispatches.push(action)
|
||||
};
|
||||
|
||||
const context = {
|
||||
module: { exports: {} },
|
||||
exports: {},
|
||||
process: { env: {} },
|
||||
console: { error: () => {} },
|
||||
wrapper: {
|
||||
getServerSideProps: (factory) => async (ctx) => factory(store)(ctx)
|
||||
},
|
||||
getSession: async () => ({
|
||||
user: {
|
||||
id: "session-user-1",
|
||||
email: "user@example.com"
|
||||
}
|
||||
}),
|
||||
getIP: () => {},
|
||||
consoleLogger: () => {},
|
||||
getPortalLogin: async () => ({
|
||||
value: [{ contactid: "crm-contact-1" }]
|
||||
}),
|
||||
createContainerProxy: async () => {},
|
||||
getCrmPersonalAccount: async () => ({
|
||||
firstname: "Test",
|
||||
pinswg_typeofinvolvement: 0,
|
||||
pinswg_preferredlanguage: 0
|
||||
}),
|
||||
getMyCases: async () => ({ value: [] }),
|
||||
getMyLPACases: async () => ({ value: [] }),
|
||||
getRepsFromBlob: async () => ({ value: [] }),
|
||||
getWatchedCases: async () => ({ value: [] }),
|
||||
getAwaitingSubmissionFromBlob: async () => ({ value: [] }),
|
||||
splitWatchedCasesBySubmissionState: () => ({
|
||||
watchedCases: { value: [] },
|
||||
submittedRepresentations: { value: [] }
|
||||
}),
|
||||
getPortalModuleDetails: async () => ({}),
|
||||
getFormCollectionByID: () => ({
|
||||
LogicalCollectionName: "pinswg_cases"
|
||||
}),
|
||||
pLimit: () => (fn) => fn(),
|
||||
setLocale: (value) => ({ type: "locale", value }),
|
||||
setShowReps: () => ({ type: "showReps" }),
|
||||
setAccountDetails: (value) => ({ type: "accountDetails", value }),
|
||||
setLoggedInUserId: (value) => ({ type: "loggedInUserId", value }),
|
||||
setMyCases: () => ({ type: "myCases" }),
|
||||
setMyCasesDetails: () => ({ type: "myCasesDetails" }),
|
||||
setMyRepresentations: () => ({ type: "myRepresentations" }),
|
||||
setMyRepresentationsDetails: () => ({
|
||||
type: "myRepresentationsDetails"
|
||||
}),
|
||||
setWatchedCases: () => ({ type: "watchedCases" }),
|
||||
setWatchedCasesDetails: () => ({ type: "watchedCasesDetails" }),
|
||||
setMySubmittedReps: () => ({ type: "mySubmittedReps" }),
|
||||
setMySubmittedRepsDetails: () => ({
|
||||
type: "mySubmittedRepsDetails"
|
||||
}),
|
||||
setAwaitingSubmissionFromBlob: () => ({
|
||||
type: "awaitingSubmission"
|
||||
}),
|
||||
setAwaitingSubmissionDetails: () => ({
|
||||
type: "awaitingSubmissionDetails"
|
||||
}),
|
||||
setContainerID: () => ({ type: "containerID" }),
|
||||
...overrides
|
||||
};
|
||||
|
||||
vm.runInNewContext(source, context, { filename: filePath });
|
||||
|
||||
return {
|
||||
getServerSideProps: context.module.exports.getServerSideProps,
|
||||
dispatches
|
||||
};
|
||||
};
|
||||
|
||||
const createContext = () => ({
|
||||
req: {
|
||||
cookies: {},
|
||||
headers: { cookie: "" }
|
||||
},
|
||||
locale: "en"
|
||||
});
|
||||
|
||||
test("missing session redirects to signin", async () => {
|
||||
const loader = loadIndexLoader({
|
||||
getSession: async () => null
|
||||
});
|
||||
|
||||
const result = await loader.getServerSideProps(createContext());
|
||||
|
||||
assert.strictEqual(result.redirect.destination, "/auth/signin");
|
||||
});
|
||||
|
||||
test("missing session identity redirects to signin", async () => {
|
||||
const loader = loadIndexLoader({
|
||||
getSession: async () => ({
|
||||
user: { id: "session-user-1" }
|
||||
})
|
||||
});
|
||||
|
||||
const result = await loader.getServerSideProps(createContext());
|
||||
|
||||
assert.strictEqual(result.redirect.destination, "/auth/signin");
|
||||
});
|
||||
|
||||
test("CRM contact lookup failure redirects to signin", async () => {
|
||||
const loader = loadIndexLoader({
|
||||
getPortalLogin: async () => {
|
||||
throw new Error("CRM unavailable");
|
||||
}
|
||||
});
|
||||
|
||||
const result = await loader.getServerSideProps(createContext());
|
||||
|
||||
assert.strictEqual(result.redirect.destination, "/auth/signin");
|
||||
});
|
||||
|
||||
test("missing CRM contact redirects to signin", async () => {
|
||||
const loader = loadIndexLoader({
|
||||
getPortalLogin: async () => ({ value: [] })
|
||||
});
|
||||
|
||||
const result = await loader.getServerSideProps(createContext());
|
||||
|
||||
assert.strictEqual(result.redirect.destination, "/auth/signin");
|
||||
});
|
||||
|
||||
test("personal account lookup exception redirects to error page", async () => {
|
||||
const loader = loadIndexLoader({
|
||||
getCrmPersonalAccount: async () => {
|
||||
throw new Error("CRM account lookup failed");
|
||||
}
|
||||
});
|
||||
|
||||
const result = await loader.getServerSideProps(createContext());
|
||||
|
||||
assert.strictEqual(result.redirect.destination, "/myportal/error");
|
||||
});
|
||||
|
||||
test("missing personal account redirects to error page", async () => {
|
||||
const loader = loadIndexLoader({
|
||||
getCrmPersonalAccount: async () => null
|
||||
});
|
||||
|
||||
const result = await loader.getServerSideProps(createContext());
|
||||
|
||||
assert.strictEqual(result.redirect.destination, "/myportal/error");
|
||||
});
|
||||
|
||||
test("personal account error response redirects to error page", async () => {
|
||||
const loader = loadIndexLoader({
|
||||
getCrmPersonalAccount: async () => ({
|
||||
errorCode: "CRM_CONTACT_MISSING"
|
||||
})
|
||||
});
|
||||
|
||||
const result = await loader.getServerSideProps(createContext());
|
||||
|
||||
assert.strictEqual(result.redirect.destination, "/myportal/error");
|
||||
});
|
||||
|
||||
test("successful lookup uses contact resolved from session email", async () => {
|
||||
let capturedEmail;
|
||||
let capturedContactId;
|
||||
|
||||
const loader = loadIndexLoader({
|
||||
getPortalLogin: async (email) => {
|
||||
capturedEmail = email;
|
||||
return { value: [{ contactid: "crm-contact-123" }] };
|
||||
},
|
||||
getCrmPersonalAccount: async (contactId) => {
|
||||
capturedContactId = contactId;
|
||||
return {
|
||||
firstname: "Test",
|
||||
pinswg_typeofinvolvement: 0,
|
||||
pinswg_preferredlanguage: 0
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const result = await loader.getServerSideProps(createContext());
|
||||
|
||||
assert.strictEqual(capturedEmail, "user@example.com");
|
||||
assert.strictEqual(capturedContactId, "crm-contact-123");
|
||||
assert.strictEqual(result.props.containerID, "session-user-1");
|
||||
|
||||
const accountDispatch = loader.dispatches.find(
|
||||
(action) => action.type === "accountDetails"
|
||||
);
|
||||
|
||||
assert.strictEqual(accountDispatch.value.firstname, "Test");
|
||||
});
|
||||
|
||||
const run = async () => {
|
||||
let passed = 0;
|
||||
|
||||
for (const currentTest of tests) {
|
||||
await currentTest.fn();
|
||||
passed += 1;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Phase 22 myportal-index-loader-guards tests passed (${passed}/${tests.length}).`
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = run;
|
||||
|
||||
if (require.main === module) {
|
||||
run().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const vm = require("vm");
|
||||
|
||||
const tests = [];
|
||||
const test = (name, fn) => tests.push({ name, fn });
|
||||
|
||||
const loadViewAllLoader = (overrides = {}) => {
|
||||
const filePath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"pages",
|
||||
"myportal",
|
||||
"viewall.js"
|
||||
);
|
||||
|
||||
let source = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
// Execute the actual SSR loader, excluding the React component.
|
||||
const start = source.indexOf("export const getServerSideProps");
|
||||
const end = source.indexOf("const mapStateToProps", start);
|
||||
|
||||
assert.ok(start >= 0, "SSR loader declaration not found");
|
||||
assert.ok(end > start, "SSR loader boundary not found");
|
||||
|
||||
source = source.slice(start, end);
|
||||
source = source.replace(
|
||||
/export\s+const\s+getServerSideProps\s*=/,
|
||||
"const getServerSideProps ="
|
||||
);
|
||||
source += "\nmodule.exports = { getServerSideProps };\n";
|
||||
|
||||
const dispatches = [];
|
||||
const store = {
|
||||
dispatch: (action) => dispatches.push(action)
|
||||
};
|
||||
|
||||
const action = (type) => (value) => ({ type, value });
|
||||
|
||||
const context = {
|
||||
module: { exports: {} },
|
||||
exports: {},
|
||||
console: {
|
||||
log: () => {},
|
||||
error: () => {}
|
||||
},
|
||||
wrapper: {
|
||||
getServerSideProps: (factory) => async (ctx) => factory(store)(ctx)
|
||||
},
|
||||
getSession: async () => ({
|
||||
user: {
|
||||
id: "session-user-1",
|
||||
email: "user@example.com"
|
||||
}
|
||||
}),
|
||||
getPortalLogin: async () => ({
|
||||
value: [{ contactid: "crm-contact-1" }]
|
||||
}),
|
||||
getCrmPersonalAccount: async () => ({
|
||||
firstname: "Test",
|
||||
pinswg_typeofinvolvement: 0
|
||||
}),
|
||||
consoleLogger: () => {},
|
||||
pLimit: () => (fn) => fn(),
|
||||
getMyCases: async () => ({ value: [] }),
|
||||
getMyLPACases: async () => ({ value: [] }),
|
||||
getWatchedCases: async () => ({ value: [] }),
|
||||
getRepsFromBlob: async () => ({ value: [] }),
|
||||
getAwaitingSubmissionFromBlob: async () => ({ value: [] }),
|
||||
splitWatchedCasesBySubmissionState: () => ({
|
||||
watchedCases: [],
|
||||
submittedRepresentations: []
|
||||
}),
|
||||
getPortalModuleDetails: async () => ({}),
|
||||
getFormCollectionByID: () => ({
|
||||
LogicalCollectionName: "pinswg_cases"
|
||||
}),
|
||||
getBasicSearch: async () => ({ value: [] }),
|
||||
setAccountDetails: action("accountDetails"),
|
||||
setLoggedInUserId: action("loggedInUserId"),
|
||||
setContainerID: action("containerID"),
|
||||
setCurrentView: action("currentView"),
|
||||
setMyCases: action("myCases"),
|
||||
setMyCasesDetails: action("myCasesDetails"),
|
||||
setWatchedCases: action("watchedCases"),
|
||||
setWatchedCasesDetails: action("watchedCasesDetails"),
|
||||
setMySubmittedReps: action("mySubmittedReps"),
|
||||
setMySubmittedRepsDetails: action("mySubmittedRepsDetails"),
|
||||
setMyRepresentations: action("myRepresentations"),
|
||||
setMyRepresentationsDetails: action("myRepresentationsDetails"),
|
||||
setAwaitingSubmissionFromBlob: action("awaitingSubmission"),
|
||||
setAwaitingSubmissionDetails: action("awaitingSubmissionDetails"),
|
||||
setSearchResults: action("searchResults"),
|
||||
setSearch: action("search"),
|
||||
...overrides
|
||||
};
|
||||
|
||||
vm.runInNewContext(source, context, { filename: filePath });
|
||||
|
||||
return {
|
||||
getServerSideProps: context.module.exports.getServerSideProps,
|
||||
dispatches
|
||||
};
|
||||
};
|
||||
|
||||
const createContext = (key = "myCases") => ({
|
||||
query: { key },
|
||||
req: { cookies: {} },
|
||||
locale: "en"
|
||||
});
|
||||
|
||||
const getDispatch = (loader, type) =>
|
||||
loader.dispatches.find((item) => item.type === type);
|
||||
|
||||
test("invalid view category redirects before account lookup", async () => {
|
||||
let accountLookupCalled = false;
|
||||
|
||||
const loader = loadViewAllLoader({
|
||||
getCrmPersonalAccount: async () => {
|
||||
accountLookupCalled = true;
|
||||
return {};
|
||||
}
|
||||
});
|
||||
|
||||
const result = await loader.getServerSideProps(createContext("invalid"));
|
||||
|
||||
assert.strictEqual(result.redirect.destination, "/myportal");
|
||||
assert.strictEqual(accountLookupCalled, false);
|
||||
});
|
||||
|
||||
test("missing session redirects to signin", async () => {
|
||||
const loader = loadViewAllLoader({
|
||||
getSession: async () => null
|
||||
});
|
||||
|
||||
const result = await loader.getServerSideProps(createContext());
|
||||
|
||||
assert.strictEqual(result.redirect.destination, "/auth/signin");
|
||||
});
|
||||
|
||||
test("missing session email redirects to signin", async () => {
|
||||
const loader = loadViewAllLoader({
|
||||
getSession: async () => ({
|
||||
user: { id: "session-user-1" }
|
||||
})
|
||||
});
|
||||
|
||||
const result = await loader.getServerSideProps(createContext());
|
||||
|
||||
assert.strictEqual(result.redirect.destination, "/auth/signin");
|
||||
});
|
||||
|
||||
test("missing CRM contact redirects to signin", async () => {
|
||||
const loader = loadViewAllLoader({
|
||||
getPortalLogin: async () => ({ value: [] })
|
||||
});
|
||||
|
||||
const result = await loader.getServerSideProps(createContext());
|
||||
|
||||
assert.strictEqual(result.redirect.destination, "/auth/signin");
|
||||
});
|
||||
|
||||
test("personal account exception redirects to error page", async () => {
|
||||
const loader = loadViewAllLoader({
|
||||
getCrmPersonalAccount: async () => {
|
||||
throw new Error("CRM unavailable");
|
||||
}
|
||||
});
|
||||
|
||||
const result = await loader.getServerSideProps(createContext());
|
||||
|
||||
assert.strictEqual(result.redirect.destination, "/myportal/error");
|
||||
});
|
||||
|
||||
test("missing personal account redirects to error page", async () => {
|
||||
const loader = loadViewAllLoader({
|
||||
getCrmPersonalAccount: async () => null
|
||||
});
|
||||
|
||||
const result = await loader.getServerSideProps(createContext());
|
||||
|
||||
assert.strictEqual(result.redirect.destination, "/myportal/error");
|
||||
});
|
||||
|
||||
test("personal account error response redirects to error page", async () => {
|
||||
const loader = loadViewAllLoader({
|
||||
getCrmPersonalAccount: async () => ({
|
||||
errorCode: "CRM_CONTACT_MISSING"
|
||||
})
|
||||
});
|
||||
|
||||
const result = await loader.getServerSideProps(createContext());
|
||||
|
||||
assert.strictEqual(result.redirect.destination, "/myportal/error");
|
||||
});
|
||||
|
||||
test("successful lookup uses session-resolved CRM contact", async () => {
|
||||
let capturedEmail;
|
||||
let capturedContactId;
|
||||
|
||||
const accountDetails = {
|
||||
firstname: "Test",
|
||||
pinswg_typeofinvolvement: 0
|
||||
};
|
||||
|
||||
const loader = loadViewAllLoader({
|
||||
getPortalLogin: async (email) => {
|
||||
capturedEmail = email;
|
||||
return {
|
||||
value: [{ contactid: "crm-contact-123" }]
|
||||
};
|
||||
},
|
||||
getCrmPersonalAccount: async (contactId) => {
|
||||
capturedContactId = contactId;
|
||||
return accountDetails;
|
||||
}
|
||||
});
|
||||
|
||||
const result = await loader.getServerSideProps(createContext());
|
||||
|
||||
assert.strictEqual(capturedEmail, "user@example.com");
|
||||
assert.strictEqual(capturedContactId, "crm-contact-123");
|
||||
assert.ok(result.props);
|
||||
|
||||
assert.strictEqual(
|
||||
getDispatch(loader, "accountDetails").value.firstname,
|
||||
"Test"
|
||||
);
|
||||
assert.strictEqual(
|
||||
getDispatch(loader, "loggedInUserId").value,
|
||||
"crm-contact-123"
|
||||
);
|
||||
assert.strictEqual(
|
||||
getDispatch(loader, "containerID").value,
|
||||
"session-user-1"
|
||||
);
|
||||
assert.strictEqual(
|
||||
getDispatch(loader, "currentView").value.viewKey,
|
||||
"myCases"
|
||||
);
|
||||
});
|
||||
|
||||
test("all supported view categories load successfully", async () => {
|
||||
const categories = [
|
||||
"myCases",
|
||||
"watchedCases",
|
||||
"mySubmittedReps",
|
||||
"myRepresentations",
|
||||
"awaitingSubmissionDetails"
|
||||
];
|
||||
|
||||
for (const category of categories) {
|
||||
const loader = loadViewAllLoader();
|
||||
|
||||
const result = await loader.getServerSideProps(createContext(category));
|
||||
|
||||
assert.ok(result.props, `${category} should return props`);
|
||||
|
||||
assert.strictEqual(
|
||||
getDispatch(loader, "currentView").value.viewKey,
|
||||
category
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("LPA account uses associated LPA for case lookup", async () => {
|
||||
let capturedLpaId;
|
||||
let personalCaseLookupCalled = false;
|
||||
|
||||
const loader = loadViewAllLoader({
|
||||
getCrmPersonalAccount: async () => ({
|
||||
pinswg_typeofinvolvement: 846040012,
|
||||
"pinswg_contact_associatedlpa@OData.Community.Display.V1.FormattedValue":
|
||||
"lpa-123"
|
||||
}),
|
||||
getMyLPACases: async (lpaId) => {
|
||||
capturedLpaId = lpaId;
|
||||
return { value: [] };
|
||||
},
|
||||
getMyCases: async () => {
|
||||
personalCaseLookupCalled = true;
|
||||
return { value: [] };
|
||||
}
|
||||
});
|
||||
|
||||
await loader.getServerSideProps(createContext());
|
||||
|
||||
assert.strictEqual(capturedLpaId, "lpa-123");
|
||||
assert.strictEqual(personalCaseLookupCalled, false);
|
||||
});
|
||||
|
||||
const run = async () => {
|
||||
let passed = 0;
|
||||
|
||||
for (const currentTest of tests) {
|
||||
await currentTest.fn();
|
||||
passed += 1;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Phase 22 myportal-viewall-loader-guards tests passed (${passed}/${tests.length}).`
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = run;
|
||||
|
||||
if (require.main === module) {
|
||||
run().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const vm = require("vm");
|
||||
|
||||
const rootDir = path.resolve(__dirname, "..", "..");
|
||||
const tests = [];
|
||||
const test = (name, fn) => tests.push({ name, fn });
|
||||
|
||||
const loadModule = (relativePath, injected = {}) => {
|
||||
const filePath = path.join(rootDir, relativePath);
|
||||
let source = fs.readFileSync(filePath, "utf8");
|
||||
const exportNames = Array.from(
|
||||
source.matchAll(/export const\s+(\w+)\s*=/g)
|
||||
).map((match) => match[1]);
|
||||
|
||||
source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, "");
|
||||
source = source.replace(/export const\s+/g, "const ");
|
||||
source = source.replace(
|
||||
/export default async function\s+(\w+)\s*\(/g,
|
||||
"async function $1("
|
||||
);
|
||||
source += `\nmodule.exports = { ${exportNames.join(", ")} };\n`;
|
||||
source +=
|
||||
'\nif (typeof handler !== "undefined") module.exports.default = handler;\n';
|
||||
|
||||
const context = {
|
||||
module: { exports: {} },
|
||||
exports: {},
|
||||
require,
|
||||
process,
|
||||
console: { log: () => {}, info: () => {}, warn: () => {}, error: () => {} },
|
||||
...injected
|
||||
};
|
||||
|
||||
vm.runInNewContext(source, context, { filename: filePath });
|
||||
return context.module.exports;
|
||||
};
|
||||
|
||||
const normalize = (value) => JSON.parse(JSON.stringify(value));
|
||||
|
||||
const createRes = () => {
|
||||
const state = { statusCode: null, jsonBody: undefined };
|
||||
|
||||
return {
|
||||
state,
|
||||
status(code) {
|
||||
state.statusCode = code;
|
||||
return this;
|
||||
},
|
||||
json(body) {
|
||||
state.jsonBody = body;
|
||||
return body;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const loadLocaleResolver = ({ relayGetData }) =>
|
||||
loadModule("lib/auth/resolveCrmLocale.js", {
|
||||
azureHeadersPaged: (accessToken) => ({ accessToken }),
|
||||
escapeODataString: (value) => value.trim().replace(/'/g, "''"),
|
||||
consoleLogger: () => {},
|
||||
relayGetData,
|
||||
RELAY_POLICY_STRICT_LOGIN: {
|
||||
method: "GET",
|
||||
timeoutMs: 5000,
|
||||
maxRetries: 0,
|
||||
retryBaseDelayMs: 0,
|
||||
retryMaxDelayMs: 0
|
||||
}
|
||||
});
|
||||
|
||||
test("pre-auth CRM locale lookup returns Welsh and selects only language", async () => {
|
||||
let capturedRequest;
|
||||
const mod = loadLocaleResolver({
|
||||
relayGetData: async (request) => {
|
||||
capturedRequest = request;
|
||||
return {
|
||||
data: {
|
||||
value: [
|
||||
{
|
||||
pinswg_preferredlanguage: 846040000,
|
||||
contactid: "must-not-be-returned",
|
||||
firstname: "Must not be returned"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const result = await mod.resolveCrmLocale(" Person@Example.com ");
|
||||
const selectClause = new URLSearchParams(
|
||||
capturedRequest.queryUrl.split("?")[1]
|
||||
).get("$select");
|
||||
|
||||
assert.strictEqual(result, "cy");
|
||||
assert.match(capturedRequest.queryUrl, /\$filter=emailaddress1 eq /);
|
||||
assert.match(capturedRequest.queryUrl, /statuscode eq 1/);
|
||||
assert.strictEqual(selectClause, "pinswg_preferredlanguage");
|
||||
assert.ok(result === "en" || result === "cy" || result === null);
|
||||
for (const field of [
|
||||
"contactid",
|
||||
"emailaddress1",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"yomifullname"
|
||||
]) {
|
||||
assert.doesNotMatch(selectClause, new RegExp(field));
|
||||
assert.doesNotMatch(result, new RegExp(field));
|
||||
}
|
||||
});
|
||||
|
||||
test("pre-auth CRM locale lookup returns English for a non-Welsh value", async () => {
|
||||
const mod = loadLocaleResolver({
|
||||
relayGetData: async () => ({
|
||||
data: { value: [{ pinswg_preferredlanguage: 846040001 }] }
|
||||
})
|
||||
});
|
||||
|
||||
assert.strictEqual(await mod.resolveCrmLocale("person@example.com"), "en");
|
||||
});
|
||||
|
||||
test("pre-auth CRM locale lookup returns null for no contact", async () => {
|
||||
const mod = loadLocaleResolver({
|
||||
relayGetData: async () => ({ data: { value: [] } })
|
||||
});
|
||||
|
||||
assert.strictEqual(await mod.resolveCrmLocale("person@example.com"), null);
|
||||
});
|
||||
|
||||
test("pre-auth CRM locale lookup preserves first-contact duplicate semantics", async () => {
|
||||
const mod = loadLocaleResolver({
|
||||
relayGetData: async () => ({
|
||||
data: {
|
||||
value: [
|
||||
{ pinswg_preferredlanguage: 846040001 },
|
||||
{ pinswg_preferredlanguage: 846040000 }
|
||||
]
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
assert.strictEqual(await mod.resolveCrmLocale("person@example.com"), "en");
|
||||
});
|
||||
|
||||
test("pre-auth CRM locale lookup rejects malformed email without CRM access", async () => {
|
||||
let relayCalls = 0;
|
||||
const mod = loadLocaleResolver({
|
||||
relayGetData: async () => {
|
||||
relayCalls += 1;
|
||||
return { data: { value: [] } };
|
||||
}
|
||||
});
|
||||
|
||||
assert.strictEqual(await mod.resolveCrmLocale("not-an-email"), null);
|
||||
assert.strictEqual(relayCalls, 0);
|
||||
});
|
||||
|
||||
test("pre-auth CRM locale lookup fails closed without leaking CRM details", async () => {
|
||||
const mod = loadLocaleResolver({
|
||||
relayGetData: async () => {
|
||||
throw new Error("CRM secret response details");
|
||||
}
|
||||
});
|
||||
|
||||
assert.strictEqual(await mod.resolveCrmLocale("person@example.com"), null);
|
||||
});
|
||||
|
||||
test("pre-auth CRM locale lookup escapes OData apostrophes", async () => {
|
||||
let queryUrl;
|
||||
const mod = loadLocaleResolver({
|
||||
relayGetData: async (request) => {
|
||||
queryUrl = request.queryUrl;
|
||||
return { data: { value: [] } };
|
||||
}
|
||||
});
|
||||
|
||||
await mod.resolveCrmLocale("o'connor@example.com");
|
||||
|
||||
assert.match(queryUrl, /o''connor@example\.com/);
|
||||
});
|
||||
|
||||
test("public sign-in locale route preserves response without CRM lookup", async () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(rootDir, "pages/api/auth/resolve-locale.js"),
|
||||
"utf8"
|
||||
);
|
||||
const mod = loadModule("pages/api/auth/resolve-locale.js");
|
||||
const res = createRes();
|
||||
|
||||
await mod.default(
|
||||
{
|
||||
method: "POST",
|
||||
body: { email: "other@example.com", locale: "cy" },
|
||||
query: {},
|
||||
cookies: {}
|
||||
},
|
||||
res
|
||||
);
|
||||
|
||||
assert.strictEqual(res.state.statusCode, 200);
|
||||
assert.deepStrictEqual(normalize(res.state.jsonBody), { locale: "cy" });
|
||||
assert.doesNotMatch(source, /getPortalLogin|resolveCrmLocale|relayGetData/);
|
||||
});
|
||||
|
||||
test("both pre-auth callers no longer depend on getPortalLogin", () => {
|
||||
const nextAuthSource = fs.readFileSync(
|
||||
path.join(rootDir, "pages/api/auth/[...nextauth].js"),
|
||||
"utf8"
|
||||
);
|
||||
const routeSource = fs.readFileSync(
|
||||
path.join(rootDir, "pages/api/auth/resolve-locale.js"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
assert.doesNotMatch(nextAuthSource, /getPortalLogin/);
|
||||
assert.doesNotMatch(routeSource, /getPortalLogin/);
|
||||
assert.match(nextAuthSource, /resolveCrmLocale/);
|
||||
});
|
||||
|
||||
const run = async () => {
|
||||
let passed = 0;
|
||||
|
||||
for (const currentTest of tests) {
|
||||
await currentTest.fn();
|
||||
passed += 1;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Phase 22 pre-auth locale tests passed (${passed}/${tests.length}).`
|
||||
);
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
run().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = run;
|
||||
@@ -0,0 +1,157 @@
|
||||
const assert = require("assert");
|
||||
const {
|
||||
loadModule,
|
||||
createRes,
|
||||
createNextConnectMock,
|
||||
respondSuccessMock,
|
||||
respondErrorMock
|
||||
} = require("../phase21/_shared.cjs");
|
||||
|
||||
const tests = [];
|
||||
const test = (name, fn) => tests.push({ name, fn });
|
||||
|
||||
const loadRepresentationList = ({ session = null, administrator = false } = {}) => {
|
||||
const calls = { list: [], download: [] };
|
||||
const mod = loadModule("pages/api/file/getrepsblob.js", {
|
||||
nextConnect: createNextConnectMock(),
|
||||
middleware: () => {},
|
||||
getServerSession: async () => session,
|
||||
authOptions: () => ({}),
|
||||
readAdminSessionCookie: () =>
|
||||
administrator ? "valid-admin-session" : null,
|
||||
validateAdminSession: () => administrator,
|
||||
hashAPIPath: () => "&hash=expected",
|
||||
respondError: respondErrorMock,
|
||||
respondSuccess: respondSuccessMock,
|
||||
consoleLogger: () => {},
|
||||
getRepsBlobs: async (...args) => {
|
||||
calls.list.push(args);
|
||||
return [{ path: "CASE/REP_rep.json" }];
|
||||
},
|
||||
downloadAllRepsFiles: async (...args) => {
|
||||
calls.download.push(args);
|
||||
return { value: [{ id: "rep-1" }] };
|
||||
}
|
||||
});
|
||||
|
||||
return { mod, calls };
|
||||
};
|
||||
|
||||
const representationListRequest = (query = {}) => ({
|
||||
query: {
|
||||
container: "organisation-container",
|
||||
hash: "expected",
|
||||
...query
|
||||
},
|
||||
headers: {}
|
||||
});
|
||||
|
||||
test("representation listing rejects a missing session before Azure access", async () => {
|
||||
const loaded = loadRepresentationList();
|
||||
const res = createRes();
|
||||
|
||||
await loaded.mod.default.handler(representationListRequest(), res);
|
||||
|
||||
assert.strictEqual(res.state.statusCode, 403);
|
||||
assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN");
|
||||
assert.strictEqual(loaded.calls.list.length, 0);
|
||||
assert.strictEqual(loaded.calls.download.length, 0);
|
||||
});
|
||||
|
||||
test("representation listing rejects cross-user container access before Azure access", async () => {
|
||||
const loaded = loadRepresentationList({
|
||||
session: { user: { id: "different-container" } }
|
||||
});
|
||||
const res = createRes();
|
||||
|
||||
await loaded.mod.default.handler(representationListRequest(), res);
|
||||
|
||||
assert.strictEqual(res.state.statusCode, 403);
|
||||
assert.strictEqual(loaded.calls.list.length, 0);
|
||||
assert.strictEqual(loaded.calls.download.length, 0);
|
||||
});
|
||||
|
||||
test("representation listing preserves authorised organisation-container access", async () => {
|
||||
const loaded = loadRepresentationList({
|
||||
session: { user: { id: "organisation-container" } }
|
||||
});
|
||||
const res = createRes();
|
||||
|
||||
await loaded.mod.default.handler(representationListRequest(), res);
|
||||
|
||||
assert.strictEqual(res.state.statusCode, 200);
|
||||
assert.deepStrictEqual(loaded.calls.list, [["organisation-container"]]);
|
||||
assert.strictEqual(loaded.calls.download.length, 1);
|
||||
});
|
||||
|
||||
test("representation listing preserves existing administrative access", async () => {
|
||||
const loaded = loadRepresentationList({ administrator: true });
|
||||
const res = createRes();
|
||||
|
||||
await loaded.mod.default.handler(representationListRequest(), res);
|
||||
|
||||
assert.strictEqual(res.state.statusCode, 200);
|
||||
assert.strictEqual(loaded.calls.list.length, 1);
|
||||
});
|
||||
|
||||
test("representation listing rejects a missing identifier before authentication or Azure access", async () => {
|
||||
let sessionCalls = 0;
|
||||
const loaded = loadRepresentationList({
|
||||
session: { user: { id: "organisation-container" } }
|
||||
});
|
||||
loaded.mod = loadModule("pages/api/file/getrepsblob.js", {
|
||||
nextConnect: createNextConnectMock(),
|
||||
middleware: () => {},
|
||||
getServerSession: async () => {
|
||||
sessionCalls += 1;
|
||||
return { user: { id: "organisation-container" } };
|
||||
},
|
||||
authOptions: () => ({}),
|
||||
readAdminSessionCookie: () => null,
|
||||
validateAdminSession: () => false,
|
||||
hashAPIPath: () => "&hash=expected",
|
||||
respondError: respondErrorMock,
|
||||
respondSuccess: respondSuccessMock,
|
||||
consoleLogger: () => {},
|
||||
getRepsBlobs: async () => {
|
||||
loaded.calls.list.push([]);
|
||||
return [];
|
||||
},
|
||||
downloadAllRepsFiles: async () => {
|
||||
loaded.calls.download.push([]);
|
||||
return {};
|
||||
}
|
||||
});
|
||||
const res = createRes();
|
||||
|
||||
await loaded.mod.default.handler(
|
||||
representationListRequest({ container: "" }),
|
||||
res
|
||||
);
|
||||
|
||||
assert.strictEqual(res.state.statusCode, 400);
|
||||
assert.strictEqual(res.state.jsonBody.error.code, "MISSING_REQUIRED_QUERY");
|
||||
assert.strictEqual(sessionCalls, 0);
|
||||
assert.strictEqual(loaded.calls.list.length, 0);
|
||||
assert.strictEqual(loaded.calls.download.length, 0);
|
||||
});
|
||||
|
||||
const run = async () => {
|
||||
let passed = 0;
|
||||
for (const currentTest of tests) {
|
||||
await currentTest.fn();
|
||||
passed += 1;
|
||||
}
|
||||
console.log(
|
||||
`Representation listing ownership tests passed (${passed}/${tests.length}).`
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = run;
|
||||
|
||||
if (require.main === module) {
|
||||
run().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -65,6 +65,93 @@ test("document/getAwaitingSubmissionFromBlob logs and returns undefined on failu
|
||||
assert.strictEqual(logger.calls.length, 1);
|
||||
});
|
||||
|
||||
test("document/getRepsFromBlob forwards cookies only to trusted origin without redirects", async () => {
|
||||
const axios = createAxiosMock();
|
||||
const logger = createLoggerMock();
|
||||
|
||||
axios.getHandler = async () => ({
|
||||
data: { value: ["representation"] }
|
||||
});
|
||||
|
||||
const document = loadServiceModule("documentDirectService.js", {
|
||||
axios,
|
||||
BASE_URL: "http://example.local",
|
||||
consoleLogger: logger.consoleLogger,
|
||||
hashAPIPath: () => "&hash=abc123"
|
||||
});
|
||||
|
||||
const result = await document.getRepsFromBlob("container-1", {
|
||||
headers: {
|
||||
Cookie: "next-auth.session-token=test-session"
|
||||
}
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(normalize(result), {
|
||||
value: ["representation"]
|
||||
});
|
||||
|
||||
assert.strictEqual(axios.calls.length, 1);
|
||||
assert.strictEqual(
|
||||
axios.calls[0].url,
|
||||
"http://example.local/api/file/getrepsblob?container=container-1&hash=abc123"
|
||||
);
|
||||
assert.strictEqual(
|
||||
axios.calls[0].config.headers.Cookie,
|
||||
"next-auth.session-token=test-session"
|
||||
);
|
||||
assert.strictEqual(axios.calls[0].config.maxRedirects, 0);
|
||||
assert.strictEqual(logger.calls.length, 0);
|
||||
});
|
||||
|
||||
test("document/getRepsFromBlob rejects untrusted origin before HTTP request", async () => {
|
||||
const axios = createAxiosMock();
|
||||
const logger = createLoggerMock();
|
||||
|
||||
const document = loadServiceModule("documentDirectService.js", {
|
||||
axios,
|
||||
BASE_URL: "https://untrusted.example",
|
||||
consoleLogger: logger.consoleLogger,
|
||||
hashAPIPath: () => "&hash=abc123"
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
document.getRepsFromBlob("container-1", {
|
||||
headers: {
|
||||
Cookie: "next-auth.session-token=test-session"
|
||||
}
|
||||
}),
|
||||
/Untrusted representation request destination/
|
||||
);
|
||||
|
||||
assert.strictEqual(axios.calls.length, 0);
|
||||
assert.strictEqual(logger.calls.length, 0);
|
||||
});
|
||||
|
||||
test("document/getRepsFromBlob preserves requests without forwarded cookies", async () => {
|
||||
const axios = createAxiosMock();
|
||||
const logger = createLoggerMock();
|
||||
|
||||
axios.getHandler = async () => ({
|
||||
data: { value: ["representation"] }
|
||||
});
|
||||
|
||||
const document = loadServiceModule("documentDirectService.js", {
|
||||
axios,
|
||||
BASE_URL: "http://example.local",
|
||||
consoleLogger: logger.consoleLogger,
|
||||
hashAPIPath: () => "&hash=abc123"
|
||||
});
|
||||
|
||||
const result = await document.getRepsFromBlob("container-1");
|
||||
|
||||
assert.deepStrictEqual(normalize(result), {
|
||||
value: ["representation"]
|
||||
});
|
||||
assert.strictEqual(axios.calls.length, 1);
|
||||
assert.strictEqual(axios.calls[0].config, undefined);
|
||||
assert.strictEqual(logger.calls.length, 0);
|
||||
});
|
||||
|
||||
test("portal/deleteWatchedCases uses signer hash and returns data", async () => {
|
||||
const axios = createAxiosMock();
|
||||
const logger = createLoggerMock();
|
||||
|
||||
@@ -140,7 +140,7 @@ test("createappealcompletemessage_api rejects invalid hash with 400", async () =
|
||||
createBlob: async () => {},
|
||||
getCaseBlob: async () => {},
|
||||
createCaseCompleteMessage: () => {},
|
||||
updateAccount: async () => {},
|
||||
updateCrmContact: async () => {},
|
||||
_: { isEmpty: (value) => !value },
|
||||
nextConnect,
|
||||
middleware: () => {}
|
||||
|
||||
@@ -89,8 +89,8 @@ const loadServiceModule = (fileName, injected = {}) => {
|
||||
return injected.axios(config).then((response) => response.data);
|
||||
};
|
||||
|
||||
const defaultGetFileJson = (url) => {
|
||||
return defaultGetJson(url);
|
||||
const defaultGetFileJson = (url, config) => {
|
||||
return defaultGetJson(url, config);
|
||||
};
|
||||
|
||||
const defaultBuildHashedQueryUrl = async (queryUrl) => {
|
||||
@@ -207,8 +207,14 @@ const loadServiceModule = (fileName, injected = {}) => {
|
||||
module: { exports: {} },
|
||||
exports: {},
|
||||
require,
|
||||
URL,
|
||||
URLSearchParams,
|
||||
encodeURIComponent,
|
||||
process: injected.process || {
|
||||
env: {
|
||||
NEXTAUTH_URL: "http://example.local"
|
||||
}
|
||||
},
|
||||
FormData: global.FormData,
|
||||
console: {
|
||||
log: () => {},
|
||||
|
||||
Reference in New Issue
Block a user