mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-22 22:23:37 +00:00
fix(ui): harden power user mode auth checks
This commit is contained in:
@@ -60,6 +60,11 @@ function normalizeRiskPhrase(value: string): string {
|
|||||||
return value.trim().replace(/\s+/g, ' ').toUpperCase();
|
return value.trim().replace(/\s+/g, ' ').toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PowerUserModeSyncOptions {
|
||||||
|
pendingMessage?: string | null;
|
||||||
|
disabledMessage?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export function AddAccountDialog({
|
export function AddAccountDialog({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -78,6 +83,8 @@ export function AddAccountDialog({
|
|||||||
const [kiroAuthMethod, setKiroAuthMethod] = useState<KiroAuthMethod>(DEFAULT_KIRO_AUTH_METHOD);
|
const [kiroAuthMethod, setKiroAuthMethod] = useState<KiroAuthMethod>(DEFAULT_KIRO_AUTH_METHOD);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const wasAuthenticatingRef = useRef(false);
|
const wasAuthenticatingRef = useRef(false);
|
||||||
|
const powerUserModeRequestIdRef = useRef(0);
|
||||||
|
const powerUserModeLoadErrorShownRef = useRef(false);
|
||||||
const authFlow = useCliproxyAuthFlow();
|
const authFlow = useCliproxyAuthFlow();
|
||||||
const kiroImportMutation = useKiroImport();
|
const kiroImportMutation = useKiroImport();
|
||||||
|
|
||||||
@@ -104,6 +111,53 @@ export function AddAccountDialog({
|
|||||||
return data.antigravityAckBypass === true;
|
return data.antigravityAckBypass === true;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const syncPowerUserModeState = useCallback(
|
||||||
|
async ({ pendingMessage = null, disabledMessage = null }: PowerUserModeSyncOptions = {}) => {
|
||||||
|
const requestId = ++powerUserModeRequestIdRef.current;
|
||||||
|
setPowerUserModeLoading(true);
|
||||||
|
|
||||||
|
if (pendingMessage !== null) {
|
||||||
|
setLocalError(pendingMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const enabled = await fetchPowerUserModeState();
|
||||||
|
if (powerUserModeRequestIdRef.current !== requestId) {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPowerUserModeEnabled(enabled);
|
||||||
|
|
||||||
|
if (disabledMessage) {
|
||||||
|
setLocalError(enabled ? null : disabledMessage);
|
||||||
|
} else if (pendingMessage !== null) {
|
||||||
|
setLocalError(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return enabled;
|
||||||
|
} catch {
|
||||||
|
if (powerUserModeRequestIdRef.current !== requestId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPowerUserModeEnabled(false);
|
||||||
|
setLocalError(disabledMessage ?? t('addAccountDialog.powerUserLoadFailed'));
|
||||||
|
|
||||||
|
if (!powerUserModeLoadErrorShownRef.current) {
|
||||||
|
powerUserModeLoadErrorShownRef.current = true;
|
||||||
|
toast.error(t('addAccountDialog.powerUserLoadFailed'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
if (powerUserModeRequestIdRef.current === requestId) {
|
||||||
|
setPowerUserModeLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[fetchPowerUserModeState, t]
|
||||||
|
);
|
||||||
|
|
||||||
const resetAndClose = () => {
|
const resetAndClose = () => {
|
||||||
setNickname('');
|
setNickname('');
|
||||||
setCallbackUrl('');
|
setCallbackUrl('');
|
||||||
@@ -114,6 +168,8 @@ export function AddAccountDialog({
|
|||||||
setPowerUserModeEnabled(false);
|
setPowerUserModeEnabled(false);
|
||||||
setPowerUserModeLoading(false);
|
setPowerUserModeLoading(false);
|
||||||
setKiroAuthMethod(DEFAULT_KIRO_AUTH_METHOD);
|
setKiroAuthMethod(DEFAULT_KIRO_AUTH_METHOD);
|
||||||
|
powerUserModeRequestIdRef.current += 1;
|
||||||
|
powerUserModeLoadErrorShownRef.current = false;
|
||||||
wasAuthenticatingRef.current = false;
|
wasAuthenticatingRef.current = false;
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
@@ -127,38 +183,21 @@ export function AddAccountDialog({
|
|||||||
}, [provider, open]);
|
}, [provider, open]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
return () => {
|
||||||
|
powerUserModeRequestIdRef.current += 1;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
if (!open || !supportsPowerUserMode) {
|
if (!open || !supportsPowerUserMode) {
|
||||||
|
powerUserModeRequestIdRef.current += 1;
|
||||||
setPowerUserModeEnabled(false);
|
setPowerUserModeEnabled(false);
|
||||||
setPowerUserModeLoading(false);
|
setPowerUserModeLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadPowerUserModeState = async () => {
|
void syncPowerUserModeState();
|
||||||
try {
|
}, [open, provider, supportsPowerUserMode, syncPowerUserModeState]);
|
||||||
setPowerUserModeLoading(true);
|
|
||||||
const enabled = await fetchPowerUserModeState();
|
|
||||||
if (!cancelled) {
|
|
||||||
setPowerUserModeEnabled(enabled);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
if (!cancelled) {
|
|
||||||
setPowerUserModeEnabled(false);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (!cancelled) {
|
|
||||||
setPowerUserModeLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadPowerUserModeState();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [fetchPowerUserModeState, open, supportsPowerUserMode]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || provider !== 'agy' || !authFlow.error || !powerUserModeEnabled) {
|
if (!open || provider !== 'agy' || !authFlow.error || !powerUserModeEnabled) {
|
||||||
@@ -172,34 +211,11 @@ export function AddAccountDialog({
|
|||||||
normalizedError.includes('responsibility checklist');
|
normalizedError.includes('responsibility checklist');
|
||||||
if (!ackRequired) return;
|
if (!ackRequired) return;
|
||||||
|
|
||||||
let cancelled = false;
|
void syncPowerUserModeState({
|
||||||
|
pendingMessage: t('addAccountDialog.powerUserLoading'),
|
||||||
const syncBypassState = async () => {
|
disabledMessage: t('addAccountDialog.powerUserUnavailableRetry'),
|
||||||
try {
|
});
|
||||||
setPowerUserModeLoading(true);
|
}, [authFlow.error, open, powerUserModeEnabled, provider, syncPowerUserModeState, t]);
|
||||||
const enabled = await fetchPowerUserModeState();
|
|
||||||
if (cancelled) return;
|
|
||||||
setPowerUserModeEnabled(enabled);
|
|
||||||
if (!enabled) {
|
|
||||||
setLocalError('Power user mode is off. Complete the AGY checklist and retry.');
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
if (cancelled) return;
|
|
||||||
setPowerUserModeEnabled(false);
|
|
||||||
setLocalError('Power user mode is off. Complete the AGY checklist and retry.');
|
|
||||||
} finally {
|
|
||||||
if (!cancelled) {
|
|
||||||
setPowerUserModeLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
void syncBypassState();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [authFlow.error, fetchPowerUserModeState, open, powerUserModeEnabled, provider]);
|
|
||||||
|
|
||||||
// When authFlow completes successfully (polling detected success), apply preset and close
|
// When authFlow completes successfully (polling detected success), apply preset and close
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -250,7 +266,7 @@ export function AddAccountDialog({
|
|||||||
*/
|
*/
|
||||||
const handleAuthenticate = () => {
|
const handleAuthenticate = () => {
|
||||||
if (isPowerUserModePending) {
|
if (isPowerUserModePending) {
|
||||||
setLocalError('Loading power user safety settings. Please wait a moment and retry.');
|
setLocalError(t('addAccountDialog.powerUserLoading'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (requiresAgyResponsibilityFlow && !isAgyRiskChecklistComplete) {
|
if (requiresAgyResponsibilityFlow && !isAgyRiskChecklistComplete) {
|
||||||
|
|||||||
@@ -484,6 +484,12 @@ const resources = {
|
|||||||
powerUserEnabled: 'Power user mode enabled',
|
powerUserEnabled: 'Power user mode enabled',
|
||||||
powerUserSkipped:
|
powerUserSkipped:
|
||||||
'Settings > Proxy power user mode is skipping the AGY responsibility checklist and Gemini dashboard risk phrase. You accept full responsibility for OAuth/account risk.',
|
'Settings > Proxy power user mode is skipping the AGY responsibility checklist and Gemini dashboard risk phrase. You accept full responsibility for OAuth/account risk.',
|
||||||
|
powerUserLoadFailed:
|
||||||
|
'Failed to load power user mode settings. Check Settings > Proxy and try again.',
|
||||||
|
powerUserLoading:
|
||||||
|
'Loading power user safety settings. Please wait a moment and retry.',
|
||||||
|
powerUserUnavailableRetry:
|
||||||
|
'Power user mode is unavailable. Complete the required provider safety step and retry.',
|
||||||
authMethod: 'Auth Method',
|
authMethod: 'Auth Method',
|
||||||
selectKiroAuthMethod: 'Select Kiro auth method',
|
selectKiroAuthMethod: 'Select Kiro auth method',
|
||||||
nicknameRequired: 'Nickname (required)',
|
nicknameRequired: 'Nickname (required)',
|
||||||
@@ -1668,6 +1674,9 @@ const resources = {
|
|||||||
powerUserEnabled: '已启用高级用户模式',
|
powerUserEnabled: '已启用高级用户模式',
|
||||||
powerUserSkipped:
|
powerUserSkipped:
|
||||||
'设置 > 代理 中的高级用户模式会跳过 AGY 责任确认清单和 Gemini Dashboard 的风险短语。OAuth / 账号风险需自行承担。',
|
'设置 > 代理 中的高级用户模式会跳过 AGY 责任确认清单和 Gemini Dashboard 的风险短语。OAuth / 账号风险需自行承担。',
|
||||||
|
powerUserLoadFailed: '加载高级用户模式设置失败。请检查“设置 > 代理”后重试。',
|
||||||
|
powerUserLoading: '正在加载高级用户安全设置。请稍候后重试。',
|
||||||
|
powerUserUnavailableRetry: '高级用户模式不可用。请完成当前提供商要求的安全步骤后重试。',
|
||||||
authMethod: '认证方式',
|
authMethod: '认证方式',
|
||||||
selectKiroAuthMethod: '选择 Kiro 认证方式',
|
selectKiroAuthMethod: '选择 Kiro 认证方式',
|
||||||
nicknameRequired: '昵称(必填)',
|
nicknameRequired: '昵称(必填)',
|
||||||
@@ -2862,6 +2871,12 @@ const resources = {
|
|||||||
powerUserEnabled: 'Đã bật chế độ power user',
|
powerUserEnabled: 'Đã bật chế độ power user',
|
||||||
powerUserSkipped:
|
powerUserSkipped:
|
||||||
'Chế độ power user trong Cài đặt > Proxy đang bỏ qua danh sách kiểm tra trách nhiệm AGY và bước nhập cụm từ rủi ro của Gemini trên dashboard. Bạn tự chịu hoàn toàn rủi ro OAuth/tài khoản.',
|
'Chế độ power user trong Cài đặt > Proxy đang bỏ qua danh sách kiểm tra trách nhiệm AGY và bước nhập cụm từ rủi ro của Gemini trên dashboard. Bạn tự chịu hoàn toàn rủi ro OAuth/tài khoản.',
|
||||||
|
powerUserLoadFailed:
|
||||||
|
'Không thể tải cài đặt chế độ power user. Hãy kiểm tra Cài đặt > Proxy rồi thử lại.',
|
||||||
|
powerUserLoading:
|
||||||
|
'Đang tải cài đặt an toàn cho chế độ power user. Vui lòng đợi một chút rồi thử lại.',
|
||||||
|
powerUserUnavailableRetry:
|
||||||
|
'Chế độ power user hiện không khả dụng. Hãy hoàn tất bước an toàn bắt buộc của nhà cung cấp rồi thử lại.',
|
||||||
authMethod: 'Phương thức xác thực',
|
authMethod: 'Phương thức xác thực',
|
||||||
selectKiroAuthMethod: 'Chọn phương thức xác thực Kiro',
|
selectKiroAuthMethod: 'Chọn phương thức xác thực Kiro',
|
||||||
nicknameRequired: 'Biệt danh (bắt buộc)',
|
nicknameRequired: 'Biệt danh (bắt buộc)',
|
||||||
@@ -4086,6 +4101,12 @@ const resources = {
|
|||||||
powerUserEnabled: '上級者モードが有効です',
|
powerUserEnabled: '上級者モードが有効です',
|
||||||
powerUserSkipped:
|
powerUserSkipped:
|
||||||
'設定 > プロキシのパワーユーザーモードにより、AGY の責任確認チェックと Gemini ダッシュボードのリスク文言入力をスキップしています。OAuth / アカウントに関するリスクはすべて自己責任です。',
|
'設定 > プロキシのパワーユーザーモードにより、AGY の責任確認チェックと Gemini ダッシュボードのリスク文言入力をスキップしています。OAuth / アカウントに関するリスクはすべて自己責任です。',
|
||||||
|
powerUserLoadFailed:
|
||||||
|
'パワーユーザーモード設定を読み込めませんでした。設定 > プロキシを確認してから再試行してください。',
|
||||||
|
powerUserLoading:
|
||||||
|
'パワーユーザーモードの安全設定を読み込み中です。少し待ってから再試行してください。',
|
||||||
|
powerUserUnavailableRetry:
|
||||||
|
'パワーユーザーモードは利用できません。必要なプロバイダーの安全確認を完了してから再試行してください。',
|
||||||
authMethod: '認証方法',
|
authMethod: '認証方法',
|
||||||
selectKiroAuthMethod: 'Kiro の認証方法を選択',
|
selectKiroAuthMethod: 'Kiro の認証方法を選択',
|
||||||
nicknameRequired: 'ニックネーム(必須)',
|
nicknameRequired: 'ニックネーム(必須)',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|||||||
import i18n from '@/lib/i18n';
|
import i18n from '@/lib/i18n';
|
||||||
import { AddAccountDialog } from '@/components/account/add-account-dialog';
|
import { AddAccountDialog } from '@/components/account/add-account-dialog';
|
||||||
import { render, screen, userEvent, waitFor } from '@tests/setup/test-utils';
|
import { render, screen, userEvent, waitFor } from '@tests/setup/test-utils';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
const authMocks = vi.hoisted(() => ({
|
const authMocks = vi.hoisted(() => ({
|
||||||
startAuth: vi.fn(),
|
startAuth: vi.fn(),
|
||||||
@@ -35,6 +36,14 @@ vi.mock('@/hooks/use-cliproxy', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('sonner', () => ({
|
||||||
|
toast: {
|
||||||
|
success: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
warning: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
function createJsonResponse(body: Record<string, unknown>): Response {
|
function createJsonResponse(body: Record<string, unknown>): Response {
|
||||||
return new Response(JSON.stringify(body), {
|
return new Response(JSON.stringify(body), {
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -42,7 +51,7 @@ function createJsonResponse(body: Record<string, unknown>): Response {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('AddAccountDialog Gemini power user mode', () => {
|
describe('AddAccountDialog power user mode', () => {
|
||||||
const fetchMock = vi.fn<typeof fetch>();
|
const fetchMock = vi.fn<typeof fetch>();
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -101,4 +110,66 @@ describe('AddAccountDialog Gemini power user mode', () => {
|
|||||||
expect(screen.queryByText('Power user mode enabled')).not.toBeInTheDocument();
|
expect(screen.queryByText('Power user mode enabled')).not.toBeInTheDocument();
|
||||||
expect(authMocks.startAuth).not.toHaveBeenCalled();
|
expect(authMocks.startAuth).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('surfaces a power user mode fetch failure and fails closed for Gemini', async () => {
|
||||||
|
fetchMock.mockRejectedValue(new Error('network down'));
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AddAccountDialog open onClose={vi.fn()} provider="gemini" displayName="Gemini" />
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(toast.error).toHaveBeenCalledWith(
|
||||||
|
'Failed to load power user mode settings. Check Settings > Proxy and try again.'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText('Failed to load power user mode settings. Check Settings > Proxy and try again.')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Type exact phrase to continue/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: 'Authenticate' })).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips the AGY responsibility checklist when power user mode is enabled', async () => {
|
||||||
|
fetchMock.mockResolvedValue(createJsonResponse({ antigravityAckBypass: true }));
|
||||||
|
|
||||||
|
render(<AddAccountDialog open onClose={vi.fn()} provider="agy" displayName="AGY" />);
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith('/api/settings/auth/antigravity-risk')
|
||||||
|
);
|
||||||
|
|
||||||
|
const authenticateButton = screen.getByRole('button', { name: 'Authenticate' });
|
||||||
|
await waitFor(() => expect(authenticateButton).toBeEnabled());
|
||||||
|
|
||||||
|
expect(screen.getByText('Power user mode enabled')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText(/Step 1: I reviewed issue #509/i)).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(authenticateButton);
|
||||||
|
|
||||||
|
expect(authMocks.startAuth).toHaveBeenCalledWith(
|
||||||
|
'agy',
|
||||||
|
expect.objectContaining({
|
||||||
|
riskAcknowledgement: undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the AGY responsibility checklist when power user mode is disabled', async () => {
|
||||||
|
fetchMock.mockResolvedValue(createJsonResponse({ antigravityAckBypass: false }));
|
||||||
|
|
||||||
|
render(<AddAccountDialog open onClose={vi.fn()} provider="agy" displayName="AGY" />);
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith('/api/settings/auth/antigravity-risk')
|
||||||
|
);
|
||||||
|
|
||||||
|
const authenticateButton = screen.getByRole('button', { name: 'Authenticate' });
|
||||||
|
await waitFor(() => expect(authenticateButton).toBeDisabled());
|
||||||
|
|
||||||
|
expect(screen.getByText(/Step 1: I reviewed issue #509/i)).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Power user mode enabled')).not.toBeInTheDocument();
|
||||||
|
expect(authMocks.startAuth).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user