auramaxx 1.0.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +26 -0
- package/README.md +112 -0
- package/bin/aurawallet.js +121 -0
- package/docs/ADAPTERS.md +467 -0
- package/docs/API.md +2679 -0
- package/docs/APPS.md +198 -0
- package/docs/ARCHITECTURE.md +350 -0
- package/docs/AUTH.md +698 -0
- package/docs/BEST-PRACTICES.md +121 -0
- package/docs/CLI.md +61 -0
- package/docs/DEVELOPING-APPS.md +452 -0
- package/docs/EXTENSION.md +97 -0
- package/docs/JOBS.md +33 -0
- package/docs/MCP.md +76 -0
- package/docs/PROTOCOL.md +142 -0
- package/docs/SETUP.md +219 -0
- package/docs/WORKSPACE.md +672 -0
- package/docs/agent-auth.md +63 -0
- package/docs/aura-file.md +48 -0
- package/docs/credentials.md +53 -0
- package/docs/external/getting-started.md +65 -0
- package/docs/external/overview.md +45 -0
- package/docs/external/use-cases.md +48 -0
- package/docs/external/why-aura.md +35 -0
- package/docs/jobs/connect-agent.md +77 -0
- package/docs/jobs/migrate-from-dotenv.md +79 -0
- package/docs/jobs/recover-from-lockout.md +72 -0
- package/docs/jobs/secure-ci.md +63 -0
- package/docs/oauth2.md +42 -0
- package/docs/passkeys.md +60 -0
- package/docs/security.md +540 -0
- package/docs/specs/aura-open-protocol.md +61 -0
- package/docs/specs/aura-provider-plugin.md +24 -0
- package/docs/specs/aura-registry-model.md +31 -0
- package/docs/specs/fixtures/invalid-bad-key.aura +1 -0
- package/docs/specs/fixtures/invalid-bad-unicode-escape.aura +1 -0
- package/docs/specs/fixtures/invalid-duplicate-key.aura +2 -0
- package/docs/specs/fixtures/valid-basic.aura +4 -0
- package/docs/specs/fixtures/valid-provider-ref.aura +1 -0
- package/docs/specs/fixtures/valid-quoted-escapes.aura +2 -0
- package/docs/templates/RELEASE_NOTES_TEMPLATE.md +22 -0
- package/docs/totp.md +40 -0
- package/docs/wallet/AI.md +508 -0
- package/docs/wallet/DEVELOPING-STRATEGIES.md +713 -0
- package/docs/wallet/README.md +47 -0
- package/docs/wallet/STRATEGY.md +89 -0
- package/next.config.ts +21 -0
- package/package.json +151 -0
- package/postcss.config.mjs +8 -0
- package/prisma/migrations/20260214170000_baseline/migration.sql +511 -0
- package/prisma/migrations/20260216214537_add_passkey_model/migration.sql +18 -0
- package/prisma/migrations/20260217150500_add_credential_access_audit/migration.sql +31 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +447 -0
- package/public/logo-chevron.svg +31 -0
- package/public/logo-concentric.svg +31 -0
- package/public/logo-crosshatch.svg +39 -0
- package/public/logo-dashed.svg +39 -0
- package/public/logo-horizontal.svg +31 -0
- package/public/logo-m56.svg +64 -0
- package/public/logo.webp +0 -0
- package/scripts/add-app.js +245 -0
- package/scripts/init.sh +57 -0
- package/scripts/migrate-apikeys-to-credentials.ts +35 -0
- package/scripts/sandbox-agent-flow.sh +235 -0
- package/scripts/sandbox.sh +175 -0
- package/scripts/validate-job-docs.mjs +125 -0
- package/server/abi/SwapHelper.json +438 -0
- package/server/cli/approval.ts +447 -0
- package/server/cli/commands/app.ts +204 -0
- package/server/cli/commands/cron.ts +24 -0
- package/server/cli/commands/doctor.ts +1007 -0
- package/server/cli/commands/env.ts +456 -0
- package/server/cli/commands/init.ts +752 -0
- package/server/cli/commands/mcp.ts +125 -0
- package/server/cli/commands/restore.ts +314 -0
- package/server/cli/commands/shell-hook.ts +468 -0
- package/server/cli/commands/start.ts +62 -0
- package/server/cli/commands/status.ts +59 -0
- package/server/cli/commands/stop.ts +14 -0
- package/server/cli/commands/token.ts +180 -0
- package/server/cli/commands/unlock.ts +49 -0
- package/server/cli/commands/vault.ts +417 -0
- package/server/cli/index.ts +328 -0
- package/server/cli/lib/aura-parser.ts +64 -0
- package/server/cli/lib/credential-create.ts +74 -0
- package/server/cli/lib/credential-resolve.ts +254 -0
- package/server/cli/lib/dotenv-migrate.ts +116 -0
- package/server/cli/lib/dotenv-parser.ts +146 -0
- package/server/cli/lib/http.ts +91 -0
- package/server/cli/lib/init-steps.ts +76 -0
- package/server/cli/lib/local-agent-trust.ts +45 -0
- package/server/cli/lib/process.ts +136 -0
- package/server/cli/lib/prompt.ts +85 -0
- package/server/cli/lib/theme.ts +240 -0
- package/server/cli/socket.ts +570 -0
- package/server/cli/transport-client.ts +50 -0
- package/server/cron/index.ts +137 -0
- package/server/cron/job.ts +31 -0
- package/server/cron/jobs/balance-sync.ts +436 -0
- package/server/cron/jobs/incoming-scan.ts +506 -0
- package/server/cron/jobs/native-price.ts +70 -0
- package/server/cron/jobs/orphan-cleanup.ts +40 -0
- package/server/cron/jobs/strategy-runner.ts +175 -0
- package/server/cron/scheduler.ts +125 -0
- package/server/index.ts +406 -0
- package/server/lib/adapters/factory.ts +110 -0
- package/server/lib/adapters/index.ts +19 -0
- package/server/lib/adapters/router.ts +297 -0
- package/server/lib/adapters/telegram.ts +645 -0
- package/server/lib/adapters/types.ts +89 -0
- package/server/lib/adapters/webhook.ts +95 -0
- package/server/lib/address.ts +49 -0
- package/server/lib/agent-auth/contracts.ts +1194 -0
- package/server/lib/agent-profiles.ts +328 -0
- package/server/lib/ai.ts +285 -0
- package/server/lib/api-registry/contracts.ts +86 -0
- package/server/lib/api-registry/validation.ts +172 -0
- package/server/lib/apikey-migration.ts +189 -0
- package/server/lib/app-installer.ts +505 -0
- package/server/lib/app-tokens.ts +247 -0
- package/server/lib/auth.ts +314 -0
- package/server/lib/batch.ts +242 -0
- package/server/lib/cold.ts +874 -0
- package/server/lib/config.ts +381 -0
- package/server/lib/credential-access-audit.ts +85 -0
- package/server/lib/credential-access-policy.ts +110 -0
- package/server/lib/credential-health.ts +343 -0
- package/server/lib/credential-import.ts +487 -0
- package/server/lib/credential-scope.ts +87 -0
- package/server/lib/credential-shares.ts +190 -0
- package/server/lib/credential-transport.ts +342 -0
- package/server/lib/credential-vault.ts +77 -0
- package/server/lib/credentials.ts +333 -0
- package/server/lib/crypto.ts +8 -0
- package/server/lib/db.ts +15 -0
- package/server/lib/defaults.ts +366 -0
- package/server/lib/dex/index.ts +80 -0
- package/server/lib/dex/relay.ts +235 -0
- package/server/lib/dex/types.ts +59 -0
- package/server/lib/dex/uniswap.ts +370 -0
- package/server/lib/e2e-agent/artifacts.ts +36 -0
- package/server/lib/e2e-agent/contracts.ts +112 -0
- package/server/lib/e2e-agent/validation.ts +135 -0
- package/server/lib/encrypt.ts +128 -0
- package/server/lib/error.ts +20 -0
- package/server/lib/events.ts +205 -0
- package/server/lib/hot.ts +357 -0
- package/server/lib/key-fingerprint.ts +28 -0
- package/server/lib/logger.ts +331 -0
- package/server/lib/network.ts +137 -0
- package/server/lib/notifications.ts +219 -0
- package/server/lib/oauth2-refresh.ts +241 -0
- package/server/lib/oursecret.ts +54 -0
- package/server/lib/passkey-credential.ts +360 -0
- package/server/lib/passkey.ts +68 -0
- package/server/lib/permissions.ts +248 -0
- package/server/lib/pino.ts +24 -0
- package/server/lib/policy-preview.ts +138 -0
- package/server/lib/price.ts +338 -0
- package/server/lib/prices.ts +34 -0
- package/server/lib/project-scope.ts +239 -0
- package/server/lib/resolve-action.ts +427 -0
- package/server/lib/resolve.ts +36 -0
- package/server/lib/sessions.ts +632 -0
- package/server/lib/solana/connection.ts +26 -0
- package/server/lib/solana/jupiter.ts +128 -0
- package/server/lib/solana/transfer.ts +108 -0
- package/server/lib/solana/wallet.ts +136 -0
- package/server/lib/strategy/emits.ts +21 -0
- package/server/lib/strategy/engine.ts +1305 -0
- package/server/lib/strategy/executor.ts +115 -0
- package/server/lib/strategy/hook-context.ts +158 -0
- package/server/lib/strategy/hooks.ts +990 -0
- package/server/lib/strategy/index.ts +28 -0
- package/server/lib/strategy/installer.ts +305 -0
- package/server/lib/strategy/loader.ts +256 -0
- package/server/lib/strategy/message.ts +235 -0
- package/server/lib/strategy/repository.ts +218 -0
- package/server/lib/strategy/session-logger.ts +693 -0
- package/server/lib/strategy/sources.ts +288 -0
- package/server/lib/strategy/state.ts +189 -0
- package/server/lib/strategy/templates.ts +403 -0
- package/server/lib/strategy/tick.ts +404 -0
- package/server/lib/strategy/types.ts +230 -0
- package/server/lib/swap.ts +3 -0
- package/server/lib/temp.ts +86 -0
- package/server/lib/token-metadata.ts +86 -0
- package/server/lib/token-safety.ts +200 -0
- package/server/lib/token-search.ts +444 -0
- package/server/lib/totp.ts +194 -0
- package/server/lib/transactions.ts +123 -0
- package/server/lib/transport.ts +75 -0
- package/server/lib/txhistory/decoder.ts +262 -0
- package/server/lib/txhistory/enricher.ts +652 -0
- package/server/lib/txhistory/index.ts +391 -0
- package/server/lib/txhistory/signatures.ts +59 -0
- package/server/lib/verified-summary.ts +421 -0
- package/server/mcp/profile-policy.ts +30 -0
- package/server/mcp/server.ts +619 -0
- package/server/mcp/tools.ts +523 -0
- package/server/middleware/auth.ts +119 -0
- package/server/middleware/requestLogger.ts +84 -0
- package/server/routes/actions.ts +459 -0
- package/server/routes/adapters.ts +703 -0
- package/server/routes/addressbook.ts +113 -0
- package/server/routes/ai.ts +34 -0
- package/server/routes/apikeys.ts +295 -0
- package/server/routes/apps.ts +601 -0
- package/server/routes/auth.ts +457 -0
- package/server/routes/backup.ts +340 -0
- package/server/routes/batch.ts +270 -0
- package/server/routes/bookmarks.ts +162 -0
- package/server/routes/credential-shares.ts +198 -0
- package/server/routes/credential-vaults.ts +154 -0
- package/server/routes/credentials.ts +1290 -0
- package/server/routes/dashboard.ts +71 -0
- package/server/routes/defaults.ts +124 -0
- package/server/routes/fund.ts +229 -0
- package/server/routes/import.ts +352 -0
- package/server/routes/launch.ts +665 -0
- package/server/routes/lock.ts +54 -0
- package/server/routes/logs.ts +68 -0
- package/server/routes/nuke.ts +111 -0
- package/server/routes/passkey-credentials.ts +99 -0
- package/server/routes/passkey.ts +346 -0
- package/server/routes/portfolio.ts +217 -0
- package/server/routes/price.ts +63 -0
- package/server/routes/resolve.ts +31 -0
- package/server/routes/security.ts +45 -0
- package/server/routes/send-evm.ts +241 -0
- package/server/routes/send-solana.ts +281 -0
- package/server/routes/send.ts +178 -0
- package/server/routes/setup.ts +210 -0
- package/server/routes/strategy.ts +894 -0
- package/server/routes/swap-evm.ts +353 -0
- package/server/routes/swap-solana.ts +177 -0
- package/server/routes/swap.ts +356 -0
- package/server/routes/token.ts +247 -0
- package/server/routes/unlock.ts +403 -0
- package/server/routes/wallet-assets.ts +361 -0
- package/server/routes/wallet-transactions.ts +515 -0
- package/server/routes/wallet.ts +710 -0
- package/server/types.ts +146 -0
- package/skills/aurawallet/SKILL.md +739 -0
- package/skills/aurawallet-setup/SKILL.md +74 -0
- package/skills/security-review/SKILL.md +148 -0
- package/src/app/api/agent-requests/route.ts +30 -0
- package/src/app/api/apps/install/route.ts +126 -0
- package/src/app/api/apps/manifests/route.ts +16 -0
- package/src/app/api/apps/static/[...path]/route.ts +57 -0
- package/src/app/api/events/route.ts +92 -0
- package/src/app/api/page.tsx +212 -0
- package/src/app/api/workspace/[id]/apps/[wid]/route.ts +119 -0
- package/src/app/api/workspace/[id]/apps/route.ts +81 -0
- package/src/app/api/workspace/[id]/export/route.ts +67 -0
- package/src/app/api/workspace/[id]/route.ts +168 -0
- package/src/app/api/workspace/auth.ts +34 -0
- package/src/app/api/workspace/config/route.ts +106 -0
- package/src/app/api/workspace/import/route.ts +127 -0
- package/src/app/api/workspace/route.ts +116 -0
- package/src/app/app/page.tsx +2122 -0
- package/src/app/apple-icon.png +0 -0
- package/src/app/docs/page.tsx +178 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +572 -0
- package/src/app/health/page.tsx +5 -0
- package/src/app/hello/page.tsx +15 -0
- package/src/app/icon.png +0 -0
- package/src/app/layout.tsx +34 -0
- package/src/app/page.tsx +986 -0
- package/src/app/providers.tsx +90 -0
- package/src/app/share/[token]/page.tsx +295 -0
- package/src/components/ChainSelector.tsx +144 -0
- package/src/components/HumanActionBar.tsx +695 -0
- package/src/components/NotificationDrawer.tsx +129 -0
- package/src/components/apps/AgentKeysApp.tsx +490 -0
- package/src/components/apps/App.tsx +153 -0
- package/src/components/apps/AppGrid.tsx +15 -0
- package/src/components/apps/DetailedAddressDrawer.tsx +325 -0
- package/src/components/apps/DraggableApp.tsx +562 -0
- package/src/components/apps/IFrameApp.tsx +73 -0
- package/src/components/apps/LogsApp.tsx +360 -0
- package/src/components/apps/SendApp.tsx +394 -0
- package/src/components/apps/SetupWizardApp.tsx +1004 -0
- package/src/components/apps/SystemDefaultsApp.tsx +845 -0
- package/src/components/apps/ThirdPartyApp.tsx +428 -0
- package/src/components/apps/TokenApp.tsx +319 -0
- package/src/components/apps/TransactionsApp.tsx +438 -0
- package/src/components/apps/WalletDetailApp.tsx +1505 -0
- package/src/components/apps/index.ts +13 -0
- package/src/components/design-system/Button.tsx +53 -0
- package/src/components/design-system/ChainIndicator.tsx +65 -0
- package/src/components/design-system/ChainSelector.tsx +137 -0
- package/src/components/design-system/ConfirmationModal.tsx +106 -0
- package/src/components/design-system/ConfirmationPopover.tsx +81 -0
- package/src/components/design-system/Drawer.tsx +123 -0
- package/src/components/design-system/FilterDropdown.tsx +72 -0
- package/src/components/design-system/Modal.tsx +206 -0
- package/src/components/design-system/Popover.tsx +142 -0
- package/src/components/design-system/TextInput.tsx +85 -0
- package/src/components/design-system/Toggle.tsx +58 -0
- package/src/components/design-system/index.ts +11 -0
- package/src/components/docs/DocsThemeToggle.tsx +49 -0
- package/src/components/health/CredentialHealthDashboard.tsx +214 -0
- package/src/components/icons/ChainIcons.tsx +72 -0
- package/src/components/layout/AppStoreDrawer.tsx +369 -0
- package/src/components/layout/ContentArea.tsx +21 -0
- package/src/components/layout/TabBar.tsx +278 -0
- package/src/components/layout/WalletSidebar.tsx +1033 -0
- package/src/components/layout/index.ts +4 -0
- package/src/components/marketing/AuraWalletSpecOverlay.tsx +635 -0
- package/src/components/marketing/DeviceMorphExperience.tsx +216 -0
- package/src/components/vault/ApiKeysConsole.tsx +1080 -0
- package/src/components/vault/AuditConsole.tsx +584 -0
- package/src/components/vault/CredentialDetail.tsx +455 -0
- package/src/components/vault/CredentialEmpty.tsx +55 -0
- package/src/components/vault/CredentialField.tsx +361 -0
- package/src/components/vault/CredentialForm.tsx +1212 -0
- package/src/components/vault/CredentialList.tsx +165 -0
- package/src/components/vault/CredentialRow.tsx +97 -0
- package/src/components/vault/CredentialShareModal.tsx +178 -0
- package/src/components/vault/CredentialVault.tsx +754 -0
- package/src/components/vault/CredentialWalletWidget.tsx +103 -0
- package/src/components/vault/ImportCredentialsModal.tsx +515 -0
- package/src/components/vault/LargeTypeModal.tsx +64 -0
- package/src/components/vault/PasswordGenerator.tsx +224 -0
- package/src/components/vault/TOTPDisplay.tsx +123 -0
- package/src/components/vault/VaultSidebar.tsx +413 -0
- package/src/components/vault/types.ts +54 -0
- package/src/context/AuthContext.tsx +337 -0
- package/src/context/PriceContext.tsx +113 -0
- package/src/context/ThemeContext.tsx +164 -0
- package/src/context/WebSocketContext.tsx +269 -0
- package/src/context/WorkspaceContext.tsx +668 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useAgentActions.ts +368 -0
- package/src/hooks/useBalance.ts +103 -0
- package/src/hooks/useBalances.ts +129 -0
- package/src/instrumentation.ts +12 -0
- package/src/lib/api.ts +449 -0
- package/src/lib/app-loader.ts +148 -0
- package/src/lib/app-registry.ts +178 -0
- package/src/lib/app-sdk.ts +157 -0
- package/src/lib/audit-console-adapter.ts +151 -0
- package/src/lib/auth-client.ts +75 -0
- package/src/lib/config.ts +74 -0
- package/src/lib/crypto.ts +112 -0
- package/src/lib/db.ts +21 -0
- package/src/lib/docs.ts +390 -0
- package/src/lib/events.ts +361 -0
- package/src/lib/pino.ts +24 -0
- package/src/lib/theme-handlers.ts +168 -0
- package/src/lib/theme.ts +351 -0
- package/src/lib/tokenData.ts +378 -0
- package/src/lib/vault-crypto.ts +129 -0
- package/src/lib/websocket-server.ts +302 -0
- package/src/lib/websocket-setup.ts +79 -0
- package/src/lib/wordlist.ts +2050 -0
- package/src/lib/workspace-handlers.ts +285 -0
- package/start.sh +80 -0
- package/tailwind.config.ts +99 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { Copy, ExternalLink, Wallet } from 'lucide-react';
|
|
5
|
+
import { api, Api } from '@/lib/api';
|
|
6
|
+
import type { WalletLinkMetaV1 } from './types';
|
|
7
|
+
|
|
8
|
+
interface WalletSummary {
|
|
9
|
+
address: string;
|
|
10
|
+
tier: 'cold' | 'hot' | 'temp';
|
|
11
|
+
chain: string;
|
|
12
|
+
balance?: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
label?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface CredentialWalletWidgetProps {
|
|
18
|
+
walletLink: WalletLinkMetaV1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const shortAddress = (addr: string) => `${addr.slice(0, 6)}...${addr.slice(-4)}`;
|
|
22
|
+
|
|
23
|
+
function explorerUrl(chain: string, address: string): string {
|
|
24
|
+
if (chain === 'solana' || chain === 'solana-devnet') return `https://solscan.io/account/${address}`;
|
|
25
|
+
if (chain === 'polygon') return `https://polygonscan.com/address/${address}`;
|
|
26
|
+
if (chain === 'ethereum') return `https://etherscan.io/address/${address}`;
|
|
27
|
+
return `https://basescan.org/address/${address}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const CredentialWalletWidget: React.FC<CredentialWalletWidgetProps> = ({ walletLink }) => {
|
|
31
|
+
const [wallet, setWallet] = useState<WalletSummary | null>(null);
|
|
32
|
+
const [loading, setLoading] = useState(true);
|
|
33
|
+
const [copyOk, setCopyOk] = useState(false);
|
|
34
|
+
const normalizedChain = walletLink.chain.toLowerCase();
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
let mounted = true;
|
|
38
|
+
(async () => {
|
|
39
|
+
try {
|
|
40
|
+
const res = await api.get<{ wallets: WalletSummary[] }>(Api.Wallet, '/wallets');
|
|
41
|
+
const match = (res.wallets || []).find((w) =>
|
|
42
|
+
w.address.toLowerCase() === walletLink.walletAddress.toLowerCase() && w.chain.toLowerCase() === normalizedChain,
|
|
43
|
+
) || null;
|
|
44
|
+
if (mounted) setWallet(match);
|
|
45
|
+
} catch {
|
|
46
|
+
if (mounted) setWallet(null);
|
|
47
|
+
} finally {
|
|
48
|
+
if (mounted) setLoading(false);
|
|
49
|
+
}
|
|
50
|
+
})();
|
|
51
|
+
return () => { mounted = false; };
|
|
52
|
+
}, [walletLink.walletAddress, normalizedChain, walletLink.chain]);
|
|
53
|
+
|
|
54
|
+
const resolvedName = useMemo(() => wallet?.name || wallet?.label || walletLink.label || 'Linked Wallet', [wallet, walletLink.label]);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="mt-2 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-background-alt,#f4f4f5)] p-3">
|
|
58
|
+
<div className="flex items-center justify-between mb-2">
|
|
59
|
+
<div className="font-mono text-[9px] font-bold uppercase tracking-widest text-[var(--color-text-muted,#6b7280)] inline-flex items-center gap-1">
|
|
60
|
+
<Wallet size={11} /> Linked Wallet
|
|
61
|
+
</div>
|
|
62
|
+
<div className="font-mono text-[8px] uppercase tracking-widest text-[var(--color-text-muted,#6b7280)]">
|
|
63
|
+
{walletLink.tier} · {walletLink.chain}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div className="font-mono text-[11px] text-[var(--color-text,#0a0a0a)] mb-1">{resolvedName}</div>
|
|
68
|
+
<div className="font-mono text-[10px] text-[var(--color-text-muted,#6b7280)] mb-2 break-all">
|
|
69
|
+
{walletLink.walletAddress}
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{loading ? (
|
|
73
|
+
<div className="font-mono text-[9px] text-[var(--color-text-muted,#6b7280)]">Loading wallet state…</div>
|
|
74
|
+
) : wallet ? (
|
|
75
|
+
<div className="font-mono text-[9px] text-[var(--color-success,#22c55e)] mb-2">Wallet resolved{wallet.balance ? ` · Balance: ${wallet.balance}` : ''}</div>
|
|
76
|
+
) : (
|
|
77
|
+
<div className="font-mono text-[9px] text-[var(--color-warning,#f59e0b)] mb-2">Stale link: wallet not found in current wallet list</div>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
<div className="flex gap-2">
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
className="px-2 py-1 border border-[var(--color-border,#d4d4d8)] font-mono text-[9px]"
|
|
84
|
+
onClick={async () => {
|
|
85
|
+
await navigator.clipboard.writeText(walletLink.walletAddress);
|
|
86
|
+
setCopyOk(true);
|
|
87
|
+
setTimeout(() => setCopyOk(false), 1200);
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<span className="inline-flex items-center gap-1"><Copy size={10} /> {copyOk ? 'COPIED' : shortAddress(walletLink.walletAddress)}</span>
|
|
91
|
+
</button>
|
|
92
|
+
<a
|
|
93
|
+
href={explorerUrl(normalizedChain, walletLink.walletAddress)}
|
|
94
|
+
target="_blank"
|
|
95
|
+
rel="noreferrer"
|
|
96
|
+
className="px-2 py-1 border border-[var(--color-border,#d4d4d8)] font-mono text-[9px] inline-flex items-center gap-1"
|
|
97
|
+
>
|
|
98
|
+
<ExternalLink size={10} /> Explorer
|
|
99
|
+
</a>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useRef, useCallback } from 'react';
|
|
4
|
+
import { Upload, FileText, AlertCircle, CheckCircle, Info } from 'lucide-react';
|
|
5
|
+
import { Modal, Button } from '@/components/design-system';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Types
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
interface ImportSource {
|
|
12
|
+
label: string;
|
|
13
|
+
format: string;
|
|
14
|
+
supported: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const IMPORT_SOURCES: ImportSource[] = [
|
|
18
|
+
{ label: '1Password', format: '1password-csv', supported: true },
|
|
19
|
+
{ label: 'Bitwarden', format: 'bitwarden-csv', supported: true },
|
|
20
|
+
{ label: 'Chrome', format: 'chrome-csv', supported: true },
|
|
21
|
+
{ label: 'Firefox', format: 'firefox-csv', supported: true },
|
|
22
|
+
{ label: 'iCloud Keychain', format: 'icloud-csv', supported: false },
|
|
23
|
+
{ label: 'LastPass', format: 'lastpass-csv', supported: false },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
type DuplicateStrategy = 'skip' | 'rename' | 'overwrite';
|
|
27
|
+
|
|
28
|
+
const STRATEGY_LABELS: Record<DuplicateStrategy, string> = {
|
|
29
|
+
skip: 'Skip',
|
|
30
|
+
rename: 'Rename',
|
|
31
|
+
overwrite: 'Create Anyway',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
interface PreviewCredential {
|
|
35
|
+
name: string;
|
|
36
|
+
type: string;
|
|
37
|
+
url?: string;
|
|
38
|
+
fieldCount: number;
|
|
39
|
+
isDuplicate: boolean;
|
|
40
|
+
duplicateMatch?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface PreviewResult {
|
|
44
|
+
success: boolean;
|
|
45
|
+
total: number;
|
|
46
|
+
duplicates: number;
|
|
47
|
+
credentials: PreviewCredential[];
|
|
48
|
+
error?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface ImportResult {
|
|
52
|
+
success: boolean;
|
|
53
|
+
imported: number;
|
|
54
|
+
skipped: number;
|
|
55
|
+
errors: { row: number; reason: string }[];
|
|
56
|
+
error?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type Step = 'select' | 'preview' | 'result';
|
|
60
|
+
|
|
61
|
+
interface ImportCredentialsModalProps {
|
|
62
|
+
isOpen: boolean;
|
|
63
|
+
onClose: () => void;
|
|
64
|
+
onComplete: () => void;
|
|
65
|
+
vaultId: string | null;
|
|
66
|
+
vaultName: string;
|
|
67
|
+
walletBaseUrl: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Helpers
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
function getToken(): string | null {
|
|
75
|
+
if (typeof window === 'undefined') return null;
|
|
76
|
+
return sessionStorage.getItem('aurawallet_admin_token');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Component
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
export const ImportCredentialsModal: React.FC<ImportCredentialsModalProps> = ({
|
|
84
|
+
isOpen,
|
|
85
|
+
onClose,
|
|
86
|
+
onComplete,
|
|
87
|
+
vaultId,
|
|
88
|
+
vaultName,
|
|
89
|
+
walletBaseUrl,
|
|
90
|
+
}) => {
|
|
91
|
+
const [step, setStep] = useState<Step>('select');
|
|
92
|
+
const [selectedFormat, setSelectedFormat] = useState<string>('1password-csv');
|
|
93
|
+
const [file, setFile] = useState<File | null>(null);
|
|
94
|
+
const [duplicateStrategy, setDuplicateStrategy] = useState<DuplicateStrategy>('skip');
|
|
95
|
+
const [preview, setPreview] = useState<PreviewResult | null>(null);
|
|
96
|
+
const [result, setResult] = useState<ImportResult | null>(null);
|
|
97
|
+
const [loading, setLoading] = useState(false);
|
|
98
|
+
const [error, setError] = useState<string | null>(null);
|
|
99
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
100
|
+
|
|
101
|
+
const reset = useCallback(() => {
|
|
102
|
+
setStep('select');
|
|
103
|
+
setSelectedFormat('1password-csv');
|
|
104
|
+
setFile(null);
|
|
105
|
+
setDuplicateStrategy('skip');
|
|
106
|
+
setPreview(null);
|
|
107
|
+
setResult(null);
|
|
108
|
+
setLoading(false);
|
|
109
|
+
setError(null);
|
|
110
|
+
if (fileInputRef.current) fileInputRef.current.value = '';
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
const handleClose = useCallback(() => {
|
|
114
|
+
reset();
|
|
115
|
+
onClose();
|
|
116
|
+
}, [reset, onClose]);
|
|
117
|
+
|
|
118
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
119
|
+
|
|
120
|
+
const handleFileChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
121
|
+
const f = e.target.files?.[0] ?? null;
|
|
122
|
+
if (f && f.size > MAX_FILE_SIZE) {
|
|
123
|
+
setError(`File too large (${(f.size / 1024 / 1024).toFixed(1)}MB). Maximum is 10MB.`);
|
|
124
|
+
setFile(null);
|
|
125
|
+
if (fileInputRef.current) fileInputRef.current.value = '';
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
setFile(f);
|
|
129
|
+
setError(null);
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
const buildFormData = useCallback(
|
|
133
|
+
(dryRun: boolean) => {
|
|
134
|
+
if (!file) return null;
|
|
135
|
+
const fd = new FormData();
|
|
136
|
+
fd.append('file', file);
|
|
137
|
+
fd.append('format', selectedFormat);
|
|
138
|
+
fd.append('vaultId', vaultId || 'primary');
|
|
139
|
+
if (dryRun) fd.append('dryRun', 'true');
|
|
140
|
+
fd.append('duplicateStrategy', duplicateStrategy);
|
|
141
|
+
return fd;
|
|
142
|
+
},
|
|
143
|
+
[file, selectedFormat, vaultId, duplicateStrategy],
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const handlePreview = useCallback(async () => {
|
|
147
|
+
if (!file) return;
|
|
148
|
+
setLoading(true);
|
|
149
|
+
setError(null);
|
|
150
|
+
try {
|
|
151
|
+
const fd = buildFormData(true);
|
|
152
|
+
if (!fd) return;
|
|
153
|
+
const token = getToken();
|
|
154
|
+
const res = await fetch(`${walletBaseUrl}/credentials/import`, {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
157
|
+
body: fd,
|
|
158
|
+
});
|
|
159
|
+
const contentType = res.headers.get('content-type') || '';
|
|
160
|
+
if (!contentType.includes('application/json')) {
|
|
161
|
+
setError(`Server error: ${res.status} ${res.statusText}`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const data: PreviewResult = await res.json();
|
|
165
|
+
if (!res.ok || !data.success) {
|
|
166
|
+
setError(data.error || `Request failed: ${res.status}`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
setPreview(data);
|
|
170
|
+
setStep('preview');
|
|
171
|
+
} catch (err) {
|
|
172
|
+
setError(err instanceof Error ? err.message : 'Preview failed');
|
|
173
|
+
} finally {
|
|
174
|
+
setLoading(false);
|
|
175
|
+
}
|
|
176
|
+
}, [file, buildFormData, walletBaseUrl]);
|
|
177
|
+
|
|
178
|
+
const handleImport = useCallback(async () => {
|
|
179
|
+
setLoading(true);
|
|
180
|
+
setError(null);
|
|
181
|
+
try {
|
|
182
|
+
const fd = buildFormData(false);
|
|
183
|
+
if (!fd) return;
|
|
184
|
+
const token = getToken();
|
|
185
|
+
const res = await fetch(`${walletBaseUrl}/credentials/import`, {
|
|
186
|
+
method: 'POST',
|
|
187
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
188
|
+
body: fd,
|
|
189
|
+
});
|
|
190
|
+
const ct = res.headers.get('content-type') || '';
|
|
191
|
+
if (!ct.includes('application/json')) {
|
|
192
|
+
setError(`Server error: ${res.status} ${res.statusText}`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const data: ImportResult = await res.json();
|
|
196
|
+
if (!res.ok) {
|
|
197
|
+
setError(data.error || `Request failed: ${res.status}`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
setResult(data);
|
|
201
|
+
setStep('result');
|
|
202
|
+
} catch (err) {
|
|
203
|
+
setError(err instanceof Error ? err.message : 'Import failed');
|
|
204
|
+
} finally {
|
|
205
|
+
setLoading(false);
|
|
206
|
+
}
|
|
207
|
+
}, [buildFormData, walletBaseUrl]);
|
|
208
|
+
|
|
209
|
+
const handleDone = useCallback(() => {
|
|
210
|
+
onComplete();
|
|
211
|
+
handleClose();
|
|
212
|
+
}, [onComplete, handleClose]);
|
|
213
|
+
|
|
214
|
+
// -------------------------------------------------------------------------
|
|
215
|
+
// Render helpers
|
|
216
|
+
// -------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
const selectedSource = IMPORT_SOURCES.find((s) => s.format === selectedFormat);
|
|
219
|
+
|
|
220
|
+
const renderSelectStep = () => (
|
|
221
|
+
<div className="space-y-5">
|
|
222
|
+
{/* Source selector */}
|
|
223
|
+
<div>
|
|
224
|
+
<label className="block text-[9px] font-bold tracking-widest uppercase text-[var(--color-text-muted,#6b7280)] mb-2">
|
|
225
|
+
Source
|
|
226
|
+
</label>
|
|
227
|
+
<div className="grid grid-cols-1 gap-1.5">
|
|
228
|
+
{IMPORT_SOURCES.map((source) => (
|
|
229
|
+
<button
|
|
230
|
+
key={source.format}
|
|
231
|
+
onClick={() => source.supported && setSelectedFormat(source.format)}
|
|
232
|
+
disabled={!source.supported}
|
|
233
|
+
className={`flex items-center gap-2 px-3 py-2 text-left transition-colors border ${
|
|
234
|
+
selectedFormat === source.format
|
|
235
|
+
? 'border-[var(--color-accent,#ccff00)] bg-[var(--color-accent,#ccff00)]/5'
|
|
236
|
+
: source.supported
|
|
237
|
+
? 'border-[var(--color-border,#d4d4d8)] hover:bg-[var(--color-background-alt,#f4f4f5)]'
|
|
238
|
+
: 'border-[var(--color-border,#d4d4d8)] opacity-40 cursor-not-allowed'
|
|
239
|
+
}`}
|
|
240
|
+
>
|
|
241
|
+
<FileText size={12} className="text-[var(--color-text-muted,#6b7280)] flex-shrink-0" />
|
|
242
|
+
<span className="text-[10px] font-mono tracking-wider uppercase text-[var(--color-text,#0a0a0a)]">
|
|
243
|
+
{source.label}
|
|
244
|
+
</span>
|
|
245
|
+
{!source.supported && (
|
|
246
|
+
<span className="ml-auto text-[8px] font-mono tracking-wider uppercase text-[var(--color-text-faint,#9ca3af)]">
|
|
247
|
+
Coming soon
|
|
248
|
+
</span>
|
|
249
|
+
)}
|
|
250
|
+
</button>
|
|
251
|
+
))}
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
{/* File input */}
|
|
256
|
+
<div>
|
|
257
|
+
<label className="block text-[9px] font-bold tracking-widest uppercase text-[var(--color-text-muted,#6b7280)] mb-2">
|
|
258
|
+
File
|
|
259
|
+
</label>
|
|
260
|
+
<input
|
|
261
|
+
ref={fileInputRef}
|
|
262
|
+
type="file"
|
|
263
|
+
accept=".csv"
|
|
264
|
+
onChange={handleFileChange}
|
|
265
|
+
className="hidden"
|
|
266
|
+
/>
|
|
267
|
+
<button
|
|
268
|
+
onClick={() => fileInputRef.current?.click()}
|
|
269
|
+
className="w-full flex items-center justify-center gap-2 px-4 py-4 border-2 border-dashed border-[var(--color-border,#d4d4d8)] hover:border-[var(--color-text-muted,#6b7280)] transition-colors"
|
|
270
|
+
>
|
|
271
|
+
<Upload size={14} className="text-[var(--color-text-muted,#6b7280)]" />
|
|
272
|
+
<span className="text-[10px] font-mono tracking-wider text-[var(--color-text-muted,#6b7280)]">
|
|
273
|
+
{file ? file.name : 'Choose CSV file'}
|
|
274
|
+
</span>
|
|
275
|
+
</button>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
{/* Vault target */}
|
|
279
|
+
<div className="flex items-center gap-2 px-2 py-1.5 bg-[var(--color-background-alt,#f4f4f5)]">
|
|
280
|
+
<Info size={10} className="text-[var(--color-text-faint,#9ca3af)] flex-shrink-0" />
|
|
281
|
+
<span className="text-[9px] font-mono tracking-wider text-[var(--color-text-muted,#6b7280)]">
|
|
282
|
+
Importing to <strong className="text-[var(--color-text,#0a0a0a)]">{vaultName}</strong>
|
|
283
|
+
</span>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
{/* Error */}
|
|
287
|
+
{error && (
|
|
288
|
+
<div className="flex items-start gap-2 px-3 py-2 bg-red-50 border border-red-200">
|
|
289
|
+
<AlertCircle size={12} className="text-red-500 flex-shrink-0 mt-0.5" />
|
|
290
|
+
<span className="text-[10px] font-mono text-red-700">{error}</span>
|
|
291
|
+
</div>
|
|
292
|
+
)}
|
|
293
|
+
|
|
294
|
+
{/* Actions */}
|
|
295
|
+
<div className="flex gap-2 justify-end">
|
|
296
|
+
<Button variant="secondary" size="sm" onClick={handleClose}>
|
|
297
|
+
CANCEL
|
|
298
|
+
</Button>
|
|
299
|
+
<Button
|
|
300
|
+
size="sm"
|
|
301
|
+
onClick={handlePreview}
|
|
302
|
+
loading={loading}
|
|
303
|
+
disabled={!file || !selectedSource?.supported}
|
|
304
|
+
>
|
|
305
|
+
PREVIEW
|
|
306
|
+
</Button>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const renderPreviewStep = () => {
|
|
312
|
+
if (!preview) return null;
|
|
313
|
+
return (
|
|
314
|
+
<div className="space-y-4">
|
|
315
|
+
{/* Summary */}
|
|
316
|
+
<div className="flex gap-4">
|
|
317
|
+
<div className="text-center">
|
|
318
|
+
<div className="text-[18px] font-bold font-mono text-[var(--color-text,#0a0a0a)]">
|
|
319
|
+
{preview.total}
|
|
320
|
+
</div>
|
|
321
|
+
<div className="text-[8px] font-mono tracking-widest uppercase text-[var(--color-text-muted,#6b7280)]">
|
|
322
|
+
Total
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
{preview.duplicates > 0 && (
|
|
326
|
+
<div className="text-center">
|
|
327
|
+
<div className="text-[18px] font-bold font-mono text-amber-600">
|
|
328
|
+
{preview.duplicates}
|
|
329
|
+
</div>
|
|
330
|
+
<div className="text-[8px] font-mono tracking-widest uppercase text-[var(--color-text-muted,#6b7280)]">
|
|
331
|
+
Duplicates
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
)}
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
{/* Preview list */}
|
|
338
|
+
<div className="max-h-[240px] overflow-y-auto border border-[var(--color-border,#d4d4d8)]">
|
|
339
|
+
{preview.credentials.length === 0 ? (
|
|
340
|
+
<div className="px-4 py-8 text-center text-[10px] font-mono text-[var(--color-text-muted,#6b7280)]">
|
|
341
|
+
No credentials found in file
|
|
342
|
+
</div>
|
|
343
|
+
) : (
|
|
344
|
+
preview.credentials.map((c, i) => (
|
|
345
|
+
<div
|
|
346
|
+
key={i}
|
|
347
|
+
className={`flex items-center gap-2 px-3 py-1.5 border-b border-[var(--color-border,#d4d4d8)] last:border-b-0 ${
|
|
348
|
+
c.isDuplicate ? 'bg-amber-50/50' : ''
|
|
349
|
+
}`}
|
|
350
|
+
>
|
|
351
|
+
<div className="flex-1 min-w-0">
|
|
352
|
+
<div className="text-[10px] font-mono text-[var(--color-text,#0a0a0a)] truncate">
|
|
353
|
+
{c.name}
|
|
354
|
+
</div>
|
|
355
|
+
{c.url && (
|
|
356
|
+
<div className="text-[8px] font-mono text-[var(--color-text-faint,#9ca3af)] truncate">
|
|
357
|
+
{c.url}
|
|
358
|
+
</div>
|
|
359
|
+
)}
|
|
360
|
+
</div>
|
|
361
|
+
<span className="text-[8px] font-mono tracking-wider uppercase text-[var(--color-text-faint,#9ca3af)] flex-shrink-0">
|
|
362
|
+
{c.type}
|
|
363
|
+
</span>
|
|
364
|
+
{c.isDuplicate && (
|
|
365
|
+
<span className="text-[8px] font-mono tracking-wider uppercase text-amber-600 flex-shrink-0">
|
|
366
|
+
DUP
|
|
367
|
+
</span>
|
|
368
|
+
)}
|
|
369
|
+
</div>
|
|
370
|
+
))
|
|
371
|
+
)}
|
|
372
|
+
{preview.total > preview.credentials.length && (
|
|
373
|
+
<div className="px-3 py-1.5 text-[9px] font-mono text-[var(--color-text-faint,#9ca3af)] text-center">
|
|
374
|
+
+ {preview.total - preview.credentials.length} more
|
|
375
|
+
</div>
|
|
376
|
+
)}
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
{/* Duplicate strategy */}
|
|
380
|
+
{preview.duplicates > 0 && (
|
|
381
|
+
<div>
|
|
382
|
+
<label className="block text-[9px] font-bold tracking-widest uppercase text-[var(--color-text-muted,#6b7280)] mb-1.5">
|
|
383
|
+
Duplicate handling
|
|
384
|
+
</label>
|
|
385
|
+
<div className="flex gap-1.5">
|
|
386
|
+
{(['skip', 'rename', 'overwrite'] as DuplicateStrategy[]).map((s) => (
|
|
387
|
+
<button
|
|
388
|
+
key={s}
|
|
389
|
+
onClick={() => setDuplicateStrategy(s)}
|
|
390
|
+
className={`px-3 py-1.5 text-[9px] font-mono tracking-wider uppercase border transition-colors ${
|
|
391
|
+
duplicateStrategy === s
|
|
392
|
+
? 'border-[var(--color-accent,#ccff00)] bg-[var(--color-accent,#ccff00)]/10 text-[var(--color-text,#0a0a0a)]'
|
|
393
|
+
: 'border-[var(--color-border,#d4d4d8)] text-[var(--color-text-muted,#6b7280)] hover:bg-[var(--color-background-alt,#f4f4f5)]'
|
|
394
|
+
}`}
|
|
395
|
+
>
|
|
396
|
+
{STRATEGY_LABELS[s]}
|
|
397
|
+
</button>
|
|
398
|
+
))}
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
)}
|
|
402
|
+
|
|
403
|
+
{/* Error */}
|
|
404
|
+
{error && (
|
|
405
|
+
<div className="flex items-start gap-2 px-3 py-2 bg-red-50 border border-red-200">
|
|
406
|
+
<AlertCircle size={12} className="text-red-500 flex-shrink-0 mt-0.5" />
|
|
407
|
+
<span className="text-[10px] font-mono text-red-700">{error}</span>
|
|
408
|
+
</div>
|
|
409
|
+
)}
|
|
410
|
+
|
|
411
|
+
{/* Actions */}
|
|
412
|
+
<div className="flex gap-2 justify-end">
|
|
413
|
+
<Button variant="secondary" size="sm" onClick={() => { setStep('select'); setError(null); if (fileInputRef.current) fileInputRef.current.value = ''; }}>
|
|
414
|
+
BACK
|
|
415
|
+
</Button>
|
|
416
|
+
<Button
|
|
417
|
+
size="sm"
|
|
418
|
+
onClick={handleImport}
|
|
419
|
+
loading={loading}
|
|
420
|
+
disabled={preview.credentials.length === 0}
|
|
421
|
+
>
|
|
422
|
+
IMPORT {duplicateStrategy === 'skip' ? preview.total - preview.duplicates : preview.total} CREDENTIAL{(duplicateStrategy === 'skip' ? preview.total - preview.duplicates : preview.total) !== 1 ? 'S' : ''}
|
|
423
|
+
</Button>
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
);
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const renderResultStep = () => {
|
|
430
|
+
if (!result) return null;
|
|
431
|
+
const hasErrors = result.errors.length > 0;
|
|
432
|
+
return (
|
|
433
|
+
<div className="space-y-4">
|
|
434
|
+
{/* Result icon */}
|
|
435
|
+
<div className="flex items-center gap-3">
|
|
436
|
+
{result.success ? (
|
|
437
|
+
<CheckCircle size={20} className="text-green-600" />
|
|
438
|
+
) : (
|
|
439
|
+
<AlertCircle size={20} className="text-red-500" />
|
|
440
|
+
)}
|
|
441
|
+
<div>
|
|
442
|
+
<div className="text-[11px] font-mono font-bold text-[var(--color-text,#0a0a0a)]">
|
|
443
|
+
{result.success ? 'Import Complete' : 'Import Failed'}
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
</div>
|
|
447
|
+
|
|
448
|
+
{/* Counts */}
|
|
449
|
+
<div className="flex gap-6">
|
|
450
|
+
<div className="text-center">
|
|
451
|
+
<div className="text-[18px] font-bold font-mono text-green-600">{result.imported}</div>
|
|
452
|
+
<div className="text-[8px] font-mono tracking-widest uppercase text-[var(--color-text-muted,#6b7280)]">
|
|
453
|
+
Imported
|
|
454
|
+
</div>
|
|
455
|
+
</div>
|
|
456
|
+
{result.skipped > 0 && (
|
|
457
|
+
<div className="text-center">
|
|
458
|
+
<div className="text-[18px] font-bold font-mono text-[var(--color-text-muted,#6b7280)]">
|
|
459
|
+
{result.skipped}
|
|
460
|
+
</div>
|
|
461
|
+
<div className="text-[8px] font-mono tracking-widest uppercase text-[var(--color-text-muted,#6b7280)]">
|
|
462
|
+
Skipped
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
)}
|
|
466
|
+
{hasErrors && (
|
|
467
|
+
<div className="text-center">
|
|
468
|
+
<div className="text-[18px] font-bold font-mono text-red-500">
|
|
469
|
+
{result.errors.length}
|
|
470
|
+
</div>
|
|
471
|
+
<div className="text-[8px] font-mono tracking-widest uppercase text-[var(--color-text-muted,#6b7280)]">
|
|
472
|
+
Errors
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
475
|
+
)}
|
|
476
|
+
</div>
|
|
477
|
+
|
|
478
|
+
{/* Error details */}
|
|
479
|
+
{hasErrors && (
|
|
480
|
+
<div className="max-h-[120px] overflow-y-auto border border-red-200 bg-red-50">
|
|
481
|
+
{result.errors.map((e, i) => (
|
|
482
|
+
<div
|
|
483
|
+
key={i}
|
|
484
|
+
className="px-3 py-1 text-[9px] font-mono text-red-700 border-b border-red-100 last:border-b-0"
|
|
485
|
+
>
|
|
486
|
+
Row {e.row}: {e.reason}
|
|
487
|
+
</div>
|
|
488
|
+
))}
|
|
489
|
+
</div>
|
|
490
|
+
)}
|
|
491
|
+
|
|
492
|
+
{/* Actions */}
|
|
493
|
+
<div className="flex justify-end">
|
|
494
|
+
<Button size="sm" onClick={handleDone}>
|
|
495
|
+
DONE
|
|
496
|
+
</Button>
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
499
|
+
);
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
return (
|
|
503
|
+
<Modal
|
|
504
|
+
isOpen={isOpen}
|
|
505
|
+
onClose={handleClose}
|
|
506
|
+
title="Import Credentials"
|
|
507
|
+
subtitle="Credential_Import"
|
|
508
|
+
size="md"
|
|
509
|
+
>
|
|
510
|
+
{step === 'select' && renderSelectStep()}
|
|
511
|
+
{step === 'preview' && renderPreviewStep()}
|
|
512
|
+
{step === 'result' && renderResultStep()}
|
|
513
|
+
</Modal>
|
|
514
|
+
);
|
|
515
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Modal } from '@/components/design-system';
|
|
5
|
+
|
|
6
|
+
interface LargeTypeModalProps {
|
|
7
|
+
isOpen: boolean;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
value: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const LargeTypeModal: React.FC<LargeTypeModalProps> = ({ isOpen, onClose, value }) => {
|
|
13
|
+
const characters = Array.from(value ?? '');
|
|
14
|
+
|
|
15
|
+
const renderCharacter = (char: string): string => {
|
|
16
|
+
if (char === ' ') return '␠';
|
|
17
|
+
if (char === '\n') return '↵';
|
|
18
|
+
if (char === '\t') return '⇥';
|
|
19
|
+
return char;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Modal
|
|
24
|
+
isOpen={isOpen}
|
|
25
|
+
onClose={onClose}
|
|
26
|
+
title=""
|
|
27
|
+
size="lg"
|
|
28
|
+
hideHeader
|
|
29
|
+
hideTitle
|
|
30
|
+
contentClassName="!p-0"
|
|
31
|
+
>
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
onClick={onClose}
|
|
35
|
+
className="w-full min-h-[360px] px-6 py-8 flex flex-col items-center justify-center text-center"
|
|
36
|
+
>
|
|
37
|
+
<div className="w-full max-w-[900px] max-h-[70vh] overflow-auto px-1">
|
|
38
|
+
<div className="grid gap-2 [grid-template-columns:repeat(auto-fit,minmax(56px,1fr))]">
|
|
39
|
+
{characters.map((char, index) => (
|
|
40
|
+
<div
|
|
41
|
+
key={`${index}-${char}`}
|
|
42
|
+
data-testid={`large-type-char-${index + 1}`}
|
|
43
|
+
className="rounded-sm border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#ffffff)] px-1 py-2"
|
|
44
|
+
>
|
|
45
|
+
<div className="font-mono text-3xl font-bold leading-none text-[var(--color-text,#0a0a0a)] select-all">
|
|
46
|
+
{renderCharacter(char)}
|
|
47
|
+
</div>
|
|
48
|
+
<div
|
|
49
|
+
data-testid={`large-type-index-${index + 1}`}
|
|
50
|
+
className="mt-2 font-mono text-[9px] uppercase tracking-widest text-[var(--color-text-faint,#9ca3af)]"
|
|
51
|
+
>
|
|
52
|
+
{index + 1}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
<div className="mt-8 font-mono text-[9px] tracking-widest uppercase text-[var(--color-text-faint,#9ca3af)]">
|
|
59
|
+
Click anywhere to dismiss
|
|
60
|
+
</div>
|
|
61
|
+
</button>
|
|
62
|
+
</Modal>
|
|
63
|
+
);
|
|
64
|
+
};
|