Skip to content

Commit 9dc1c74

Browse files
committed
Handle MFA-required state in onboarding flow
Updated onboarding logic and slides to support cases where MFA is required, not just password changes. Adjusted orchestrator, slide props, and login/auth callback routes to trigger onboarding when either password change or MFA setup is needed.
1 parent 5b8d971 commit 9dc1c74

File tree

6 files changed

+30
-14
lines changed

6 files changed

+30
-14
lines changed

frontend/src/core/components/onboarding/Onboarding.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,11 +271,12 @@ export default function Onboarding() {
271271
onPasswordChanged: handlePasswordChanged,
272272
usingDefaultCredentials: runtimeState.usingDefaultCredentials,
273273
mfaRequired: runtimeState.mfaRequired,
274+
requiresPasswordChange: runtimeState.requiresPasswordChange,
274275
analyticsError,
275276
analyticsLoading,
276277
});
277-
}, [analyticsError, analyticsLoading, currentSlideDefinition, osInfo, osOptions, runtimeState.selectedRole, runtimeState.licenseNotice, handleRoleSelect, serverExperience.loginEnabled, setSelectedDownloadUrl, runtimeState.firstLoginUsername, handlePasswordChanged, runtimeState.usingDefaultCredentials, runtimeState.mfaRequired]);
278-
278+
}, [analyticsError, analyticsLoading, currentSlideDefinition, osInfo, osOptions, runtimeState.selectedRole, runtimeState.licenseNotice, handleRoleSelect, serverExperience.loginEnabled, setSelectedDownloadUrl, runtimeState.firstLoginUsername, handlePasswordChanged, runtimeState.usingDefaultCredentials, runtimeState.mfaRequired, runtimeState.requiresPasswordChange]);
279+
279280
const modalSlideCount = useMemo(() => {
280281
return activeFlow.filter((step) => step.type === 'modal-slide').length;
281282
}, [activeFlow]);

frontend/src/core/components/onboarding/onboardingFlowConfig.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export interface SlideFactoryParams {
6262
mfaRequired?: boolean;
6363
analyticsError?: string | null;
6464
analyticsLoading?: boolean;
65+
requiresPasswordChange?: boolean;
6566
}
6667

6768
export interface HeroDefinition {
@@ -89,12 +90,13 @@ export interface SlideDefinition {
8990
export const SLIDE_DEFINITIONS: Record<SlideId, SlideDefinition> = {
9091
'first-login': {
9192
id: 'first-login',
92-
createSlide: ({ firstLoginUsername, onPasswordChanged, usingDefaultCredentials, mfaRequired }) =>
93+
createSlide: ({ firstLoginUsername, onPasswordChanged, usingDefaultCredentials, mfaRequired, requiresPasswordChange }) =>
9394
FirstLoginSlide({
9495
username: firstLoginUsername || '',
9596
onPasswordChanged: onPasswordChanged || (() => {}),
9697
usingDefaultCredentials: usingDefaultCredentials || false,
9798
mfaRequired: mfaRequired || false,
99+
requiresPasswordChange: requiresPasswordChange || false,
98100
}),
99101
hero: { type: 'lock' },
100102
buttons: [], // Form has its own submit button

frontend/src/core/components/onboarding/orchestrator/useOnboardingOrchestrator.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -235,11 +235,11 @@ export function useOnboardingOrchestrator(
235235

236236
const activeFlow = useMemo(() => {
237237
// If password change is required, ONLY show the first-login step
238-
if (runtimeState.requiresPasswordChange !== false) {
238+
if (runtimeState.requiresPasswordChange !== false || runtimeState.mfaRequired !== false) {
239239
return ONBOARDING_STEPS.filter((step) => step.id === 'first-login');
240240
}
241241
return ONBOARDING_STEPS.filter((step) => step.condition(conditionContext));
242-
}, [conditionContext, runtimeState.requiresPasswordChange]);
242+
}, [conditionContext, runtimeState.requiresPasswordChange, runtimeState.mfaRequired]);
243243

244244
// Wait for config AND admin status before calculating initial step
245245
const adminStatusResolved = !configLoading && (
@@ -259,7 +259,7 @@ export function useOnboardingOrchestrator(
259259
}
260260

261261
// If onboarding has been completed, don't show it
262-
if (isOnboardingCompleted() && !runtimeState.requiresPasswordChange) {
262+
if (isOnboardingCompleted() && runtimeState.requiresPasswordChange === false && runtimeState.mfaRequired === false) {
263263
setCurrentStepIndex(activeFlow.length);
264264
initialIndexSet.current = true;
265265
return;
@@ -270,7 +270,7 @@ export function useOnboardingOrchestrator(
270270
setCurrentStepIndex(0);
271271
initialIndexSet.current = true;
272272
}
273-
}, [activeFlow, configLoading, adminStatusResolved, runtimeState.requiresPasswordChange]);
273+
}, [activeFlow, configLoading, adminStatusResolved, runtimeState.requiresPasswordChange, runtimeState.mfaRequired]);
274274

275275
const totalSteps = activeFlow.length;
276276

@@ -308,7 +308,7 @@ export function useOnboardingOrchestrator(
308308
// Skip marks the entire onboarding as completed
309309
markOnboardingCompleted();
310310
setCurrentStepIndex(totalSteps);
311-
}, [totalSteps, runtimeState.requiresPasswordChange]);
311+
}, [totalSteps, runtimeState.requiresPasswordChange, runtimeState.mfaRequired]);
312312

313313
const complete = useCallback(() => {
314314
const nextIndex = currentStepIndex + 1;
@@ -317,7 +317,7 @@ export function useOnboardingOrchestrator(
317317
markOnboardingCompleted();
318318
}
319319
setCurrentStepIndex(nextIndex);
320-
}, [currentStepIndex, totalSteps]);
320+
}, [currentStepIndex, totalSteps, runtimeState.requiresPasswordChange, runtimeState.mfaRequired]);
321321

322322

323323
const updateRuntimeState = useCallback((updates: Partial<OnboardingRuntimeState>) => {

frontend/src/core/components/onboarding/slides/FirstLoginSlide.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ interface FirstLoginSlideProps {
1414
onPasswordChanged: () => void;
1515
usingDefaultCredentials?: boolean;
1616
mfaRequired?: boolean;
17+
requiresPasswordChange?: boolean;
1718
}
1819

1920
const DEFAULT_PASSWORD = 'stirling';
@@ -23,6 +24,7 @@ function FirstLoginForm({
2324
onPasswordChanged,
2425
usingDefaultCredentials = false,
2526
mfaRequired = false,
27+
requiresPasswordChange = false,
2628
}: FirstLoginSlideProps) {
2729
const { t } = useTranslation();
2830
// If using default credentials, pre-fill with "stirling" - user won't see this field
@@ -35,8 +37,10 @@ function FirstLoginForm({
3537
const [mfaSetupCode, setMfaSetupCode] = useState('');
3638
const [mfaError, setMfaError] = useState('');
3739
const [mfaLoading, setMfaLoading] = useState(false);
38-
const [stepPassword, setStepPassword] = useState(true);
40+
const [stepPassword, setStepPassword] = useState(requiresPasswordChange);
41+
const [stepMfa, setStepMfa] = useState(mfaRequired);
3942

43+
const tempStepPassword = requiresPasswordChange;
4044
const normalizeMfaCode = useCallback((value: string) => value.replace(/\D/g, '').slice(0, 6), []);
4145

4246
useEffect(() => {
@@ -140,7 +144,8 @@ function FirstLoginForm({
140144
});
141145
setMfaSetupCode('');
142146
setMfaSetupData(null);
143-
setStepPassword(true);
147+
setStepPassword(tempStepPassword);
148+
setStepMfa(false);
144149
} catch (enableError) {
145150
console.error('Failed to enable MFA:', enableError);
146151
setMfaError(
@@ -151,6 +156,12 @@ function FirstLoginForm({
151156
);
152157
} finally {
153158
setMfaLoading(false);
159+
if (!stepPassword) {
160+
// Wait a moment for the user to see the success message
161+
setTimeout(() => {
162+
onPasswordChanged();
163+
}, 1500);
164+
}
154165
}
155166
};
156167

@@ -173,7 +184,7 @@ function FirstLoginForm({
173184
</Text>
174185

175186
{/* MFA Setup Section */}
176-
{mfaRequired && mfaSetupData && (
187+
{stepMfa && (
177188
<Stack gap="sm">
178189
<Alert
179190
icon={<LocalIcon icon="security" width="1rem" height="1rem" />}
@@ -322,6 +333,7 @@ export default function FirstLoginSlide({
322333
onPasswordChanged,
323334
usingDefaultCredentials = false,
324335
mfaRequired = false,
336+
requiresPasswordChange = false,
325337
}: FirstLoginSlideProps): SlideConfig {
326338
return {
327339
key: 'first-login',
@@ -332,6 +344,7 @@ export default function FirstLoginSlide({
332344
onPasswordChanged={onPasswordChanged}
333345
usingDefaultCredentials={usingDefaultCredentials}
334346
mfaRequired={mfaRequired}
347+
requiresPasswordChange={requiresPasswordChange}
335348
/>
336349
),
337350
background: {

frontend/src/proprietary/routes/AuthCallback.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export default function AuthCallback() {
6262

6363
try {
6464
const accountData = await accountService.getAccountData();
65-
if (accountData.changeCredsFlag) {
65+
if (accountData.changeCredsFlag || accountData.mfaRequired) {
6666
requestFirstLoginSlide();
6767
if (isOnboardingCompleted()) {
6868
markOnboardingIncomplete();

frontend/src/proprietary/routes/Login.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ export default function Login() {
310310
setMfaCode('');
311311
try {
312312
const accountData = await accountService.getAccountData();
313-
if (accountData.changeCredsFlag) {
313+
if (accountData.changeCredsFlag || accountData.mfaRequired) {
314314
requestFirstLoginSlide();
315315
if (isOnboardingCompleted()) {
316316
markOnboardingIncomplete();

0 commit comments

Comments
 (0)