diff --git a/.gitignore b/.gitignore index f650315..37c2b6f 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,4 @@ yarn-error.log* # typescript *.tsbuildinfo -next-env.d.ts \ No newline at end of file +next-env.d.ts diff --git a/README.md b/README.md index 8c6e960..9d1a730 100644 --- a/README.md +++ b/README.md @@ -27,4 +27,4 @@ Continue building your app on: 1. Create and modify your project using [v0.app](https://v0.app) 2. Deploy your chats from the v0 interface 3. Changes are automatically pushed to this repository -4. Vercel deploys the latest version from this repository \ No newline at end of file +4. Vercel deploys the latest version from this repository diff --git a/app/page.tsx b/app/page.tsx index ecefe9c..862a151 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -31,7 +31,7 @@ import { import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Card, CardContent } from "@/components/ui/card" import { Dialog, DialogContent, @@ -45,7 +45,6 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Switch } from "@/components/ui/switch" -import { Badge } from "@/components/ui/badge" import { Progress } from "@/components/ui/progress" import { Toaster } from "@/components/ui/toaster" import { useToast } from "@/hooks/use-toast" @@ -66,7 +65,6 @@ interface TOTPToken { interface AppSettings { showCodes: boolean autoRefresh: boolean - compactMode: boolean sortBy: "name" | "issuer" | "recent" } @@ -184,7 +182,6 @@ export default function TwoFactorAuth() { const [settings, setSettings] = useState({ showCodes: true, autoRefresh: true, - compactMode: false, sortBy: "name", }) @@ -252,6 +249,16 @@ export default function TwoFactorAuth() { const cleanSecret = newToken.secret.replace(/\s/g, "").toUpperCase() + const isDuplicate = tokens.some((token) => token.secret === cleanSecret) + if (isDuplicate) { + toast({ + title: t.duplicateToken, + description: t.duplicateTokenDesc, + variant: "destructive", + }) + return + } + const token: TOTPToken = { id: crypto.randomUUID(), name: newToken.name, @@ -281,6 +288,65 @@ export default function TwoFactorAuth() { }) } + const addTokenDirectly = (parsed: { + name?: string + issuer?: string + secret?: string + algorithm?: string + digits?: number + period?: number + }) => { + if (!parsed.name || !parsed.secret) { + toast({ + title: t.error, + description: t.fillRequired, + variant: "destructive", + }) + return false + } + + const cleanSecret = parsed.secret.replace(/\s/g, "").toUpperCase() + + const isDuplicate = tokens.some((token) => token.secret === cleanSecret) + if (isDuplicate) { + toast({ + title: t.duplicateToken, + description: t.duplicateTokenDesc, + variant: "destructive", + }) + return false + } + + const token: TOTPToken = { + id: crypto.randomUUID(), + name: parsed.name, + issuer: parsed.issuer || "", + secret: cleanSecret, + algorithm: (parsed.algorithm as "SHA1" | "SHA256" | "SHA512") || "SHA1", + digits: parsed.digits || 6, + period: parsed.period || 30, + createdAt: Date.now(), + } + + setTokens((prev) => [...prev, token]) + setNewToken({ + name: "", + issuer: "", + secret: "", + algorithm: "SHA1", + digits: 6, + period: 30, + }) + setShowAdvanced(false) + setIsAddDialogOpen(false) + + toast({ + title: t.addSuccess, + description: `${t.added} ${token.name}`, + }) + return true + } + const deleteToken = (id: string) => { const token = tokens.find((t) => t.id === id) setTokens(tokens.filter((t) => t.id !== id)) @@ -400,19 +466,7 @@ export default function TwoFactorAuth() { const parsed = parseOTPAuthURI(code.data) if (parsed) { stopCamera() - setNewToken({ - name: parsed.name || "", - issuer: parsed.issuer || "", - secret: parsed.secret || "", - algorithm: parsed.algorithm || "SHA1", - digits: parsed.digits || 6, - period: parsed.period || 30, - }) - setIsAddDialogOpen(true) - toast({ - title: t.scanSuccess, - description: t.qrCodeDetected, - }) + addTokenDirectly(parsed) return } } @@ -454,46 +508,27 @@ export default function TwoFactorAuth() { if (code && code.data) { const parsed = parseOTPAuthURI(code.data) if (parsed) { - setNewToken({ - name: parsed.name || "", - issuer: parsed.issuer || "", - secret: parsed.secret || "", - algorithm: parsed.algorithm || "SHA1", - digits: parsed.digits || 6, - period: parsed.period || 30, - }) - setIsAddDialogOpen(true) - toast({ - title: t.scanSuccess, - description: t.qrCodeDetected, - }) + addTokenDirectly(parsed) } else { toast({ - title: t.parseFailed, - description: t.invalidQrCode, + title: t.scanFailed, + description: t.invalidQRCode, variant: "destructive", }) } } else { toast({ title: t.scanFailed, - description: t.noQrCodeFound, + description: t.noQRCodeFound, variant: "destructive", }) } } - img.onerror = () => { - toast({ - title: t.error, - description: t.imageLoadFailed, - variant: "destructive", - }) - } img.src = URL.createObjectURL(file) // Reset file input - if (fileInputRef.current) { - fileInputRef.current.value = "" + if (event.target) { + event.target.value = "" } } @@ -669,16 +704,6 @@ export default function TwoFactorAuth() { onCheckedChange={(checked) => setSettings({ ...settings, showCodes: checked })} /> -
-
- -

{t.compactModeDesc}

-
- setSettings({ ...settings, compactMode: checked })} - /> -