diff --git a/scripts/chatPaymentSettlementUpdater.js b/scripts/chatPaymentSettlementUpdater.js new file mode 100644 index 00000000..76e43682 --- /dev/null +++ b/scripts/chatPaymentSettlementUpdater.js @@ -0,0 +1,34 @@ +// Issue #449-1을 해결하기 위한 DB 마이그레이션 스크립트입니다. +// chat type 중 settlement와 payment를 서로 교체합니다. +// https://github.com/sparcs-kaist/taxi-back/issues/449 + +const { MongoClient } = require("mongodb"); +const { mongo: mongoUrl } = require("../loadenv"); + +const client = new MongoClient(mongoUrl); +const db = client.db("taxi"); +const chats = db.collection("chats"); + +async function run() { + try { + for await (const doc of chats.find()) { + if (doc.type === "settlement" || doc.type === "payment") { + await chats.findOneAndUpdate( + { _id: doc._id }, + { + $set: { + type: doc.type === "settlement" ? "payment" : "settlement", + }, + } + ); + } + } + } catch (err) { + console.log(err); + } finally { + await client.close(); + } +} +run().then(() => { + console.log("Done!"); +}); diff --git a/src/lottery/modules/contracts.js b/src/lottery/modules/contracts.js index 87a24a84..72a62deb 100644 --- a/src/lottery/modules/contracts.js +++ b/src/lottery/modules/contracts.js @@ -127,7 +127,7 @@ const completeFirstLoginQuest = async (userId, timestamp) => { * @param {Date} roomObject.time - 출발 시각입니다. * @returns {Promise} * @description 정산 요청 또는 송금이 이루어질 때마다 호출해 주세요. - * @usage rooms - commitPaymentHandler, rooms - settlementHandler + * @usage rooms - commitSettlementHandler, rooms - commitPaymentHandler */ const completePayingAndSendingQuest = async (userId, timestamp, roomObject) => { logger.info( @@ -167,7 +167,7 @@ const completeFirstRoomCreationQuest = async (userId, timestamp) => { * @param {Date} roomObject.time - 출발 시각입니다. * @returns {Promise} * @description 정산 요청이 이루어질 때마다 호출해 주세요. - * @usage rooms - commitPaymentHandler + * @usage rooms - commitSettlementHandler */ const completePayingQuest = async (userId, timestamp, roomObject) => { logger.info( @@ -195,7 +195,7 @@ const completePayingQuest = async (userId, timestamp, roomObject) => { * @param {Date} roomObject.time - 출발 시각입니다. * @returns {Promise} * @description 송금이 이루어질 때마다 호출해 주세요. - * @usage rooms - settlementHandler + * @usage rooms - commitPaymentHandler */ const completeSendingQuest = async (userId, timestamp, roomObject) => { logger.info( diff --git a/src/modules/socket.js b/src/modules/socket.js index c238ce64..9cda8991 100644 --- a/src/modules/socket.js +++ b/src/modules/socket.js @@ -82,11 +82,11 @@ const getMessageBody = (type, nickname = "", content = "") => { const suffix = "님이 퇴장하였습니다"; return `${ellipsisedNickname} ${suffix}`; } - case "payment": { + case "settlement": { const suffix = "님이 정산을 시작하였습니다"; return `${ellipsisedNickname} ${suffix}`; } - case "settlement": { + case "payment": { const suffix = "님이 송금을 완료하였습니다"; return `${ellipsisedNickname} ${suffix}`; } diff --git a/src/modules/stores/mongo.js b/src/modules/stores/mongo.js index bf2a8c53..6bc93246 100755 --- a/src/modules/stores/mongo.js +++ b/src/modules/stores/mongo.js @@ -26,6 +26,39 @@ const userSchema = Schema({ account: { type: String, default: "" }, //계좌번호 정보 }); +const banSchema = Schema({ + // 정지 시킬 사용자를 기제함. + userId: { type: mongoose.Types.ObjectId, ref: "User", required: true }, + // 정지 사유 + reason: { + type: String, + required: true, + }, + bannedAt: { + type: Date, // 정지 당한 시각 + required: true, + }, + expireAt: { + type: Date, // 정지 만료 시각 + required: true, + }, + services: [ + { + // 정지를 당한 서비스를 기제함 + serviceName: { + type: String, + required: true, + // 필요시 이곳에 정지를 시킬 서비스를 추가함. + enum: [ + "all", // all -> 과거/미래 모든 서비스 및 이벤트 이용 제한 + "service", // service -> 방 생성/참여 제한 + "2023-fall-event", // event -> 특정 이벤트 참여 제한 + ], + }, + }, + ], +}); + const participantSchema = Schema({ user: { type: Schema.Types.ObjectId, ref: "User", required: true }, settlementStatus: { @@ -207,6 +240,7 @@ const connectDatabase = (mongoUrl) => { module.exports = { connectDatabase, userModel: mongoose.model("User", userSchema), + banModel: mongoose.model("Ban", banSchema), deviceTokenModel: mongoose.model("DeviceToken", deviceTokenSchema), notificationOptionModel: mongoose.model( "NotificationOption", diff --git a/src/routes/admin.js b/src/routes/admin.js index da20d60d..7cd28735 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -4,6 +4,7 @@ const AdminJSExpress = require("@adminjs/express"); const AdminJSMongoose = require("@adminjs/mongoose"); const { userModel, + banModel, roomModel, locationModel, chatModel, @@ -26,6 +27,7 @@ AdminJS.registerAdapter(AdminJSMongoose); const resources = [ userModel, + banModel, roomModel, locationModel, chatModel, diff --git a/src/routes/docs/chats.js b/src/routes/docs/chats.js index 0aaa1c6d..f9107201 100644 --- a/src/routes/docs/chats.js +++ b/src/routes/docs/chats.js @@ -211,7 +211,7 @@ chatsDocs[`${apiPrefix}/send`] = { Chat { roomId: ObjectId, //방의 objectId - type: String, // 메시지 종류 ("text": 일반 메시지, "s3img": S3에 업로드된 이미지, "in": 입장 메시지, "out": 퇴장 메시지, "payment": 결제 메시지, "settlement": 정산 완료 메시지, "account": 계좌 전송 메시지) + type: String, // 메시지 종류 ("text": 일반 메시지, "s3img": S3에 업로드된 이미지, "in": 입장 메시지, "out": 퇴장 메시지, "settlement": 정산 메시지, "payment": 송금 메시지, "account": 계좌 전송 메시지) authorId: ObejctId, //작성자의 objectId content: String, // 메시지 내용 (메시지 종류에 따라 포맷이 상이함) time: String(ISO 8601), // ex) 2024-01-08T01:52:00.000Z diff --git a/src/routes/docs/rooms.js b/src/routes/docs/rooms.js index 710bf649..87488b11 100644 --- a/src/routes/docs/rooms.js +++ b/src/routes/docs/rooms.js @@ -683,11 +683,11 @@ roomsDocs[`${apiPrefix}/searchByUser`] = { }, }; -roomsDocs[`${apiPrefix}/commitPayment`] = { +roomsDocs[`${apiPrefix}/commitSettlement`] = { post: { tags: [tag], - summary: "방 결제 처리", - description: `해당 방에 요청을 보낸 유저를 결제자로 처리합니다.
+ summary: "방 정산 요청 처리", + description: `해당 방에 요청을 보낸 유저를 결제자로 처리하여, 다른 유저들에게 정산을 요청합니다.
이미 출발한 방에 대해서만 요청을 처리합니다.
방의 \`part\` 배열에서 요청을 보낸 유저의 \`isSettlement\` 속성은 \`paid\`로 설정됩니다.
나머지 유저들의 \`isSettlement\` 속성을 \`send-required\`로 설정합니다.`, @@ -731,7 +731,7 @@ roomsDocs[`${apiPrefix}/commitPayment`] = { }, }, example: { - error: "Rooms/:id/commitPayment : cannot find settlement info", + error: "Rooms/:id/commitSettlement : cannot find settlement info", }, }, }, @@ -749,7 +749,7 @@ roomsDocs[`${apiPrefix}/commitPayment`] = { }, }, example: { - error: "Rooms/:id/commitPayment : internal server error", + error: "Rooms/:id/commitSettlement : internal server error", }, }, }, @@ -758,11 +758,11 @@ roomsDocs[`${apiPrefix}/commitPayment`] = { }, }; -roomsDocs[`${apiPrefix}/commitSettlement`] = { +roomsDocs[`${apiPrefix}/commitPayment`] = { post: { tags: [tag], - summary: "방 정산 완료 처리", - description: `해당 방에 요청을 보낸 유저를 정산 완료로 처리합니다.
+ summary: "방 송금 처리", + description: `해당 방에 요청을 보낸 유저를 송금을 완료한 정산 완료로 처리합니다.
방의 \`part\` 배열에서 요청을 보낸 유저의 \`isSettlement\` 속성은 \`send-required\`에서 \`sent\`로 변경합니다.
방의 참여한 유저들이 모두 정산완료를 하면 방의 \`isOver\` 속성이 \`true\`로 변경되며, 과거 방으로 취급됩니다.`, requestBody: { @@ -805,7 +805,7 @@ roomsDocs[`${apiPrefix}/commitSettlement`] = { }, }, example: { - error: "Rooms/:id/settlement : cannot find settlement info", + error: "Rooms/:id/commitPayment : cannot find settlement info", }, }, }, @@ -823,7 +823,7 @@ roomsDocs[`${apiPrefix}/commitSettlement`] = { }, }, example: { - error: "Rooms/:id/settlement : internal server error", + error: "Rooms/:id/commitPayment : internal server error", }, }, }, diff --git a/src/routes/docs/schemas/roomsSchema.js b/src/routes/docs/schemas/roomsSchema.js index 39dcd8bf..a0af753e 100644 --- a/src/routes/docs/schemas/roomsSchema.js +++ b/src/routes/docs/schemas/roomsSchema.js @@ -31,6 +31,14 @@ roomsZod["room"] = z }) .partial({ settlementTotal: true, isOver: true }); +roomsZod["commitSettlement"] = z.object({ + roomId: z.string().regex(objectId), +}); + +roomsZod["commitPayment"] = z.object({ + roomId: z.string().regex(objectId), +}); + const roomsSchema = zodToSchemaObject(roomsZod); module.exports = { roomsSchema, roomsZod }; diff --git a/src/routes/docs/users.js b/src/routes/docs/users.js index 3cd8aa96..deaeb113 100644 --- a/src/routes/docs/users.js +++ b/src/routes/docs/users.js @@ -1,5 +1,6 @@ const tag = "users"; const apiPrefix = "/users"; +const { objectId } = require("../../modules/patterns"); const usersDocs = {}; usersDocs[`${apiPrefix}/agreeOnTermsOfService`] = { @@ -329,4 +330,142 @@ usersDocs[`${apiPrefix}/resetProfileImg`] = { }, }; +usersDocs[`${apiPrefix}/isBanned`] = { + get: { + tags: [tag], + summary: "본인의 현재 정지 기록을 가져움", + description: + "정지 기록들 중 본인이고, 서버 시간을 기준으로 expireAt 보다 작은 경우에 해당하는 정지 기록을 모두 가져옴", + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "array", + items: { + properties: { + userId: { + type: "string", + description: "사용자의 ObjectId", + pattern: objectId.source, + }, + reason: { + type: "string", + description: "정지 사유", + example: "미정산", + }, + bannedAt: { + type: "date", + description: "정지 당한 시각", + example: "2024-05-20 12:00", + }, + expireAt: { + type: "date", + description: "정지 만료 시각", + example: "2024-05-21 12:00", + }, + services: { + type: "array", + items: { + properties: { + serviceName: { + type: "string", + description: "정지를 당한 서비스 또는 이벤트 이름", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + 400: { + content: { + "text/html": { + example: "Users/isBanned : there is no ban record", + }, + }, + }, + 500: { + content: { + "text/html": { + example: "Users/isBanned : internal server error", + }, + }, + }, + }, + }, +}; + +usersDocs[`${apiPrefix}/getBanRecord`] = { + get: { + tags: [tag], + summary: "본인의 모든 정지 기록을 가져움", + description: + "정지 기록들 중 본인인 경우에 해당하는 정지 기록을 모두 가져옴", + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "array", + items: { + properties: { + userId: { + type: "string", + description: "사용자의 ObjectId", + pattern: objectId.source, + }, + reason: { + type: "string", + description: "정지 사유", + example: "미정산", + }, + bannedAt: { + type: "date", + description: "정지 당한 시각", + example: "2024-05-20 12:00", + }, + expireAt: { + type: "date", + description: "정지 만료 시각", + example: "2024-05-21 12:00", + }, + services: { + type: "array", + items: { + properties: { + serviceName: { + type: "string", + description: "정지를 당한 서비스 또는 이벤트 이름", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + 400: { + content: { + "text/html": { + example: "Users/getBanRecord : there is no ban record", + }, + }, + }, + 500: { + content: { + "text/html": { + example: "Users/getBanRecord : internal server error", + }, + }, + }, + }, + }, +}; + module.exports = usersDocs; diff --git a/src/routes/rooms.js b/src/routes/rooms.js index 6334136d..6345fa6f 100644 --- a/src/routes/rooms.js +++ b/src/routes/rooms.js @@ -1,5 +1,7 @@ const express = require("express"); const { query, body } = require("express-validator"); +const { validateBody } = require("../middlewares/zod"); +const { roomsZod } = require("./docs/schemas/roomsSchema"); const router = express.Router(); const roomHandlers = require("../services/rooms"); @@ -91,19 +93,18 @@ router.post( // 로그인된 사용자의 모든 방들을 반환한다. router.get("/searchByUser", roomHandlers.searchByUserHandler); +// 해당 방에 요청을 보낸 유저의 정산을 처리한다. router.post( - "/commitPayment", - body("roomId").isMongoId(), - validator, - roomHandlers.commitPaymentHandler + "/commitSettlement", + validateBody(roomsZod.commitSettlement), + roomHandlers.commitSettlementHandler ); -// 해당 룸의 요청을 보낸 유저의 정산을 완료로 처리한다. +// 해당 방에 요청을 보낸 유저의 송금을 처리한다. router.post( - "/commitSettlement", - body("roomId").isMongoId(), - validator, - roomHandlers.settlementHandler + "/commitPayment", + validateBody(roomsZod.commitPayment), + roomHandlers.commitPaymentHandler ); // json으로 수정할 값들을 받아 방의 정보를 수정합니다. diff --git a/src/routes/users.js b/src/routes/users.js index 31bde597..d5366155 100755 --- a/src/routes/users.js +++ b/src/routes/users.js @@ -56,4 +56,10 @@ router.get("/editProfileImg/done", userHandlers.editProfileImgDoneHandler); // 프로필 이미지를 기본값으로 재설정합니다. router.get("/resetProfileImg", userHandlers.resetProfileImgHandler); +// 유저의 현재 유효한 서비스 정지 기록들만 반환합니다. +router.get("/isBanned", userHandlers.isBannedHandler); + +// 유저의 서비스 정지 기록들을 모두 반환합니다. +router.get("/getBanRecord", userHandlers.getBanRecordHandler); + module.exports = router; diff --git a/src/services/auth.js b/src/services/auth.js index dab7cd64..fdca15dd 100644 --- a/src/services/auth.js +++ b/src/services/auth.js @@ -33,7 +33,7 @@ const transUserData = (userData) => { twitter: userData.twitter_id || "", kaist: kaistInfo?.ku_std_no || "", sparcs: userData.sparcs_id || "", - email: userData.email, + email: kaistInfo?.mail || userData.email, isEligible: userPattern.allowedEmployeeTypes.test(kaistInfo?.employeeType), }; return info; @@ -58,22 +58,36 @@ const joinus = async (req, userData) => { }; const update = async (userData) => { - const updateInfo = { name: userData.name }; + const updateInfo = { + name: userData.name, + email: userData.email, + "subinfo.kaist": userData.kaist, + }; await userModel.updateOne({ id: userData.id }, updateInfo); + logger.info( + `Update user info: ${userData.id} ${userData.name} ${userData.email} ${userData.kaist}` + ); }; const tryLogin = async (req, res, userData, redirectOrigin, redirectPath) => { try { const user = await userModel.findOne( { id: userData.id }, - "_id name id withdraw ban" + "_id name email subinfo id withdraw ban" ); if (!user) { await joinus(req, userData); return tryLogin(req, res, userData, redirectOrigin, redirectPath); } - if (user.name != userData.name) { + if ( + user.name !== userData.name || + user.email !== userData.email || + user.subinfo.kaist !== userData.kaist + ) { await update(userData); + logger.info( + `Past user info: ${user.id} ${user.name} ${user.email} ${user.subinfo.kaist}` + ); return tryLogin(req, res, userData, redirectOrigin, redirectPath); } diff --git a/src/services/rooms.js b/src/services/rooms.js index cf8b6e9e..a1a61913 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -559,7 +559,7 @@ const searchByUserHandler = async (req, res) => { } }; -const commitPaymentHandler = async (req, res) => { +const commitSettlementHandler = async (req, res) => { try { const user = await userModel.findOne({ id: req.userId }); const { roomId } = req.body; @@ -593,7 +593,7 @@ const commitPaymentHandler = async (req, res) => { if (!roomObject) { return res.status(404).json({ - error: "Rooms/:id/commitPayment : cannot find settlement info", + error: "Rooms/:id/commitSettlement : cannot find settlement info", }); } @@ -606,17 +606,17 @@ const commitPaymentHandler = async (req, res) => { if (userOngoingRoomIndex === -1) { await user.save(); return res.status(500).json({ - error: "Rooms/:id/settlement : internal server error", + error: "Rooms/:id/commitSettlement : internal server error", }); } user.ongoingRoom.splice(userOngoingRoomIndex, 1); await user.save(); - // 결제 채팅을 보냅니다. + // 정산 채팅을 보냅니다. await emitChatEvent(req.app.get("io"), { roomId, - type: "payment", + type: "settlement", content: user.id, authorId: user._id, }); @@ -638,12 +638,12 @@ const commitPaymentHandler = async (req, res) => { } catch (err) { logger.error(err); res.status(500).json({ - error: "Rooms/:id/commitPayment : internal server error", + error: "Rooms/:id/commitSettlement : internal server error", }); } }; -const settlementHandler = async (req, res) => { +const commitPaymentHandler = async (req, res) => { try { const { roomId } = req.body; const user = await userModel.findOne({ id: req.userId }); @@ -671,7 +671,7 @@ const settlementHandler = async (req, res) => { if (!roomObject) { return res.status(404).json({ - error: "Rooms/:id/settlement : cannot find settlement info", + error: "Rooms/:id/commitPayment : cannot find settlement info", }); } @@ -684,17 +684,17 @@ const settlementHandler = async (req, res) => { if (userOngoingRoomIndex === -1) { await user.save(); return res.status(500).json({ - error: "Rooms/:id/settlement : internal server error", + error: "Rooms/:id/commitPayment : internal server error", }); } user.ongoingRoom.splice(userOngoingRoomIndex, 1); await user.save(); - // 정산 채팅을 보냅니다. + // 송금 채팅을 보냅니다. await emitChatEvent(req.app.get("io"), { roomId, - type: "settlement", + type: "payment", content: user.id, authorId: user._id, }); @@ -716,7 +716,7 @@ const settlementHandler = async (req, res) => { } catch (err) { logger.error(err); res.status(500).json({ - error: "Rooms/:id/settlement : internal server error", + error: "Rooms/:id/commitPayment : internal server error", }); } }; @@ -811,6 +811,6 @@ module.exports = { searchHandler, searchByUserHandler, commitPaymentHandler, - settlementHandler, + commitSettlementHandler, // editHandler, }; diff --git a/src/services/users.js b/src/services/users.js index 514f2b4a..5d8ee632 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -1,4 +1,4 @@ -const { userModel } = require("../modules/stores/mongo"); +const { userModel, banModel } = require("../modules/stores/mongo"); const logger = require("../modules/logger"); const aws = require("../modules/stores/aws"); @@ -200,6 +200,35 @@ const resetProfileImgHandler = async (req, res) => { } }; +const isBannedHandler = async (req, res) => { + try { + // 현재 시각이 expireAt 보다 작고 본인인 경우(ban의 userId가 userOid랑 같은 경우)의 record를 모두 가져옴 + const result = await banModel.find({ + userId: req.userOid, + expireAt: { + $gte: req.timestamp, + }, + }); + if (!result) + return res.status(500).send("Users/isBanned : internal server error"); + res.status(200).json(result); + } catch (err) { + res.status(500).send("Users/isBanned : internal server error"); + } +}; + +const getBanRecordHandler = async (req, res) => { + try { + // 본인인 경우(ban의 userId가 userOid랑 같은 경우)의 record를 모두 가져옴 + const result = await banModel.find({ userId: req.userOid }); + if (!result) + return res.status(500).send("Users/getBanRecord : internal server error"); + res.status(200).json(result); + } catch (err) { + res.status(500).send("Users/getBanRecord : internal server error"); + } +}; + module.exports = { agreeOnTermsOfServiceHandler, getAgreeOnTermsOfServiceHandler, @@ -209,4 +238,6 @@ module.exports = { editProfileImgDoneHandler, resetNicknameHandler, resetProfileImgHandler, + isBannedHandler, + getBanRecordHandler, }; diff --git a/test/services/rooms.js b/test/services/rooms.js index e17ae6af..4e948ad1 100644 --- a/test/services/rooms.js +++ b/test/services/rooms.js @@ -141,8 +141,8 @@ describe("[rooms] 6.searchByUserHandler", () => { }); // 7. 1분이 지난 후, 정산 정보를 불러옴. 예상과 같은 정보를 불러오는지 확인 -describe("[rooms] 7.commitPaymentHandler", () => { - it("should return information of room and commit payment", async () => { +describe("[rooms] 7.commitSettlementHandler", () => { + it("should return information of room and commit settlement", async () => { const testUser1 = await userModel.findOne({ id: "test1" }); const testRoom = await roomModel.findOne({ name: "test-room" }); let req = httpMocks.createRequest({ @@ -152,7 +152,7 @@ describe("[rooms] 7.commitPaymentHandler", () => { app, }); let res = httpMocks.createResponse(); - await roomsHandlers.commitPaymentHandler(req, res); + await roomsHandlers.commitSettlementHandler(req, res); const resData = res._getData(); expect(resData).to.has.property("name", "test-room"); @@ -161,9 +161,9 @@ describe("[rooms] 7.commitPaymentHandler", () => { }); }); -// 8. 도착 정보를 불러옴. 예상과 같은 정보를 불러오는지 확인 -describe("[rooms] 8.settlementHandler", () => { - it("should return information of room and set settlement", async () => { +// 8. 송금 후 정산 정보를 불러옴. 예상과 같은 정보를 불러오는지 확인 +describe("[rooms] 8.commitPaymentHandler", () => { + it("should return information of room and commit payment", async () => { const testUser2 = await userModel.findOne({ id: "test2" }); const testRoom = await roomModel.findOne({ name: "test-room" }); let req = httpMocks.createRequest({ @@ -172,7 +172,7 @@ describe("[rooms] 8.settlementHandler", () => { app, }); let res = httpMocks.createResponse(); - await roomsHandlers.settlementHandler(req, res); + await roomsHandlers.commitPaymentHandler(req, res); const resData = res._getData(); expect(resData).to.has.property("name", "test-room");