프로젝트를 만들어보며 velog 같이 이메일로만으로도 로그인과 회원가입 둘 다 할 수 있도록 하고 싶었다.
const formSubmitHandler = async (e: React.MouseEvent<HTMLFormElement>) => {
e.preventDefault();
if (emailref.current) {
const res = await fetch("http://localhost:3002/api/users/signUp", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: emailref.current.value,
}),
});
const data = await res.json();
if (!res.ok) {
throw new Error("에러가 발생했습니다!");
} else {
console.log(data);
emailref.current.value = "";
}
}
};
import * as crypto from "crypto";
export const encryptEmail = async (email: string) => {
const key = crypto.randomBytes(24);
const iv = crypto.randomBytes(16);
const derivedKey = crypto.scryptSync(key.toString("hex"), "salt", 24);
const cipher = crypto.createCipheriv("aes-192-cbc", derivedKey, iv);
const encryptedBuffer = Buffer.concat([
cipher.update(email, "utf8"),
cipher.final(),
]);
const encrypted = encryptedBuffer.toString("base64");
return encrypted;
};
const sendMail = async (email: string, login: string) => {
try {
// 만약에 이미 인증 링크를 받은 사용자가 또 이메일 인증 메일을 받는다면
// db에서 삭제 후 갱신
await EmailVerify.destroy({ where: { email: email } });
const encryptedCode = await encryptEmail(email);
await EmailVerify.create({ email: email, encryptedCode: encryptedCode });
let loginState = login === "login" ? "로그인" : "회원가입";
const html = renderHtml(loginState, encryptedCode);
let mailOptions = {
from: process.env.GMAIL_USER,
to: email,
subject: `제목 ${loginState}`,
html: html,
};
await transporter.sendMail(mailOptions);
} catch (err) {
console.error(err);
throw new HttpError("이메일을 보내는데 실패 했어요!", 503);
}
};
여기서 중요한건, 사용자가 이메일을 처음에 받았다가 다시 이메일을 요청했을때, db에서 원래 저장되어있던 코드를 지워주고 갱신시켜주어야한다는 점이다.
import {
DataTypes,
Model,
Sequelize,
InferAttributes,
InferCreationAttributes,
CreationOptional,
} from "sequelize";
export class EmailVerify extends Model<
InferAttributes<EmailVerify>,
InferCreationAttributes<EmailVerify>
> {
declare id: CreationOptional<number>;
declare email: string;
declare encryptedCode: string;
declare createdAt: CreationOptional<Date>;
declare updatedAt: CreationOptional<Date>;
}
export function initEmail(sequelize: Sequelize): void {
EmailVerify.init(
{
id: {
type: DataTypes.INTEGER,
allowNull: false,
autoIncrement: true,
primaryKey: true,
},
email: {
type: DataTypes.STRING(45),
allowNull: false,
},
encryptedCode: {
type: DataTypes.STRING,
allowNull: false,
},
createdAt: DataTypes.DATE,
updatedAt: DataTypes.DATE,
},
{
modelName: "EmailVerifies",
tableName: "EmailVerifies",
timestamps: true,
sequelize,
}
);
}
전송 받은 링크를 클릭했을때, 암호화된 코드가 쿼리로 나온다.
import Register from "@/components/Register/Register";
import { getUserEmail } from "@/utils/getUserEmail";
type SearchParams = {
searchParams: {
code: string;
};
};
export default async function Page({ searchParams }: SearchParams) {
let encryptedCode = searchParams.code;
// + 같은 문자열을 searchParams에서 공백으로 인식하므로, 저 공백을 다시 "+"로 변환시켜주기
encryptedCode = encryptedCode.replace(/ /g, "+");
let userEmail: string | undefined;
const emailObj = await getUserEmail(encryptedCode);
userEmail = emailObj.email;
return (
<>
<Register userEmail={userEmail} />
</>
);
}
const registerUser = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { encryptedCode } = req.body;
let email;
try {
const emailData = await EmailVerify.findOne({
where: { encryptedCode: encryptedCode },
});
// console.log(emailData);
// 해당 코드에 맞는 이메일이 존재하지 않는다면
if (!emailData) {
return res.json({ encryptedCode: undefined });
}
// 이메일 발견, 날짜 확인
email = emailData.email;
const createdAt = emailData.createdAt.getTime();
// 현재 시간과 createdAt 시간의 차이 계산
const timeDifference = Date.now() - createdAt;
// 시간 차이가 24시간(86400000밀리초)이 넘었다면
if (timeDifference > 86400000) {
res.json({ encryptedCode: null });
}
} catch (err) {
console.error(err);
const error = new HttpError("이메일을 전송하는데 실패했습니다.", 503);
return next(error);
}
return res.json({ email: email });
};
해당 이메일을 정상적으로 받아올 수 있었다.