Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#527 회원 탈퇴 구현 #528

Open
wants to merge 13 commits into
base: dev
Choose a base branch
from
6 changes: 4 additions & 2 deletions src/lottery/services/globalState.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ const checkIsUserEligible = (user) => {
const getUserGlobalStateHandler = async (req, res) => {
try {
const userId = isLogin(req) ? getLoginInfo(req).oid : null;
const user = userId && (await userModel.findOne({ _id: userId }).lean());
const user =
userId &&
(await userModel.findOne({ _id: userId, withdraw: false }).lean());

const eventStatus =
userId &&
Expand Down Expand Up @@ -99,7 +101,7 @@ const createUserGlobalStateHandler = async (req, res) => {
error: "GlobalState/Create : inviter did not participate in the event",
});

const user = await userModel.findOne({ _id: req.userOid });
const user = await userModel.findOne({ _id: req.userOid, withdraw: false });
if (!user)
return res
.status(500)
Expand Down
5 changes: 4 additions & 1 deletion src/lottery/services/invite.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ const searchInviterHandler = async (req, res) => {
)
return res.status(400).json({ error: "Invite/Search : invalid inviter" });

const inviterInfo = await userModel.findOne({ _id: inviterStatus.userId });
const inviterInfo = await userModel.findOne({
_id: inviterStatus.userId,
withdraw: false,
});
if (!inviterInfo)
return res
.status(500)
Expand Down
10 changes: 7 additions & 3 deletions src/lottery/services/publicNotice.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const getRecentPurchaceItemListHandler = async (req, res) => {
.find({ type: "use", itemType: 0 })
.sort({ createAt: -1 })
.limit(1000)
.populate(publicNoticePopulateOption)
.populate(publicNoticePopulateOption) // TODO: 회원 탈퇴 핸들링
.lean()
)
.sort(
Expand Down Expand Up @@ -132,7 +132,9 @@ const getTicketLeaderboardHandler = async (req, res) => {
);
const leaderboard = await Promise.all(
sortedUsers.slice(0, 20).map(async (user) => {
const userInfo = await userModel.findOne({ _id: user.userId }).lean();
const userInfo = await userModel
.findOne({ _id: user.userId, withdraw: false })
.lean();
if (!userInfo) {
logger.error(`Fail to find user ${user.userId}`);
return null;
Expand Down Expand Up @@ -211,7 +213,9 @@ const getGroupLeaderboardHandler = async (req, res) => {
if (mvp?.length !== 1)
throw new Error(`Fail to find MVP in group ${group.group}`);

const mvpInfo = await userModel.findOne({ _id: mvp[0].userId }).lean();
const mvpInfo = await userModel
.findOne({ _id: mvp[0].userId, withdraw: false })
.lean();
if (!mvpInfo) throw new Error(`Fail to find user ${mvp[0].userId}`);

return {
Expand Down
3 changes: 1 addition & 2 deletions src/middlewares/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ const authMiddleware = (req, res, next) => {
error: "not logged in",
});
} else {
const { id, oid } = getLoginInfo(req);
req.userId = id;
const { oid } = getLoginInfo(req);
req.userOid = oid;
next();
}
Expand Down
4 changes: 2 additions & 2 deletions src/middlewares/authAdmin.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ const authAdminMiddleware = async (req, res, next) => {
if (!isLogin(req)) return res.redirect(req.origin);

// 관리자 유무를 확인
const { id } = getLoginInfo(req);
const user = await userModel.findOne({ id });
const { oid } = getLoginInfo(req);
const user = await userModel.findOne({ _id: oid, withdraw: false });
if (!user.isAdmin) return res.redirect(req.origin);

// 접속한 IP가 화이트리스트에 있는지 확인
Expand Down
23 changes: 23 additions & 0 deletions src/modules/fcm.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,28 @@ const unregisterDeviceToken = async (deviceToken) => {
}
};

/**
* 사용자의 ObjectId가 주어졌을 때, 해당 사용자의 모든 deviceToken을 DB에서 삭제합니다.
* @param {string} userId - 사용자의 ObjectId입니다.
* @return {Promise<boolean>} 해당 사용자로부터 deviceToken을 삭제하는 데 성공하면 true, 실패하면 false를 반환합니다. 삭제할 deviceToken이 존재하지 않는 경우에는 true를 반환합니다.
*/
const unregisterAllDeviceTokens = async (userId) => {
try {
// 사용자의 디바이스 토큰을 DB에서 가져옵니다.
// getTokensOfUsers 함수의 정의는 아래에 있습니다. (호이스팅)
const tokens = await getTokensOfUsers([userId]);

// 디바이스 토큰과 관련 설정을 DB에서 삭제합니다.
await deviceTokenModel.deleteMany({ userId });
await notificationOptionModel.deleteMany({ deviceToken: { $in: tokens } });

return true;
} catch (error) {
logger.error(error);
return false;
}
};

/**
* 메시지 전송에 실패한 deviceToken을 DB에서 삭제합니다.
* @param {Array<string>} deviceTokens - 사용자의 ObjectId입니다.
Expand Down Expand Up @@ -351,6 +373,7 @@ module.exports = {
initializeApp,
registerDeviceToken,
unregisterDeviceToken,
unregisterAllDeviceTokens,
validateDeviceToken,
getTokensOfUsers,
sendMessageByTokens,
Expand Down
5 changes: 4 additions & 1 deletion src/modules/populates/chats.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
* 쿼리를 통해 얻은 Chat Document를 populate할 설정값을 정의합니다.
*/
const chatPopulateOption = [
{ path: "authorId", select: "_id nickname profileImageUrl" },
{
path: "authorId",
select: "_id nickname profileImageUrl withdraw",
},
];

module.exports = {
Expand Down
2 changes: 1 addition & 1 deletion src/modules/populates/reports.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const reportPopulateOption = [
{
path: "reportedId",
select: "_id id name nickname profileImageUrl",
select: "_id id name nickname profileImageUrl withdraw",
},
];

Expand Down
10 changes: 8 additions & 2 deletions src/modules/populates/rooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ const roomPopulateOption = [
{
path: "part",
select: "-_id user settlementStatus readAt",
populate: { path: "user", select: "_id id name nickname profileImageUrl" },
populate: {
path: "user",
select: "_id id name nickname profileImageUrl withdraw",
},
},
];

Expand All @@ -27,14 +30,17 @@ const formatSettlement = (
{ includeSettlement = true, isOver = false, timestamp = Date.now() } = {}
) => {
roomObject.part = roomObject.part.map((participantSubDocument) => {
const { _id, name, nickname, profileImageUrl } =
if (!participantSubDocument.user) return null;

const { _id, name, nickname, profileImageUrl, withdraw } =
participantSubDocument.user;
const { settlementStatus, readAt } = participantSubDocument;
return {
_id,
name,
nickname,
profileImageUrl,
withdraw,
isSettlement: includeSettlement ? settlementStatus : undefined,
readAt: readAt ?? roomObject.madeat,
};
Expand Down
16 changes: 10 additions & 6 deletions src/modules/socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ const transformChatsForRoom = async (chats) => {
const inOutUserIds = chat.content.split("|");
chat.inOutNames = await Promise.all(
inOutUserIds.map(async (userId) => {
const user = await userModel.findOne({ id: userId }, "nickname");
return user.nickname;
const user = await userModel.findOne(
{ id: userId, withdraw: false }, // NOTE: SSO uid 쓰는 곳
"nickname"
);
return user?.nickname;
Comment on lines +38 to +42
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chat 의 authorId 값이 User._id인데, 굳이 userId를 따로 채팅에서 긁어와서 처리하는 이유가 있나요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

원래 구현이 이렇게 되어 있더라구요..! 개인적인 뇌피셜로는 한 번에 여러 명이 입장할 수도 있어서 이렇게 만들어 두신게 아닌가 싶습니다.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이번 기회에 함께 수정해 버릴까요?

})
);
}
Expand All @@ -46,6 +49,7 @@ const transformChatsForRoom = async (chats) => {
authorId: chat.authorId?._id,
authorName: chat.authorId?.nickname,
authorProfileUrl: chat.authorId?.profileImageUrl,
authorIsWithdrew: chat.authorId?.withdraw,
content: chat.content,
time: chat.time,
isValid: chat.isValid,
Expand Down Expand Up @@ -136,7 +140,10 @@ const emitChatEvent = async (io, chat) => {

// chat optionally contains authorId
const { nickname, profileImageUrl } = authorId
? await userModel.findById(authorId, "nickname profileImageUrl")
? await userModel.findOne(
{ _id: authorId, withdraw: false },
"nickname profileImageUrl"
)
: {};
if (authorId && (!nickname || !profileImageUrl)) {
throw new IllegalArgumentsException();
Expand All @@ -163,9 +170,6 @@ const emitChatEvent = async (io, chat) => {
.lean()
.populate(chatPopulateOption);

chatDocument.authorName = nickname;
chatDocument.authorProfileUrl = profileImageUrl;

const userIds = part.map((participant) => participant.user);
const userIdsExceptAuthor = authorId
? part
Expand Down
5 changes: 3 additions & 2 deletions src/modules/stores/mongo.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ const logger = require("../logger");
const userSchema = Schema({
name: { type: String, required: true }, //실명
nickname: { type: String, required: true }, //닉네임
id: { type: String, required: true, unique: true }, //택시 서비스에서만 사용되는 id
id: { type: String, required: true }, //택시 서비스에서만 사용되는 id
profileImageUrl: { type: String, required: true }, //백엔드에서의 프로필 이미지 경로
ongoingRoom: [{ type: Schema.Types.ObjectId, ref: "Room" }], // 참여중인 진행중인 방 배열
doneRoom: [{ type: Schema.Types.ObjectId, ref: "Room" }], // 참여중인 완료된 방 배열
withdraw: { type: Boolean, default: false },
withdraw: { type: Boolean, default: false }, //탈퇴 여부
withdrawAt: { type: Date }, //탈퇴 시각
phoneNumber: { type: String }, // 전화번호 (2023FALL 이벤트부터 추가)
ban: { type: Boolean, default: false }, //계정 정지 여부
joinat: { type: Date, required: true }, //가입 시각
Expand Down
31 changes: 31 additions & 0 deletions src/routes/docs/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -468,4 +468,35 @@ usersDocs[`${apiPrefix}/getBanRecord`] = {
},
};

usersDocs[`${apiPrefix}/withdraw`] = {
post: {
tags: [tag],
summary: "회원 탈퇴",
description: "회원 탈퇴를 요청합니다.",
responses: {
200: {
content: {
"text/html": {
example: "Users/withdraw : withdraw successful",
},
},
},
400: {
content: {
"text/html": {
example: "Users/withdraw : ongoing room exists",
},
},
},
500: {
content: {
"text/html": {
example: "Users/withdraw : internal server error",
},
},
},
},
},
};

module.exports = usersDocs;
3 changes: 3 additions & 0 deletions src/routes/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,7 @@ router.get("/isBanned", userHandlers.isBannedHandler);
// 유저의 서비스 정지 기록들을 모두 반환합니다.
router.get("/getBanRecord", userHandlers.getBanRecordHandler);

// 회원 탈퇴를 요청합니다.
router.post("/withdraw", userHandlers.withdrawHandler);

module.exports = router;
40 changes: 40 additions & 0 deletions src/schedules/deleteUserInfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const { userModel } = require("../modules/stores/mongo");
const logger = require("../modules/logger");

module.exports = async () => {
try {
// 탈퇴일로부터 1년 이상 경과한 사용자의 개인정보 삭제
await userModel.updateMany(
{
withdraw: true,
withdrawAt: { $lte: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000) },
name: { $ne: "" },
},
{
$set: {
name: "",
nickname: "",
id: "",
profileImageUrl: "",
// ongoingRoom
// doneRoom
ban: false,
// joinat
agreeOnTermsOfService: false,
"subinfo.kaist": "",
"subinfo.sparcs": "",
"subinfo.facebook": "",
"subinfo.twitter": "",
email: "",
isAdmin: false,
account: "",
},
$unset: {
phoneNumber: "",
},
}
);
} catch (err) {
logger.error(err);
}
};
1 change: 1 addition & 0 deletions src/schedules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const cron = require("node-cron");
const registerSchedules = (app) => {
cron.schedule("*/5 * * * *", require("./notifyBeforeDepart")(app));
cron.schedule("*/10 * * * *", require("./notifyAfterArrival")(app));
cron.schedule("0 0 1 * *", require("./deleteUserInfo"));
};

module.exports = registerSchedules;
1 change: 0 additions & 1 deletion src/schedules/notifyAfterArrival.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const { roomModel, chatModel } = require("../modules/stores/mongo");
// const { roomPopulateOption } = require("../modules/populates/rooms");
const { emitChatEvent } = require("../modules/socket");
const logger = require("../modules/logger");

Expand Down
4 changes: 2 additions & 2 deletions src/services/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const update = async (userData) => {
email: userData.email,
"subinfo.kaist": userData.kaist,
};
await userModel.updateOne({ id: userData.id }, updateInfo);
await userModel.updateOne({ id: userData.id, withdraw: false }, updateInfo); // NOTE: SSO uid 쓰는 곳
logger.info(
`Update user info: ${userData.id} ${userData.name} ${userData.email} ${userData.kaist}`
);
Expand All @@ -72,7 +72,7 @@ const update = async (userData) => {
const tryLogin = async (req, res, userData, redirectOrigin, redirectPath) => {
try {
const user = await userModel.findOne(
{ id: userData.id },
{ id: userData.id, withdraw: false }, // NOTE: SSO uid 쓰는 곳
"_id name email subinfo id withdraw ban"
);
if (!user) {
Expand Down
2 changes: 1 addition & 1 deletion src/services/auth.mobile.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const tokenLoginHandler = async (req, res) => {
return res.status(401).json({ message: "Not Access token" });
}

const user = await userModel.findOne({ _id: data.id });
const user = await userModel.findOne({ _id: data.id, withdraw: false });
if (!user) {
return res.status(401).json({ message: "No corresponding user" });
}
Expand Down
Loading