diff --git a/.gitignore b/.gitignore index e2c776d2..2f0cba51 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ pages/polling.js # dev apis to no include pages/api/endpoint/getaccounts_api.js + +# certs +certificates/* diff --git a/actions/index.js b/actions/index.js index 20cdbef0..3e43f62d 100644 --- a/actions/index.js +++ b/actions/index.js @@ -2144,3 +2144,19 @@ export const createCRMTask = async (formValues) => { consoleLogger(error); } }; + +export const getPreferredLanguage = async (email) => { + return axios + .get( + BASE_URL + + "/api/endpoint/getpreferredlanguage_api?emailAddress=" + + email + ) + .then((res) => { + return res.data; + }) + .catch((error) => { + consoleLogger(error); + return error.response; + }); +}; diff --git a/components/account/personaldetailsComplete.js b/components/account/personaldetailsComplete.js index 533de234..3b7dde4d 100644 --- a/components/account/personaldetailsComplete.js +++ b/components/account/personaldetailsComplete.js @@ -4,6 +4,7 @@ import { useRouter } from "next/router"; import React, { useEffect } from "react"; import { connect } from "react-redux"; import { updateAccount } from "../../actions"; +import { setCookie } from "nookies"; const PersonalDetailsComplete = (props) => { const { @@ -17,14 +18,39 @@ const PersonalDetailsComplete = (props) => { const router = useRouter(); const { locale } = router; + // let contactid = props.accountDetails.loggedinUserId; + // let updateBody = props.props.props.form.personalDetailsForm.values; + // let prefLang = + // props.props.props.form.personalDetailsForm.values + // .pinswg_preferredlanguage; + + const { loggedinUserId } = props.accountDetails || {}; + const formValues = props.props?.props?.form?.personalDetailsForm?.values; + const prefLang = formValues?.pinswg_preferredlanguage; + useEffect(() => { - let contactid = props.accountDetails.loggedinUserId; - let updateBody = props.props.props.form.personalDetailsForm.values; - updateAccount(contactid, updateBody); - }, [ - props.accountDetails.loggedinUserId, - props.props.props.form.personalDetailsForm.values, - ]); + // let contactid = props.accountDetails.loggedinUserId; + // let updateBody = props.props.props.form.personalDetailsForm.values; + // let prefLang = + // props.props.props.form.personalDetailsForm.values + // .pinswg_preferredlanguage; + if (!loggedinUserId || !formValues) return; + + console.log("=============== pref lang", prefLang); + const userLang = prefLang == "846040000" ? "cy" : "en"; + setCookie(null, "pedw_locale", userLang, { + path: "/", + maxAge: 60 * 60 * 24 * 365, + }); + + console.log("Set cookie to:", userLang); + + async function doUpdate() { + const response = await updateAccount(loggedinUserId, formValues); + } + + doUpdate(); + }, [loggedinUserId, formValues, prefLang]); return ( <> @@ -33,10 +59,7 @@ const PersonalDetailsComplete = (props) => { {/*

{t("account:account-complete-para-2")}

*/}

- + {" "} {t("account:account-complete-return-to-portal-link")} diff --git a/pages/api/auth/[...nextauth].js b/pages/api/auth/[...nextauth].js index 2f5616e6..c52977ff 100644 --- a/pages/api/auth/[...nextauth].js +++ b/pages/api/auth/[...nextauth].js @@ -14,80 +14,9 @@ import { PrismaAdapter } from "@next-auth/prisma-adapter"; import { PrismaClient } from "@prisma/client"; import NextAuth from "next-auth"; import EmailProvider from "next-auth/providers/email"; -import { consoleLogger } from "../../../actions"; +import { consoleLogger, getPreferredLanguage } from "../../../actions"; const prisma = new PrismaClient(); -// For more information on each option (and a full list of options) go to -// https://next-auth.js.org/configuration/options -// export default NextAuth({ -// // https://next-auth.js.org/configuration/providers -// providers: [ -// EmailProvider({ -// // server: { -// // host: process.env.EMAIL_SERVER_HOST, -// // port: process.env.EMAIL_SERVER_PORT, -// // auth: { -// // user: process.env.EMAIL_SERVER_USER, -// // pass: process.env.EMAIL_SERVER_PASSWORD, -// // }, -// // }, -// // from: process.env.EMAIL_FROM, -// maxAge: 10 * 60, // Magic links are valid for 10 min only -// async sendVerificationRequest({ -// identifier: email, -// url, -// provider: { server, from }, -// }) { -// const { host } = new URL(url); -// const templateId = "b1b5704b-9bb8-4deb-a75c-d887ca902661"; -// const emailAddress = email; -// const personalisation = { -// "emailAddress": email, -// "signInLink": url, -// "linkExpiry": 10 * 60, -// }; -// const reference = "PEDW-SIGNIN"; - -// var NotifyClient = -// require("notifications-node-client").NotifyClient; - -// const notifyClient = new NotifyClient( -// process.env.NOTIFY_API_KEY -// ); - -// await notifyClient -// .sendEmail(templateId, emailAddress, { -// personalisation: personalisation, -// reference: reference, -// // emailReplyToId: emailReplyToId, -// }) -// //.then((response) => console.log(response)) -// .catch((error) => consoleLogger(err)); -// }, -// }), -// ], -// adapter: PrismaAdapter(prisma), -// secret: process.env.SECRET, -// session: { -// jwt: true, -// pages: {}, -// theme: "dark", -// debug: true, -// }, -// pages: { -// signIn: "/auth/signin", -// //signOut: "/auth/signout", -// error: "/auth/error", // Error code passed in query string as ?error= -// verifyRequest: "/auth/verify-request", // (used for check email message) -// newUser: "/account/register", // New users will be directed here on first sign in (leave the property out if not of interest) -// }, -// callbacks: { -// session: async (session, user) => { -// //console.log(user); -// return Promise.resolve(session); -// }, -// }, -// }); const parseUrl = (url) => { const defaultUrl = new URL( @@ -169,7 +98,9 @@ const authOptions = (locale, req, res) => { "&token=" + searchParams.get("token") + "&email=" + - encodeURIComponent(email) + encodeURIComponent(email) + + "&locale=" + + effectiveLocale : url; let formURL = appendParamsAndPathToNewUrl(url, newURL); @@ -191,23 +122,43 @@ const authOptions = (locale, req, res) => { const { host, port, protocol, token, searchParams } = new URL(url); + const baseDomain = `${protocol}//${host}`; const templateId = "b1b5704b-9bb8-4deb-a75c-d887ca902661"; const templateIdcy = "0614ce53-cd5f-421f-a1a5-8c8486a9113a"; const emailAddress = email; + + const queryLocale = req?.query?.locale || req?.body?.locale; + const cookieLocale = req.cookies["pedw_locale"]; + let locale = queryLocale || cookieLocale || "en"; + console.log( "============================================================\n", "verification url----: " + url + "\n", "============================================================\n" ); + + const prefLang = await getPreferredLanguage(emailAddress); + + const registeredPrefLanguage = + prefLang.value.length > 0 + ? prefLang.value[0].pinswg_preferredlanguage === + 846040000 + ? "cy" + : "en" + : "en"; + + const effectiveLocale = registeredPrefLanguage ?? locale; let newURL = - locale == "cy" - ? process.env.CY_API_ROOT + + effectiveLocale == "cy" + ? baseDomain + "/api/auth/callback/email?callbackUrl=" + - encodeURIComponent(process.env.CY_API_ROOT) + + encodeURIComponent(baseDomain + "/cy") + "&token=" + searchParams.get("token") + "&email=" + - encodeURIComponent(email) + encodeURIComponent(email) + + "&locale=" + + effectiveLocale : url; let formURL = appendParamsAndPathToNewUrl(url, newURL); @@ -240,7 +191,7 @@ const authOptions = (locale, req, res) => { await notifyClient .sendEmail( - locale == "cy" ? templateIdcy : templateId, + effectiveLocale == "cy" ? templateIdcy : templateId, emailAddress, { personalisation: personalisation, diff --git a/pages/api/endpoint/getpreferredlanguage_api.js b/pages/api/endpoint/getpreferredlanguage_api.js new file mode 100644 index 00000000..5744db1e --- /dev/null +++ b/pages/api/endpoint/getpreferredlanguage_api.js @@ -0,0 +1,73 @@ +/** + * @swagger + * /api/endpoint/getemailaccountcheck_api: + * get: + * tags: [Login] + * description: Returns portal login + * summary: Phase 2 + * parameters: + * - name: emailAddress + * in: query + * required: true + * schema: + * type: string + * responses: + * 200: + * description: hello world + */ +import axios from "axios"; +import CryptoJS from "crypto-js"; +import _ from "lodash"; +import { + azureHeaders, + consoleLogger, + getToken, + getPreferredLanguage, +} from "../../../actions"; + +const WORDKEY = process.env.HASHKEY; + +const WEBAPI_URL = + process.env.RELAY_ROOT || + "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; + +const hashAPIPath = (queryPath) => { + var hashlink = CryptoJS.HmacSHA256( + queryPath, + CryptoJS.enc.Hex.parse(WORDKEY) + ); + hashlink = hashlink.toString(CryptoJS.enc.Hex); + return (queryPath.indexOf("?") > -1 ? "&hash=" : "?hash=") + hashlink; +}; + +export default async function ApiProxy(req, res) { + var emailAddress = req.query.emailAddress; + var token = await getToken(); + + var queryUrl = + "contacts?$filter=emailaddress1 eq '" + + emailAddress + + "'&$count=true&$select=pinswg_preferredlanguage,contactid"; + + console.log(queryUrl); + // var queryUrl = "contacts/?$count=true&$select=emailaddress1, contactid"; + + var apiResponse = _.isEmpty(req.query) + ? res.status(400).json() + : typeof emailAddress != "undefined" && emailAddress.length > 0 + ? axios + .get( + WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), + azureHeaders(token.access_token) + ) + .then(({ data }) => { + res.status(200).json(data); + }) + .catch((error) => { + consoleLogger(error); + res.status(400).json(error); + }) + : res.status(400).json(); + + return apiResponse; +} diff --git a/pages/auth/error.js b/pages/auth/error.js index 6ed03279..668a9d21 100644 --- a/pages/auth/error.js +++ b/pages/auth/error.js @@ -7,6 +7,8 @@ import CookieBanner from "../../components/cookieBanner"; import Footer from "../../components/footer"; import Header from "../../components/header"; import ServiceBanner from "../../components/servicebanner"; +import { destroyCookie, setCookie } from "nookies"; +import { useEffect } from "react"; import { getCsrfToken } from "next-auth/react"; @@ -16,6 +18,14 @@ const Error = (props) => { const { error } = useRouter().query; + useEffect(() => { + window.localStorage.clear(), + destroyCookie(null, "next-auth.csrf-token", { path: "/" }), + destroyCookie(null, "next-auth.callback-url", { path: "/" }), + destroyCookie(null, "pedw_locale", { path: "/" }), + destroyCookie(null, "pinsUser", { path: "/" }); + }, []); + const errors = { Signin: t("auth:auth-error-Signin-label"), OAuthSignin: t("auth:auth-error-OAuthSignin-label"), diff --git a/pages/auth/signin.js b/pages/auth/signin.js index 747c76cb..c107aa8d 100644 --- a/pages/auth/signin.js +++ b/pages/auth/signin.js @@ -7,6 +7,8 @@ import CookieBanner from "../../components/cookieBanner"; import Footer from "../../components/footer"; import Header from "../../components/header"; import ServiceBanner from "../../components/servicebanner"; +import { getPreferredLanguage } from "../../actions"; +import { setCookie } from "nookies"; const SignIn = (props) => { let { t, lang } = useTranslation(); @@ -25,6 +27,35 @@ const SignIn = (props) => { const handleSubmit = async (event) => { event.preventDefault(); setButtonDisabled(true); + + const email = event.target.email.value; + + try { + const res = await getPreferredLanguage(encodeURIComponent(email)); + const data = await res; + + const userLang = + data.value.length > 0 + ? data.value[0].pinswg_preferredlanguage === 846040000 + ? "cy" + : "en" + : "en"; + + // Modify callbackUrl hidden input to add/update lang param + const url = new URL(event.target.callbackUrl.value); + url.searchParams.set("locale", userLang); + + setCookie(null, "pedw_locale", userLang, { + path: "/", + maxAge: 60 * 60 * 24 * 365, + }); + + event.target.callbackUrl.value = url.toString(); + } catch (error) { + console.error("Failed to get preferred language", error); + } + + // Submit form after updating callbackUrl event.target.submit(); }; diff --git a/pages/contactus.js b/pages/contactus.js index 7de8951d..25c8f333 100644 --- a/pages/contactus.js +++ b/pages/contactus.js @@ -15,7 +15,7 @@ import { touch, SubmissionError, } from "redux-form"; -import { connect, submitError } from "react-redux"; +import { connect } from "react-redux"; import { createCRMTask, sendEmail } from "../actions"; import { useState } from "react"; @@ -527,7 +527,7 @@ export default connect( destroyOnUnmount: false, touchOnBlur: true, touchOnChange: true, - onSubmitFail: (errors, dispatch, subnittError, props) => { + onSubmitFail: (errors, dispatch, submitError, props) => { if (errors) { // Touch all fields with errors so validation messages appear const errorFields = Object.keys(errors); diff --git a/pages/myportal/contactus.js b/pages/myportal/contactus.js index 5c4007ab..b503ac2d 100644 --- a/pages/myportal/contactus.js +++ b/pages/myportal/contactus.js @@ -15,7 +15,7 @@ import { touch, SubmissionError, } from "redux-form"; -import { connect, submitError } from "react-redux"; +import { connect } from "react-redux"; import { createCRMTask, sendEmail } from "../../actions"; import { useState } from "react"; @@ -528,7 +528,7 @@ export default connect( destroyOnUnmount: false, touchOnBlur: true, touchOnChange: true, - onSubmitFail: (errors, dispatch, subnittError, props) => { + onSubmitFail: (errors, dispatch, submitError, props) => { if (errors) { // Touch all fields with errors so validation messages appear const errorFields = Object.keys(errors); diff --git a/pages/myportal/index.js b/pages/myportal/index.js index 5ba7fb94..a6a3f242 100644 --- a/pages/myportal/index.js +++ b/pages/myportal/index.js @@ -222,9 +222,7 @@ export const getServerSideProps = wrapper.getServerSideProps( // Determine preferred locale (cookie overrides) let preferredLocale = - accountDetails.pinswg_preferredlanguage === "846040000" - ? "cy" - : "en"; + accountDetails.pinswg_preferredlanguage === 846040000 ? "cy" : "en"; if (cookies.pedw_locale) { preferredLocale = cookies.pedw_locale; } diff --git a/server.js b/server.js index bd36f333..60d82f33 100644 --- a/server.js +++ b/server.js @@ -1,5 +1,8 @@ const next = require("next"); -const { createServer } = require("http"); +const { createServer: createHttpServer } = require("http"); +const { createServer: createHttpsServer } = require("https"); +const fs = require("fs"); +const path = require("path"); const appInsights = require("applicationinsights"); const initAppInsights = (instrumentationKey) => { @@ -26,44 +29,75 @@ const startServer = async (config) => { }; const app = next(serverOptions); - await app.prepare(); // prepare BEFORE starting the server + await app.prepare(); const handleNextRequests = app.getRequestHandler(); - const srv = createServer((req, res) => { + const httpsOptions = { + key: fs.readFileSync( + path.join(__dirname, "certificates", "cert-key.pem") + ), + cert: fs.readFileSync(path.join(__dirname, "certificates", "cert.pem")), + }; + + // HTTPS server on port 3000 + const httpsServer = createHttpsServer(httpsOptions, (req, res) => { if (config.useAppInsights) { appInsights.defaultClient.trackNodeHttpRequest({ request: req, response: res, }); } - handleNextRequests(req, res); }); - await new Promise((resolve, reject) => { - srv.on("error", reject); - srv.on("listening", () => resolve()); - srv.listen(config.port, config.hostname); + // HTTP server on port 3001 for redirect + const httpServer = createHttpServer((req, res) => { + const host = req.headers.host?.split(":")[0] || "localhost"; + res.writeHead(301, { + Location: `https://${host}:3000${req.url}`, + }); + res.end(); + }); + + await new Promise((resolve, reject) => { + httpsServer.on("error", reject); + httpsServer.on("listening", () => { + console.log(`HTTPS server listening on port ${config.port}`); + resolve(); + }); + httpsServer.listen(config.port, config.hostname); + }); + + await new Promise((resolve, reject) => { + httpServer.on("error", reject); + httpServer.on("listening", () => { + console.log( + `HTTP server listening on port ${config.httpPort} (redirecting to HTTPS)` + ); + resolve(); + }); + httpServer.listen(config.httpPort, config.hostname); }); - // Graceful shutdown handling process.on("SIGTERM", () => { console.log("Received SIGTERM, shutting down gracefully..."); - srv.close(() => { - console.log("Server closed."); - process.exit(0); + httpsServer.close(() => { + httpServer.close(() => { + console.log("Servers closed."); + process.exit(0); + }); }); }); return app; }; -// App configuration const startTime = Date.now(); const serverConfig = { - hostname: "0.0.0.0", // Required for Azure App Service - port: parseInt(process.env.PORT, 10) || 3000, + hostname: "0.0.0.0", + port: 3000, // HTTPS port + httpPort: 3001, // HTTP redirect port env: process.env.APP_ENV || process.env.NODE_ENV || "production", useAppInsights: initAppInsights( process.env.NEXT_PUBLIC_APPINSIGHTS_INSTRUMENTATIONKEY @@ -73,7 +107,7 @@ const serverConfig = { startServer(serverConfig) .then(() => { console.log( - `Server ready on http://${serverConfig.hostname}:${serverConfig.port} [${serverConfig.env}]` + `Server ready on https://${serverConfig.hostname}:${serverConfig.port} [${serverConfig.env}]` ); if (serverConfig.useAppInsights) {