Skip to content

Commit

Permalink
Merge pull request #243 from oreumi-dreamer/jihun
Browse files Browse the repository at this point in the history
비밀번호 찾기 기능 추가
  • Loading branch information
jihun-io authored Dec 20, 2024
2 parents 3c43433 + e23d9fe commit 220f4b3
Show file tree
Hide file tree
Showing 7 changed files with 492 additions and 24 deletions.
13 changes: 7 additions & 6 deletions src/app/api/auth/check-email/route.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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: "이메일 확인 중 오류가 발생했습니다." },
Expand Down
114 changes: 114 additions & 0 deletions src/app/api/auth/reset-password/route.js
Original file line number Diff line number Diff line change
@@ -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 }
);
}
}
2 changes: 1 addition & 1 deletion src/app/join/page.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
239 changes: 239 additions & 0 deletions src/components/login/FindPassword.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<h2 className={styles["login-title"]}>비밀번호 찾기</h2>
<p>가입하신 이메일을 입력해주세요.</p>
<LoginForm onSubmit={handleFindPassword} className={styles["find-form"]}>
<label className={styles["id-label"]}>
이메일
<div className={styles["input-wrap"]}>
<Input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") handleVerifyEmail(e);
}}
required
/>
<Button
onClick={handleVerifyEmail}
disabled={isEmailClicked}
highlight={true}
>
{isEmailClicked ? (
<Loading type="miniCircle" />
) : isEmailSent ? (
"발송됨"
) : (
"코드 발송"
)}
</Button>
</div>
<span className={styles["invalid-text"]}>
{emailError ? emailError : ""}
</span>
</label>
<label className={styles["id-label"]}>
인증번호
<Input
type="text"
id="code"
value={code}
onChange={(e) => setCode(e.target.value)}
required
/>
</label>
<label className={styles["id-label"]}>
새 비밀번호
{/* <div className={styles["password-wrap"]}> */}
<Input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
{/* <img
src={
isPasswordValid ? "/images/valid.svg" : "/images/invalid.svg"
}
width={40}
height={40}
alt={
isPasswordValid ? "유효한 비밀번호" : "유효하지 않은 비밀번호"
}
/> */}
{/* </div> */}
<span className={styles["invalid-text"]}>
{isPasswordValid
? ""
: "비밀번호는 6자 이상, 영문 대,소문자, 숫자, 특수문자를 포함해주세요"}
</span>
</label>
<label className={styles["id-label"]}>
비밀번호 재확인
{/* <div className={styles["password-wrap"]}> */}
<Input
type="password"
id="password"
value={passwordConfirm}
onChange={(e) => setPasswordConfirm(e.target.value)}
required
/>
{/* <img
src={
passwordConfirm !== "" && isPasswordMatch
? "/images/valid.svg"
: "/images/invalid.svg"
}
width={40}
height={40}
alt={
passwordConfirm !== "" && isPasswordMatch
? "일치하는 비밀번호"
: "일치하지 않은 비밀번호"
}
/> */}
{/* </div> */}
<span className={styles["invalid-text"]}>
{passwordConfirm !== "" && isPasswordMatch
? ""
: "비밀번호가 일치하지 않습니다."}
</span>
</label>

{/* <p role="alert" className={styles["error-message"]}>
{error}
</p> */}
<ul className={styles["bottom-btns"]}>
<li>
<Button
className={styles["login-button"]}
onClick={(e) => {
e.preventDefault();
setShowFindPassword(false);
}}
>
돌아가기
</Button>
</li>
<li>
<Button
type="submit"
highlight={true}
className={styles["login-button"]}
disabled={email !== "" ? false : true}
>
비밀번호 변경
</Button>
</li>
</ul>
</LoginForm>
</>
);
}
Loading

0 comments on commit 220f4b3

Please sign in to comment.