From c4ce04bd8131a387f27aed5afadcd41885f1d5ac Mon Sep 17 00:00:00 2001 From: static Date: Tue, 23 Jul 2024 16:36:24 +0900 Subject: [PATCH 01/27] Add: /users/delete endpoint --- src/modules/stores/mongo.js | 1 + src/routes/users.js | 3 +++ src/services/users.js | 21 +++++++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/src/modules/stores/mongo.js b/src/modules/stores/mongo.js index 6bc93246..d4bda9d6 100755 --- a/src/modules/stores/mongo.js +++ b/src/modules/stores/mongo.js @@ -24,6 +24,7 @@ const userSchema = Schema({ email: { type: String, required: true }, isAdmin: { type: Boolean, default: false }, //관리자 여부 account: { type: String, default: "" }, //계좌번호 정보 + deleteAt: { type: Date, required: false }, //탈퇴 시각 }); const banSchema = Schema({ diff --git a/src/routes/users.js b/src/routes/users.js index d5366155..29c8c4f0 100755 --- a/src/routes/users.js +++ b/src/routes/users.js @@ -62,4 +62,7 @@ router.get("/isBanned", userHandlers.isBannedHandler); // 유저의 서비스 정지 기록들을 모두 반환합니다. router.get("/getBanRecord", userHandlers.getBanRecordHandler); +// 회원 탈퇴를 요청합니다. +router.delete("/delete", userHandlers.deleteUserHandler); + module.exports = router; diff --git a/src/services/users.js b/src/services/users.js index 5d8ee632..77f94a44 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -229,6 +229,26 @@ const getBanRecordHandler = async (req, res) => { } }; +const deleteUserHandler = async (req, res) => { + try { + const user = await userModel.findOne({ id: req.userId }); + if (!user) { + return res.status(500).send("Users/delete : internal server error"); + } else if (user.deleteAt) { + return res.status(400).send("Users/delete : already deleted"); + } else if (user.ongoingRoom.length !== 0) { + return res.status(400).send("Users/delete : ongoing room exists"); + } + + user.deleteAt = req.timestamp; + await user.save(); + + res.status(200).send("Users/delete : delete user successful"); + } catch (err) { + res.status(500).send("Users/delete : internal server error"); + } +}; + module.exports = { agreeOnTermsOfServiceHandler, getAgreeOnTermsOfServiceHandler, @@ -240,4 +260,5 @@ module.exports = { resetProfileImgHandler, isBannedHandler, getBanRecordHandler, + deleteUserHandler, }; From dd6d7c1e46c3feeeae4e457c29899b797fac06e8 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 23 Jul 2024 16:55:17 +0900 Subject: [PATCH 02/27] Add: personal information delete cronjob --- src/schedules/deleteUserInfo.js | 40 +++++++++++++++++++++++++++++++++ src/schedules/index.js | 1 + 2 files changed, 41 insertions(+) create mode 100644 src/schedules/deleteUserInfo.js diff --git a/src/schedules/deleteUserInfo.js b/src/schedules/deleteUserInfo.js new file mode 100644 index 00000000..be7105a2 --- /dev/null +++ b/src/schedules/deleteUserInfo.js @@ -0,0 +1,40 @@ +const { userModel } = require("../modules/stores/mongo"); +const logger = require("../modules/logger"); + +module.exports = async () => { + try { + // 탈퇴일로부터 1년 이상 경과한 사용자 + const expiredUsers = await userModel.find({ + withdraw: { $lte: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000) }, + }); + + await Promise.all( + expiredUsers.map(async (user) => { + if (user.name === "") return; // 이미 개인정보가 삭제된 사용자 + + user.name = ""; + user.nickname = ""; + //user.id + user.profileImageUrl = ""; + //user.ongoingRoom + //user.doneRoom + //user.withdraw + user.phoneNumber = undefined; + user.ban = false; + //user.joinat + user.agreeOnTermsOfService = false; + user.subinfo.kaist = ""; + user.subinfo.sparcs = ""; + user.subinfo.facebook = ""; + user.subinfo.twitter = ""; + user.email = ""; + user.isAdmin = false; + user.account = ""; + + await user.save(); + }) + ); + } catch (err) { + logger.error(err); + } +}; diff --git a/src/schedules/index.js b/src/schedules/index.js index 97818b92..0358217a 100644 --- a/src/schedules/index.js +++ b/src/schedules/index.js @@ -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; From f11f158e137d07700dea751311b105db4a23374e Mon Sep 17 00:00:00 2001 From: static Date: Sun, 28 Jul 2024 06:41:26 +0900 Subject: [PATCH 03/27] Refactor: rename withdraw-related endpoint/schema --- src/modules/stores/mongo.js | 3 +- src/routes/users.js | 2 +- src/schedules/deleteUserInfo.js | 61 +++++++++++++++++---------------- src/services/users.js | 18 +++++----- 4 files changed, 42 insertions(+), 42 deletions(-) diff --git a/src/modules/stores/mongo.js b/src/modules/stores/mongo.js index d4bda9d6..d9dbb26b 100755 --- a/src/modules/stores/mongo.js +++ b/src/modules/stores/mongo.js @@ -10,7 +10,7 @@ const userSchema = Schema({ profileImageUrl: { type: String, required: true }, //백엔드에서의 프로필 이미지 경로 ongoingRoom: [{ type: Schema.Types.ObjectId, ref: "Room" }], // 참여중인 진행중인 방 배열 doneRoom: [{ type: Schema.Types.ObjectId, ref: "Room" }], // 참여중인 완료된 방 배열 - withdraw: { type: Boolean, default: false }, + withdrawAt: { type: Date, default: null }, //탈퇴 시각 (null이면 탈퇴하지 않은 상태) phoneNumber: { type: String }, // 전화번호 (2023FALL 이벤트부터 추가) ban: { type: Boolean, default: false }, //계정 정지 여부 joinat: { type: Date, required: true }, //가입 시각 @@ -24,7 +24,6 @@ const userSchema = Schema({ email: { type: String, required: true }, isAdmin: { type: Boolean, default: false }, //관리자 여부 account: { type: String, default: "" }, //계좌번호 정보 - deleteAt: { type: Date, required: false }, //탈퇴 시각 }); const banSchema = Schema({ diff --git a/src/routes/users.js b/src/routes/users.js index 29c8c4f0..b5d8be38 100755 --- a/src/routes/users.js +++ b/src/routes/users.js @@ -63,6 +63,6 @@ router.get("/isBanned", userHandlers.isBannedHandler); router.get("/getBanRecord", userHandlers.getBanRecordHandler); // 회원 탈퇴를 요청합니다. -router.delete("/delete", userHandlers.deleteUserHandler); +router.post("/withdraw", userHandlers.withdrawHandler); module.exports = router; diff --git a/src/schedules/deleteUserInfo.js b/src/schedules/deleteUserInfo.js index be7105a2..92697c8b 100644 --- a/src/schedules/deleteUserInfo.js +++ b/src/schedules/deleteUserInfo.js @@ -3,36 +3,37 @@ const logger = require("../modules/logger"); module.exports = async () => { try { - // 탈퇴일로부터 1년 이상 경과한 사용자 - const expiredUsers = await userModel.find({ - withdraw: { $lte: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000) }, - }); - - await Promise.all( - expiredUsers.map(async (user) => { - if (user.name === "") return; // 이미 개인정보가 삭제된 사용자 - - user.name = ""; - user.nickname = ""; - //user.id - user.profileImageUrl = ""; - //user.ongoingRoom - //user.doneRoom - //user.withdraw - user.phoneNumber = undefined; - user.ban = false; - //user.joinat - user.agreeOnTermsOfService = false; - user.subinfo.kaist = ""; - user.subinfo.sparcs = ""; - user.subinfo.facebook = ""; - user.subinfo.twitter = ""; - user.email = ""; - user.isAdmin = false; - user.account = ""; - - await user.save(); - }) + // 탈퇴일로부터 1년 이상 경과한 사용자의 개인정보 삭제 + await userModel.updateMany( + { + 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); diff --git a/src/services/users.js b/src/services/users.js index 77f94a44..40ab0923 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -229,23 +229,23 @@ const getBanRecordHandler = async (req, res) => { } }; -const deleteUserHandler = async (req, res) => { +const withdrawHandler = async (req, res) => { try { const user = await userModel.findOne({ id: req.userId }); if (!user) { - return res.status(500).send("Users/delete : internal server error"); - } else if (user.deleteAt) { - return res.status(400).send("Users/delete : already deleted"); + return res.status(500).send("Users/withdraw : internal server error"); + } else if (user.withdrawAt) { + return res.status(400).send("Users/withdraw : already withdrawn"); } else if (user.ongoingRoom.length !== 0) { - return res.status(400).send("Users/delete : ongoing room exists"); + return res.status(400).send("Users/withdraw : ongoing room exists"); } - user.deleteAt = req.timestamp; + user.withdrawAt = req.timestamp; await user.save(); - res.status(200).send("Users/delete : delete user successful"); + res.status(200).send("Users/withdraw : withdraw successful"); } catch (err) { - res.status(500).send("Users/delete : internal server error"); + res.status(500).send("Users/withdraw : internal server error"); } }; @@ -260,5 +260,5 @@ module.exports = { resetProfileImgHandler, isBannedHandler, getBanRecordHandler, - deleteUserHandler, + withdrawHandler, }; From 3e8761993e791ed5e191ed20d2d7291e816f45b4 Mon Sep 17 00:00:00 2001 From: static Date: Sun, 28 Jul 2024 06:48:05 +0900 Subject: [PATCH 04/27] Add: restore withdraw field of userSchema --- src/modules/stores/mongo.js | 3 ++- src/schedules/deleteUserInfo.js | 5 ++--- src/services/users.js | 4 +++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/modules/stores/mongo.js b/src/modules/stores/mongo.js index d9dbb26b..a680e605 100755 --- a/src/modules/stores/mongo.js +++ b/src/modules/stores/mongo.js @@ -10,7 +10,8 @@ const userSchema = Schema({ profileImageUrl: { type: String, required: true }, //백엔드에서의 프로필 이미지 경로 ongoingRoom: [{ type: Schema.Types.ObjectId, ref: "Room" }], // 참여중인 진행중인 방 배열 doneRoom: [{ type: Schema.Types.ObjectId, ref: "Room" }], // 참여중인 완료된 방 배열 - withdrawAt: { type: Date, default: null }, //탈퇴 시각 (null이면 탈퇴하지 않은 상태) + withdraw: { type: Boolean, default: false }, //탈퇴 여부 + withdrawAt: { type: Date }, //탈퇴 시각 phoneNumber: { type: String }, // 전화번호 (2023FALL 이벤트부터 추가) ban: { type: Boolean, default: false }, //계정 정지 여부 joinat: { type: Date, required: true }, //가입 시각 diff --git a/src/schedules/deleteUserInfo.js b/src/schedules/deleteUserInfo.js index 92697c8b..de22cbde 100644 --- a/src/schedules/deleteUserInfo.js +++ b/src/schedules/deleteUserInfo.js @@ -6,9 +6,8 @@ module.exports = async () => { // 탈퇴일로부터 1년 이상 경과한 사용자의 개인정보 삭제 await userModel.updateMany( { - withdrawAt: { - $lte: new Date(Date.now() - /*365 * 24 * 60 */ 60 * 1000), - }, + withdraw: true, + withdrawAt: { $lte: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000) }, name: { $ne: "" }, }, { diff --git a/src/services/users.js b/src/services/users.js index 40ab0923..12d0afe1 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -234,13 +234,15 @@ const withdrawHandler = async (req, res) => { const user = await userModel.findOne({ id: req.userId }); if (!user) { return res.status(500).send("Users/withdraw : internal server error"); - } else if (user.withdrawAt) { + } else if (user.withdraw) { return res.status(400).send("Users/withdraw : already withdrawn"); } else if (user.ongoingRoom.length !== 0) { return res.status(400).send("Users/withdraw : ongoing room exists"); } + user.withdraw = true; user.withdrawAt = req.timestamp; + await user.save(); res.status(200).send("Users/withdraw : withdraw successful"); From e7926e5102075e26b04af1d294ef431dc8143c0b Mon Sep 17 00:00:00 2001 From: static Date: Sun, 28 Jul 2024 09:18:27 +0900 Subject: [PATCH 05/27] Refactor: check if user withdrew when populating chat and user documents --- src/modules/populates/chats.js | 6 +++++- src/modules/socket.js | 9 +++------ src/schedules/notifyAfterArrival.js | 1 - src/services/chats.js | 1 - 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/modules/populates/chats.js b/src/modules/populates/chats.js index 2e18ccb6..38006b4c 100644 --- a/src/modules/populates/chats.js +++ b/src/modules/populates/chats.js @@ -2,7 +2,11 @@ * 쿼리를 통해 얻은 Chat Document를 populate할 설정값을 정의합니다. */ const chatPopulateOption = [ - { path: "authorId", select: "_id nickname profileImageUrl" }, + { + path: "authorId", + select: "_id nickname profileImageUrl", + match: { withdraw: false }, + }, ]; module.exports = { diff --git a/src/modules/socket.js b/src/modules/socket.js index 9cda8991..be6b65ee 100644 --- a/src/modules/socket.js +++ b/src/modules/socket.js @@ -43,9 +43,9 @@ const transformChatsForRoom = async (chats) => { chatsToSend.push({ roomId: chat.roomId, type: chat.type, - authorId: chat.authorId?._id, - authorName: chat.authorId?.nickname, - authorProfileUrl: chat.authorId?.profileImageUrl, + authorId: chat.authorId?._id ?? null, + authorName: chat.authorId?.nickname ?? null, + authorProfileUrl: chat.authorId?.profileImageUrl ?? null, content: chat.content, time: chat.time, isValid: chat.isValid, @@ -163,9 +163,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 diff --git a/src/schedules/notifyAfterArrival.js b/src/schedules/notifyAfterArrival.js index 3202af08..dc3b668b 100644 --- a/src/schedules/notifyAfterArrival.js +++ b/src/schedules/notifyAfterArrival.js @@ -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"); diff --git a/src/services/chats.js b/src/services/chats.js index ca74d774..4b1476dc 100644 --- a/src/services/chats.js +++ b/src/services/chats.js @@ -1,6 +1,5 @@ const { chatModel, userModel, roomModel } = require("../modules/stores/mongo"); const { chatPopulateOption } = require("../modules/populates/chats"); -const { roomPopulateOption } = require("../modules/populates/rooms"); const aws = require("../modules/stores/aws"); const { transformChatsForRoom, From 996b9ee5f5acd433ea3c4467218229f3c33c64c8 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 30 Jul 2024 11:32:33 +0900 Subject: [PATCH 06/27] Refactor: use userOid instead of userId --- src/lottery/services/globalState.js | 6 ++-- src/lottery/services/invite.js | 5 +++- src/lottery/services/publicNotice.js | 8 +++-- src/middlewares/auth.js | 3 +- src/middlewares/authAdmin.js | 4 +-- src/modules/socket.js | 10 +++++-- src/modules/stores/mongo.js | 2 +- src/schedules/deleteUserInfo.js | 2 +- src/services/auth.js | 4 +-- src/services/auth.mobile.js | 2 +- src/services/chats.js | 45 ++++++++++++++-------------- src/services/logininfo.js | 4 +-- src/services/notifications.js | 5 +++- src/services/reports.js | 9 ++++-- src/services/rooms.js | 20 +++++++------ src/services/users.js | 26 +++++++++------- 16 files changed, 91 insertions(+), 64 deletions(-) diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index 5459f851..1dfdb10c 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -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 && @@ -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) diff --git a/src/lottery/services/invite.js b/src/lottery/services/invite.js index c7871273..ffe219f1 100644 --- a/src/lottery/services/invite.js +++ b/src/lottery/services/invite.js @@ -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) diff --git a/src/lottery/services/publicNotice.js b/src/lottery/services/publicNotice.js index dd3dc91b..52f824aa 100644 --- a/src/lottery/services/publicNotice.js +++ b/src/lottery/services/publicNotice.js @@ -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; @@ -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 { diff --git a/src/middlewares/auth.js b/src/middlewares/auth.js index e521f9f4..23272af2 100644 --- a/src/middlewares/auth.js +++ b/src/middlewares/auth.js @@ -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(); } diff --git a/src/middlewares/authAdmin.js b/src/middlewares/authAdmin.js index 8db1f2a0..7b69a795 100644 --- a/src/middlewares/authAdmin.js +++ b/src/middlewares/authAdmin.js @@ -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가 화이트리스트에 있는지 확인 diff --git a/src/modules/socket.js b/src/modules/socket.js index be6b65ee..838226db 100644 --- a/src/modules/socket.js +++ b/src/modules/socket.js @@ -35,7 +35,10 @@ 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"); + const user = await userModel.findOne( + { id: userId, withdraw: false }, // NOTE: SSO uid 쓰는 곳 + "nickname" + ); return user.nickname; }) ); @@ -136,7 +139,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(); diff --git a/src/modules/stores/mongo.js b/src/modules/stores/mongo.js index a680e605..1e7194fe 100755 --- a/src/modules/stores/mongo.js +++ b/src/modules/stores/mongo.js @@ -6,7 +6,7 @@ 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" }], // 참여중인 완료된 방 배열 diff --git a/src/schedules/deleteUserInfo.js b/src/schedules/deleteUserInfo.js index de22cbde..283d1eae 100644 --- a/src/schedules/deleteUserInfo.js +++ b/src/schedules/deleteUserInfo.js @@ -14,7 +14,7 @@ module.exports = async () => { $set: { name: "", nickname: "", - // id + id: "", profileImageUrl: "", // ongoingRoom // doneRoom diff --git a/src/services/auth.js b/src/services/auth.js index fdca15dd..225276da 100644 --- a/src/services/auth.js +++ b/src/services/auth.js @@ -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}` ); @@ -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) { diff --git a/src/services/auth.mobile.js b/src/services/auth.mobile.js index 0e537b33..6e373f43 100644 --- a/src/services/auth.mobile.js +++ b/src/services/auth.mobile.js @@ -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" }); } diff --git a/src/services/chats.js b/src/services/chats.js index 4b1476dc..c080411c 100644 --- a/src/services/chats.js +++ b/src/services/chats.js @@ -13,17 +13,17 @@ const chatCount = 60; const loadRecentChatHandler = async (req, res) => { try { const io = req.app.get("io"); - const { userId } = req; + const { userOid } = req; const { roomId } = req.body; const { id: sessionId } = req.session; - if (!userId) { + if (!userOid) { return res.status(500).send("Chat/ : internal server error"); } if (!io) { return res.status(403).send("Chat/ : socket did not connected"); } - const isPart = await isUserInRoom(userId, roomId); + const isPart = await isUserInRoom(userOid, roomId); if (!isPart) { return res .status(403) @@ -55,10 +55,10 @@ const loadRecentChatHandler = async (req, res) => { const loadBeforeChatHandler = async (req, res) => { try { const io = req.app.get("io"); - const { userId } = req; + const { userOid } = req; const { roomId, lastMsgDate } = req.body; const { id: sessionId } = req.session; - if (!userId) { + if (!userOid) { return res.status(500).send("Chat/load/before : internal server error"); } if (!io) { @@ -67,7 +67,7 @@ const loadBeforeChatHandler = async (req, res) => { .send("Chat/load/before : socket did not connected"); } - const isPart = await isUserInRoom(userId, roomId); + const isPart = await isUserInRoom(userOid, roomId); if (!isPart) { return res .status(403) @@ -99,17 +99,17 @@ const loadBeforeChatHandler = async (req, res) => { const loadAfterChatHandler = async (req, res) => { try { const io = req.app.get("io"); - const { userId } = req; + const { userOid } = req; const { roomId, lastMsgDate } = req.body; const { id: sessionId } = req.session; - if (!userId) { + if (!userOid) { return res.status(500).send("Chat/load/after : internal server error"); } if (!io) { return res.status(403).send("Chat/load/after : socket did not connected"); } - const isPart = await isUserInRoom(userId, roomId); + const isPart = await isUserInRoom(userOid, roomId); if (!isPart) { return res .status(403) @@ -140,18 +140,18 @@ const loadAfterChatHandler = async (req, res) => { const sendChatHandler = async (req, res) => { try { const io = req.app.get("io"); - const { userId } = req; + const { userOid } = req; const { roomId, type, content } = req.body; - const user = await userModel.findOne({ id: userId }); + const user = await userModel.findOne({ _id: userOid, withdraw: false }); - if (!userId || !user) { + if (!userOid || !user) { return res.status(500).send("Chat/send : internal server error"); } if (!io) { return res.status(403).send("Chat/send : socket did not connected"); } - const isPart = await isUserInRoom(userId, roomId); + const isPart = await isUserInRoom(userOid, roomId); if (!isPart) { return res .status(403) @@ -177,11 +177,11 @@ const sendChatHandler = async (req, res) => { const readChatHandler = async (req, res) => { try { const io = req.app.get("io"); - const { userId } = req; + const { userOid } = req; const { roomId } = req.body; - const user = await userModel.findOne({ id: userId }); + const user = await userModel.findOne({ _id: userOid, withdraw: false }); - if (!userId || !user) { + if (!userOid || !user) { return res.status(500).send("Chat/read : internal server error"); } if (!io) { @@ -222,7 +222,7 @@ const readChatHandler = async (req, res) => { const uploadChatImgGetPUrlHandler = async (req, res) => { try { const { type, roomId } = req.body; - const user = await userModel.findOne({ id: req.userId }); + const user = await userModel.findOne({ _id: req.userOid, withdraw: false }); if (!roomId) { return res .status(403) @@ -265,7 +265,7 @@ const uploadChatImgGetPUrlHandler = async (req, res) => { const uploadChatImgDoneHandler = async (req, res) => { try { const user = await userModel.findOne( - { id: req.userId }, + { _id: req.userOid, withdraw: false }, "_id nickname profileImageUrl" ); const chat = await chatModel.findById(req.body.id).lean(); @@ -312,18 +312,17 @@ const uploadChatImgDoneHandler = async (req, res) => { /** * 주어진 유저가 주어진 방에 참여하는 중인지 확인합니다. * @summary 채팅 load/send 관련 api에서 검증을 위하여 함수로 분리하였습니다. - * @param {string} userId - 확인하고픈 user의 Id 입니다. + * @param {string} userOid - 확인하고픈 user의 objectId 입니다. * @param {string} roomId - 참여하는지 확인하고픈 방의 objectId 입니다. * @return {Promise} userId가 방에 포함되어 있다면 true, 그 외의 경우 false를 반환합니다. */ -const isUserInRoom = async (userId, roomId) => { - const user = await userModel.findOne({ id: userId }); +const isUserInRoom = async (userOid, roomId) => { const { part } = await roomModel.findById(roomId); - if (!part || !user) return false; + if (!part) return false; return part .map((participant) => participant.user) - .some((user) => user.equals(user._id)); + .some((user) => user.equals(userOid)); }; module.exports = { diff --git a/src/services/logininfo.js b/src/services/logininfo.js index eebb09ff..0253a6c6 100644 --- a/src/services/logininfo.js +++ b/src/services/logininfo.js @@ -5,10 +5,10 @@ const logger = require("../modules/logger"); const logininfoHandler = async (req, res) => { try { const user = getLoginInfo(req); - if (!user.id) return res.json({ id: undefined }); + if (!user.oid) return res.json({ id: undefined }); const userDetail = await userModel.findOne( - { id: user.id }, + { _id: user.oid, withdraw: false }, "_id name nickname id withdraw phoneNumber ban joinat agreeOnTermsOfService subinfo email profileImageUrl account" ); diff --git a/src/services/notifications.js b/src/services/notifications.js index 633f6739..ac6753e9 100644 --- a/src/services/notifications.js +++ b/src/services/notifications.js @@ -19,7 +19,10 @@ const registerDeviceTokenHandler = async (req, res) => { } // 데이터베이스에 deviceToken 레코드를 추가합니다. - const user = await userModel.findOne({ id: req.userId }, "_id"); + const user = await userModel.findOne( + { _id: req.userOid, withdraw: false }, + "_id" + ); const newDeviceToken = await registerDeviceToken(user._id, deviceToken); // 세션에 현재 사용자 기기의 deviceToken을 저장합니다. diff --git a/src/services/reports.js b/src/services/reports.js index dbd30d53..1ab77e90 100644 --- a/src/services/reports.js +++ b/src/services/reports.js @@ -12,10 +12,13 @@ const { notifyReportToReportChannel } = require("../modules/slackNotification"); const createHandler = async (req, res) => { try { const { reportedId, type, etcDetail, time, roomId } = req.body; - const user = await userModel.findOne({ id: req.userId }); + const user = await userModel.findOne({ _id: req.userOid, withdraw: false }); const creatorId = user._id; - const reported = await userModel.findById(reportedId); + const reported = await userModel.findOne({ + _id: reportedId, + withdraw: false, + }); if (!reported) { return res.status(400).json({ error: "User/report: no corresponding user", @@ -68,7 +71,7 @@ const createHandler = async (req, res) => { const searchByUserHandler = async (req, res) => { try { // 해당 user가 신고한 사람인지, 신고 받은 사람인지 기준으로 신고를 분리해서 응답을 전송합니다. - const user = await userModel.findOne({ id: req.userId }); + const user = await userModel.findOne({ _id: req.userOid, withdraw: false }); const response = { reporting: await reportModel .find({ creatorId: user._id }) diff --git a/src/services/rooms.js b/src/services/rooms.js index 218a23d5..b121306a 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -62,7 +62,7 @@ const createHandler = async (req, res) => { // 방 생성 요청을 한 사용자의 ObjectID를 room의 part 리스트에 추가 const user = await userModel - .findOne({ id: req.userId }) + .findOne({ _id: req.userOid, withdraw: false }) .populate("ongoingRoom"); // 사용자의 참여중인 진행중인 방이 5개 이상이면 오류를 반환합니다. @@ -169,7 +169,9 @@ const createTestHandler = async (req, res) => { candidateRooms ); if (isAbusing) { - const user = await userModel.findById(req.userOid).lean(); + const user = await userModel + .findOne({ _id: req.userOid, withdraw: false }) + .lean(); notifyRoomCreationAbuseToReportChannel( req.userOid, user?.nickname ?? req.userOid, @@ -210,7 +212,7 @@ const publicInfoHandler = async (req, res) => { const infoHandler = async (req, res) => { try { - const user = await userModel.findOne({ id: req.userId }); + const user = await userModel.findOne({ _id: req.userOid, withdraw: false }); const roomObject = await roomModel .findOne({ _id: req.query.id, "part.user": user._id }) .lean() @@ -234,7 +236,7 @@ const infoHandler = async (req, res) => { const joinHandler = async (req, res) => { try { const user = await userModel - .findOne({ id: req.userId }) + .findOne({ _id: req.userOid, withdraw: false }) .populate("ongoingRoom"); // 사용자의 참여중인 진행중인 방이 5개 이상이면 오류를 반환합니다. @@ -317,7 +319,7 @@ const abortHandler = async (req, res) => { }; try { - const user = await userModel.findOne({ id: req.userId }); + const user = await userModel.findOne({ _id: req.userOid, withdraw: false }); const room = await roomModel.findById(req.body.roomId); if (!user) { res.status(400).json({ @@ -485,7 +487,7 @@ const searchHandler = async (req, res) => { const searchByUserHandler = async (req, res) => { try { const user = await userModel - .findOne({ id: req.userId }) + .findOne({ _id: req.userOid, withdraw: false }) .populate({ path: "ongoingRoom", options: { @@ -525,7 +527,7 @@ const searchByUserHandler = async (req, res) => { const commitSettlementHandler = async (req, res) => { try { - const user = await userModel.findOne({ id: req.userId }); + const user = await userModel.findOne({ _id: req.userOid, withdraw: false }); const { roomId } = req.body; const roomObject = await roomModel .findOneAndUpdate( @@ -610,7 +612,7 @@ const commitSettlementHandler = async (req, res) => { const commitPaymentHandler = async (req, res) => { try { const { roomId } = req.body; - const user = await userModel.findOne({ id: req.userId }); + const user = await userModel.findOne({ _id: req.userOid, withdraw: false }); let roomObject = await roomModel .findOneAndUpdate( { @@ -781,7 +783,7 @@ const checkIsSendRequired = (userObject) => { // // Room update query에 사용할 filter입니다. // // 방에 참여중인 인원만 방 정보를 수정할 수 있습니다. -// const user = await userModel.findOne({ id: req.userId }, "_id"); +// const user = await userModel.findOne({ _id: req.userOid, withdraw: false }, "_id"); // const roomFilter = { // _id: roomId, // part: { diff --git a/src/services/users.js b/src/services/users.js index 12d0afe1..f4487334 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -11,7 +11,7 @@ const { const agreeOnTermsOfServiceHandler = async (req, res) => { try { - let user = await userModel.findOne({ id: req.userId }); + let user = await userModel.findOne({ _id: req.userOid, withdraw: false }); if (user.agreeOnTermsOfService !== true) { user.agreeOnTermsOfService = true; await user.save(); @@ -31,7 +31,7 @@ const agreeOnTermsOfServiceHandler = async (req, res) => { const getAgreeOnTermsOfServiceHandler = async (req, res) => { try { const user = await userModel - .findOne({ id: req.userId }, "agreeOnTermsOfService") + .findOne({ _id: req.userOid, withdraw: false }, "agreeOnTermsOfService") .lean(); const agreeOnTermsOfService = user.agreeOnTermsOfService === true; res.json({ agreeOnTermsOfService }); @@ -46,7 +46,7 @@ const editNicknameHandler = async (req, res) => { try { const newNickname = req.body.nickname; const result = await userModel.findOneAndUpdate( - { id: req.userId }, + { _id: req.userOid, withdraw: false }, { nickname: newNickname } ); @@ -73,7 +73,7 @@ const editAccountHandler = async (req, res) => { try { const newAccount = req.body.account; const result = await userModel.findOneAndUpdate( - { id: req.userId }, + { _id: req.userOid, withdraw: false }, { account: newAccount } ); @@ -98,7 +98,10 @@ const editAccountHandler = async (req, res) => { const editProfileImgGetPUrlHandler = async (req, res) => { try { const type = req.body.type; - const user = await userModel.findOne({ id: req.userId }, "_id"); + const user = await userModel.findOne( + { _id: req.userOid, withdraw: false }, + "_id" + ); if (!user) { return res .status(500) @@ -127,7 +130,10 @@ const editProfileImgGetPUrlHandler = async (req, res) => { const editProfileImgDoneHandler = async (req, res) => { try { - const user = await userModel.findOne({ id: req.userId }, "_id"); + const user = await userModel.findOne( + { _id: req.userOid, withdraw: false }, + "_id" + ); if (!user) { return res .status(500) @@ -142,7 +148,7 @@ const editProfileImgDoneHandler = async (req, res) => { .send("Users/editProfileImg/done : internal server error"); } const userAfter = await userModel.findOneAndUpdate( - { id: req.userId }, + { _id: req.userOid, withdraw: false }, { profileImageUrl: aws.getS3Url(`/${key}?token=${req.timestamp}`) }, { new: true } ); @@ -164,7 +170,7 @@ const editProfileImgDoneHandler = async (req, res) => { const resetNicknameHandler = async (req, res) => { try { const result = await userModel.findOneAndUpdate( - { id: req.userId }, + { _id: req.userOid, withdraw: false }, { nickname: generateNickname(req.body.id) }, { new: true } ); @@ -184,7 +190,7 @@ const resetNicknameHandler = async (req, res) => { const resetProfileImgHandler = async (req, res) => { try { const result = await userModel.findOneAndUpdate( - { id: req.userId }, + { _id: req.userOid, withdraw: false }, { profileImageUrl: generateProfileImageUrl() }, { new: true } ); @@ -231,7 +237,7 @@ const getBanRecordHandler = async (req, res) => { const withdrawHandler = async (req, res) => { try { - const user = await userModel.findOne({ id: req.userId }); + const user = await userModel.findOne({ _id: req.userOid, withdraw: false }); if (!user) { return res.status(500).send("Users/withdraw : internal server error"); } else if (user.withdraw) { From c340df94ac09b2e7954646794ac3e1a8b38e9f8f Mon Sep 17 00:00:00 2001 From: static Date: Tue, 13 Aug 2024 03:13:22 +0900 Subject: [PATCH 07/27] Refactor: check if user withdrew when populating documents --- src/lottery/services/publicNotice.js | 2 +- src/modules/populates/reports.js | 1 + src/modules/populates/rooms.js | 8 +++++++- src/modules/socket.js | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/lottery/services/publicNotice.js b/src/lottery/services/publicNotice.js index 52f824aa..009b46fc 100644 --- a/src/lottery/services/publicNotice.js +++ b/src/lottery/services/publicNotice.js @@ -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( diff --git a/src/modules/populates/reports.js b/src/modules/populates/reports.js index 9f5b3fa4..24b09bda 100644 --- a/src/modules/populates/reports.js +++ b/src/modules/populates/reports.js @@ -2,6 +2,7 @@ const reportPopulateOption = [ { path: "reportedId", select: "_id id name nickname profileImageUrl", + match: { withdraw: false }, }, ]; diff --git a/src/modules/populates/rooms.js b/src/modules/populates/rooms.js index 7243d648..7cdf5948 100644 --- a/src/modules/populates/rooms.js +++ b/src/modules/populates/rooms.js @@ -8,7 +8,11 @@ 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", + match: { withdraw: false }, + }, }, ]; @@ -27,6 +31,8 @@ const formatSettlement = ( { includeSettlement = true, isOver = false, timestamp = Date.now() } = {} ) => { roomObject.part = roomObject.part.map((participantSubDocument) => { + if (!participantSubDocument.user) return null; + const { _id, name, nickname, profileImageUrl } = participantSubDocument.user; const { settlementStatus, readAt } = participantSubDocument; diff --git a/src/modules/socket.js b/src/modules/socket.js index 838226db..f4807bcc 100644 --- a/src/modules/socket.js +++ b/src/modules/socket.js @@ -39,7 +39,7 @@ const transformChatsForRoom = async (chats) => { { id: userId, withdraw: false }, // NOTE: SSO uid 쓰는 곳 "nickname" ); - return user.nickname; + return user?.nickname; }) ); } From 670eae6d8ee886212540290b1b0e0176c8aeb556 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 20 Aug 2024 20:55:10 +0900 Subject: [PATCH 08/27] Refactor: now remove FCM device tokens when withdrawing --- src/modules/fcm.js | 23 +++++++++++++++++++++++ src/services/users.js | 10 +++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/modules/fcm.js b/src/modules/fcm.js index 1fc53946..7faaaad2 100644 --- a/src/modules/fcm.js +++ b/src/modules/fcm.js @@ -91,6 +91,28 @@ const unregisterDeviceToken = async (deviceToken) => { } }; +/** + * 사용자의 ObjectId가 주어졌을 때, 해당 사용자의 모든 deviceToken을 DB에서 삭제합니다. + * @param {string} userId - 사용자의 ObjectId입니다. + * @return {Promise} 해당 사용자로부터 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} deviceTokens - 사용자의 ObjectId입니다. @@ -351,6 +373,7 @@ module.exports = { initializeApp, registerDeviceToken, unregisterDeviceToken, + unregisterAllDeviceTokens, validateDeviceToken, getTokensOfUsers, sendMessageByTokens, diff --git a/src/services/users.js b/src/services/users.js index f4487334..2c73d1cf 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -1,4 +1,5 @@ const { userModel, banModel } = require("../modules/stores/mongo"); +const { unregisterAllDeviceTokens } = require("../modules/fcm"); const logger = require("../modules/logger"); const aws = require("../modules/stores/aws"); @@ -240,12 +241,19 @@ const withdrawHandler = async (req, res) => { const user = await userModel.findOne({ _id: req.userOid, withdraw: false }); if (!user) { return res.status(500).send("Users/withdraw : internal server error"); - } else if (user.withdraw) { + } + + // 회원 탈퇴가 가능한 조건인지 확인 + if (user.withdraw) { return res.status(400).send("Users/withdraw : already withdrawn"); } else if (user.ongoingRoom.length !== 0) { return res.status(400).send("Users/withdraw : ongoing room exists"); } + // 등록된 모든 디바이스 토큰 삭제 + await unregisterAllDeviceTokens(req.userOid); + + // 회원 탈퇴 처리 (Soft Delete) user.withdraw = true; user.withdrawAt = req.timestamp; From 00ddb69bc0302b2426199c9310facb9755c41848 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 21 Aug 2024 15:52:12 +0900 Subject: [PATCH 09/27] Docs: withdraw endpoint swagger --- src/routes/docs/users.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/routes/docs/users.js b/src/routes/docs/users.js index deaeb113..e7d6e989 100644 --- a/src/routes/docs/users.js +++ b/src/routes/docs/users.js @@ -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; From 7c596a9cc07827c2c2616d593100e590b64eac12 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 8 Oct 2024 23:28:59 +0900 Subject: [PATCH 10/27] Add: authorIsWithdraw field --- src/modules/populates/chats.js | 3 +-- src/modules/socket.js | 7 ++++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/modules/populates/chats.js b/src/modules/populates/chats.js index 38006b4c..64a7fd90 100644 --- a/src/modules/populates/chats.js +++ b/src/modules/populates/chats.js @@ -4,8 +4,7 @@ const chatPopulateOption = [ { path: "authorId", - select: "_id nickname profileImageUrl", - match: { withdraw: false }, + select: "_id nickname profileImageUrl withdraw", }, ]; diff --git a/src/modules/socket.js b/src/modules/socket.js index f4807bcc..64ac3ba5 100644 --- a/src/modules/socket.js +++ b/src/modules/socket.js @@ -46,9 +46,10 @@ const transformChatsForRoom = async (chats) => { chatsToSend.push({ roomId: chat.roomId, type: chat.type, - authorId: chat.authorId?._id ?? null, - authorName: chat.authorId?.nickname ?? null, - authorProfileUrl: chat.authorId?.profileImageUrl ?? null, + 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, From d9ed57b1e2468a5766b3420545e85f00998e0e20 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 5 Nov 2024 23:25:37 +0900 Subject: [PATCH 11/27] Fix: chat.authorId can be nullish value --- src/modules/socket.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/modules/socket.js b/src/modules/socket.js index 64ac3ba5..080f7814 100644 --- a/src/modules/socket.js +++ b/src/modules/socket.js @@ -46,10 +46,10 @@ const transformChatsForRoom = async (chats) => { chatsToSend.push({ roomId: chat.roomId, type: chat.type, - authorId: chat.authorId._id, - authorName: chat.authorId.nickname, - authorProfileUrl: chat.authorId.profileImageUrl, - authorIsWithdrew: chat.authorId.withdraw, + 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, From 826ee1a534b24c383075de7ac71d72748f68ecae Mon Sep 17 00:00:00 2001 From: static Date: Tue, 5 Nov 2024 23:43:18 +0900 Subject: [PATCH 12/27] Fix: withdrew user populate error --- src/modules/populates/reports.js | 1 - src/modules/populates/rooms.js | 1 - 2 files changed, 2 deletions(-) diff --git a/src/modules/populates/reports.js b/src/modules/populates/reports.js index 24b09bda..9f5b3fa4 100644 --- a/src/modules/populates/reports.js +++ b/src/modules/populates/reports.js @@ -2,7 +2,6 @@ const reportPopulateOption = [ { path: "reportedId", select: "_id id name nickname profileImageUrl", - match: { withdraw: false }, }, ]; diff --git a/src/modules/populates/rooms.js b/src/modules/populates/rooms.js index 7cdf5948..5b8866be 100644 --- a/src/modules/populates/rooms.js +++ b/src/modules/populates/rooms.js @@ -11,7 +11,6 @@ const roomPopulateOption = [ populate: { path: "user", select: "_id id name nickname profileImageUrl", - match: { withdraw: false }, }, }, ]; From 69127d59f465510ae60364744d91e37b9805006f Mon Sep 17 00:00:00 2001 From: static Date: Tue, 5 Nov 2024 23:51:21 +0900 Subject: [PATCH 13/27] Add: withdraw field in room/report related responses --- src/modules/populates/reports.js | 2 +- src/modules/populates/rooms.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/modules/populates/reports.js b/src/modules/populates/reports.js index 9f5b3fa4..baa7b4ee 100644 --- a/src/modules/populates/reports.js +++ b/src/modules/populates/reports.js @@ -1,7 +1,7 @@ const reportPopulateOption = [ { path: "reportedId", - select: "_id id name nickname profileImageUrl", + select: "_id id name nickname profileImageUrl withdraw", }, ]; diff --git a/src/modules/populates/rooms.js b/src/modules/populates/rooms.js index 5b8866be..a538a47f 100644 --- a/src/modules/populates/rooms.js +++ b/src/modules/populates/rooms.js @@ -10,7 +10,7 @@ const roomPopulateOption = [ select: "-_id user settlementStatus readAt", populate: { path: "user", - select: "_id id name nickname profileImageUrl", + select: "_id id name nickname profileImageUrl withdraw", }, }, ]; @@ -32,7 +32,7 @@ const formatSettlement = ( roomObject.part = roomObject.part.map((participantSubDocument) => { if (!participantSubDocument.user) return null; - const { _id, name, nickname, profileImageUrl } = + const { _id, name, nickname, profileImageUrl, withdraw } = participantSubDocument.user; const { settlementStatus, readAt } = participantSubDocument; return { @@ -40,6 +40,7 @@ const formatSettlement = ( name, nickname, profileImageUrl, + withdraw, isSettlement: includeSettlement ? settlementStatus : undefined, readAt: readAt ?? roomObject.madeat, }; From 3af8bf73913ea112d6e4de229aff876c75bff40d Mon Sep 17 00:00:00 2001 From: static Date: Fri, 29 Nov 2024 22:45:29 +0900 Subject: [PATCH 14/27] Refactor: compile speed & ts-node startup speed optmization --- .gitignore | 1 + tsconfig.build.json | 6 +++++- tsconfig.json | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 12d89e61..c2d85744 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /node_modules /dist +.tsbuildinfo /dump .env .env.test diff --git a/tsconfig.build.json b/tsconfig.build.json index 68db80dd..0e742aa9 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,4 +1,8 @@ { "extends": "./tsconfig.json", - "include": ["src"], + "compilerOptions": { + "incremental": true, + "tsBuildInfoFile": ".tsbuildinfo" + }, + "include": ["src"] } diff --git a/tsconfig.json b/tsconfig.json index 166f2de6..63239482 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,7 @@ "include": ["src", "scripts"], "exclude": ["dist", "node_modules"], "ts-node": { - "files": true + "files": true, + "transpileOnly": true } } From 372ecabec205cda311f28739093c0c068e087e91 Mon Sep 17 00:00:00 2001 From: static Date: Fri, 29 Nov 2024 23:03:51 +0900 Subject: [PATCH 15/27] Fix: test error --- test/services/reports.js | 4 ++-- test/services/rooms.js | 14 +++++++------- test/services/users.js | 12 ++++++------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/test/services/reports.js b/test/services/reports.js index 86347d4a..96c9aa42 100644 --- a/test/services/reports.js +++ b/test/services/reports.js @@ -18,7 +18,7 @@ describe("[reports] 1.createHandler", () => { const testRoom = await roomGenerator("test1", testData); const msg = "User/report : report successful"; let req = httpMocks.createRequest({ - userId: testUser1.id, + userOid: testUser1._id, body: { reportedId: testUser2._id, type: "etc-reason", @@ -41,7 +41,7 @@ describe("[reports] 2.searchByUserHandler", () => { it("should return correct reporting/reported reports of users", async () => { const testUser1 = await userModel.findOne({ id: "test1" }); let req = httpMocks.createRequest({ - userId: testUser1.id, + userOid: testUser1._id, }); let res = httpMocks.createResponse(); await reportHandlers.searchByUserHandler(req, res); diff --git a/test/services/rooms.js b/test/services/rooms.js index 4e948ad1..894901da 100644 --- a/test/services/rooms.js +++ b/test/services/rooms.js @@ -31,7 +31,7 @@ describe("[rooms] 1.createHandler", () => { time: Date.now() + 60 * 1000, maxPartLength: 4, }, - userId: testUser1.id, + userOid: testUser1._id, app, }); let res = httpMocks.createResponse(); @@ -51,7 +51,7 @@ describe("[rooms] 2.infoHandler", () => { const testRoom = await roomModel.findOne({ name: "test-room" }); let req = httpMocks.createRequest({ query: { id: testRoom._id }, - userId: testUser1.id, + userOid: testUser1._id, }); let res = httpMocks.createResponse(); await roomsHandlers.infoHandler(req, res); @@ -87,7 +87,7 @@ describe("[rooms] 4.joinHandler", () => { body: { roomId: testRoom._id, }, - userId: testUser2.id, + userOid: testUser2._id, app, }); let res = httpMocks.createResponse(); @@ -129,7 +129,7 @@ describe("[rooms] 6.searchByUserHandler", () => { it("should return information of searching room", async () => { const testUser1 = await userModel.findOne({ id: "test1" }); let req = httpMocks.createRequest({ - userId: testUser1.id, + userOid: testUser1._id, }); let res = httpMocks.createResponse(); await roomsHandlers.searchByUserHandler(req, res); @@ -147,7 +147,7 @@ describe("[rooms] 7.commitSettlementHandler", () => { const testRoom = await roomModel.findOne({ name: "test-room" }); let req = httpMocks.createRequest({ body: { roomId: testRoom._id }, - userId: testUser1.id, + userOid: testUser1._id, timestamp: Date.now() + 60 * 1000, app, }); @@ -168,7 +168,7 @@ describe("[rooms] 8.commitPaymentHandler", () => { const testRoom = await roomModel.findOne({ name: "test-room" }); let req = httpMocks.createRequest({ body: { roomId: testRoom._id }, - userId: testUser2.id, + userOid: testUser2._id, app, }); let res = httpMocks.createResponse(); @@ -188,7 +188,7 @@ describe("[rooms] 9.abortHandler", () => { const testRoom = await roomModel.findOne({ name: "test-room" }); let req = httpMocks.createRequest({ body: { roomId: testRoom._id }, - userId: testUser2.id, + userOid: testUser2._id, session: {}, app, }); diff --git a/test/services/users.js b/test/services/users.js index 4f2195da..2cf3245e 100644 --- a/test/services/users.js +++ b/test/services/users.js @@ -17,7 +17,7 @@ describe("[users] 1.agreeOnTermsOfServiceHandler", () => { const msg = "Users/agreeOnTermsOfService : agree on Terms of Service successful"; let req = httpMocks.createRequest({ - userId: testUser1.id, + userOid: testUser1._id, }); let res = httpMocks.createResponse(); await usersHandlers.agreeOnTermsOfServiceHandler(req, res); @@ -33,7 +33,7 @@ describe("[users] 2.getAgreeOnTermsOfServiceHandler", () => { it("should return AgreeOnTermsOfService of user", async () => { const testUser1 = await userModel.findOne({ id: "test1" }); let req = httpMocks.createRequest({ - userId: testUser1.id, + userOid: testUser1._id, }); let res = httpMocks.createResponse(); await usersHandlers.getAgreeOnTermsOfServiceHandler(req, res); @@ -52,7 +52,7 @@ describe("[users] 3.editNicknameHandler", () => { const testUser1 = await userModel.findOne({ id: "test1" }); const msg = "Users/editNickname : edit user nickname successful"; let req = httpMocks.createRequest({ - userId: testUser1.id, + userOid: testUser1._id, body: { nickname: testNickname, }, @@ -79,7 +79,7 @@ describe("[users] 4.editAccountHandler", () => { const testUser1 = await userModel.findOne({ id: "test1" }); const msg = "Users/editAccount : edit user account successful"; let req = httpMocks.createRequest({ - userId: testUser1.id, + userOid: testUser1._id, body: { account: testAccount, }, @@ -105,7 +105,7 @@ describe("[users] 5.editProfileImgGetPUrlHandler", () => { const testUser1 = await userModel.findOne({ id: "test1" }); const testImgType = "image/jpg"; let req = httpMocks.createRequest({ - userId: testUser1.id, + userOid: testUser1._id, body: { type: testImgType, }, @@ -129,7 +129,7 @@ describe("[users] 6.editProfileImgDoneHandler", () => { it("should return correct result and new profileImageUrl", async () => { const testUser1 = await userModel.findOne({ id: "test1" }); let req = httpMocks.createRequest({ - userId: testUser1.id, + userOid: testUser1._id, }); let res = httpMocks.createResponse(); await usersHandlers.editProfileImgDoneHandler(req, res); From f58f645ef37ccabd57ea1db82ad6b3ec8874a36c Mon Sep 17 00:00:00 2001 From: static Date: Sat, 30 Nov 2024 01:00:13 +0900 Subject: [PATCH 16/27] Add: withdraw check into items.js --- src/lottery/services/items.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js index 07730574..c5f5a2c5 100644 --- a/src/lottery/services/items.js +++ b/src/lottery/services/items.js @@ -125,7 +125,13 @@ const getItemLeaderboardHandler = async (req, res) => { leaderboardBase .filter((user) => user.rank <= 20) .map(async (user) => { - const userInfo = await userModel.findById(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; + } return { nickname: userInfo.nickname, profileImageUrl: userInfo.profileImageUrl, @@ -135,6 +141,10 @@ const getItemLeaderboardHandler = async (req, res) => { }; }) ); + if (leaderboard.includes(null)) + return res + .status(500) + .json({ error: "Items/leaderboard : internal server error" }); const userId = isLogin(req) ? getLoginInfo(req).oid : null; const user = leaderboardBase.find( From 069241b986851ea98fe445f49a33653e281239fd Mon Sep 17 00:00:00 2001 From: static Date: Tue, 3 Dec 2024 21:49:09 +0900 Subject: [PATCH 17/27] Refactor: use sid in responseTimeMiddleware --- src/middlewares/responseTime.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middlewares/responseTime.ts b/src/middlewares/responseTime.ts index ba84115a..71a8245a 100644 --- a/src/middlewares/responseTime.ts +++ b/src/middlewares/responseTime.ts @@ -6,7 +6,7 @@ const responseTimeMiddleware = responseTime( (req: Request, res: Response, time: number) => { const { method, originalUrl, clientIP } = req; const { statusCode } = res; - const userId = req.session?.loginInfo?.oid || "anonymous"; + const userId = req.session?.loginInfo?.id || "anonymous"; logger.info( `${userId}(${clientIP}) "${method} ${originalUrl}" ${statusCode} on ${time}ms` ); From e54cca304d16a873b4a1953b78a809a4575c6d8b Mon Sep 17 00:00:00 2001 From: static Date: Tue, 21 Jan 2025 18:56:43 +0900 Subject: [PATCH 18/27] Add: migration script that converts userId to userOid in chats --- scripts/chatContentUserIdUpdater.js | 41 +++++++++++++++++++++++++++++ src/modules/socket.js | 7 ++--- src/services/rooms.js | 10 +++---- 3 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 scripts/chatContentUserIdUpdater.js diff --git a/scripts/chatContentUserIdUpdater.js b/scripts/chatContentUserIdUpdater.js new file mode 100644 index 00000000..0063542a --- /dev/null +++ b/scripts/chatContentUserIdUpdater.js @@ -0,0 +1,41 @@ +const { MongoClient } = require("mongodb"); +const { mongo: mongoUrl } = require("@/loadenv"); + +const client = new MongoClient(mongoUrl); +const db = client.db("taxi"); +const chats = db.collection("chats"); +const users = db.collection("users"); + +async function convertUserIdToOid(userId) { + const user = await users.findOne({ id: userId, withdraw: false }, "_id"); + if (!user) throw new Error(`User not found: ${userId}`); + return user._id.toString(); +} + +async function run() { + try { + for await (const doc of chats.find()) { + if (doc.type === "in" || doc.type === "out") { + const inOutUserIds = doc.content.split("|"); + const inOutUserOids = await Promise.all( + inOutUserIds.map(convertUserIdToOid) + ); + await chats.updateOne( + { _id: doc._id }, + { $set: { content: inOutUserOids.join("|") } } + ); + } else if (doc.type === "payment" || doc.type === "settlement") { + const userId = doc.content; + const userOid = await convertUserIdToOid(userId); + await chats.updateOne({ _id: doc._id }, { $set: { content: userOid } }); + } + } + } catch (err) { + console.error(err); + } finally { + await client.close(); + } +} +run().then(() => { + console.log("Done!"); +}); diff --git a/src/modules/socket.js b/src/modules/socket.js index b509d230..36b1f1bc 100644 --- a/src/modules/socket.js +++ b/src/modules/socket.js @@ -35,10 +35,7 @@ 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, withdraw: false }, // NOTE: SSO uid 쓰는 곳 - "nickname" - ); + const user = await userModel.findById(userId, "nickname"); return user?.nickname; }) ); @@ -115,7 +112,7 @@ const getMessageBody = (type, nickname = "", content = "") => { * @param {Object} chat - 채팅 메시지 내용입니다. * @param {string} chat.roomId - 채팅 및 채팅 알림을 보낼 방의 ObjectId입니다. * @param {string} chat.type - 채팅 메시지의 유형입니다. "text" | "s3img" | "in" | "out" | "payment" | "settlement" | "account" | "departure" | "arrival" 입니다. - * @param {string} chat.content - 채팅 메시지의 본문입니다. chat.type이 "s3img"인 경우에는 채팅의 objectId입니다. chat.type이 "in"이거나 "out"인 경우 입퇴장한 사용자의 id(!==ObjectId)입니다. + * @param {string} chat.content - 채팅 메시지의 본문입니다. chat.type이 "s3img"인 경우에는 채팅의 objectId입니다. chat.type이 "in"이거나 "out"인 경우 입퇴장한 사용자의 oid입니다. * @param {string} chat.authorId - optional. 채팅을 보낸 사용자의 ObjectId입니다. * @param {Date?} chat.time - optional. 채팅 메시지 전송 시각입니다. * @return {Promise} 채팅 및 알림 전송에 성공하면 true, 중간에 오류가 발생하면 false를 반환합니다. diff --git a/src/services/rooms.js b/src/services/rooms.js index ed93bcf8..42ec6e1b 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -102,7 +102,7 @@ const createHandler = async (req, res) => { await emitChatEvent(req.app.get("io"), { roomId: room._id, type: "in", - content: user.id, + content: user._id, authorId: user._id, }); @@ -299,7 +299,7 @@ const joinHandler = async (req, res) => { await emitChatEvent(req.app.get("io"), { roomId: room._id, type: "in", - content: user.id, + content: user._id, authorId: user._id, }); @@ -386,7 +386,7 @@ const abortHandler = async (req, res) => { await emitChatEvent(req.app.get("io"), { roomId: room._id, type: "out", - content: user.id, + content: user._id, authorId: user._id, }); @@ -584,7 +584,7 @@ const commitSettlementHandler = async (req, res) => { await emitChatEvent(req.app.get("io"), { roomId, type: "settlement", - content: user.id, + content: user._id, authorId: user._id, }); @@ -657,7 +657,7 @@ const commitPaymentHandler = async (req, res) => { await emitChatEvent(req.app.get("io"), { roomId, type: "payment", - content: user.id, + content: user._id, authorId: user._id, }); From 1338fcc4bce1fb82b014b868b0ffc30a2f66a020 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 21 Jan 2025 20:03:42 +0900 Subject: [PATCH 19/27] Refactor: getIsOver function now uses userOid instead of userId --- src/modules/populates/rooms.ts | 8 ++++---- src/schedules/deleteUserInfo.js | 6 ++++-- src/services/auth.js | 2 +- src/services/rooms.js | 6 +++--- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/modules/populates/rooms.ts b/src/modules/populates/rooms.ts index a63dc69a..25b0bfff 100644 --- a/src/modules/populates/rooms.ts +++ b/src/modules/populates/rooms.ts @@ -112,15 +112,15 @@ export const formatSettlement = ( }; /** - * roomPopulateOption을 사용해 populate된 Room Object와 사용자의 id(userId)가 주어졌을 때, 해당 사용자의 정산 상태를 반환합니다. + * roomPopulateOption을 사용해 populate된 Room Object와 사용자의 objectId가 주어졌을 때, 해당 사용자의 정산 상태를 반환합니다. * @param roomObject - roomPopulateOption을 사용해 populate된 변환한 Room Object입니다. - * @param userId - 방 완료 상태를 확인하려는 사용자의 id(user.id)입니다. + * @param userOid - 방 완료 상태를 확인하려는 사용자의 objectId입니다. * @return 사용자의 해당 방에 대한 완료 여부(true | false)를 반환합니다. 사용자가 참여중인 방이 아닐 경우 undefined를 반환합니다. **/ -export const getIsOver = (roomObject: PopulatedRoom, userId: string) => { +export const getIsOver = (roomObject: PopulatedRoom, userOid: string) => { // room document의 part subdoocument에서 사용자 id와 일치하는 정산 정보를 찾습니다. const participantSubDocuments = roomObject.part?.filter((part) => { - return part.user?.id === userId; + return part.user?._id?.toString() === userOid; }); // 방에 참여중이지 않은 사용자의 경우, undefined을 반환합니다. diff --git a/src/schedules/deleteUserInfo.js b/src/schedules/deleteUserInfo.js index 283d1eae..e1db09ab 100644 --- a/src/schedules/deleteUserInfo.js +++ b/src/schedules/deleteUserInfo.js @@ -3,11 +3,13 @@ const logger = require("../modules/logger"); module.exports = async () => { try { - // 탈퇴일로부터 1년 이상 경과한 사용자의 개인정보 삭제 + // 탈퇴일로부터 5년 이상 경과한 사용자의 개인정보 삭제 await userModel.updateMany( { withdraw: true, - withdrawAt: { $lte: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000) }, + withdrawAt: { + $lte: new Date(Date.now() - 5 * 365 * 24 * 60 * 60 * 1000), + }, name: { $ne: "" }, }, { diff --git a/src/services/auth.js b/src/services/auth.js index c047cfd8..484fa891 100644 --- a/src/services/auth.js +++ b/src/services/auth.js @@ -37,7 +37,7 @@ const transUserData = (userData) => { const joinus = async (req, userData) => { const newUser = new userModel({ - id: userData.id, + id: userData.id, // NOTE: SSO uid name: userData.name, nickname: generateNickname(userData.id), profileImageUrl: generateProfileImageUrl(), diff --git a/src/services/rooms.js b/src/services/rooms.js index 42ec6e1b..8adc28c8 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -219,7 +219,7 @@ const infoHandler = async (req, res) => { .lean() .populate(roomPopulateOption); if (roomObject) { - const isOver = getIsOver(roomObject, user.id); + const isOver = getIsOver(roomObject, user._id); res.send(formatSettlement(roomObject, { isOver })); } else { res.status(404).json({ @@ -391,7 +391,7 @@ const abortHandler = async (req, res) => { }); const roomObject = (await room.populate(roomPopulateOption)).toObject(); - const isOver = getIsOver(roomObject, user.id); + const isOver = getIsOver(roomObject, user._id); res.send(formatSettlement(roomObject, { isOver })); } catch (err) { @@ -817,7 +817,7 @@ const checkIsSendRequired = (userObject) => { // }); // if (result) { // const roomObject = (await result.populate(roomPopulateOption)).toObject(); -// const isOver = getIsOver(room, user.id); +// const isOver = getIsOver(room, user._id); // res.send(formatSettlement(roomObject, { isOver })); // } else { // res.status(404).json({ From 0f7c9ddd58231c1fb202655d293a8585e6cc481e Mon Sep 17 00:00:00 2001 From: static Date: Tue, 21 Jan 2025 20:07:21 +0900 Subject: [PATCH 20/27] Fix: sample generator inserts userId into chat.content --- src/sampleGenerator/src/testData.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sampleGenerator/src/testData.js b/src/sampleGenerator/src/testData.js index 1209c52a..2866eafa 100644 --- a/src/sampleGenerator/src/testData.js +++ b/src/sampleGenerator/src/testData.js @@ -125,7 +125,7 @@ const generateJoinAbortChat = async (roomId, userOid, isJoining, time) => { roomId: roomId, type: isJoining ? "in" : "out", authorId: user._id, - content: user.id, + content: user._id, time: time, isValid: false, }); From d2909458bd380f44de121daf9f62acc27018d2d9 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 21 Jan 2025 20:44:51 +0900 Subject: [PATCH 21/27] Fix: isOver is always false --- src/services/rooms.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/rooms.js b/src/services/rooms.js index 8adc28c8..fa8a14b9 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -219,7 +219,7 @@ const infoHandler = async (req, res) => { .lean() .populate(roomPopulateOption); if (roomObject) { - const isOver = getIsOver(roomObject, user._id); + const isOver = getIsOver(roomObject, user._id.toString()); res.send(formatSettlement(roomObject, { isOver })); } else { res.status(404).json({ @@ -391,7 +391,7 @@ const abortHandler = async (req, res) => { }); const roomObject = (await room.populate(roomPopulateOption)).toObject(); - const isOver = getIsOver(roomObject, user._id); + const isOver = getIsOver(roomObject, user._id.toString()); res.send(formatSettlement(roomObject, { isOver })); } catch (err) { From aff0f8667164e544795be13c843450cde08ec6dc Mon Sep 17 00:00:00 2001 From: static Date: Wed, 22 Jan 2025 16:21:23 +0900 Subject: [PATCH 22/27] Add: rejoin restriction after user withdrawal --- src/modules/stores/mongo.ts | 2 +- src/schedules/deleteUserInfo.js | 2 +- src/services/auth.js | 28 ++++++++++++++++++++++++++-- src/services/users.ts | 2 +- src/types/mongo.d.ts | 2 +- 5 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/modules/stores/mongo.ts b/src/modules/stores/mongo.ts index 6c162fe4..41ccfa3e 100755 --- a/src/modules/stores/mongo.ts +++ b/src/modules/stores/mongo.ts @@ -24,7 +24,7 @@ const userSchema = new Schema({ ongoingRoom: [{ type: Schema.Types.ObjectId, ref: "Room" }], // 참여중인 진행중인 방 배열 doneRoom: [{ type: Schema.Types.ObjectId, ref: "Room" }], // 참여중인 완료된 방 배열 withdraw: { type: Boolean, default: false }, //탈퇴 여부 - withdrawAt: { type: Date }, //탈퇴 시각 + withdrewAt: { type: Date }, //탈퇴 시각 phoneNumber: { type: String }, // 전화번호 (2023FALL 이벤트부터 추가) ban: { type: Boolean, default: false }, //계정 정지 여부 joinat: { type: Date, required: true }, //가입 시각 diff --git a/src/schedules/deleteUserInfo.js b/src/schedules/deleteUserInfo.js index e1db09ab..0c7c5ce6 100644 --- a/src/schedules/deleteUserInfo.js +++ b/src/schedules/deleteUserInfo.js @@ -7,7 +7,7 @@ module.exports = async () => { await userModel.updateMany( { withdraw: true, - withdrawAt: { + withdrewAt: { $lte: new Date(Date.now() - 5 * 365 * 24 * 60 * 60 * 1000), }, name: { $ne: "" }, diff --git a/src/services/auth.js b/src/services/auth.js index 484fa891..298ed9a7 100644 --- a/src/services/auth.js +++ b/src/services/auth.js @@ -36,6 +36,24 @@ const transUserData = (userData) => { }; const joinus = async (req, userData) => { + const oldUser = await userModel + .findOne( + { + id: userData.id, + withdraw: true, + }, + "withdrewAt" + ) + .sort({ withdrewAt: -1 }) + .lean(); + if (oldUser && oldUser.withdrewAt) { + // 탈퇴 후 7일이 지나지 않았을 경우, 가입을 거부합니다. + const diff = req.timestamp - oldUser.withdrewAt.getTime(); + if (diff < 7 * 24 * 60 * 60 * 1000) { + return false; + } + } + const newUser = new userModel({ id: userData.id, // NOTE: SSO uid name: userData.name, @@ -51,6 +69,7 @@ const joinus = async (req, userData) => { email: userData.email, }); await newUser.save(); + return true; }; const update = async (userData) => { @@ -72,8 +91,13 @@ const tryLogin = async (req, res, userData, redirectOrigin, redirectPath) => { "_id name email subinfo id withdraw ban" ); if (!user) { - await joinus(req, userData); - return tryLogin(req, res, userData, redirectOrigin, redirectPath); + if (await joinus(req, userData)) { + return tryLogin(req, res, userData, redirectOrigin, redirectPath); + } else { + const redirectUrl = new URL("/login/fail", redirectOrigin).href; + res.redirect(redirectUrl); + return; + } } if ( user.name !== userData.name || diff --git a/src/services/users.ts b/src/services/users.ts index bd95a06e..7811bfe9 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -274,7 +274,7 @@ export const withdrawHandler: RequestHandler = async (req, res) => { // 회원 탈퇴 처리 (Soft Delete) user.withdraw = true; - user.withdrawAt = new Date(req.timestamp!); + user.withdrewAt = new Date(req.timestamp!); await user.save(); diff --git a/src/types/mongo.d.ts b/src/types/mongo.d.ts index 39b66199..9848f4e0 100644 --- a/src/types/mongo.d.ts +++ b/src/types/mongo.d.ts @@ -16,7 +16,7 @@ export interface User extends Document { /** 계정 탈퇴 여부. */ withdraw: boolean; /** 계정 탈퇴 시각. */ - withdrawAt?: Date; + withdrewAt?: Date; /** 사용자의 전화번호. 2023 가을 이벤트부터 추가됨. */ phoneNumber?: string; /** 계정 정지 여부. */ From 4d63ff818b8556334806cdd8787df44edf34e926 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 22 Jan 2025 17:13:06 +0900 Subject: [PATCH 23/27] Refactor: ensure user is logged out immediately upon withdrawal --- src/modules/auths/login.ts | 9 ++++++++- src/routes/auth.js | 5 +---- src/services/auth.js | 21 +++++++++++---------- src/services/users.ts | 14 ++++++++++++-- 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/modules/auths/login.ts b/src/modules/auths/login.ts index 962c260f..fe662503 100644 --- a/src/modules/auths/login.ts +++ b/src/modules/auths/login.ts @@ -1,6 +1,13 @@ import type { Request } from "express"; -import { session as sessionConfig } from "@/loadenv"; +import { session as sessionConfig, sparcssso as sparcsssoEnv } from "@/loadenv"; import logger from "@/modules/logger"; +import SsoClient from "./sparcssso"; + +// 환경변수 SPARCSSSO_CLIENT_ID 유무에 따라 로그인 방식이 변경됩니다. +export const isAuthReplace = !sparcsssoEnv.id; +export const ssoClient = !isAuthReplace + ? new SsoClient(sparcsssoEnv.id, sparcsssoEnv.key) + : undefined; export interface LoginInfo { id: string; diff --git a/src/routes/auth.js b/src/routes/auth.js index 534814f6..15fe76f0 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -6,10 +6,7 @@ const validator = require("@/middlewares/validator").default; const authHandlers = require("@/services/auth"); const authReplaceHandlers = require("@/services/auth.replace"); const mobileAuthHandlers = require("@/services/auth.mobile"); - -// 환경변수 SPARCSSSO_CLIENT_ID 유무에 따라 로그인 방식이 변경됩니다. -const { sparcssso: sparcsssoEnv } = require("@/loadenv"); -const isAuthReplace = !sparcsssoEnv?.id; +const { isAuthReplace } = require("@/modules/auths/login"); // 로그인 페이지로 redirect합니다. router.get( diff --git a/src/services/auth.js b/src/services/auth.js index 298ed9a7..b6e15fd2 100644 --- a/src/services/auth.js +++ b/src/services/auth.js @@ -1,7 +1,12 @@ -const { sparcssso: sparcsssoEnv, nodeEnv, testAccounts } = require("@/loadenv"); +const { nodeEnv, testAccounts } = require("@/loadenv"); const { userModel } = require("@/modules/stores/mongo"); const { user: userPattern } = require("@/modules/patterns").default; -const { getLoginInfo, logout, login } = require("@/modules/auths/login"); +const { + ssoClient, + getLoginInfo, + logout, + login, +} = require("@/modules/auths/login"); const { unregisterDeviceToken } = require("@/modules/fcm"); const { @@ -12,10 +17,6 @@ const { const jwt = require("@/modules/auths/jwt"); const logger = require("@/modules/logger").default; -// SPARCS SSO -const Client = require("@/modules/auths/sparcssso"); -const client = new Client(sparcsssoEnv?.id, sparcsssoEnv?.key); - const transUserData = (userData) => { const kaistInfo = userData.kaist_info ? JSON.parse(userData.kaist_info) : {}; @@ -137,7 +138,7 @@ const tryLogin = async (req, res, userData, redirectOrigin, redirectPath) => { const sparcsssoHandler = (req, res) => { const redirectPath = decodeURIComponent(req.query?.redirect || "%2F"); const isApp = !!req.query.isApp; - const { url, state } = client.getLoginParams(); + const { url, state } = ssoClient.getLoginParams(); req.session.loginAfterState = { state: state, @@ -167,7 +168,7 @@ const sparcsssoCallbackHandler = (req, res) => { return res.redirect(redirectUrl); } - client.getUserInfo(code).then((userDataBefore) => { + ssoClient.getUserInfo(code).then((userDataBefore) => { const userData = transUserData(userDataBefore); const isTestAccount = testAccounts?.includes(userData.email); if (userData.isEligible || nodeEnv !== "production" || isTestAccount) { @@ -176,7 +177,7 @@ const sparcsssoCallbackHandler = (req, res) => { // 카이스트 구성원이 아닌 경우, SSO 로그아웃 이후, 로그인 실패 URI 로 이동합니다 const { sid } = userData; const redirectUrl = new URL("/login/fail", redirectOrigin).href; - const ssoLogoutUrl = client.getLogoutUrl(sid, redirectUrl); + const ssoLogoutUrl = ssoClient.getLogoutUrl(sid, redirectUrl); res.redirect(ssoLogoutUrl); } }); @@ -202,7 +203,7 @@ const logoutHandler = async (req, res) => { // 로그아웃 URL을 생성 및 반환 const redirectUrl = new URL(redirectPath, req.origin).href; - const ssoLogoutUrl = client.getLogoutUrl(sid, redirectUrl); + const ssoLogoutUrl = ssoClient.getLogoutUrl(sid, redirectUrl); logout(req, res); res.json({ ssoLogoutUrl }); } catch (e) { diff --git a/src/services/users.ts b/src/services/users.ts index 7811bfe9..88d7f9c6 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -1,4 +1,5 @@ import type { RequestHandler } from "express"; +import { ssoClient, getLoginInfo, logout } from "@/modules/auths/login"; import { unregisterAllDeviceTokens } from "@/modules/fcm"; import logger from "@/modules/logger"; import { @@ -257,6 +258,11 @@ export const getBanRecordHandler: RequestHandler = async (req, res) => { export const withdrawHandler: RequestHandler = async (req, res) => { try { + const redirectPath = decodeURIComponent( + (req.query.redirect as string | undefined) || "%2F" + ); // TODO: Typing or Validation + const { sid } = getLoginInfo(req); + const user = await userModel.findOne({ _id: req.userOid, withdraw: false }); if (!user) { return res.status(500).send("Users/withdraw : internal server error"); @@ -275,10 +281,14 @@ export const withdrawHandler: RequestHandler = async (req, res) => { // 회원 탈퇴 처리 (Soft Delete) user.withdraw = true; user.withdrewAt = new Date(req.timestamp!); - await user.save(); - res.status(200).send("Users/withdraw : withdraw successful"); + // 로그아웃 처리 + const redirectUrl = new URL(redirectPath, req.origin).href; + const ssoLogoutUrl = + ssoClient?.getLogoutUrl(sid, redirectUrl) ?? redirectUrl; + logout(req); + res.json({ ssoLogoutUrl }); } catch (err) { res.status(500).send("Users/withdraw : internal server error"); } From 7665b97984802fd1a1528e38778838c57ad6a0ec Mon Sep 17 00:00:00 2001 From: static Date: Wed, 22 Jan 2025 17:16:58 +0900 Subject: [PATCH 24/27] Fix: docs and validator for withdraw endpoint --- src/routes/docs/users.js | 22 ++++++++++++++++++++-- src/routes/users.ts | 9 +++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/routes/docs/users.js b/src/routes/docs/users.js index e477bf5b..0e6f1b41 100644 --- a/src/routes/docs/users.js +++ b/src/routes/docs/users.js @@ -398,11 +398,29 @@ usersDocs[`${apiPrefix}/withdraw`] = { tags: [tag], summary: "회원 탈퇴", description: "회원 탈퇴를 요청합니다.", + parameters: [ + { + in: "query", + name: "redirect", + schema: { + type: "string", + }, + description: "로그아웃 후 리다이렉트 URI", + }, + ], responses: { 200: { content: { - "text/html": { - example: "Users/withdraw : withdraw successful", + "application/json": { + schema: { + type: "object", + properties: { + ssoLogoutUrl: { + type: "string", + description: "SSO 로그아웃 URL", + }, + }, + }, }, }, }, diff --git a/src/routes/users.ts b/src/routes/users.ts index c5afa7a8..2f431d64 100755 --- a/src/routes/users.ts +++ b/src/routes/users.ts @@ -1,5 +1,5 @@ import express from "express"; -import { body } from "express-validator"; +import { body, query } from "express-validator"; import { authMiddleware, validatorMiddleware } from "@/middlewares"; import patterns from "@/modules/patterns"; @@ -60,6 +60,11 @@ router.get("/resetProfileImg", userHandlers.resetProfileImgHandler); router.get("/getBanRecord", userHandlers.getBanRecordHandler); // 회원 탈퇴를 요청합니다. -router.post("/withdraw", userHandlers.withdrawHandler); +router.post( + "/withdraw", + query("redirect").optional().isString(), + validatorMiddleware, + userHandlers.withdrawHandler +); export default router; From 2d5c367a1ba3928c40ef5dad843b5bf2bc749394 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 5 Feb 2025 00:23:36 +0900 Subject: [PATCH 25/27] Refactor: redirect url --- src/routes/users.ts | 7 +------ src/services/users.ts | 5 +---- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/routes/users.ts b/src/routes/users.ts index 2f431d64..261207bf 100755 --- a/src/routes/users.ts +++ b/src/routes/users.ts @@ -60,11 +60,6 @@ router.get("/resetProfileImg", userHandlers.resetProfileImgHandler); router.get("/getBanRecord", userHandlers.getBanRecordHandler); // 회원 탈퇴를 요청합니다. -router.post( - "/withdraw", - query("redirect").optional().isString(), - validatorMiddleware, - userHandlers.withdrawHandler -); +router.post("/withdraw", validatorMiddleware, userHandlers.withdrawHandler); export default router; diff --git a/src/services/users.ts b/src/services/users.ts index 88d7f9c6..bfc37e30 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -258,9 +258,6 @@ export const getBanRecordHandler: RequestHandler = async (req, res) => { export const withdrawHandler: RequestHandler = async (req, res) => { try { - const redirectPath = decodeURIComponent( - (req.query.redirect as string | undefined) || "%2F" - ); // TODO: Typing or Validation const { sid } = getLoginInfo(req); const user = await userModel.findOne({ _id: req.userOid, withdraw: false }); @@ -284,7 +281,7 @@ export const withdrawHandler: RequestHandler = async (req, res) => { await user.save(); // 로그아웃 처리 - const redirectUrl = new URL(redirectPath, req.origin).href; + const redirectUrl = new URL("/mypage?withdraw=true", req.origin).href; const ssoLogoutUrl = ssoClient?.getLogoutUrl(sid, redirectUrl) ?? redirectUrl; logout(req); From e91c89f72841afa19248fbbd25a5cf9b9cb7ecff Mon Sep 17 00:00:00 2001 From: static Date: Wed, 5 Feb 2025 00:25:50 +0900 Subject: [PATCH 26/27] Docs: withdraw endpoint --- src/routes/docs/users.js | 10 ---------- src/routes/users.ts | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/routes/docs/users.js b/src/routes/docs/users.js index 0e6f1b41..f9a5c270 100644 --- a/src/routes/docs/users.js +++ b/src/routes/docs/users.js @@ -398,16 +398,6 @@ usersDocs[`${apiPrefix}/withdraw`] = { tags: [tag], summary: "회원 탈퇴", description: "회원 탈퇴를 요청합니다.", - parameters: [ - { - in: "query", - name: "redirect", - schema: { - type: "string", - }, - description: "로그아웃 후 리다이렉트 URI", - }, - ], responses: { 200: { content: { diff --git a/src/routes/users.ts b/src/routes/users.ts index 261207bf..58d8addc 100755 --- a/src/routes/users.ts +++ b/src/routes/users.ts @@ -1,5 +1,5 @@ import express from "express"; -import { body, query } from "express-validator"; +import { body } from "express-validator"; import { authMiddleware, validatorMiddleware } from "@/middlewares"; import patterns from "@/modules/patterns"; From 430833580484e5d360510bf5ce0d4111543b997e Mon Sep 17 00:00:00 2001 From: static Date: Wed, 5 Feb 2025 03:31:18 +0900 Subject: [PATCH 27/27] Refactor: review apply --- src/lottery/services/publicNotice.js | 1 + src/modules/socket.js | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lottery/services/publicNotice.js b/src/lottery/services/publicNotice.js index 009b46fc..f0244421 100644 --- a/src/lottery/services/publicNotice.js +++ b/src/lottery/services/publicNotice.js @@ -132,6 +132,7 @@ const getTicketLeaderboardHandler = async (req, res) => { ); const leaderboard = await Promise.all( sortedUsers.slice(0, 20).map(async (user) => { + // 여기서 userId는 oid입니다. const userInfo = await userModel .findOne({ _id: user.userId, withdraw: false }) .lean(); diff --git a/src/modules/socket.js b/src/modules/socket.js index 36b1f1bc..72c672fe 100644 --- a/src/modules/socket.js +++ b/src/modules/socket.js @@ -32,10 +32,10 @@ const transformChatsForRoom = async (chats) => { // inOutNames 배열(들어오거나 나간 사용자들의 닉네임으로 이루어진 배열)을 생성합니다. chat.inOutNames = []; if (chat.type === "in" || chat.type === "out") { - const inOutUserIds = chat.content.split("|"); + const inOutUserOids = chat.content.split("|"); chat.inOutNames = await Promise.all( - inOutUserIds.map(async (userId) => { - const user = await userModel.findById(userId, "nickname"); + inOutUserOids.map(async (userOid) => { + const user = await userModel.findById(userOid, "nickname"); return user?.nickname; }) );