@authdog/react-elements 0.0.28 → 0.0.30
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/.turbo/turbo-build.log +34 -34
- package/CHANGELOG.md +12 -0
- package/dist/index.d.mts +11 -2
- package/dist/index.d.ts +11 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +76 -0
- package/package.json +6 -5
- package/src/components/core/client-only.tsx +20 -0
- package/src/components/flow/totp-validator.tsx +231 -0
- package/src/index.ts +3 -1
- package/src/stories/TotpValidator.stories.tsx +14 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import type React from "react"
|
|
4
|
+
|
|
5
|
+
import { useState, useRef } from "react"
|
|
6
|
+
import { Button } from "../../components/ui/button"
|
|
7
|
+
import { Card, CardContent } from "../../components/ui/card"
|
|
8
|
+
import { Alert, AlertDescription } from "../../components/ui/alert"
|
|
9
|
+
import { Shield, CheckCircle, AlertCircle } from "lucide-react"
|
|
10
|
+
|
|
11
|
+
interface TOTPValidatorProps {
|
|
12
|
+
onValidate: (code: string) => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const TOTPValidator = (
|
|
16
|
+
{
|
|
17
|
+
onValidate
|
|
18
|
+
}: TOTPValidatorProps
|
|
19
|
+
) => {
|
|
20
|
+
const [code, setCode] = useState(["", "", "", "", "", ""])
|
|
21
|
+
const [loading, setLoading] = useState(false)
|
|
22
|
+
const [error, setError] = useState("")
|
|
23
|
+
const [success, setSuccess] = useState(false)
|
|
24
|
+
const inputRefs = useRef<(HTMLInputElement | null)[]>([])
|
|
25
|
+
|
|
26
|
+
const handleInputChange = (index: number, value: string) => {
|
|
27
|
+
// Only allow digits
|
|
28
|
+
if (!/^\d*$/.test(value)) return
|
|
29
|
+
|
|
30
|
+
const newCode = [...code]
|
|
31
|
+
newCode[index] = value.slice(-1) // Only take the last character
|
|
32
|
+
|
|
33
|
+
setCode(newCode)
|
|
34
|
+
setError("")
|
|
35
|
+
setSuccess(false)
|
|
36
|
+
|
|
37
|
+
// Auto-focus next input
|
|
38
|
+
if (value && index < 5) {
|
|
39
|
+
inputRefs.current[index + 1]?.focus()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const handleKeyDown = (index: number, e: React.KeyboardEvent) => {
|
|
44
|
+
// Handle backspace
|
|
45
|
+
if (e.key === "Backspace" && !code[index] && index > 0) {
|
|
46
|
+
inputRefs.current[index - 1]?.focus()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Handle paste
|
|
50
|
+
if (e.key === "v" && (e.ctrlKey || e.metaKey)) {
|
|
51
|
+
e.preventDefault()
|
|
52
|
+
navigator.clipboard.readText().then((text) => {
|
|
53
|
+
const digits = text.replace(/\D/g, "").slice(0, 6).split("")
|
|
54
|
+
const newCode = [...code]
|
|
55
|
+
digits.forEach((digit, i) => {
|
|
56
|
+
if (i < 6) newCode[i] = digit
|
|
57
|
+
})
|
|
58
|
+
setCode(newCode)
|
|
59
|
+
|
|
60
|
+
// Focus the next empty input or the last one
|
|
61
|
+
const nextIndex = Math.min(digits.length, 5)
|
|
62
|
+
inputRefs.current[nextIndex]?.focus()
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const validateTOTP = async () => {
|
|
68
|
+
const totpCode = code.join("")
|
|
69
|
+
|
|
70
|
+
if (totpCode.length !== 6) {
|
|
71
|
+
setError("Please enter all 6 digits")
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
setLoading(true)
|
|
76
|
+
setError("")
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
await onValidate(totpCode)
|
|
80
|
+
setSuccess(true)
|
|
81
|
+
} catch (error) {
|
|
82
|
+
setError("Invalid TOTP code. Please try again.")
|
|
83
|
+
} finally {
|
|
84
|
+
setLoading(false)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
// try {
|
|
90
|
+
// Call your TOTP validation endpoint
|
|
91
|
+
// const response = await fetch("/api/validate-totp", {
|
|
92
|
+
// method: "POST",
|
|
93
|
+
// headers: {
|
|
94
|
+
// "Content-Type": "application/json",
|
|
95
|
+
// },
|
|
96
|
+
// body: JSON.stringify({ code: totpCode }),
|
|
97
|
+
// })
|
|
98
|
+
|
|
99
|
+
// // Check if response is JSON
|
|
100
|
+
// const contentType = response.headers.get("content-type")
|
|
101
|
+
// if (!contentType || !contentType.includes("application/json")) {
|
|
102
|
+
// throw new Error("Server returned non-JSON response")
|
|
103
|
+
// }
|
|
104
|
+
|
|
105
|
+
// const result = await response.json()
|
|
106
|
+
|
|
107
|
+
// if (response.ok && result.valid) {
|
|
108
|
+
// setSuccess(true)
|
|
109
|
+
// setError("")
|
|
110
|
+
// } else {
|
|
111
|
+
// setError(result.message || "Invalid TOTP code. Please try again.")
|
|
112
|
+
// // Clear the code on error
|
|
113
|
+
// setCode(["", "", "", "", "", ""])
|
|
114
|
+
// inputRefs.current[0]?.focus()
|
|
115
|
+
// }
|
|
116
|
+
// } catch (err) {
|
|
117
|
+
// console.error("TOTP validation error:", err)
|
|
118
|
+
// if (err instanceof Error && err.message.includes("non-JSON")) {
|
|
119
|
+
// setError("Server error. Please try again later.")
|
|
120
|
+
// } else {
|
|
121
|
+
// setError("Network error. Please try again.")
|
|
122
|
+
// }
|
|
123
|
+
// // Clear the code on error
|
|
124
|
+
// setCode(["", "", "", "", "", ""])
|
|
125
|
+
// inputRefs.current[0]?.focus()
|
|
126
|
+
// } finally {
|
|
127
|
+
// setLoading(false)
|
|
128
|
+
// }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
132
|
+
e.preventDefault()
|
|
133
|
+
validateTOTP()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const clearCode = () => {
|
|
137
|
+
setCode(["", "", "", "", "", ""])
|
|
138
|
+
setError("")
|
|
139
|
+
setSuccess(false)
|
|
140
|
+
setLoading(false)
|
|
141
|
+
inputRefs.current[0]?.focus()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (success) {
|
|
145
|
+
return (
|
|
146
|
+
<div className="max-w-md mx-auto">
|
|
147
|
+
<Card className="border-green-200 bg-green-50">
|
|
148
|
+
<CardContent className="pt-6">
|
|
149
|
+
<div className="text-center space-y-4">
|
|
150
|
+
<div className="mx-auto w-16 h-16 bg-green-100 rounded-full flex items-center justify-center">
|
|
151
|
+
<CheckCircle className="w-8 h-8 text-green-600" />
|
|
152
|
+
</div>
|
|
153
|
+
<div>
|
|
154
|
+
<h3 className="text-lg font-semibold text-green-900">Verification Successful!</h3>
|
|
155
|
+
<p className="text-sm text-green-700 mt-1">Your TOTP code has been validated.</p>
|
|
156
|
+
</div>
|
|
157
|
+
<Button onClick={clearCode} variant="outline" className="bg-white">
|
|
158
|
+
Verify Another Code
|
|
159
|
+
</Button>
|
|
160
|
+
</div>
|
|
161
|
+
</CardContent>
|
|
162
|
+
</Card>
|
|
163
|
+
</div>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div className="max-w-md mx-auto">
|
|
169
|
+
<Card>
|
|
170
|
+
<CardContent className="pt-6">
|
|
171
|
+
<div className="text-center space-y-6">
|
|
172
|
+
<div>
|
|
173
|
+
<div className="mx-auto w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center mb-4">
|
|
174
|
+
<Shield className="w-6 h-6 text-blue-600" />
|
|
175
|
+
</div>
|
|
176
|
+
<h3 className="text-lg font-semibold">Enter Verification Code</h3>
|
|
177
|
+
<p className="text-sm text-muted-foreground mt-1">Enter the 6-digit code from your authenticator app</p>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
181
|
+
<div className="flex justify-center gap-2">
|
|
182
|
+
{code.map((digit, index) => (
|
|
183
|
+
<Card key={index} className="w-12 h-14 border-2 focus-within:border-blue-500 transition-colors">
|
|
184
|
+
<CardContent className="p-0 h-full flex items-center justify-center">
|
|
185
|
+
<input
|
|
186
|
+
ref={(el) => {
|
|
187
|
+
inputRefs.current[index] = el
|
|
188
|
+
}}
|
|
189
|
+
type="tel"
|
|
190
|
+
inputMode="numeric"
|
|
191
|
+
maxLength={1}
|
|
192
|
+
value={digit}
|
|
193
|
+
onChange={(e) => handleInputChange(index, e.target.value)}
|
|
194
|
+
onKeyDown={(e) => handleKeyDown(index, e)}
|
|
195
|
+
className="w-full h-full text-center text-2xl font-bold border-none outline-none bg-transparent"
|
|
196
|
+
autoComplete="one-time-code"
|
|
197
|
+
disabled={loading}
|
|
198
|
+
style={{
|
|
199
|
+
height: 'auto'
|
|
200
|
+
}}
|
|
201
|
+
/>
|
|
202
|
+
</CardContent>
|
|
203
|
+
</Card>
|
|
204
|
+
))}
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
{error && (
|
|
208
|
+
<Alert variant="destructive">
|
|
209
|
+
<AlertCircle className="h-4 w-4" />
|
|
210
|
+
<AlertDescription>{error}</AlertDescription>
|
|
211
|
+
</Alert>
|
|
212
|
+
)}
|
|
213
|
+
|
|
214
|
+
<div className="space-y-3">
|
|
215
|
+
<Button type="submit" className="w-full" disabled={loading || code.some((digit) => digit === "")}>
|
|
216
|
+
{loading ? "Verifying..." : "Verify Code"}
|
|
217
|
+
</Button>
|
|
218
|
+
|
|
219
|
+
<Button type="button" variant="ghost" size="sm" onClick={clearCode} className="w-full text-xs">
|
|
220
|
+
Clear Code
|
|
221
|
+
</Button>
|
|
222
|
+
</div>
|
|
223
|
+
</form>
|
|
224
|
+
|
|
225
|
+
<p className="text-xs text-muted-foreground">Codes refresh every 30 seconds</p>
|
|
226
|
+
</div>
|
|
227
|
+
</CardContent>
|
|
228
|
+
</Card>
|
|
229
|
+
</div>
|
|
230
|
+
)
|
|
231
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export {Button} from "./components/ui/button";
|
|
2
|
+
export { ClientOnly } from "./components/core/client-only";
|
|
2
3
|
export {Navbar} from "./components/core/navbar";
|
|
3
4
|
export {UserProfile} from "./components/core/user-profile";
|
|
4
|
-
export {PlaceholderAlert} from "./components/core/placeholder-alert";
|
|
5
|
+
export {PlaceholderAlert} from "./components/core/placeholder-alert";
|
|
6
|
+
export {TOTPValidator} from "./components/flow/totp-validator";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Story } from '@ladle/react';
|
|
2
|
+
import { TOTPValidator } from "../components/flow/totp-validator"
|
|
3
|
+
import "../global.css"
|
|
4
|
+
|
|
5
|
+
export const Default: Story = () => <TOTPValidator
|
|
6
|
+
onValidate={async (code) => {
|
|
7
|
+
console.log(code)
|
|
8
|
+
}}
|
|
9
|
+
/>;
|
|
10
|
+
Default.storyName = 'Default TOTP Validator';
|
|
11
|
+
|
|
12
|
+
// export const defaultTOTPValidator: Story = () => (
|
|
13
|
+
// <TOTPValidator />
|
|
14
|
+
// );
|