diff --git a/components/settings/account-security-settings.tsx b/components/settings/account-security-settings.tsx index 5f2b445c..571dccb3 100644 --- a/components/settings/account-security-settings.tsx +++ b/components/settings/account-security-settings.tsx @@ -4,11 +4,11 @@ import { useState, useEffect, useMemo } from 'react'; import { useTranslations } from 'next-intl'; import QRCode from 'qrcode'; import * as OTPAuth from 'otpauth'; -import { Shield, Key, Smartphone, Lock, Trash2, Plus, Eye, EyeOff, Copy, Check, Loader2, Monitor } from 'lucide-react'; +import { Shield, Key, Smartphone, Lock, Trash2, Plus, Eye, EyeOff, Copy, Check, Loader2, Monitor, Terminal } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { SettingsSection, SettingItem, ToggleSwitch } from './settings-section'; -import { useAccountSecurityStore, type AppPasswordInfo } from '@/stores/account-security-store'; +import { useAccountSecurityStore, type AppPasswordInfo, type ApiKeyInfo, type AppCredentialInput } from '@/stores/account-security-store'; import { useAuthStore } from '@/stores/auth-store'; import { toast } from '@/stores/toast-store'; import { cn } from '@/lib/utils'; @@ -345,24 +345,43 @@ function TotpSection() { ); } -function AppPasswordRow({ password, onRemove, isSaving }: { password: AppPasswordInfo; onRemove: (id: string) => void; isSaving: boolean }) { +function parseIpList(raw: string): string[] { + return raw + .split(/[\s,]+/) + .map((s) => s.trim()) + .filter(Boolean); +} + +function CredentialRow({ entry, onRemove, isSaving }: { entry: AppPasswordInfo | ApiKeyInfo; onRemove: (id: string) => void; isSaving: boolean }) { return ( -
-
- {password.description || password.id} - {password.createdAt && ( +
+
+ {entry.description || entry.id} + {entry.createdAt && ( - {new Date(password.createdAt).toLocaleDateString()} - {password.expiresAt ? ` · expires ${new Date(password.expiresAt).toLocaleDateString()}` : ''} + {new Date(entry.createdAt).toLocaleDateString()} + {entry.expiresAt ? ` · expires ${new Date(entry.expiresAt).toLocaleDateString()}` : ''} )} + {entry.allowedIps.length > 0 && ( +
+ {entry.allowedIps.map((ip) => ( + + {ip} + + ))} +
+ )}
@@ -370,12 +389,22 @@ function AppPasswordRow({ password, onRemove, isSaving }: { password: AppPasswor ); } -function AppPasswordsSection() { +interface CredentialSectionProps { + icon: typeof Smartphone; + i18nNamespace: 'app_passwords' | 'api_keys'; + entries: Array; + onCreate: (input: AppCredentialInput) => Promise<{ id: string; secret: string }>; + onRemove: (id: string) => Promise; +} + +function CredentialSection({ icon: Icon, i18nNamespace, entries, onCreate, onRemove }: CredentialSectionProps) { const t = useTranslations('settings.security'); - const { appPasswords, createAppPassword, removeAppPassword, isSaving, isLoadingAuth } = useAccountSecurityStore(); + const tk = (key: string) => t(`${i18nNamespace}.${key}`); + const { isSaving, isLoadingAuth } = useAccountSecurityStore(); const [showAdd, setShowAdd] = useState(false); const [newDescription, setNewDescription] = useState(''); const [expiresAt, setExpiresAt] = useState(''); + const [allowedIpsRaw, setAllowedIpsRaw] = useState(''); const [createdSecret, setCreatedSecret] = useState(null); const [copied, setCopied] = useState(false); @@ -384,26 +413,28 @@ function AppPasswordsSection() { if (!newDescription.trim()) return; try { - const result = await createAppPassword( - newDescription.trim(), - expiresAt ? new Date(expiresAt).toISOString() : null, - ); + const result = await onCreate({ + description: newDescription.trim(), + expiresAt: expiresAt ? new Date(expiresAt).toISOString() : null, + allowedIps: parseIpList(allowedIpsRaw), + }); setCreatedSecret(result.secret); setNewDescription(''); setExpiresAt(''); + setAllowedIpsRaw(''); setShowAdd(false); - toast.success(t('app_passwords.added')); + toast.success(tk('added')); } catch (err) { - toast.error(t('app_passwords.add_error'), err instanceof Error ? err.message : undefined); + toast.error(tk('add_error'), err instanceof Error ? err.message : undefined); } }; const handleRemove = async (id: string) => { try { - await removeAppPassword(id); - toast.success(t('app_passwords.removed')); + await onRemove(id); + toast.success(tk('removed')); } catch (err) { - toast.error(t('app_passwords.remove_error'), err instanceof Error ? err.message : undefined); + toast.error(tk('remove_error'), err instanceof Error ? err.message : undefined); } }; @@ -419,8 +450,8 @@ function AppPasswordsSection() { return (
- -

{t('app_passwords.title')}

+ +

{tk('title')}

@@ -431,21 +462,21 @@ function AppPasswordsSection() {
- -

{t('app_passwords.title')}

+ +

{tk('title')}

-

{t('app_passwords.description')}

+

{tk('description')}

{createdSecret && (
-

{t('app_passwords.copy_now_warning')}

+

{tk('copy_now_warning')}

- + {createdSecret}
+
+ +