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:
@@ -37,7 +37,7 @@ export default async function Overview({
|
||||
|
||||
return (
|
||||
<Section>
|
||||
{env.ENABLE_DTMC ? <DigitalTeamMemberCardAlert /> : null}
|
||||
<DigitalTeamMemberCardAlert />
|
||||
<SectionHeader
|
||||
link={link}
|
||||
preamble={subtitle}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
265
apps/scandic-web/docs/DTMC_FLOW.md
Normal file
265
apps/scandic-web/docs/DTMC_FLOW.md
Normal 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
|
||||
8
apps/scandic-web/env/server.ts
vendored
8
apps/scandic-web/env/server.ts
vendored
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user