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/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/lottery/services/globalState.js b/src/lottery/services/globalState.js index ee01f47e..7a16d0e7 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -95,7 +95,7 @@ const createUserGlobalStateHandler = async (req, res) => { error: "GlobalState/create : invalid inviter", }); - const user = await userModel.findById(req.userOid); + const user = await userModel.findOne({ _id: req.userOid, withdraw: false }); if (!user) return res .status(500) diff --git a/src/lottery/services/invites.js b/src/lottery/services/invites.js index 6479bce8..935e158e 100644 --- a/src/lottery/services/invites.js +++ b/src/lottery/services/invites.js @@ -24,7 +24,10 @@ const searchInviterHandler = async (req, res) => { // 해당되는 유저의 닉네임과 프로필 이미지를 가져옵니다. const inviter = await userModel - .findById(inviterStatus.userId, "nickname profileImageUrl") + .findOne( + { _id: inviterStatus.userId, withdraw: false }, + "nickname profileImageUrl" + ) .lean(); if (!inviter) return res 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( diff --git a/src/lottery/services/publicNotice.js b/src/lottery/services/publicNotice.js index dd3dc91b..f0244421 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( @@ -132,7 +132,10 @@ 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(); + // 여기서 userId는 oid입니다. + 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 +214,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.ts b/src/middlewares/auth.ts index a0215b9d..c9f6c0d7 100644 --- a/src/middlewares/auth.ts +++ b/src/middlewares/auth.ts @@ -8,9 +8,9 @@ const authMiddleware: RequestHandler = (req, res, next) => { error: "not logged in", }); - const { id, oid } = getLoginInfo(req); - req.userId = id; + const { oid } = getLoginInfo(req); req.userOid = oid; + next(); }; diff --git a/src/middlewares/authAdmin.ts b/src/middlewares/authAdmin.ts index cc79ef29..5f323cc2 100644 --- a/src/middlewares/authAdmin.ts +++ b/src/middlewares/authAdmin.ts @@ -11,8 +11,8 @@ const authAdminMiddleware: RequestHandler = async (req, res, next) => { if (!isLogin(req)) return res.redirect(redirectUrl); // 관리자 유무를 확인 - 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(redirectUrl); // 접속한 IP가 화이트리스트에 있는지 확인 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/modules/ban.ts b/src/modules/ban.ts index f5276f23..bb62214a 100644 --- a/src/modules/ban.ts +++ b/src/modules/ban.ts @@ -32,7 +32,7 @@ export const validateServiceBanRecord = async ( .toISOString() .replace("T", " ") .split(".")[0]; - const banErrorMessage = `${req.originalUrl} : user ${req.userId} (${ + const banErrorMessage = `${req.originalUrl} : user ${req.userOid} (${ req.session.loginInfo!.sid }) is temporarily restricted from service until ${formattedExpireAt}.`; return banErrorMessage; diff --git a/src/modules/fcm.ts b/src/modules/fcm.ts index 50ee26db..9f80c4f3 100644 --- a/src/modules/fcm.ts +++ b/src/modules/fcm.ts @@ -93,6 +93,28 @@ export const unregisterDeviceToken = async (deviceToken: string) => { } }; +/** + * 사용자의 ObjectId가 주어졌을 때, 해당 사용자의 모든 deviceToken을 DB에서 삭제합니다. + * @param userId - 사용자의 ObjectId입니다. + * @return 해당 사용자로부터 deviceToken을 삭제하는 데 성공하면 true, 실패하면 false를 반환합니다. 삭제할 deviceToken이 존재하지 않는 경우에는 true를 반환합니다. + */ +export const unregisterAllDeviceTokens = async (userId: string) => { + 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 deviceTokens - 사용자의 ObjectId입니다. diff --git a/src/modules/populates/chats.ts b/src/modules/populates/chats.ts index 077f7dad..efb1b392 100644 --- a/src/modules/populates/chats.ts +++ b/src/modules/populates/chats.ts @@ -4,9 +4,15 @@ import type { User, Chat } from "@/types/mongo"; * 쿼리를 통해 얻은 Chat Document를 populate할 설정값을 정의합니다. */ export const chatPopulateOption = [ - { path: "authorId", select: "_id nickname profileImageUrl" }, + { + path: "authorId", + select: "_id nickname profileImageUrl withdraw", + }, ]; export interface PopulatedChat extends Omit { - authorId?: Pick; + authorId: Pick< + User, + "_id" | "nickname" | "profileImageUrl" | "withdraw" + > | null; } diff --git a/src/modules/populates/reports.ts b/src/modules/populates/reports.ts index 6fcf60f3..1d30a07e 100644 --- a/src/modules/populates/reports.ts +++ b/src/modules/populates/reports.ts @@ -3,13 +3,13 @@ import type { User, Report } from "@/types/mongo"; export const reportPopulateOption = [ { path: "reportedId", - select: "_id id name nickname profileImageUrl", + select: "_id id name nickname profileImageUrl withdraw", }, ]; export interface PopulatedReport extends Omit { reportedId: Pick< User, - "_id" | "id" | "name" | "nickname" | "profileImageUrl" - >; + "_id" | "id" | "name" | "nickname" | "profileImageUrl" | "withdraw" + > | null; } diff --git a/src/modules/populates/rooms.ts b/src/modules/populates/rooms.ts index 6eb87461..25b0bfff 100644 --- a/src/modules/populates/rooms.ts +++ b/src/modules/populates/rooms.ts @@ -15,28 +15,47 @@ export 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", + }, }, ]; interface PopulatedParticipant extends Pick { - user: Pick; + user: Pick< + User, + "_id" | "id" | "name" | "nickname" | "profileImageUrl" | "withdraw" + > | null; } export interface PopulatedRoom extends Omit { - from: Pick; - to: Pick; - part?: PopulatedParticipant[]; + from: Pick | null; + to: Pick | null; + part: PopulatedParticipant[]; } -export interface FormattedRoom - extends Omit { - part?: { +interface FormattedLocation { + _id: string; + enName: string; + koName: string; +} + +export interface FormattedRoom { + _id: string; + name: string; + from: FormattedLocation; + to: FormattedLocation; + time: Date; + madeat: Date; + maxPartLength: number; + part: { _id: string; name: string; nickname: string; profileImageUrl: string; + withdraw: boolean; isSettlement?: SettlementStatus; readAt: Date; }[]; @@ -61,15 +80,27 @@ export const formatSettlement = ( ): FormattedRoom => { return { ...roomObject, - part: roomObject.part?.map((participantSubDocument) => { - const { _id, name, nickname, profileImageUrl } = - participantSubDocument.user; + _id: roomObject._id!.toString(), + from: { + _id: roomObject.from!._id!.toString(), + enName: roomObject.from!.enName, + koName: roomObject.from!.koName, + }, + to: { + _id: roomObject.to!._id!.toString(), + enName: roomObject.to!.enName, + koName: roomObject.to!.koName, + }, + part: roomObject.part.map((participantSubDocument) => { + const { _id, name, nickname, profileImageUrl, withdraw } = + participantSubDocument.user!; const { settlementStatus, readAt } = participantSubDocument; return { - _id, + _id: _id!.toString(), name, nickname, profileImageUrl, + withdraw, isSettlement: includeSettlement ? settlementStatus : undefined, readAt: readAt ?? roomObject.madeat, }; @@ -81,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/modules/socket.js b/src/modules/socket.js index 2e8fa12e..72c672fe 100644 --- a/src/modules/socket.js +++ b/src/modules/socket.js @@ -32,11 +32,11 @@ 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.findOne({ id: userId }, "nickname"); - return user.nickname; + inOutUserOids.map(async (userOid) => { + const user = await userModel.findById(userOid, "nickname"); + return user?.nickname; }) ); } @@ -46,6 +46,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, @@ -111,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를 반환합니다. @@ -136,7 +137,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(); @@ -163,9 +167,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/modules/stores/mongo.ts b/src/modules/stores/mongo.ts index 76924f2a..41ccfa3e 100755 --- a/src/modules/stores/mongo.ts +++ b/src/modules/stores/mongo.ts @@ -19,11 +19,12 @@ import type { const userSchema = new 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 }, //탈퇴 여부 + withdrewAt: { type: Date }, //탈퇴 시각 phoneNumber: { type: String }, // 전화번호 (2023FALL 이벤트부터 추가) ban: { type: Boolean, default: false }, //계정 정지 여부 joinat: { type: Date, required: true }, //가입 시각 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/routes/docs/users.js b/src/routes/docs/users.js index f21c037a..f9a5c270 100644 --- a/src/routes/docs/users.js +++ b/src/routes/docs/users.js @@ -393,4 +393,43 @@ usersDocs[`${apiPrefix}/getBanRecord`] = { }, }; +usersDocs[`${apiPrefix}/withdraw`] = { + post: { + tags: [tag], + summary: "회원 탈퇴", + description: "회원 탈퇴를 요청합니다.", + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + ssoLogoutUrl: { + type: "string", + description: "SSO 로그아웃 URL", + }, + }, + }, + }, + }, + }, + 400: { + content: { + "text/html": { + example: "Users/withdraw : ongoing room exists", + }, + }, + }, + 500: { + content: { + "text/html": { + example: "Users/withdraw : internal server error", + }, + }, + }, + }, + }, +}; + module.exports = usersDocs; diff --git a/src/routes/users.ts b/src/routes/users.ts index 45514925..58d8addc 100755 --- a/src/routes/users.ts +++ b/src/routes/users.ts @@ -59,4 +59,7 @@ router.get("/resetProfileImg", userHandlers.resetProfileImgHandler); // 유저의 서비스 정지 기록들을 모두 반환합니다. router.get("/getBanRecord", userHandlers.getBanRecordHandler); +// 회원 탈퇴를 요청합니다. +router.post("/withdraw", validatorMiddleware, userHandlers.withdrawHandler); + export default router; 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, }); diff --git a/src/schedules/deleteUserInfo.js b/src/schedules/deleteUserInfo.js new file mode 100644 index 00000000..0c7c5ce6 --- /dev/null +++ b/src/schedules/deleteUserInfo.js @@ -0,0 +1,42 @@ +const { userModel } = require("../modules/stores/mongo"); +const logger = require("../modules/logger"); + +module.exports = async () => { + try { + // 탈퇴일로부터 5년 이상 경과한 사용자의 개인정보 삭제 + await userModel.updateMany( + { + withdraw: true, + withdrewAt: { + $lte: new Date(Date.now() - 5 * 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/schedules/index.ts b/src/schedules/index.ts index ea1bb96f..473a0c6c 100644 --- a/src/schedules/index.ts +++ b/src/schedules/index.ts @@ -1,16 +1,20 @@ import type { Express } from "express"; import cron from "node-cron"; import { naverMap } from "@/loadenv"; + import notifyBeforeDepart from "./notifyBeforeDepart"; import notifyAfterArrival from "./notifyAfterArrival"; import updateMajorTaxiFare from "./updateMajorTaxiFare"; import updateMinorTaxiFare from "./updateMinorTaxiFare"; +import deleteUserInfo from "./deleteUserInfo"; import autoSettlement from "./autoSettlement"; const registerSchedules = (app: Express) => { cron.schedule("*/5 * * * *", notifyBeforeDepart(app)); cron.schedule("*/10 * * * *", notifyAfterArrival(app)); + cron.schedule("0 0 1 * *", deleteUserInfo); cron.schedule("1,11,21,31,41,51 * * * *", autoSettlement(app)); + if (naverMap.apiId && naverMap.apiKey) { cron.schedule("0,30 * * * * ", updateMajorTaxiFare(app)); cron.schedule("0 18 * * *", updateMinorTaxiFare(app)); diff --git a/src/schedules/notifyAfterArrival.js b/src/schedules/notifyAfterArrival.js index c2c01b25..31c0b354 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").default; diff --git a/src/services/auth.js b/src/services/auth.js index 2b9ba658..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) : {}; @@ -36,8 +37,26 @@ 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, + id: userData.id, // NOTE: SSO uid name: userData.name, nickname: generateNickname(userData.id), profileImageUrl: generateProfileImageUrl(), @@ -51,6 +70,7 @@ const joinus = async (req, userData) => { email: userData.email, }); await newUser.save(); + return true; }; const update = async (userData) => { @@ -59,7 +79,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}` ); @@ -68,12 +88,17 @@ 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) { - 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 || @@ -113,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, @@ -143,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) { @@ -152,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); } }); @@ -178,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/auth.mobile.js b/src/services/auth.mobile.js index 6ae14db2..e2cee5bb 100644 --- a/src/services/auth.mobile.js +++ b/src/services/auth.mobile.js @@ -25,7 +25,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 b6bd6467..d9631c9b 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, @@ -14,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) @@ -56,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) { @@ -68,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) @@ -100,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) @@ -141,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) @@ -178,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) { @@ -223,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) @@ -266,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(); @@ -313,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 affac40a..749f2e15 100644 --- a/src/services/logininfo.js +++ b/src/services/logininfo.js @@ -5,10 +5,10 @@ const logger = require("@/modules/logger").default; 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 fc2090cf..f0c4bf94 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 f4cb17f4..d0d707c2 100644 --- a/src/services/reports.js +++ b/src/services/reports.js @@ -8,10 +8,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", @@ -64,7 +67,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 76350db0..7cc1370b 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개 이상이면 오류를 반환합니다. @@ -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, }); @@ -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, @@ -211,13 +213,13 @@ 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() .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({ @@ -235,7 +237,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개 이상이면 오류를 반환합니다. @@ -297,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, }); @@ -318,7 +320,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({ @@ -384,12 +386,12 @@ 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, }); 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) { @@ -487,7 +489,7 @@ const searchByUserHandler = async (req, res) => { try { // lean()이 적용된 user를 response에 반환해줘야 하기 때문에 user를 한 번 더 지정한다. const user = await userModel - .findOne({ id: req.userId }) + .findOne({ _id: req.userOid, withdraw: false }) .populate({ path: "ongoingRoom", options: { @@ -527,7 +529,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( @@ -583,7 +585,7 @@ const commitSettlementHandler = async (req, res) => { await emitChatEvent(req.app.get("io"), { roomId, type: "settlement", - content: user.id, + content: user._id, authorId: user._id, }); @@ -607,7 +609,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( { @@ -656,7 +658,7 @@ const commitPaymentHandler = async (req, res) => { await emitChatEvent(req.app.get("io"), { roomId, type: "payment", - content: user.id, + content: user._id, authorId: user._id, }); @@ -773,7 +775,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: { @@ -816,7 +818,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({ diff --git a/src/services/users.ts b/src/services/users.ts index 3caf35cf..bfc37e30 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -1,11 +1,13 @@ import type { RequestHandler } from "express"; -import { userModel, banModel } from "@/modules/stores/mongo"; +import { ssoClient, getLoginInfo, logout } from "@/modules/auths/login"; +import { unregisterAllDeviceTokens } from "@/modules/fcm"; import logger from "@/modules/logger"; import { generateNickname, generateProfileImageUrl, } from "@/modules/modifyProfile"; import * as aws from "@/modules/stores/aws"; +import { userModel, banModel } from "@/modules/stores/mongo"; // 이벤트 코드입니다. // const { contracts } = require("@/lottery"); @@ -15,18 +17,24 @@ export const agreeOnTermsOfServiceHandler: RequestHandler = async ( res ) => { try { - let user = await userModel.findOne({ id: req.userId }); + let user = await userModel.findOne({ _id: req.userOid, withdraw: false }); if (!user) { return res.status(400).send("Users/agreeOnTermsOfService : no such user"); } if (user.agreeOnTermsOfService === true) { - return res.status(400).send("Users/agreeOnTermsOfService : already agreed"); + return res + .status(400) + .send("Users/agreeOnTermsOfService : already agreed"); } user.agreeOnTermsOfService = true; await user.save(); - return res.status(200).send("Users/agreeOnTermsOfService : agree on Terms of Service successful"); + return res + .status(200) + .send( + "Users/agreeOnTermsOfService : agree on Terms of Service successful" + ); } catch { return res .status(500) @@ -40,7 +48,7 @@ export const getAgreeOnTermsOfServiceHandler: RequestHandler = async ( ) => { try { const user = await userModel - .findOne({ id: req.userId }, "agreeOnTermsOfService") + .findOne({ _id: req.userOid, withdraw: false }, "agreeOnTermsOfService") .lean(); if (!user) { return res.status(400).send("Users/agreeOnTermsOfService : no such user"); @@ -59,7 +67,7 @@ export const editNicknameHandler: RequestHandler = async (req, res) => { try { const newNickname = req.body.nickname; // TODO: Typing const result = await userModel.findOneAndUpdate( - { id: req.userId }, + { _id: req.userOid, withdraw: false }, { nickname: newNickname } ); @@ -88,7 +96,7 @@ export const editAccountHandler: RequestHandler = async (req, res) => { try { const newAccount = req.body.account; // TODO: Typing const result = await userModel.findOneAndUpdate( - { id: req.userId }, + { _id: req.userOid, withdraw: false }, { account: newAccount } ); @@ -120,7 +128,10 @@ export const editProfileImgGetPUrlHandler: RequestHandler = async ( ) => { try { const type = req.body.type; // TODO: Typing - 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) @@ -149,7 +160,10 @@ export const editProfileImgGetPUrlHandler: RequestHandler = async ( export const editProfileImgDoneHandler: RequestHandler = 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) @@ -164,7 +178,7 @@ export const editProfileImgDoneHandler: RequestHandler = 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 } ); @@ -188,7 +202,7 @@ export const editProfileImgDoneHandler: RequestHandler = async (req, res) => { export const resetNicknameHandler: RequestHandler = async (req, res) => { try { const result = await userModel.findOneAndUpdate( - { id: req.userId }, + { _id: req.userOid, withdraw: false }, { nickname: generateNickname(req.body.id) }, // TODO: Typing or Validation { new: true } ); @@ -208,7 +222,7 @@ export const resetNicknameHandler: RequestHandler = async (req, res) => { export const resetProfileImgHandler: RequestHandler = async (req, res) => { try { const result = await userModel.findOneAndUpdate( - { id: req.userId }, + { _id: req.userOid, withdraw: false }, { profileImageUrl: generateProfileImageUrl() }, { new: true } ); @@ -241,3 +255,38 @@ export const getBanRecordHandler: RequestHandler = async (req, res) => { return res.status(500).send("Users/getBanRecord : internal server error"); } }; + +export const withdrawHandler: RequestHandler = async (req, res) => { + try { + 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"); + } + + // 회원 탈퇴가 가능한 조건인지 확인 + 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.withdrewAt = new Date(req.timestamp!); + await user.save(); + + // 로그아웃 처리 + const redirectUrl = new URL("/mypage?withdraw=true", 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"); + } +}; diff --git a/src/types/index.d.ts b/src/types/index.d.ts index d679d420..f8a54993 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -4,8 +4,6 @@ export {}; declare global { namespace Express { export interface Request { - /** 사용자 ID. SPARCS SSO로부터 전달받습니다. */ - userId?: string; /** 사용자의 ObjectID. MongoDB에서 사용됩니다. */ userOid?: string; /** 요청의 origin. */ diff --git a/src/types/mongo.d.ts b/src/types/mongo.d.ts index 718b342e..9848f4e0 100644 --- a/src/types/mongo.d.ts +++ b/src/types/mongo.d.ts @@ -1,6 +1,6 @@ import type { Document, Types } from "mongoose"; -export interface User extends Document { +export interface User extends Document { /** 사용자의 실명. */ name: string; /** 사용자의 닉네임. */ @@ -15,6 +15,8 @@ export interface User extends Document { doneRoom?: Types.Array; /** 계정 탈퇴 여부. */ withdraw: boolean; + /** 계정 탈퇴 시각. */ + withdrewAt?: Date; /** 사용자의 전화번호. 2023 가을 이벤트부터 추가됨. */ phoneNumber?: string; /** 계정 정지 여부. */ @@ -38,7 +40,7 @@ export interface User extends Document { account: string; } -export interface Ban extends Document { +export interface Ban extends Document { /** 정지된 사용자의 ID. */ userSid: string; /** 정지 사유. */ @@ -57,7 +59,7 @@ export type SettlementStatus = | "send-required" | "sent"; -export interface Participant extends Document { +export interface Participant extends Document { /** 방 참여자의 User ObjectID. */ user: Types.ObjectId; /** 방 참여자의 정산 상태. */ @@ -66,14 +68,14 @@ export interface Participant extends Document { readAt?: Date; } -export interface DeviceToken extends Document { +export interface DeviceToken extends Document { /** 디바이스 토큰 소유자의 User ObjectID. */ userId: Types.ObjectId; /** 소유한 디바이스 토큰의 배열. */ deviceTokens: Types.Array; } -export interface NotificationOption extends Document { +export interface NotificationOption extends Document { deviceToken: string; /** 채팅 알림 수신 여부. */ chatting: boolean; @@ -87,13 +89,13 @@ export interface NotificationOption extends Document { advertisement: boolean; } -export interface TopicSubscription extends Document { +export interface TopicSubscription extends Document { deviceToken?: string; topic?: string; subscribedAt: Date; } -export interface Room extends Document { +export interface Room extends Document { /** 방의 이름. */ name: string; /** 방의 출발지의 Location ObjectID. */ @@ -112,7 +114,7 @@ export interface Room extends Document { maxPartLength: number; } -export interface Location extends Document { +export interface Location extends Document { enName: string; koName: string; priority: number; @@ -134,7 +136,7 @@ export type ChatType = | "departure" | "arrival"; -export interface Chat extends Document { +export interface Chat extends Document { /** 메세지가 전송된 방의 Room ObjectID. */ roomId: Types.ObjectId; /** 메세지의 종류. */ @@ -146,7 +148,7 @@ export interface Chat extends Document { isValid: boolean; } -export interface Report extends Document { +export interface Report extends Document { /** 신고한 사용자의 ObjectID. */ creatorId: Types.ObjectId; /** 신고받은 사용자의 ObjectID. */ @@ -161,14 +163,14 @@ export interface Report extends Document { roomId?: Types.ObjectId; } -export interface AdminIPWhitelist extends Document { +export interface AdminIPWhitelist extends Document { ip: string; description: string; } export type AdminLogAction = "create" | "read" | "update" | "delete"; -export interface AdminLog extends Document { +export interface AdminLog extends Document { /** 로그 발생자의 User ObjectID. */ user: Types.ObjectId; /** 로그의 발생 시각. */ @@ -181,7 +183,7 @@ export interface AdminLog extends Document { action: AdminLogAction; } -export interface TaxiFare extends Document { +export interface TaxiFare extends Document { /** 출발지의 Location ObjectID. */ from: Types.ObjectId; /** 목적지의 Location ObjectID. */ 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); 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 } }