diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-27 01:16:20 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-27 01:16:20 +0000 |
| commit | e9897d416b3e7327bbd4d4aef887eee37751ae82 (patch) | |
| tree | bd20ce6eadf9b21755bd7425492d2d31c7700a0e /components/login/next-auth-reauth-modal.tsx | |
| parent | 3bf1952c1dad9d479bb8b22031b06a7434d37c37 (diff) | |
(대표님) 20250627 오전 10시 작업사항
Diffstat (limited to 'components/login/next-auth-reauth-modal.tsx')
| -rw-r--r-- | components/login/next-auth-reauth-modal.tsx | 215 |
1 files changed, 215 insertions, 0 deletions
diff --git a/components/login/next-auth-reauth-modal.tsx b/components/login/next-auth-reauth-modal.tsx new file mode 100644 index 00000000..5aa61b7d --- /dev/null +++ b/components/login/next-auth-reauth-modal.tsx @@ -0,0 +1,215 @@ +// components/auth/next-auth-reauth-modal.tsx +"use client" + +import * as React from "react" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { z } from "zod" +import { signIn } from "next-auth/react" + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { toast } from "@/hooks/use-toast" +import { AlertCircle, Shield } from "lucide-react" + +const reAuthSchema = z.object({ + password: z.string().min(1, "Password is required"), +}) + +type ReAuthFormValues = z.infer<typeof reAuthSchema> + +interface NextAuthReAuthModalProps { + isOpen: boolean + onSuccess: () => void + userEmail: string +} + +export function NextAuthReAuthModal({ + isOpen, + onSuccess, + userEmail +}: NextAuthReAuthModalProps) { + const [isLoading, setIsLoading] = React.useState(false) + const [attemptCount, setAttemptCount] = React.useState(0) + + const form = useForm<ReAuthFormValues>({ + resolver: zodResolver(reAuthSchema), + defaultValues: { + password: "", + }, + }) + + async function onSubmit(data: ReAuthFormValues) { + setIsLoading(true) + + try { + // Next-auth의 signIn 함수를 사용하여 재인증 + const result = await signIn("credentials", { + email: userEmail, + password: data.password, + redirect: false, // 리다이렉트 하지 않음 + callbackUrl: undefined, + }) + + if (result?.error) { + setAttemptCount(prev => prev + 1) + + // 3회 이상 실패 시 추가 보안 조치 + if (attemptCount >= 2) { + toast({ + title: "Too many failed attempts", + description: "Please wait a moment before trying again.", + variant: "destructive", + }) + // 30초 대기 + setTimeout(() => { + setAttemptCount(0) + }, 30000) + return + } + + toast({ + title: "Authentication failed", + description: `Invalid password. ${2 - attemptCount} attempts remaining.`, + variant: "destructive", + }) + + form.setError("password", { + type: "manual", + message: "Invalid password" + }) + } else { + // 재인증 성공 + setAttemptCount(0) + onSuccess() + form.reset() + + toast({ + title: "Authentication successful", + description: "You can now access account settings.", + }) + } + } catch (error) { + console.error("Re-authentication error:", error) + toast({ + title: "Error", + description: "An unexpected error occurred. Please try again.", + variant: "destructive", + }) + } finally { + setIsLoading(false) + } + } + + // 모달이 닫힐 때 폼 리셋 + React.useEffect(() => { + if (!isOpen) { + form.reset() + setAttemptCount(0) + } + }, [isOpen, form]) + + return ( + <Dialog open={isOpen} onOpenChange={() => {}}> + <DialogContent className="sm:max-w-[425px]"> + <DialogHeader> + <DialogTitle className="flex items-center gap-3"> + <div className="h-8 w-8 rounded-full bg-amber-100 flex items-center justify-center"> + <Shield className="h-5 w-5 text-amber-600" /> + </div> + Security Verification + </DialogTitle> + <DialogDescription className="text-left"> + For your security, please confirm your password to access sensitive account settings. + This verification is valid for 5 minutes. + </DialogDescription> + </DialogHeader> + + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> + {/* 사용자 정보 표시 */} + <div className="rounded-lg bg-blue-50 border border-blue-200 p-3"> + <div className="flex items-center gap-2"> + <div className="h-2 w-2 bg-blue-500 rounded-full"></div> + <span className="text-sm font-medium text-blue-900"> + Signed in as: {userEmail} + </span> + </div> + </div> + + {/* 경고 메시지 (실패 횟수가 많을 때) */} + {attemptCount >= 2 && ( + <div className="rounded-lg bg-red-50 border border-red-200 p-3"> + <div className="flex items-start gap-2"> + <AlertCircle className="h-4 w-4 text-red-500 mt-0.5 flex-shrink-0" /> + <div className="text-sm text-red-800"> + <p className="font-medium">Security Alert</p> + <p>Multiple failed attempts detected. Please wait 30 seconds before trying again.</p> + </div> + </div> + </div> + )} + + <FormField + control={form.control} + name="password" + render={({ field }) => ( + <FormItem> + <FormLabel>Current Password</FormLabel> + <FormControl> + <Input + type="password" + placeholder="Enter your password" + disabled={attemptCount >= 3 || isLoading} + {...field} + autoFocus + autoComplete="current-password" + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <Button + type="submit" + className="w-full" + disabled={isLoading || attemptCount >= 3} + > + {isLoading ? ( + <> + <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div> + Verifying... + </> + ) : attemptCount >= 3 ? ( + "Please wait..." + ) : ( + "Verify Identity" + )} + </Button> + </form> + </Form> + + <div className="text-xs text-muted-foreground text-center space-y-1"> + <p>This helps protect your account from unauthorized changes.</p> + <p>Your session will remain active during verification.</p> + </div> + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file |
