diff --git a/src/app/api/auth/check-email/route.js b/src/app/api/auth/check-email/route.js index 37bf825a..20db2ead 100644 --- a/src/app/api/auth/check-email/route.js +++ b/src/app/api/auth/check-email/route.js @@ -1,5 +1,5 @@ // app/api/auth/check-email/route.js -import { auth } from "@/lib/firebaseAdmin"; +import { db } from "@/lib/firebaseAdmin"; import { NextResponse } from "next/server"; export async function POST(req) { @@ -14,13 +14,14 @@ export async function POST(req) { } try { - const userRecord = await auth.getUserByEmail(email); + const usersSnapshot = await db + .collection("users") + .where("email", "==", email) + .limit(1) + .get(); - return NextResponse.json({ exists: userRecord.emailVerified }); + return NextResponse.json({ exists: !usersSnapshot.empty }); } catch (error) { - if (error.code === "auth/user-not-found") { - return NextResponse.json({ exists: false }); - } console.error("Error in check-email:", error); return NextResponse.json( { error: "이메일 확인 중 오류가 발생했습니다." }, diff --git a/src/app/api/auth/reset-password/route.js b/src/app/api/auth/reset-password/route.js new file mode 100644 index 00000000..e3ef13a3 --- /dev/null +++ b/src/app/api/auth/reset-password/route.js @@ -0,0 +1,114 @@ +import { db } from "@/lib/firebaseAdmin"; +import { getAuth } from "firebase-admin/auth"; +import { Timestamp } from "firebase-admin/firestore"; + +function validatePassword(password) { + const minLength = 6; + const maxLength = 4096; + const hasUpperCase = /[A-Z]/.test(password); + const hasLowerCase = /[a-z]/.test(password); + const hasNumber = /[0-9]/.test(password); + const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password); + const isValidLength = + password.length >= minLength && password.length <= maxLength; + + return ( + hasUpperCase && hasLowerCase && hasNumber && hasSpecialChar && isValidLength + ); +} + +export async function PUT(req) { + try { + const { email, code, newPassword } = await req.json(); + + // 비밀번호 유효성 검사 + if (!validatePassword(newPassword)) { + return Response.json( + { + error: "비밀번호는 최소 8자 이상이며, 문자와 숫자를 포함해야 합니다.", + }, + { status: 400 } + ); + } + + // 가장 최근의 인증 코드 조회 + const codesSnapshot = await db + .collection("verification_codes") + .where("email", "==", email) + .where("verified", "==", false) + .orderBy("createdAt", "desc") + .limit(1) + .get(); + + if (codesSnapshot.empty) { + return Response.json( + { error: "유효하지 않은 인증 코드입니다." }, + { status: 400 } + ); + } + + const verificationDoc = codesSnapshot.docs[0]; + const verificationData = verificationDoc.data(); + + // 만료 확인 + if (verificationData.expiresAt.toMillis() < Date.now()) { + return Response.json( + { error: "만료된 인증 코드입니다." }, + { status: 400 } + ); + } + + // 시도 횟수 확인 + if (verificationData.attempts >= 5) { + return Response.json( + { error: "최대 시도 횟수를 초과했습니다." }, + { status: 400 } + ); + } + + // 코드 확인 + if (verificationData.code !== code) { + await verificationDoc.ref.update({ + attempts: FieldValue.increment(1), + }); + return Response.json( + { error: "잘못된 인증 코드입니다." }, + { status: 400 } + ); + } + + // Firebase Auth에서 사용자 찾기 + const auth = getAuth(); + const userRecord = await auth.getUserByEmail(email); + + // 비밀번호 변경 + await auth.updateUser(userRecord.uid, { + password: newPassword, + }); + + // 인증 코드를 verified로 표시 + await verificationDoc.ref.update({ + verified: true, + verifiedAt: Timestamp.now(), + }); + + return Response.json({ + success: true, + message: "비밀번호가 성공적으로 변경되었습니다.", + }); + } catch (error) { + console.error("비밀번호 재설정 오류:", error); + + if (error.code === "auth/invalid-password") { + return Response.json( + { error: "유효하지 않은 비밀번호 형식입니다." }, + { status: 400 } + ); + } + + return Response.json( + { error: "비밀번호 재설정 중 오류가 발생했습니다." }, + { status: 500 } + ); + } +} diff --git a/src/app/join/page.module.css b/src/app/join/page.module.css index a3c03979..4dfd9076 100644 --- a/src/app/join/page.module.css +++ b/src/app/join/page.module.css @@ -24,7 +24,7 @@ body:has(.login-title) { .main h1 img { width: 85vw; - max-width: 48rem; + max-width: 36rem; margin-right: 10rem; filter: var(--filter-EEF0DE); } diff --git a/src/components/login/FindPassword.jsx b/src/components/login/FindPassword.jsx new file mode 100644 index 00000000..fed8c43b --- /dev/null +++ b/src/components/login/FindPassword.jsx @@ -0,0 +1,239 @@ +import { useState, useEffect } from "react"; +import { Button, Input, LoginForm } from "../Controls"; +import { validatePassword } from "@/utils/validation"; +import Link from "next/link"; +import Loading from "../Loading"; + +export default function FindPassword({ styles, setShowFindPassword }) { + const [email, setEmail] = useState(""); + const [code, setCode] = useState(""); + const [password, setPassword] = useState(""); + const [passwordConfirm, setPasswordConfirm] = useState(""); + const [isPasswordValid, setIsPasswordValid] = useState(false); + const [isPasswordMatch, setIsPasswordMatch] = useState(false); + const [error, setError] = useState(""); + const [emailError, setEmailError] = useState(""); + const [isEmailClicked, setIsEmailClicked] = useState(false); + const [isEmailSent, setIsEmailSent] = useState(false); + + useEffect(() => { + if (password === "") { + setIsPasswordValid(false); + } else { + setIsPasswordValid(validatePassword(password)); + } + }, [password]); + + useEffect(() => { + if (password === passwordConfirm) { + setIsPasswordMatch(true); + } else { + setIsPasswordMatch(false); + } + }, [password, passwordConfirm]); + + const handleVerifyEmail = async (e) => { + e.preventDefault(); + setIsEmailClicked(true); + + try { + const checkEmail = await fetch("/api/auth/check-email", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email: email }), + }); + + const result = await checkEmail.json(); + + if (!result.exists || result.error) { + setError("이메일 확인에 실패했습니다. 다시 시도해주세요."); + setEmailError("이메일을 확인해주세요."); + return; + } else { + const sendEmail = await fetch("/api/auth/send-email-verification", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email: email }), + }); + + if (!sendEmail.ok) { + setError("인증 코드 발송에 실패했습니다. 다시 시도해주세요."); + setEmailError("인증 코드 발송에 실패했습니다."); + return; + } else { + setIsEmailSent(true); + setError(""); + } + setEmailError(""); + } + } catch (err) { + setError("인증 코드 발송에 실패했습니다. 다시 시도해주세요."); + setEmailError("인증 코드 발송에 실패했습니다."); + } finally { + setIsEmailClicked(false); + } + }; + + const handleFindPassword = async (e) => { + e.preventDefault(); + + try { + const response = await fetch("/api/auth/reset-password", { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, code, newPassword: password }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error); + } + + alert("비밀번호가 변경되었습니다. 다시 로그인해주세요."); + setShowFindPassword(false); + } catch (error) { + setError(error.message); + } + }; + + return ( + <> +
가입하신 이메일을 입력해주세요.
++ {error} +
*/} +다시 꿈꾸러 오셔서 기뻐요!
@@ -195,12 +197,29 @@ export default function SocialLogin() { /> -