feat(FN-2407): add blocking auth token recovery flow
- Emit an auth-token recovery event when dashboard daemon calls fail with bearer-token 401 responses - Render a non-dismissable recovery dialog with set-token and clear-token reload actions - Keep the recovery overlay topmost while aligning modal markup with shared modal conventions - Add focused regression coverage for dialog behavior and include a patch changeset for @runfusion/fusion
This commit is contained in:
5
.changeset/auth-token-recovery-dialog.md
Normal file
5
.changeset/auth-token-recovery-dialog.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add a blocking dashboard token-recovery dialog that appears only for daemon bearer-token 401 responses, with set-token or clear-token recovery actions that reload the app.
|
||||||
@@ -2,13 +2,8 @@
|
|||||||
z-index: 210;
|
z-index: 210;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-token-recovery-modal {
|
.auth-token-recovery-header h3 {
|
||||||
width: min(560px, calc(100vw - (var(--space-xl) * 2)));
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-token-recovery-header h2 {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-token-recovery-content {
|
.auth-token-recovery-content {
|
||||||
|
|||||||
@@ -44,14 +44,14 @@ export function AuthTokenRecoveryDialog({ open }: AuthTokenRecoveryDialogProps)
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="modal auth-token-recovery-modal"
|
className="modal modal-md"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="auth-token-recovery-title"
|
aria-labelledby="auth-token-recovery-title"
|
||||||
aria-describedby="auth-token-recovery-description"
|
aria-describedby="auth-token-recovery-description"
|
||||||
>
|
>
|
||||||
<div className="modal-header auth-token-recovery-header">
|
<div className="modal-header auth-token-recovery-header">
|
||||||
<h2 id="auth-token-recovery-title">Authentication token required</h2>
|
<h3 id="auth-token-recovery-title">Authentication token required</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="auth-token-recovery-content">
|
<div className="auth-token-recovery-content">
|
||||||
|
|||||||
@@ -29,9 +29,19 @@ describe("AuthTokenRecoveryDialog", () => {
|
|||||||
it("renders a blocking dialog with disabled set button until token is entered", () => {
|
it("renders a blocking dialog with disabled set button until token is entered", () => {
|
||||||
render(<AuthTokenRecoveryDialog open={true} />);
|
render(<AuthTokenRecoveryDialog open={true} />);
|
||||||
|
|
||||||
expect(screen.getByRole("dialog", { name: "Authentication token required" })).toBeInTheDocument();
|
const dialog = screen.getByRole("dialog", { name: "Authentication token required" });
|
||||||
|
expect(dialog).toBeInTheDocument();
|
||||||
expect(screen.queryByRole("button", { name: /close/i })).toBeNull();
|
expect(screen.queryByRole("button", { name: /close/i })).toBeNull();
|
||||||
|
|
||||||
|
const overlay = dialog.closest(".auth-token-recovery-overlay");
|
||||||
|
expect(overlay).toBeTruthy();
|
||||||
|
|
||||||
|
if (!overlay) {
|
||||||
|
throw new Error("Expected auth token recovery overlay");
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(dialog.className).toContain("modal-md");
|
||||||
|
|
||||||
const setTokenButton = screen.getByRole("button", { name: "Set token and reload" });
|
const setTokenButton = screen.getByRole("button", { name: "Set token and reload" });
|
||||||
expect(setTokenButton).toBeDisabled();
|
expect(setTokenButton).toBeDisabled();
|
||||||
|
|
||||||
|
|||||||
@@ -2361,7 +2361,7 @@ describe("ModelOnboardingModal", () => {
|
|||||||
expect(mockFetchGlobalSettings).toHaveBeenCalled();
|
expect(mockFetchGlobalSettings).toHaveBeenCalled();
|
||||||
|
|
||||||
// The model dropdown should be pre-populated with the saved default
|
// The model dropdown should be pre-populated with the saved default
|
||||||
const dropdown = screen.getByTestId("mock-model-dropdown") as HTMLSelectElement;
|
const dropdown = await screen.findByTestId("mock-model-dropdown") as HTMLSelectElement;
|
||||||
expect(dropdown.value).toBe("anthropic/claude-sonnet-4-5");
|
expect(dropdown.value).toBe("anthropic/claude-sonnet-4-5");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2407,7 +2407,7 @@ describe("ModelOnboardingModal", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// The model dropdown should be pre-populated with the saved default
|
// The model dropdown should be pre-populated with the saved default
|
||||||
const dropdown = screen.getByTestId("mock-model-dropdown") as HTMLSelectElement;
|
const dropdown = await screen.findByTestId("mock-model-dropdown") as HTMLSelectElement;
|
||||||
expect(dropdown.value).toBe("openai/gpt-4o");
|
expect(dropdown.value).toBe("openai/gpt-4o");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user