mirror of
https://github.com/handsomezhuzhu/2fa-tool.git
synced 2026-02-20 19:50:15 +00:00
feat: Implement a 2FA authenticator tool with token management, QR scanning, and UI components.
This commit is contained in:
@@ -18,6 +18,14 @@
|
|||||||
- **样式**: Tailwind CSS
|
- **样式**: Tailwind CSS
|
||||||
- **工具库**: jsQR, date-fns
|
- **工具库**: jsQR, date-fns
|
||||||
|
|
||||||
|
## 环境变量
|
||||||
|
|
||||||
|
可以在部署时设置以下环境变量来配置页脚信息:
|
||||||
|
|
||||||
|
- `NEXT_PUBLIC_SHOW_FOOTER`: 是否显示页脚 (默认: true, 设置为 "false" 隐藏)
|
||||||
|
- `NEXT_PUBLIC_FILING_ICP`: ICP 备案号 (例如: 滇ICP备xxxxxxxx号)
|
||||||
|
- `NEXT_PUBLIC_FILING_SECURITY`: 公安联网备案号 (例如: 滇公网安备xxxxxxxxxxxxxx号)
|
||||||
|
|
||||||
## 声明
|
## 声明
|
||||||
|
|
||||||
本项目由阿里云ESA提供加速、计算和保护
|
本项目由阿里云ESA提供加速、计算和保护
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
--accent: oklch(0.97 0 0);
|
--accent: oklch(0.97 0 0);
|
||||||
--accent-foreground: oklch(0.205 0 0);
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
--destructive-foreground: oklch(0.577 0.245 27.325);
|
--destructive-foreground: oklch(0.985 0 0);
|
||||||
--border: oklch(0.922 0 0);
|
--border: oklch(0.922 0 0);
|
||||||
--input: oklch(0.922 0 0);
|
--input: oklch(0.922 0 0);
|
||||||
--ring: oklch(0.708 0 0);
|
--ring: oklch(0.708 0 0);
|
||||||
|
|||||||
109
app/page.tsx
109
app/page.tsx
@@ -27,6 +27,7 @@ import {
|
|||||||
Monitor,
|
Monitor,
|
||||||
Languages,
|
Languages,
|
||||||
Check,
|
Check,
|
||||||
|
Github,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
@@ -162,6 +163,7 @@ export default function TwoFactorAuth() {
|
|||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
|
||||||
const [editingToken, setEditingToken] = useState<TOTPToken | null>(null)
|
const [editingToken, setEditingToken] = useState<TOTPToken | null>(null)
|
||||||
const [showAdvanced, setShowAdvanced] = useState(false)
|
const [showAdvanced, setShowAdvanced] = useState(false)
|
||||||
|
const [mounted, setMounted] = useState(false)
|
||||||
const videoRef = useRef<HTMLVideoElement>(null)
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
@@ -195,6 +197,7 @@ export default function TwoFactorAuth() {
|
|||||||
if (savedSettings) {
|
if (savedSettings) {
|
||||||
setSettings(JSON.parse(savedSettings))
|
setSettings(JSON.parse(savedSettings))
|
||||||
}
|
}
|
||||||
|
setMounted(true)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Save tokens to localStorage
|
// Save tokens to localStorage
|
||||||
@@ -655,15 +658,33 @@ export default function TwoFactorAuth() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="ghost" size="icon" asChild>
|
||||||
|
<a
|
||||||
|
href="https://github.com/handsomezhuzhu/2fa-tool"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
title="GitHub"
|
||||||
|
>
|
||||||
|
<Github className="h-5 w-5" />
|
||||||
|
<span className="sr-only">GitHub</span>
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={cycleTheme}
|
onClick={cycleTheme}
|
||||||
title={theme === "light" ? t.themeLight : theme === "dark" ? t.themeDark : t.themeSystem}
|
title={mounted ? (theme === "light" ? t.themeLight : theme === "dark" ? t.themeDark : t.themeSystem) : t.themeSystem}
|
||||||
>
|
>
|
||||||
{theme === "light" && <Sun className="h-5 w-5" />}
|
{!mounted ? (
|
||||||
{theme === "dark" && <Moon className="h-5 w-5" />}
|
<Monitor className="h-5 w-5" />
|
||||||
{theme === "system" && <Monitor className="h-5 w-5" />}
|
) : (
|
||||||
|
<>
|
||||||
|
{theme === "light" && <Sun className="h-5 w-5" />}
|
||||||
|
{theme === "dark" && <Moon className="h-5 w-5" />}
|
||||||
|
{theme === "system" && <Monitor className="h-5 w-5" />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<span className="sr-only">Toggle theme</span>
|
<span className="sr-only">Toggle theme</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -1028,39 +1049,50 @@ export default function TwoFactorAuth() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<footer className="border-t py-6 mt-auto">
|
{/* Footer */}
|
||||||
<div className="mx-auto flex max-w-4xl flex-col items-center gap-2 text-center md:flex-row md:justify-between md:gap-4 px-4">
|
{(process.env.NEXT_PUBLIC_SHOW_FOOTER !== "false") && (
|
||||||
<p className="text-xs tracking-wider text-muted-foreground">© 2025 Simon. All rights reserved.</p>
|
<footer className="border-t py-6 mt-auto">
|
||||||
<div className="flex items-center gap-4 text-xs tracking-wider text-muted-foreground/60">
|
<div className="mx-auto flex max-w-4xl flex-col items-center gap-2 text-center md:flex-row md:justify-between md:gap-4 px-4">
|
||||||
<a
|
<p className="text-xs tracking-wider text-muted-foreground">© 2025 Simon. All rights reserved.</p>
|
||||||
href="https://beian.miit.gov.cn/"
|
{(process.env.NEXT_PUBLIC_FILING_ICP || process.env.NEXT_PUBLIC_FILING_SECURITY) && (
|
||||||
target="_blank"
|
<div className="flex items-center gap-4 text-xs tracking-wider text-muted-foreground/60">
|
||||||
rel="noopener noreferrer"
|
{process.env.NEXT_PUBLIC_FILING_ICP && (
|
||||||
className="transition-colors hover:text-muted-foreground"
|
<a
|
||||||
>
|
href="https://beian.miit.gov.cn/"
|
||||||
滇ICP备2025074424号
|
target="_blank"
|
||||||
</a>
|
rel="noopener noreferrer"
|
||||||
<span className="text-muted-foreground/30">|</span>
|
className="transition-colors hover:text-muted-foreground"
|
||||||
<a
|
>
|
||||||
href="https://beian.mps.gov.cn"
|
{process.env.NEXT_PUBLIC_FILING_ICP}
|
||||||
target="_blank"
|
</a>
|
||||||
rel="noopener noreferrer"
|
)}
|
||||||
className="flex items-center gap-1.5 transition-colors hover:text-muted-foreground"
|
{process.env.NEXT_PUBLIC_FILING_ICP && process.env.NEXT_PUBLIC_FILING_SECURITY && (
|
||||||
>
|
<span className="text-muted-foreground/30">|</span>
|
||||||
<img
|
)}
|
||||||
alt="公安备案"
|
{process.env.NEXT_PUBLIC_FILING_SECURITY && (
|
||||||
loading="lazy"
|
<a
|
||||||
width="14"
|
href="https://beian.mps.gov.cn"
|
||||||
height="14"
|
target="_blank"
|
||||||
decoding="async"
|
rel="noopener noreferrer"
|
||||||
className="opacity-60"
|
className="flex items-center gap-1.5 transition-colors hover:text-muted-foreground"
|
||||||
src="/images/beian.png"
|
>
|
||||||
/>
|
<img
|
||||||
滇公网安备53250402000233号
|
alt="公安备案"
|
||||||
</a>
|
loading="lazy"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
decoding="async"
|
||||||
|
className="opacity-60"
|
||||||
|
src="/images/beian.png"
|
||||||
|
/>
|
||||||
|
{process.env.NEXT_PUBLIC_FILING_SECURITY}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</footer>
|
||||||
</footer>
|
)}
|
||||||
|
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</div>
|
</div>
|
||||||
@@ -1136,9 +1168,8 @@ function TokenCard({ token, code, timeLeft, showCode, onCopy, onEdit, onDelete,
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
className={`font-mono text-base font-bold min-w-[90px] text-center transition-colors ${
|
className={`font-mono text-base font-bold min-w-[90px] text-center transition-colors ${copied ? "text-green-500" : ""
|
||||||
copied ? "text-green-500" : ""
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{visible ? formattedCode : "••• •••"}
|
{visible ? formattedCode : "••• •••"}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ const ToastClose = React.forwardRef<
|
|||||||
<ToastPrimitives.Close
|
<ToastPrimitives.Close
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
|
'absolute right-2 top-2 rounded-md p-1 text-foreground/50 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
toast-close=""
|
toast-close=""
|
||||||
|
|||||||
@@ -69,6 +69,6 @@
|
|||||||
"postcss": "^8.5",
|
"postcss": "^8.5",
|
||||||
"tailwindcss": "^4.1.9",
|
"tailwindcss": "^4.1.9",
|
||||||
"tw-animate-css": "1.3.3",
|
"tw-animate-css": "1.3.3",
|
||||||
"typescript": "^5"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@@ -184,8 +184,8 @@ importers:
|
|||||||
specifier: 1.3.3
|
specifier: 1.3.3
|
||||||
version: 1.3.3
|
version: 1.3.3
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5
|
specifier: ^5.9.3
|
||||||
version: 5.0.2
|
version: 5.9.3
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -1694,9 +1694,9 @@ packages:
|
|||||||
tw-animate-css@1.3.3:
|
tw-animate-css@1.3.3:
|
||||||
resolution: {integrity: sha512-tXE2TRWrskc4TU3RDd7T8n8Np/wCfoeH9gz22c7PzYqNPQ9FBGFbWWzwL0JyHcFp+jHozmF76tbHfPAx22ua2Q==}
|
resolution: {integrity: sha512-tXE2TRWrskc4TU3RDd7T8n8Np/wCfoeH9gz22c7PzYqNPQ9FBGFbWWzwL0JyHcFp+jHozmF76tbHfPAx22ua2Q==}
|
||||||
|
|
||||||
typescript@5.0.2:
|
typescript@5.9.3:
|
||||||
resolution: {integrity: sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==}
|
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||||
engines: {node: '>=12.20'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
undici-types@6.11.1:
|
undici-types@6.11.1:
|
||||||
@@ -3173,7 +3173,7 @@ snapshots:
|
|||||||
|
|
||||||
tw-animate-css@1.3.3: {}
|
tw-animate-css@1.3.3: {}
|
||||||
|
|
||||||
typescript@5.0.2: {}
|
typescript@5.9.3: {}
|
||||||
|
|
||||||
undici-types@6.11.1: {}
|
undici-types@6.11.1: {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"target": "ES6",
|
"target": "ES6",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
@@ -11,7 +15,7 @@
|
|||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "react-jsx",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
@@ -19,9 +23,19 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": [
|
||||||
"exclude": ["node_modules"]
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user