Merged in chore/LOY-445-remove-dtmc-flag (pull request #3043)

chore(LOY-445): remove ENABLE_DTMC flag & add documentation

* chore(LOY-445): remove ENABLE_DTMC flag & add documentation


Approved-by: Matilda Landström
This commit is contained in:
Chuma Mcphoy (We Ahead)
2025-10-31 08:39:13 +00:00
parent 742fcbe588
commit 7abe190bed
6 changed files with 267 additions and 24 deletions

View File

@@ -37,7 +37,7 @@ export default async function Overview({
return (
<Section>
{env.ENABLE_DTMC ? <DigitalTeamMemberCardAlert /> : null}
<DigitalTeamMemberCardAlert />
<SectionHeader
link={link}
preamble={subtitle}

View File

@@ -1,5 +1,3 @@
import { env } from "@/env/server"
import { isEmployeeLinked } from "@/utils/user"
import DigitalTeamMemberCardClient from "./Client"
@@ -15,10 +13,6 @@ export default async function DigitalTeamMemberCard({
user,
children,
}: DigitalTeamMemberCardProps) {
if (!env.ENABLE_DTMC) {
return null
}
const hasEmploymentData = isEmployeeLinked(user)
if (!hasEmploymentData) {
return null

View File

@@ -29,7 +29,6 @@ export const dtmcLogin: LangRoute = {
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),

View File

@@ -0,0 +1,265 @@
# DTMC (Digital Team Member Card) Flow Documentation
## Overview
The DTMC feature allows existing Scandic Friends members to link their employee
status to their account by authenticating with Microsoft Entra ID.
This provides employees access to additional benefits.
## Current Implementation Architecture
### Separate Auth Configuration Approach
We use a completely separate auth configuration to avoid session conflicts between:
- **Curity**: `auth.ts`
- Regular user authentication.
- **DTMC Auth**: `auth.dtmc.ts`
- Short-lived (10min) session specifically for employee verification.
### Key Files & Responsibilities
#### Flow Handlers
- `app/[lang]/(live)/(public)/dtmc/route.ts`
- Microsoft auth initiation.
- `app/api/web/auth/dtmc/route.ts`
- Employee linking callback handler with API integration.
#### Core Authentication
- `auth.dtmc.ts`
- Microsoft Entra ID provider configuration, JWT/session callbacks.
- `app/api/web/auth/[...nextauth]/route.ts`
- Routes Microsoft requests to DTMC handlers.
#### API Integration
- `packages/trpc/lib/api/endpoints.ts`
- API endpoint definitions including TeamMemberCard.
- `server/routers/user/output.ts`
- Profile schema with employmentDetails.
- `utils/user.ts`
- Employment utility functions.
#### UI Components
- `apps/scandic-web/components/MyPages/DigitalTeamMemberCard/index.tsx`
- Main card component.
- `apps/scandic-web/components/MyPages/DigitalTeamMemberCard/Alert/index.tsx`
- Banner shown on the overview page for employee's who have just linked their account.
#### Configuration
- `constants/routes/dtmc.ts` - Route definitions
- `components/DigitalTeamMemberCard/EmployeeBenefits/CallToActions/index.tsx`
- "Link Employment" buttons used within the Employee Benefits page
(a Contentstack "content page") to initiate the linking process.
## Technical Flow
### 1. User Journey
```
User (Curity authenticated) → Employee Benefits page → "Link Employment" button →
Microsoft Auth → Employee ID extraction → API linking → Success redirect
```
### 2. Detailed Technical Sequence
```mermaid
sequenceDiagram
participant U as User
participant C as Curity Session
participant D as DTMC Route
participant M as Microsoft Auth
participant J as JWT Callback
participant S as Session Callback
participant H as DTMC Handler
participant A as Employee API
U->>C: Already authenticated (Curity)
U->>D: Click "Link Employment" → /[lang]/dtmc
D->>M: signIn("microsoft-entra-id")
M->>U: Microsoft login prompt
U->>M: Authenticate with corporate credentials
M->>J: Profile with user.employeeid
J->>J: Store employeeId directly in token
J->>S: Expose as session.employeeId
S->>H: Callback to /api/web/auth/dtmc
H->>H: Validate both Curity + DTMC sessions
H->>A: Call linkEmployeeToUser(employeeId)
A->>H: Success response
H->>U: Redirect to overview with success banner
```
### 3. Code Implementation Details
#### DTMC Auth Configuration (`auth.dtmc.ts`)
```typescript
// Separate session cookie to avoid conflicts
cookies: {
sessionToken: {
name: "dtmc.session-token",
},
}
// Short-lived session for security
session: {
strategy: "jwt",
maxAge: 10 * 60, // 10 minutes
}
// Storage of employeeId
jwt({ account, profile, token }) {
if (account?.provider === "microsoft-entra-id") {
const employeeId = profile["user.employeeid"]
return {
access_token: "", // Empty to save cookie size
loginType: "dtmc",
employeeId,
}
}
}
```
#### Route Routing (`[...nextauth]/route.ts`)
Routes Microsoft requests to DTMC handlers by checking if the pathname contains "microsoft-entra-id".
#### Auth Initiation (`/dtmc/route.ts`)
```typescript
// Starts Microsoft signin with callback redirect
const redirectUrl = await signIn(
"microsoft-entra-id",
{
redirectTo: `${env.PUBLIC_URL}${dtmcApiCallback}`,
redirect: false,
},
{
prompt: "login",
}
)
```
#### Callback Handler (`/api/web/auth/dtmc/route.ts`)
```typescript
// Validates dual sessions and processes linking
const dtmcSession = await dtmcAuth() // Microsoft session
const session = await auth() // Curity session
// Both sessions required for security
if (!isValidSession(session) || !isValidSession(dtmcSession)) {
// Redirect to error page
}
const employeeId = dtmcSession.employeeId
await linkEmployeeToUser(employeeId, accessToken)
```
## Routes & URLs
### User-Facing Routes
- `/[lang]/dtmc` - Microsoft auth initiation
- `/[lang]/link-employment-error` - Error handling page
- `/[lang]/employee-benefits` - Employee benefits overview
### API Routes
- `/api/web/auth/dtmc` - Employee linking callback handler
- `/api/web/auth/[...nextauth]` - NextAuth routing (handles Microsoft provider)
## API Integration Details
### TeamMemberCard Endpoint
```typescript
// Added to packages/trpc/lib/api/endpoints.ts
export namespace v2 {
export namespace Profile {
export function teamMemberCard(employeeId: string) {
return `${profile}/${employeeId}/TeamMemberCard`
}
}
}
```
### Profile Schema Extension
```typescript
// Added to server/routers/user/output.ts
export const employmentDetailsSchema = z
.object({
employeeId: z.string(),
location: z.string(),
country: z.string(),
retired: z.boolean(),
})
.optional()
```
### Employee Utility Functions
```typescript
// Added to utils/user.ts
export function isEmployeeLinked(user: User): boolean
export function getEmployeeInfo(user: User)
export function canUseEmployeeBenefits(user: User): boolean
```
### Status Code Handling
- **200/202**: Success → Continue to overview page
- **400/404**: Client errors → "unable_to_verify_employee_id" error page
- **500**: Server error → Default error message
- **Network errors**: → Default error message
## Digital Team Member Card Behavior
```typescript
// Current implementation
const hasEmploymentData = isEmployeeLinked(user)
if (!hasEmploymentData) {
return null
}
return <DigitalTeamMemberCardClient user={user} />
```
## Testing Checklist
### Happy Path
- [x] User has valid Curity session
- [x] Microsoft auth extracts employee ID successfully
- [x] Both sessions validated in callback handler
- [x] Employee linking API call succeeds
- [x] User redirected to overview with success banner
- [x] Original Curity session remains functional
- [x] Digital Team Member Card displays with employment data
### Error Scenarios
- [x] Missing Curity session during flow
- [x] Microsoft auth failure/cancellation
- [x] Missing employee ID in Microsoft profile
- [x] Employee API failure scenarios
- [x] DTMC session expiration
- [x] Status code mapping (400/404/500/network errors)
### Security Validation
- [x] No employee ID in browser network logs
- [x] No employee ID in URL parameters
- [x] Dual session architecture working
- [x] Proper session validation in callback
- [x] Short-lived DTMC session (10 minutes) for security
### UI/UX Validation
- [x] Card shows real data when employment details exist
- [x] Proper fallbacks for missing employment data fields

View File

@@ -93,13 +93,6 @@ export const env = createEnv({
// transform to boolean
.transform((s) => s === "true")
.default("false"),
ENABLE_DTMC: z
.string()
// only allow "true" or "false"
.refine((s) => s === "true" || s === "false")
// transform to boolean
.transform((s) => s === "true")
.default("false"),
SHOW_SITE_WIDE_ALERT: z
.string()
// only allow "true" or "false"
@@ -228,7 +221,6 @@ export const env = createEnv({
GOOGLE_DYNAMIC_MAP_ID: process.env.GOOGLE_DYNAMIC_MAP_ID,
USE_NEW_REWARD_MODEL: process.env.USE_NEW_REWARD_MODEL,
ENABLE_SURPRISES: process.env.ENABLE_SURPRISES,
ENABLE_DTMC: process.env.ENABLE_DTMC,
SHOW_SITE_WIDE_ALERT: process.env.SHOW_SITE_WIDE_ALERT,
SENTRY_ENVIRONMENT: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT,
SENTRY_SERVER_SAMPLERATE: process.env.SENTRY_SERVER_SAMPLERATE,

View File

@@ -1,17 +1,10 @@
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}`
)
}
export const middleware: NextMiddleware = () => {
return NextResponse.next()
}