Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

#541 24-chuseok event ban middleware #544

Open
wants to merge 8 commits into
base: dev
Choose a base branch
from
Open
32 changes: 32 additions & 0 deletions src/lottery/middlewares/eventValidator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const { eventStatusModel } = require("../modules/stores/mongo");
const logger = require("../../modules/logger");

/**
* 사용자가 차단 되었는지 여부를 판단합니다.
* 차단된 사용자는 이벤트에 한하여 서비스 이용에 제재를 받습니다.
* @param {*} req eventStatus가 성공적일 경우 req.eventStatus = eventStatus로 들어갑니다.
* @param {*} res
* @param {*} next
* @returns
*/
const eventValidator = async (req, res, next) => {
try {
const eventStatus = await eventStatusModel
Copy link
Member

Choose a reason for hiding this comment

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

개인적으로는 이렇게 eventStatus를 가져오는 미들웨어보다는 필요한 services에서만 가져다 쓰는게 좋을 것 같다고 생각해요. 기존 코드에 user를 가져오는 미들웨어가 없고 필요한 services에서 직접 조회해서 쓰는 것처럼요. 물론 성능 차이는 거의 없겠지만, service에 따라 eventStatus를 단순히 조회만 하는 경우가 있고, 수정해야 하는 경우도 있는데 후자의 경우에는 (미들웨어가 있으면) query가 2번 발생하게 됩니다.

Copy link
Member

Choose a reason for hiding this comment

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

다른 백엔드 분들은 어케 생각하시는지도 궁금하네요!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

계속 사용하는 것으로 결정

.findOne({ userId: req.userOid })
.lean();
if (!eventStatus) {
return res
.status(400)
.json({ error: "eventValidator: nonexistent eventStatus" });
}
req.eventStatus = eventStatus;
next();
} catch (err) {
logger.error(err);
res.error(500).json({
error: "eventValidator: internal server error",
});
}
};

module.exports = eventValidator;
122 changes: 95 additions & 27 deletions src/lottery/modules/contracts.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,31 +108,43 @@ const quests = buildQuests({

/**
* firstLogin 퀘스트의 완료를 요청합니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @param {string} sid - user의 sid입니다.
* @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
* @param {string} url - 요청한 url입니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @returns {Promise}
* @usage lottery/globalState - createUserGlobalStateHandler
*/
const completeFirstLoginQuest = async (userId, timestamp) => {
return await completeQuest(userId, timestamp, quests.firstLogin);
const completeFirstLoginQuest = async (sid, timestamp, url, userId) => {
return await completeQuest(sid, timestamp, url, userId, quests.firstLogin);
};

/**
* firstRoomCreation 퀘스트의 완료를 요청합니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @param {string} sid - user의 sid입니다.
* @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
* @param {string} url - 요청한 url입니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @returns {Promise}
* @description 방을 만들 때마다 호출해 주세요.
* @usage rooms - createHandler
*/
const completeFirstRoomCreationQuest = async (userId, timestamp) => {
return await completeQuest(userId, timestamp, quests.firstRoomCreation);
const completeFirstRoomCreationQuest = async (sid, timestamp, url, userId) => {
return await completeQuest(
sid,
timestamp,
url,
userId,
quests.firstRoomCreation
);
};

/**
* fareSettlement 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @param {string} sid - user의 sid입니다.
* @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
* @param {string} url - 요청한 url입니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @param {Object} roomObject - 방의 정보입니다.
* @param {mongoose.Types.ObjectId} roomObject._id - 방의 ObjectId입니다.
* @param {Array<{ user: mongoose.Types.ObjectId }>} roomObject.part - 참여자 목록입니다.
Expand All @@ -141,7 +153,13 @@ const completeFirstRoomCreationQuest = async (userId, timestamp) => {
* @description 정산 요청이 이루어질 때마다 호출해 주세요.
* @usage rooms - commitSettlementHandler
*/
const completeFareSettlementQuest = async (userId, timestamp, roomObject) => {
const completeFareSettlementQuest = async (
sid,
timestamp,
url,
userId,
roomObject
) => {
logger.info(
`User ${userId} requested to complete fareSettlementQuest in Room ${roomObject._id}`
);
Expand All @@ -154,13 +172,21 @@ const completeFareSettlementQuest = async (userId, timestamp, roomObject) => {
)
return null; // 택시 출발 시각이 이벤트 기간 내에 포함되지 않는 경우 퀘스트 완료 요청을 하지 않습니다.

return await completeQuest(userId, timestamp, quests.fareSettlement);
return await completeQuest(
sid,
timestamp,
url,
userId,
quests.fareSettlement
);
};

/**
* farePayment 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @param {string} sid - user의 sid입니다.
* @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
* @param {string} url - 요청한 url입니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @param {Object} roomObject - 방의 정보입니다.
* @param {mongoose.Types.ObjectId} roomObject._id - 방의 ObjectId입니다.
* @param {Array<{ user: mongoose.Types.ObjectId }>} roomObject.part - 참여자 목록입니다.
Expand All @@ -169,7 +195,13 @@ const completeFareSettlementQuest = async (userId, timestamp, roomObject) => {
* @description 송금이 이루어질 때마다 호출해 주세요.
* @usage rooms - commitPaymentHandler
*/
const completeFarePaymentQuest = async (userId, timestamp, roomObject) => {
const completeFarePaymentQuest = async (
sid,
timestamp,
url,
userId,
roomObject
) => {
logger.info(
`User ${userId} requested to complete farePaymentQuest in Room ${roomObject._id}`
);
Expand All @@ -182,75 +214,111 @@ const completeFarePaymentQuest = async (userId, timestamp, roomObject) => {
)
return null; // 택시 출발 시각이 이벤트 기간 내에 포함되지 않는 경우 퀘스트 완료 요청을 하지 않습니다.

return await completeQuest(userId, timestamp, quests.farePayment);
return await completeQuest(sid, timestamp, url, userId, quests.farePayment);
};

/**
* nicknameChanging 퀘스트의 완료를 요청합니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @param {string} sid - user의 sid입니다.
* @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
* @param {string} url - 요청한 url입니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @returns {Promise}
* @description 닉네임을 변경할 때마다 호출해 주세요.
* @usage users - editNicknameHandler
*/
const completeNicknameChangingQuest = async (userId, timestamp) => {
return await completeQuest(userId, timestamp, quests.nicknameChanging);
const completeNicknameChangingQuest = async (sid, timestamp, url, userId) => {
return await completeQuest(
sid,
timestamp,
url,
userId,
quests.nicknameChanging
);
};

/**
* accountChanging 퀘스트의 완료를 요청합니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @param {string} sid - user의 sid입니다.
* @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
* @param {string} url - 요청한 url입니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @param {string} newAccount - 변경된 계좌입니다.
* @returns {Promise}
* @description 계좌를 변경할 때마다 호출해 주세요.
* @usage users - editAccountHandler
*/
const completeAccountChangingQuest = async (userId, timestamp, newAccount) => {
const completeAccountChangingQuest = async (
sid,
timestamp,
url,
userId,
newAccount
) => {
if (newAccount === "") return null;

return await completeQuest(userId, timestamp, quests.accountChanging);
return await completeQuest(
sid,
timestamp,
url,
userId,
quests.accountChanging
);
};

/**
* adPushAgreement 퀘스트의 완료를 요청합니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @param {string} sid - user의 sid입니다.
* @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
* @param {string} url - 요청한 url입니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @param {boolean} advertisement - 변경된 광고성 알림 수신 동의 여부입니다.
* @returns {Promise}
* @description 알림 옵션을 변경할 때마다 호출해 주세요.
* @usage notifications - editOptionsHandler
*/
const completeAdPushAgreementQuest = async (
userId,
sid,
timestamp,
url,
userId,
advertisement
) => {
if (!advertisement) return null;

return await completeQuest(userId, timestamp, quests.adPushAgreement);
return await completeQuest(
sid,
timestamp,
url,
userId,
quests.adPushAgreement
);
};

/**
* eventSharing 퀘스트의 완료를 요청합니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @param {string} sid - user의 sid입니다.
* @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
* @param {string} url - 요청한 url입니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @returns {Promise}
* @usage lottery/globalState - createUserGlobalStateHandler
*/
const completeEventSharingQuest = async (userId, timestamp) => {
return await completeQuest(userId, timestamp, quests.eventSharing);
const completeEventSharingQuest = async (sid, timestamp, url, userId) => {
return await completeQuest(sid, timestamp, url, userId, quests.eventSharing);
};

/**
* itemPurchase 퀘스트의 완료를 요청합니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @param {string} sid - user의 sid입니다.
* @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
* @param {string} url - 요청한 url입니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @returns {Promise}
* @description 상품을 구입할 때마다 호출해 주세요.
*/
const completeItemPurchaseQuest = async (userId, timestamp) => {
return await completeQuest(userId, timestamp, quests.itemPurchase);
const completeItemPurchaseQuest = async (sid, timestamp, url, userId) => {
return await completeQuest(sid, timestamp, url, userId, quests.itemPurchase);
};

module.exports = {
Expand Down
12 changes: 10 additions & 2 deletions src/lottery/modules/quests.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const logger = require("../../modules/logger");
const mongoose = require("mongoose");

const { eventConfig } = require("../../../loadenv");
const { validateServiceBanRecord } = require("../../modules/ban");
const eventPeriod = eventConfig && {
startAt: new Date(eventConfig.period.startAt),
endAt: new Date(eventConfig.period.endAt),
Expand Down Expand Up @@ -64,11 +65,18 @@ const buildQuests = (quests) => {
* @param {number} quest.maxCount - 퀘스트의 최대 완료 가능 횟수입니다.
* @returns {Object|null} 성공한 경우 Object를, 실패한 경우 null을 반환합니다. 이미 최대 완료 횟수에 도달했거나, 퀘스트가 원격으로 비활성화된 경우에도 실패로 처리됩니다.
*/
const completeQuest = async (userId, timestamp, quest) => {
const completeQuest = async (sid, timestamp, url, userId, quest) => {
try {
// 1단계: 유저의 EventStatus를 가져옵니다. 블록드리스트인지도 확인합니다.
const eventStatus = await eventStatusModel.findOne({ userId }).lean();
if (!eventStatus || eventStatus.isBanned) return null;
const banErrorMessage = await validateServiceBanRecord(
sid,
timestamp,
url,
eventConfig.mode
);

if (!eventStatus || !!banErrorMessage) return null;

// 2단계: 이벤트 기간인지 확인합니다.
if (
Expand Down
3 changes: 2 additions & 1 deletion src/lottery/routes/invites.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ router.get(

// 아래의 Endpoint 접근 시 로그인, 차단 여부 및 시각 체크 필요
router.use(require("../../middlewares/auth"));
router.use(require("../middlewares/checkBanned"));
router.use(require("../../middlewares/ban"));
router.use(require("../middlewares/eventValidator"));
router.use(require("../middlewares/timestampValidator"));

router.post("/create", invitesHandlers.createInviteUrlHandler);
Expand Down
3 changes: 2 additions & 1 deletion src/lottery/routes/items.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ router.get(

// 아래의 Endpoint 접근 시 로그인, 차단 여부 및 시각 체크 필요
router.use(require("../../middlewares/auth"));
router.use(require("../middlewares/checkBanned"));
router.use(require("../../middlewares/ban"));
router.use(require("../middlewares/eventValidator"));
router.use(require("../middlewares/timestampValidator"));

router.post(
Expand Down
3 changes: 2 additions & 1 deletion src/lottery/routes/quests.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ const questsHandlers = require("../services/quests");

// 아래의 Endpoint 접근 시 로그인, 차단 여부 및 시각 체크 필요
router.use(require("../../middlewares/auth"));
router.use(require("../middlewares/checkBanned"));
router.use(require("../../middlewares/ban"));
router.use(require("../middlewares/eventValidator"));
router.use(require("../middlewares/timestampValidator"));

router.post(
Expand Down
3 changes: 2 additions & 1 deletion src/lottery/routes/transactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ const router = express.Router();

const transactionsHandlers = require("../services/transactions");

// 아래의 Endpoint 접근 시 로그인 필요
// 아래의 Endpoint 접근 시 로그인 체크 필요
router.use(require("../../middlewares/auth"));
router.use(require("../middlewares/eventValidator"));

router.get("/", transactionsHandlers.getUserTransactionsHandler);

Expand Down
31 changes: 24 additions & 7 deletions src/lottery/services/globalState.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const { nodeEnv } = require("../../../loadenv");

const { eventConfig } = require("../../../loadenv");
const contracts = require("../modules/contracts");
const { validateServiceBanRecord } = require("../../modules/ban");
const quests = Object.values(contracts.quests);

// 아래의 함수는 2024 추석 이벤트에서 사용되지 않습니다.
Expand Down Expand Up @@ -85,11 +86,15 @@ const createUserGlobalStateHandler = async (req, res) => {
const inviterStatus =
req.body.inviter &&
(await eventStatusModel.findById(req.body.inviter).lean());
const banErrorMessage = await validateServiceBanRecord(
req.session.loginInfo.sid,
req.timestamp,
req.originalUrl,
eventConfig.mode
);
if (
req.body.inviter &&
(!inviterStatus ||
inviterStatus.isBanned ||
!inviterStatus.isInviteUrlEnabled)
(!inviterStatus || !!banErrorMessage || !inviterStatus.isInviteUrlEnabled)
)
return res.status(400).json({
error: "GlobalState/create : invalid inviter",
Expand Down Expand Up @@ -128,13 +133,25 @@ const createUserGlobalStateHandler = async (req, res) => {
await eventStatus.save();

// 퀘스트를 완료 처리합니다.
await contracts.completeFirstLoginQuest(req.userOid, req.timestamp);
await contracts.completeFirstLoginQuest(
req.session.loginInfo.sid,
req.timestamp,
req.originalUrl,
req.userOid
);

if (inviterStatus) {
await contracts.completeEventSharingQuest(req.userOid, req.timestamp);
await contracts.completeEventSharingQuest(
inviterStatus.userId,
req.timestamp
req.session.loginInfo.sid,
req.timestamp,
req.originalUrl,
req.userOid
);
await contracts.completeEventSharingQuest(
req.session.loginInfo.sid,
req.timestamp,
req.originalUrl,
inviterStatus.userId
);
}

Expand Down
Loading
Loading