diff --git a/apps/scandic-web/.env.local.example b/apps/scandic-web/.env.local.example index 66a471991..905167b2c 100644 --- a/apps/scandic-web/.env.local.example +++ b/apps/scandic-web/.env.local.example @@ -73,4 +73,8 @@ SAS_AUTH_CLIENTID="" LOKALISE_API_KEY="" +DTMC_ENTRA_ID_CLIENT="" +DTMC_ENTRA_ID_ISSUER="" +DTMC_ENTRA_ID_SECRET="" + CAMPAIGN_PAGES_ENABLED="0" # 0 - disabled, 1 - enabled diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/dtmc/route.ts b/apps/scandic-web/app/[lang]/(live)/(public)/dtmc/route.ts new file mode 100644 index 000000000..de09e6690 --- /dev/null +++ b/apps/scandic-web/app/[lang]/(live)/(public)/dtmc/route.ts @@ -0,0 +1,45 @@ +import { NextResponse } from "next/server" +import { AuthError } from "next-auth" + +import { dtmcApiCallback } from "@/constants/routes/dtmc" +import { env } from "@/env/server" +import { internalServerError, serviceUnavailable } from "@/server/errors/next" + +import { signIn } from "@/auth.dtmc" + +export async function GET() { + try { + const redirectUrl = await signIn( + "microsoft-entra-id", + { + redirectTo: `${env.PUBLIC_URL}${dtmcApiCallback}`, + redirect: false, + }, + { + prompt: "login", + } + ) + + if (redirectUrl) { + console.log(`[dtmc] redirecting to: ${redirectUrl}`) + return NextResponse.redirect(redirectUrl) + } else { + console.error(`[dtmc] missing redirectUrl response from signIn()`) + return internalServerError( + "[dtmc] Missing redirect URL from authentication service" + ) + } + } catch (error) { + if (error instanceof AuthError) { + console.error({ signInAuthError: error }) + return serviceUnavailable( + "[dtmc] Microsoft authentication service unavailable" + ) + } else { + console.error({ signInError: error }) + return internalServerError( + "[dtmc] Unexpected error during authentication" + ) + } + } +} diff --git a/apps/scandic-web/app/[lang]/(no-layout)/(protected)/link-employment/linkEmploymentPage.module.css b/apps/scandic-web/app/[lang]/(no-layout)/(protected)/link-employment-error/linkEmploymentError.module.css similarity index 84% rename from apps/scandic-web/app/[lang]/(no-layout)/(protected)/link-employment/linkEmploymentPage.module.css rename to apps/scandic-web/app/[lang]/(no-layout)/(protected)/link-employment-error/linkEmploymentError.module.css index ac9d8e8e1..5b6c33bc2 100644 --- a/apps/scandic-web/app/[lang]/(no-layout)/(protected)/link-employment/linkEmploymentPage.module.css +++ b/apps/scandic-web/app/[lang]/(no-layout)/(protected)/link-employment-error/linkEmploymentError.module.css @@ -20,7 +20,7 @@ } .nav { - padding: var(--Spacing-x2); + padding: var(--Space-x2); display: flex; align-items: center; justify-content: space-between; @@ -50,8 +50,8 @@ display: flex; align-items: center; justify-content: center; - padding-left: var(--Spacing-x3); - padding-right: var(--Spacing-x3); + padding-left: var(--Space-x3); + padding-right: var(--Space-x3); } .card { @@ -62,25 +62,25 @@ padding: var(--Spacing-x5); border-radius: var(--Corner-radius-lg); display: grid; - gap: var(--Spacing-x3); + gap: var(--Space-x3); } .formElements { display: flex; flex-direction: column; - gap: var(--Spacing-x3); + gap: var(--Space-x3); } .checkboxContainer { display: flex; flex-direction: column; - gap: var(--Spacing-x1); + gap: var(--Space-x1); } .checkboxWrapper { display: flex; align-items: center; - gap: var(--Spacing-x-one-and-half); + gap: var(--Space-x15); cursor: pointer; } @@ -110,7 +110,7 @@ } .termsTextFull { - padding-left: var(--Spacing-x5); + padding-left: var(--Space-x5); } .link { @@ -118,3 +118,11 @@ text-decoration: underline; font-weight: var(--Font-weight-Bold); } + +.contactSection { + padding-top: var(--Space-x3); + gap: var(--Space-x15); + display: flex; + flex-direction: column; + place-items: center; +} diff --git a/apps/scandic-web/app/[lang]/(no-layout)/(protected)/link-employment-error/page.tsx b/apps/scandic-web/app/[lang]/(no-layout)/(protected)/link-employment-error/page.tsx new file mode 100644 index 000000000..a27a6a700 --- /dev/null +++ b/apps/scandic-web/app/[lang]/(no-layout)/(protected)/link-employment-error/page.tsx @@ -0,0 +1,146 @@ +"use client" + +import Image from "next/image" +import { useSearchParams } from "next/navigation" +import { type IntlShape, useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import ScandicLogoIcon from "@scandic-hotels/design-system/Icons/ScandicLogoIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { supportEmail, supportPhone } from "@/constants/contactSupport" +import { employeeBenefits } from "@/constants/routes/dtmc" + +import ButtonLink from "@/components/ButtonLink" +import Link from "@/components/TempDesignSystem/Link" +import useLang from "@/hooks/useLang" +import background from "@/public/_static/img/Scandic_Computer_Coffee.png" + +import styles from "./linkEmploymentError.module.css" + +export default function LinkEmploymentErrorPage() { + const lang = useLang() + const intl = useIntl() + const searchParams = useSearchParams() + + const error = searchParams.get("error") + const errorContent = getErrorContent(error, intl) + + return ( +
+ + +
+ + +
+
+ +

{errorContent.heading}

+
+ + +

{errorContent.message}

+
+
+ +

+ {intl.formatMessage({ + defaultMessage: "Contact our member service", + })} +

+
+ + + {supportPhone[lang]} + + + + + {supportEmail[lang]} + + +
+
+
+
+
+ ) +} + +const getErrorContent = (error: string | null, intl: IntlShape) => { + const defaultErrorContent = { + heading: intl.formatMessage({ + defaultMessage: "Your account could not be connected", + }), + message: intl.formatMessage({ + defaultMessage: + "We could not connect your accounts to give you access. Please contact us and we'll help you resolve this issue.", + }), + } + + switch (error) { + case "unable_to_verify_employee_id": + return { + heading: intl.formatMessage({ + defaultMessage: "You're not eligible for employee benefits", + }), + message: intl.formatMessage({ + defaultMessage: + "This may be because your employment has not yet started, has ended, or you are a consultant. If you believe this is an error, please contact us for assistance.", + }), + } + case "employee_id_already_linked": + return { + heading: intl.formatMessage({ + defaultMessage: + "Employee number already linked to another Scandic Friends membership.", + }), + message: intl.formatMessage({ + defaultMessage: + "If you believe this is an error, please contact us for assistance.", + }), + } + case "missing_employee_id_profile": + case "missing_employee_id": + case "no_session": + return defaultErrorContent + default: + return defaultErrorContent + } +} diff --git a/apps/scandic-web/app/[lang]/(no-layout)/(protected)/link-employment/page.tsx b/apps/scandic-web/app/[lang]/(no-layout)/(protected)/link-employment/page.tsx deleted file mode 100644 index d4626cfbb..000000000 --- a/apps/scandic-web/app/[lang]/(no-layout)/(protected)/link-employment/page.tsx +++ /dev/null @@ -1,139 +0,0 @@ -"use client" - -import Image from "next/image" -import { useState } from "react" -import { Checkbox as AriaCheckbox } from "react-aria-components" -import { useIntl } from "react-intl" - -import { Button } from "@scandic-hotels/design-system/Button" -import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" -import ScandicLogoIcon from "@scandic-hotels/design-system/Icons/ScandicLogoIcon" -import { Typography } from "@scandic-hotels/design-system/Typography" - -import { employeeBenefits } from "@/constants/routes/dtmc" - -import ButtonLink from "@/components/ButtonLink" -import useLang from "@/hooks/useLang" - -import styles from "./linkEmploymentPage.module.css" - -export default function LinkEmploymentPage() { - const lang = useLang() - const intl = useIntl() - const [isChecked, setIsChecked] = useState(false) - const linkMyEmploymentText = intl.formatMessage({ - defaultMessage: "Link my employment", - }) - - return ( -
- - -
- - -
-
- -

- {intl.formatMessage({ - defaultMessage: "Link your employment to access benefits", - })} -

-
- -
-
- - {({ isSelected: isAriaSelected }) => ( - <> - - {isAriaSelected && ( - - )} - - - - {intl.formatMessage({ - defaultMessage: "I accept the terms and conditions", - })} - - - - )} - - -

- {intl.formatMessage( - { - defaultMessage: - "By accepting the {termsLink}, I agree to link my employment to access benefits. The connection will remain active during my employment or until I opt out by sending an email to Scandic's customer service.", - }, - { - termsLink: ( - // TODO: Update with actual URL for terms and conditions. - - {intl.formatMessage({ - defaultMessage: - "Scandic Family Terms and Conditions", - })} - - ), - } - )} -

-
-
- - {isChecked ? ( - - {linkMyEmploymentText} - - ) : ( - - )} -
-
-
-
-
- ) -} diff --git a/apps/scandic-web/app/api/web/auth/[...nextauth]/route.ts b/apps/scandic-web/app/api/web/auth/[...nextauth]/route.ts index b44a389af..59fc3dee8 100644 --- a/apps/scandic-web/app/api/web/auth/[...nextauth]/route.ts +++ b/apps/scandic-web/app/api/web/auth/[...nextauth]/route.ts @@ -1 +1,20 @@ -export { GET, POST } from "@/auth" +import { GET as DEFAULT_GET, POST as DEFAULT_POST } from "@/auth" +import { GET as DTMC_GET, POST as DTMC_POST } from "@/auth.dtmc" + +import type { NextRequest } from "next/server" + +export function GET(req: NextRequest) { + if (req.nextUrl.pathname.includes("microsoft-entra-id")) { + return DTMC_GET(req) + } + + return DEFAULT_GET(req) +} + +export function POST(req: NextRequest) { + if (req.nextUrl.pathname.includes("microsoft-entra-id")) { + return DTMC_POST(req) + } + + return DEFAULT_POST(req) +} diff --git a/apps/scandic-web/app/api/web/auth/dtmc/route.ts b/apps/scandic-web/app/api/web/auth/dtmc/route.ts new file mode 100644 index 000000000..5bddd4015 --- /dev/null +++ b/apps/scandic-web/app/api/web/auth/dtmc/route.ts @@ -0,0 +1,106 @@ +import { type NextRequest, NextResponse } from "next/server" + +import { DTMC_SUCCESS_BANNER_KEY } from "@/constants/dtmc" +import { linkEmploymentError } from "@/constants/routes/dtmc" +import { overview } from "@/constants/routes/myPages" +import { internalServerError } from "@/server/errors/next" +import { getPublicURL } from "@/server/utils" + +import { auth } from "@/auth" +import { auth as dtmcAuth } from "@/auth.dtmc" +import { getLang } from "@/i18n/serverContext" +import { isValidSession } from "@/utils/session" + +async function linkEmployeeToUser(employeeId: string) { + try { + console.log(`[dtmc] Linking employee ID ${employeeId}`) + // TODO: Use the actual API once available. For now, return a mock success response. + return { success: true } + } catch (error) { + console.error("[dtmc] Error linking employee to user:", error) + throw error + } +} + +/** + * This is the route that the NextAuth callback for Microsoft Entra ID provider + * will redirect too once it has created the session. Since it is its own cookie, + * here we can check both sessions, the Scandic Friends one and the Azure one. + */ +export async function GET(request: NextRequest) { + try { + const lang = await getLang() + const dtmcSession = await dtmcAuth() + const session = await auth() + const baseUrl = getPublicURL(request) + console.log("[dtmc] DTMC Callback handler - using baseUrl:", baseUrl) + + if (!isValidSession(session)) { + console.error( + "[dtmc] DTMC Callback handler - No valid user session found" + ) + const errorUrl = new URL(linkEmploymentError[lang], baseUrl) + errorUrl.searchParams.set("error", "no_session") + return NextResponse.redirect(errorUrl) + } + + if (!isValidSession(dtmcSession)) { + console.error( + "[dtmc] DTMC Callback handler - No valid entra id session found" + ) + const errorUrl = new URL(linkEmploymentError[lang], baseUrl) + errorUrl.searchParams.set("error", "no_entra_id_session") + return NextResponse.redirect(errorUrl) + } + + const employeeId = dtmcSession.employeeId + + console.log( + "[dtmc] DTMC Callback handler - Extracted employeeId:", + employeeId + ) + + if (!employeeId) { + console.error("[dtmc] DTMC Callback handler - No employeeId in session") + const errorUrl = new URL(linkEmploymentError[lang], baseUrl) + errorUrl.searchParams.set("error", "missing_employee_id") + return NextResponse.redirect(errorUrl) + } + + console.log( + "[dtmc] DTMC Callback handler - Calling linkEmployeeToUser with ID:", + employeeId + ) + const result = await linkEmployeeToUser(employeeId) + console.log( + "[dtmc] DTMC Callback handler - linkEmployeeToUser result:", + result + ) + + if (!result.success) { + console.error( + "[dtmc] DTMC Callback handler - Failed to verify employment" + ) + const errorUrl = new URL(linkEmploymentError[lang], baseUrl) + errorUrl.searchParams.set("error", "unable_to_verify_employee_id") + return NextResponse.redirect(errorUrl) + } + + console.log( + "[dtmc] DTMC Callback handler - Success! Employee linked with ID:", + employeeId + ) + + console.log("[dtmc] overview[lang]:", overview[lang]) + const successUrl = new URL(overview[lang], baseUrl) + successUrl.searchParams.set(DTMC_SUCCESS_BANNER_KEY, "true") + console.log( + "[dtmc] DTMC Callback handler - Redirecting to success URL:", + successUrl.toString() + ) + return NextResponse.redirect(successUrl) + } catch (error) { + console.error("[dtmc] DTMC Callback handler - Error in handler:", error) + return internalServerError() + } +} diff --git a/apps/scandic-web/auth.dtmc.ts b/apps/scandic-web/auth.dtmc.ts new file mode 100644 index 000000000..d8e4e9b8b --- /dev/null +++ b/apps/scandic-web/auth.dtmc.ts @@ -0,0 +1,105 @@ +import NextAuth, { type NextAuthConfig } from "next-auth" +import MicrosoftEntraID from "next-auth/providers/microsoft-entra-id" + +import { env } from "@/env/server" + +const config = { + basePath: "/api/web/auth", + debug: env.NEXTAUTH_DEBUG, + cookies: { + sessionToken: { + name: "dtmc.session-token", + }, + }, + providers: [ + MicrosoftEntraID({ + clientId: env.DTMC_ENTRA_ID_CLIENT, + clientSecret: env.DTMC_ENTRA_ID_SECRET, + issuer: env.DTMC_ENTRA_ID_ISSUER, + authorization: { + params: { + scope: "openid profile email User.Read", + }, + }, + }), + ], + redirectProxyUrl: env.NEXTAUTH_REDIRECT_PROXY_URL, + trustHost: true, + session: { + strategy: "jwt", + maxAge: 10 * 60, // 10 minutes + }, + callbacks: { + async signIn() { + return true + }, + async session({ session, token }) { + if (token && token.employeeId && typeof token.employeeId === "string") { + session.employeeId = token.employeeId + } + return session + }, + async redirect({ baseUrl, url }) { + console.log(`[auth.dtmc] deciding redirect URL`, { baseUrl, url }) + if (url.startsWith("/")) { + console.log( + `[auth.dtmc] relative URL accepted, returning: ${baseUrl}${url}` + ) + // Allows relative callback URLs + return `${baseUrl}${url}` + } else { + // Assume absolute URL + try { + const parsedUrl = new URL(url) + if ( + /\.scandichotels\.(dk|de|com|fi|no|se)$/.test(parsedUrl.hostname) + ) { + console.log(`[auth.dtmc] subdomain URL accepted, returning: ${url}`) + // Allows any subdomains on all top level domains above + return url + } else if (parsedUrl.origin === baseUrl) { + // Allows callback URLs on the same origin + console.log(`[auth.dtmc] origin URL accepted, returning: ${url}`) + return url + } + } catch (e) { + console.error( + `[auth.dtmc] error parsing incoming URL for redirection`, + e + ) + } + } + console.log(`[auth.dtmc] URL denied, returning base URL: ${baseUrl}`) + return baseUrl + }, + async authorized() { + return true + }, + async jwt({ account, trigger, profile, token }) { + if ( + trigger === "signIn" && + account && + account.provider === "microsoft-entra-id" && + profile + ) { + const employeeId = profile["user.employeeid"] + if (employeeId && typeof employeeId === "string") { + return { + access_token: "", // JWT requires it, but DTMC does not need it, so save on cookie size by using empty string + loginType: "dtmc", + employeeId, + } + } + } + + return token + }, + }, +} satisfies NextAuthConfig + +export const { + auth, + handlers: { GET, POST }, + signIn, + signOut, +} = NextAuth(config) diff --git a/apps/scandic-web/auth.ts b/apps/scandic-web/auth.ts index f62dc4a02..33e206511 100644 --- a/apps/scandic-web/auth.ts +++ b/apps/scandic-web/auth.ts @@ -20,6 +20,10 @@ function getLoginType(user: User) { async function refreshTokens(token: JWT) { try { + if (!token.refresh_token) { + throw "Refresh token missing." + } + console.log("token-debug Access token expired, trying to refresh it.", { expires_at: token.expires_at, sub: token.sub, diff --git a/apps/scandic-web/components/DigitalTeamMemberCard/EmployeeBenefits/CallToActions/index.tsx b/apps/scandic-web/components/DigitalTeamMemberCard/EmployeeBenefits/CallToActions/index.tsx index b0e34bf17..28a5e88ea 100644 --- a/apps/scandic-web/components/DigitalTeamMemberCard/EmployeeBenefits/CallToActions/index.tsx +++ b/apps/scandic-web/components/DigitalTeamMemberCard/EmployeeBenefits/CallToActions/index.tsx @@ -2,7 +2,7 @@ import React from "react" import { Typography } from "@scandic-hotels/design-system/Typography" -import { linkEmployment } from "@/constants/routes/dtmc" +import { dtmcLogin } from "@/constants/routes/dtmc" import { login } from "@/constants/routes/handleAuth" import { signup } from "@/constants/routes/signup" @@ -19,6 +19,8 @@ export default async function EmployeeBenefitsCallToActions() { const intl = await getIntl() const lang = await getLang() + const loginAndLinkURL = `${login[lang]}?redirectTo=${encodeURIComponent(dtmcLogin[lang])}` + if (!isValidSession(session)) { return ( <> @@ -30,7 +32,7 @@ export default async function EmployeeBenefitsCallToActions() { })}

- + {intl.formatMessage({ defaultMessage: "Log in and link employment", })} @@ -62,7 +64,7 @@ export default async function EmployeeBenefitsCallToActions() { return (
= { + [Lang.da]: "+45 33 48 04 01", + [Lang.de]: mainNumber, + [Lang.en]: mainNumber, + [Lang.fi]: "0200 81800", + [Lang.no]: "+47 23 15 50 00", + [Lang.sv]: mainNumber, +} + +export const supportEmail: Record = { + [Lang.da]: "memberdk.scandic@scandichotels.com", + [Lang.de]: "memberde@scandichotels.com", + [Lang.en]: "member@scandichotels.com", + [Lang.fi]: "memberfi@scandichotels.com", + [Lang.no]: "memberno@scandichotels.com", + [Lang.sv]: "memberse@scandichotels.com", +} diff --git a/apps/scandic-web/constants/dtmc.ts b/apps/scandic-web/constants/dtmc.ts new file mode 100644 index 000000000..fe0e0a0f8 --- /dev/null +++ b/apps/scandic-web/constants/dtmc.ts @@ -0,0 +1 @@ +export const DTMC_SUCCESS_BANNER_KEY = "card_added" diff --git a/apps/scandic-web/constants/routes/dtmc.ts b/apps/scandic-web/constants/routes/dtmc.ts index 8357488e1..3106d67c2 100644 --- a/apps/scandic-web/constants/routes/dtmc.ts +++ b/apps/scandic-web/constants/routes/dtmc.ts @@ -9,11 +9,30 @@ export const employeeBenefits: LangRoute = { de: "/de/employee-benefits", } -export const linkEmployment: LangRoute = { - en: "/en/link-employment", - sv: "/sv/link-employment", - no: "/no/link-employment", - fi: "/fi/link-employment", - da: "/da/link-employment", - de: "/de/link-employment", +export const linkEmploymentError: LangRoute = { + en: `/en/link-employment-error`, + sv: `/sv/link-employment-error`, + no: `/no/link-employment-error`, + fi: `/fi/link-employment-error`, + da: `/da/link-employment-error`, + de: `/de/link-employment-error`, } + +export const dtmcLogin: LangRoute = { + en: "/en/dtmc", + sv: "/sv/dtmc", + no: "/no/dtmc", + fi: "/fi/dtmc", + da: "/da/dtmc", + de: "/de/dtmc", +} + +export const dtmcApiCallback = "/api/web/auth/dtmc" + +// All DTMC routes that should be protected by the ENABLE_DTMC flag. +export const handleDTMC = [ + // ...Object.values(employeeBenefits), + ...Object.values(dtmcLogin), + ...Object.values(linkEmploymentError), + dtmcApiCallback, +] diff --git a/apps/scandic-web/env/server.ts b/apps/scandic-web/env/server.ts index d44bd6a8c..0b3ebb91c 100644 --- a/apps/scandic-web/env/server.ts +++ b/apps/scandic-web/env/server.ts @@ -203,6 +203,13 @@ const _env = createEnv({ return val.split(",") }) .default(""), + DTMC_ENTRA_ID_CLIENT: z.string(), + DTMC_ENTRA_ID_ISSUER: z.string(), + /** + * Optional until we have the secret in all environments. + * We currently have the secret in local and test environments. + */ + DTMC_ENTRA_ID_SECRET: z.string().optional(), CAMPAIGN_PAGES_ENABLED: z .string() .refine((s) => s === "1" || s === "0") @@ -301,6 +308,10 @@ const _env = createEnv({ ENABLE_WARMUP_HOTEL: process.env.ENABLE_WARMUP_HOTEL, WARMUP_TOKEN: process.env.WARMUP_TOKEN, NEW_SITE_LIVE_FOR_LANGS: process.env.NEXT_PUBLIC_NEW_SITE_LIVE_FOR_LANGS, + + DTMC_ENTRA_ID_CLIENT: process.env.DTMC_ENTRA_ID_CLIENT, + DTMC_ENTRA_ID_ISSUER: process.env.DTMC_ENTRA_ID_ISSUER, + DTMC_ENTRA_ID_SECRET: process.env.DTMC_ENTRA_ID_SECRET, CAMPAIGN_PAGES_ENABLED: process.env.CAMPAIGN_PAGES_ENABLED, }, }) diff --git a/apps/scandic-web/middleware.ts b/apps/scandic-web/middleware.ts index cbdf373ca..878a91788 100644 --- a/apps/scandic-web/middleware.ts +++ b/apps/scandic-web/middleware.ts @@ -12,6 +12,7 @@ import * as currentWebLogout from "@/middlewares/currentWebLogout" import * as dateQueryParams from "@/middlewares/dateQueryParams" import * as familyAndFriends from "@/middlewares/familyAndFriends" import * as handleAuth from "@/middlewares/handleAuth" +import * as handleDTMC from "@/middlewares/handleDTMC" import * as invalidUrl from "@/middlewares/invalidUrl" import * as legacySearchParams from "@/middlewares/legacySearchParams" import * as myPages from "@/middlewares/myPages" @@ -59,6 +60,7 @@ export const middleware: NextMiddleware = async (request, event) => { currentWebLogout, authRequired, handleAuth, + handleDTMC, myPages, webView, dateQueryParams, diff --git a/apps/scandic-web/middlewares/authRequired.ts b/apps/scandic-web/middlewares/authRequired.ts index b663cc724..ce8997a82 100644 --- a/apps/scandic-web/middlewares/authRequired.ts +++ b/apps/scandic-web/middlewares/authRequired.ts @@ -51,7 +51,7 @@ export const middleware = auth(async (request) => { * @returns boolean */ function isMFAInvalid() { - const isMFATokenValid = request.auth + const isMFATokenValid = request.auth?.token.mfa_expires_at ? request.auth.token.mfa_expires_at > Date.now() : false return !(request.auth?.token.mfa_scope && isMFATokenValid) diff --git a/apps/scandic-web/middlewares/handleDTMC.ts b/apps/scandic-web/middlewares/handleDTMC.ts new file mode 100644 index 000000000..5bdf1a052 --- /dev/null +++ b/apps/scandic-web/middlewares/handleDTMC.ts @@ -0,0 +1,20 @@ +import { type NextMiddleware, NextResponse } from "next/server" + +import { handleDTMC } from "@/constants/routes/dtmc" +import { env } from "@/env/server" +import { notFound } from "@/server/errors/next" + +import type { MiddlewareMatcher } from "@/types/middleware" + +export const middleware: NextMiddleware = (request) => { + if (!env.ENABLE_DTMC) { + throw notFound( + `ENABLE_DTMC is disabled, returning notFound for DTMC Route: ${request.nextUrl.pathname}` + ) + } + return NextResponse.next() +} + +export const matcher: MiddlewareMatcher = (request) => { + return handleDTMC.includes(request.nextUrl.pathname) +} diff --git a/apps/scandic-web/server/errors/next.ts b/apps/scandic-web/server/errors/next.ts index d38784020..2f0f473e3 100644 --- a/apps/scandic-web/server/errors/next.ts +++ b/apps/scandic-web/server/errors/next.ts @@ -41,3 +41,17 @@ export function internalServerError(cause?: unknown) { resInit ) } + +export function serviceUnavailable(cause?: unknown) { + const resInit = { + status: 503, + statusText: "Service Unavailable", + } + + return NextResponse.json( + { + cause, + }, + resInit + ) +} diff --git a/apps/scandic-web/server/routers/user/query.ts b/apps/scandic-web/server/routers/user/query.ts index 26040a14f..2f1bbe981 100644 --- a/apps/scandic-web/server/routers/user/query.ts +++ b/apps/scandic-web/server/routers/user/query.ts @@ -40,7 +40,8 @@ export const userQueryRouter = router({ ctx: { ...opts.ctx, isMFA: - opts.ctx.session.token.mfa_scope && + !!opts.ctx.session.token.mfa_scope && + !!opts.ctx.session.token.mfa_expires_at && opts.ctx.session.token.mfa_expires_at > Date.now(), }, }) diff --git a/apps/scandic-web/types/auth.d.ts b/apps/scandic-web/types/auth.d.ts index e3ac612d2..3c7f8fb02 100644 --- a/apps/scandic-web/types/auth.d.ts +++ b/apps/scandic-web/types/auth.d.ts @@ -16,6 +16,7 @@ declare module "next-auth" { */ interface Session extends RefreshTokenError { token: JWT + employeeId?: string | null } /** diff --git a/apps/scandic-web/types/components/tracking.ts b/apps/scandic-web/types/components/tracking.ts index e9ec7829c..439a09e12 100644 --- a/apps/scandic-web/types/components/tracking.ts +++ b/apps/scandic-web/types/components/tracking.ts @@ -34,6 +34,7 @@ export enum LoginTypeEnum { email = "email", "membership number" = "membership number", "email link" = "email link", + "dtmc" = "dtmc", } export type LoginType = keyof typeof LoginTypeEnum diff --git a/apps/scandic-web/types/jwt.d.ts b/apps/scandic-web/types/jwt.d.ts index d8cf97543..d3019dc12 100644 --- a/apps/scandic-web/types/jwt.d.ts +++ b/apps/scandic-web/types/jwt.d.ts @@ -11,8 +11,9 @@ declare module "next-auth/jwt" { access_token: string expires_at?: number loginType: LoginType - mfa_expires_at: number - mfa_scope: boolean - refresh_token: string + mfa_expires_at?: number + mfa_scope?: boolean + refresh_token?: string + employeeId?: string } }