From 6c119b3042444221ab03e3531f2aec30f6bc2748 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 20 Feb 2024 16:19:37 +0900 Subject: [PATCH 1/6] Add: /events/2024spring/invite/search endpoint --- src/lottery/index.js | 1 + src/lottery/modules/stores/mongo.js | 4 +++ src/lottery/routes/invite.js | 17 +++++++++++ src/lottery/services/invite.js | 44 +++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 src/lottery/routes/invite.js create mode 100644 src/lottery/services/invite.js diff --git a/src/lottery/index.js b/src/lottery/index.js index 0cddcb43..ac00ffe2 100644 --- a/src/lottery/index.js +++ b/src/lottery/index.js @@ -25,6 +25,7 @@ lotteryRouter.use(require("../middlewares/originValidator")); // [Router] APIs lotteryRouter.use("/globalState", require("./routes/globalState")); +lotteryRouter.use("/invite", require("./routes/invite")); lotteryRouter.use("/transactions", require("./routes/transactions")); lotteryRouter.use("/items", require("./routes/items")); lotteryRouter.use("/publicNotice", require("./routes/publicNotice")); diff --git a/src/lottery/modules/stores/mongo.js b/src/lottery/modules/stores/mongo.js index 9efe9a96..600c99ec 100644 --- a/src/lottery/modules/stores/mongo.js +++ b/src/lottery/modules/stores/mongo.js @@ -52,6 +52,10 @@ const eventStatusSchema = Schema({ type: Schema.Types.ObjectId, ref: "User", }, // 이 사용자를 초대한 사용자 + isEnabledInviteUrl: { + type: Boolean, + default: false, + }, // 초대 링크 활성화 여부 }); const questSchema = Schema({ diff --git a/src/lottery/routes/invite.js b/src/lottery/routes/invite.js new file mode 100644 index 00000000..ffc2fcf7 --- /dev/null +++ b/src/lottery/routes/invite.js @@ -0,0 +1,17 @@ +const express = require("express"); + +const router = express.Router(); +const inviteHandlers = require("../services/invite"); + +// TODO: Validations + +router.get("/search/:inviter", inviteHandlers.searchInviterHandler); + +// 아래의 Endpoint 접근 시 로그인, 차단 여부 체크 및 시각 체크 필요 +router.use(require("../../middlewares/auth")); +router.use(require("../middlewares/checkBanned")); +router.use(require("../middlewares/timestampValidator")); + +router.post("/create", inviteHandlers.createInviteUrlHandler); + +module.exports = router; diff --git a/src/lottery/services/invite.js b/src/lottery/services/invite.js new file mode 100644 index 00000000..9f89e972 --- /dev/null +++ b/src/lottery/services/invite.js @@ -0,0 +1,44 @@ +const { eventStatusModel } = require("../modules/stores/mongo"); +const { userModel } = require("../../modules/stores/mongo"); +const logger = require("../../modules/logger"); + +const searchInviterHandler = async (req, res) => { + try { + const { inviter } = req.params; + const inviterStatus = await eventStatusModel.findOne({ _id: inviter }); + if ( + !inviterStatus || + !inviterStatus.isEnabledInviteUrl || + inviterStatus.isBanned + ) + return res.status(400).json({ error: "Invite/Search : invalid inviter" }); + + const inviterInfo = await userModel.findOne({ _id: inviterStatus.userId }); + if (!inviterInfo) + return res + .status(500) + .json({ error: "Invite/Search : internal server error" }); + + return res.json({ + nickname: inviterInfo.nickname, + profileImageUrl: inviterInfo.profileImageUrl, + }); + } catch (err) { + logger.error(err); + res.status(500).json({ error: "Invite/Search : internal server error" }); + } +}; + +const createInviteUrlHandler = async (req, res) => { + try { + // TODO + } catch (err) { + logger.error(err); + res.status(500).json({ error: "Invite/Create : internal server error" }); + } +}; + +module.exports = { + searchInviterHandler, + createInviteUrlHandler, +}; From 638be518f9a2be4121a84544f78a10c4c6e10c4b Mon Sep 17 00:00:00 2001 From: static Date: Tue, 20 Feb 2024 16:38:01 +0900 Subject: [PATCH 2/6] Add: /events/2024spring/invite/create endpoint --- src/lottery/services/invite.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/lottery/services/invite.js b/src/lottery/services/invite.js index 9f89e972..53a7bb31 100644 --- a/src/lottery/services/invite.js +++ b/src/lottery/services/invite.js @@ -31,7 +31,27 @@ const searchInviterHandler = async (req, res) => { const createInviteUrlHandler = async (req, res) => { try { - // TODO + const inviteUrl = `https://temp/${req.eventStatus._id}`; + + if (req.eventStatus.isEnabledInviteUrl) return res.json({ inviteUrl }); + + const eventStatus = await eventStatusModel + .findOneAndUpdate( + { + _id: req.eventStatus._id, + isEnabledInviteUrl: false, + }, + { + isEnabledInviteUrl: true, + } + ) + .lean(); + if (!eventStatus) + return res + .status(500) + .json({ error: "Invite/Create : internal server error" }); + + return res.json({ inviteUrl }); } catch (err) { logger.error(err); res.status(500).json({ error: "Invite/Create : internal server error" }); From 05ca3fa4b6ac1eba8ea9ee4d92ce36be88628c92 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 20 Feb 2024 23:26:56 +0900 Subject: [PATCH 3/6] Fix: invite url --- src/lottery/services/invite.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lottery/services/invite.js b/src/lottery/services/invite.js index 53a7bb31..c7871273 100644 --- a/src/lottery/services/invite.js +++ b/src/lottery/services/invite.js @@ -2,6 +2,8 @@ const { eventStatusModel } = require("../modules/stores/mongo"); const { userModel } = require("../../modules/stores/mongo"); const logger = require("../../modules/logger"); +const { eventConfig } = require("../../../loadenv"); + const searchInviterHandler = async (req, res) => { try { const { inviter } = req.params; @@ -31,7 +33,7 @@ const searchInviterHandler = async (req, res) => { const createInviteUrlHandler = async (req, res) => { try { - const inviteUrl = `https://temp/${req.eventStatus._id}`; + const inviteUrl = `${req.origin}/event/${eventConfig?.mode}-invite/${req.eventStatus._id}`; if (req.eventStatus.isEnabledInviteUrl) return res.json({ inviteUrl }); From 77d9e7fd774909e462cf0905e2d6528b021b61a1 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 20 Feb 2024 23:43:47 +0900 Subject: [PATCH 4/6] Docs: /events/2024spring/invite router --- src/lottery/routes/docs/invite.js | 75 +++++++++++++++++++++++++ src/lottery/routes/docs/inviteSchema.js | 15 +++++ src/lottery/routes/docs/quests.js | 6 +- src/lottery/routes/docs/questsSchema.js | 1 + src/lottery/routes/docs/swaggerDocs.js | 10 +++- src/lottery/routes/invite.js | 9 ++- 6 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 src/lottery/routes/docs/invite.js create mode 100644 src/lottery/routes/docs/inviteSchema.js diff --git a/src/lottery/routes/docs/invite.js b/src/lottery/routes/docs/invite.js new file mode 100644 index 00000000..3a3972da --- /dev/null +++ b/src/lottery/routes/docs/invite.js @@ -0,0 +1,75 @@ +const { eventConfig } = require("../../../../loadenv"); +const apiPrefix = `/events/${eventConfig?.mode}/invite`; + +const inviteDocs = {}; +inviteDocs[`${apiPrefix}/search/:inviter`] = { + get: { + tags: [`${apiPrefix}`], + summary: "초대자 정보 조회", + description: "초대자의 정보를 조회합니다.", + requestBody: { + description: "", + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/searchInviterHandler", + }, + }, + }, + }, + responses: { + 200: { + description: "", + content: { + "application/json": { + schema: { + type: "object", + required: ["nickname", "profileImageUrl"], + properties: { + nickname: { + type: "string", + description: "초대자의 닉네임", + example: "asdf", + }, + profileImageUrl: { + type: "string", + description: "초대자의 프로필 이미지 URL", + example: "IMAGE URL", + }, + }, + }, + }, + }, + }, + }, + }, +}; +inviteDocs[`${apiPrefix}/create`] = { + post: { + tags: [`${apiPrefix}`], + summary: "초대 링크 생성", + description: "초대 링크를 생성합니다.", + responses: { + 200: { + description: "", + content: { + "application/json": { + schema: { + type: "object", + required: ["inviteUrl"], + properties: { + inviteUrl: { + type: "string", + description: "초대 링크", + example: "INVITE URL", + }, + }, + }, + }, + }, + }, + }, + }, +}; + +module.exports = inviteDocs; diff --git a/src/lottery/routes/docs/inviteSchema.js b/src/lottery/routes/docs/inviteSchema.js new file mode 100644 index 00000000..b76a3c04 --- /dev/null +++ b/src/lottery/routes/docs/inviteSchema.js @@ -0,0 +1,15 @@ +const inviteSchema = { + searchInviterHandler: { + type: "object", + required: ["inviter"], + properties: { + inviter: { + type: "string", + pattern: "^[a-fA-F\\d]{24}$", + }, + }, + errorMessage: "validation: bad request", + }, +}; + +module.exports = inviteSchema; diff --git a/src/lottery/routes/docs/quests.js b/src/lottery/routes/docs/quests.js index 71268b2c..14694f3e 100644 --- a/src/lottery/routes/docs/quests.js +++ b/src/lottery/routes/docs/quests.js @@ -1,8 +1,8 @@ const { eventConfig } = require("../../../../loadenv"); const apiPrefix = `/events/${eventConfig?.mode}/quests`; -const eventsDocs = {}; -eventsDocs[`${apiPrefix}/complete/:questId`] = { +const questsDocs = {}; +questsDocs[`${apiPrefix}/complete/:questId`] = { post: { tags: [`${apiPrefix}`], summary: "퀘스트 완료 요청", @@ -59,4 +59,4 @@ eventsDocs[`${apiPrefix}/complete/:questId`] = { }, }; -module.exports = eventsDocs; +module.exports = questsDocs; diff --git a/src/lottery/routes/docs/questsSchema.js b/src/lottery/routes/docs/questsSchema.js index e4d6786c..aaf3d806 100644 --- a/src/lottery/routes/docs/questsSchema.js +++ b/src/lottery/routes/docs/questsSchema.js @@ -8,6 +8,7 @@ const questsSchema = { enum: ["roomSharing"], }, }, + errorMessage: "validation: bad request", }, }; diff --git a/src/lottery/routes/docs/swaggerDocs.js b/src/lottery/routes/docs/swaggerDocs.js index 9b1d21f8..37effe3d 100644 --- a/src/lottery/routes/docs/swaggerDocs.js +++ b/src/lottery/routes/docs/swaggerDocs.js @@ -1,11 +1,13 @@ const globalStateDocs = require("./globalState"); +const inviteDocs = require("./invite"); const itemsDocs = require("./items"); const publicNoticeDocs = require("./publicNotice"); const questsDocs = require("./quests"); const transactionsDocs = require("./transactions"); -const itemsSchema = require("./itemsSchema"); const globalStateSchema = require("./globalStateSchema"); +const inviteSchema = require("./inviteSchema"); +const itemsSchema = require("./itemsSchema"); const questsSchema = require("./questsSchema"); const { eventConfig } = require("../../../../loadenv"); @@ -17,6 +19,10 @@ const eventSwaggerDocs = { name: `${apiPrefix}/globalState`, description: "이벤트 - Global State 관련 API", }, + { + name: `${apiPrefix}/invite`, + description: "이벤트 - 초대 링크 관련 API", + }, // 이 태그는 2024 봄학기 이벤트에서 사용되지 않습니다. // // { @@ -38,6 +44,7 @@ const eventSwaggerDocs = { ], paths: { ...globalStateDocs, + ...inviteDocs, //...itemsDocs, ...publicNoticeDocs, ...questsDocs, @@ -46,6 +53,7 @@ const eventSwaggerDocs = { components: { schemas: { ...globalStateSchema, + ...inviteSchema, //...itemsSchema, ...questsSchema, }, diff --git a/src/lottery/routes/invite.js b/src/lottery/routes/invite.js index ffc2fcf7..3311f3d0 100644 --- a/src/lottery/routes/invite.js +++ b/src/lottery/routes/invite.js @@ -3,9 +3,14 @@ const express = require("express"); const router = express.Router(); const inviteHandlers = require("../services/invite"); -// TODO: Validations +const { validateParams } = require("../../middlewares/ajv"); +const inviteSchema = require("./docs/inviteSchema"); -router.get("/search/:inviter", inviteHandlers.searchInviterHandler); +router.get( + "/search/:inviter", + validateParams(inviteSchema.searchInviterHandler), + inviteHandlers.searchInviterHandler +); // 아래의 Endpoint 접근 시 로그인, 차단 여부 체크 및 시각 체크 필요 router.use(require("../../middlewares/auth")); From a21235f8c9f68343e19d0b3793f310d6bfebe689 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 21 Feb 2024 00:02:32 +0900 Subject: [PATCH 5/6] Fix: inviter field validation in /events/2024spring/globalState/create api --- src/lottery/services/globalState.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index 76f8b15a..3771663d 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -81,9 +81,19 @@ const createUserGlobalStateHandler = async (req, res) => { .status(400) .json({ error: "GlobalState/Create : already created" }); + /* Request의 inviter 필드가 설정되어 있는데, + 1. 해당되는 유저가 이벤트에 참여하지 않았거나, + 2. 해당되는 유저의 이벤트 참여가 제한된 상태이거나, + 3. 해당되는 유저의 초대 링크가 활성화되지 않았으면, + 에러를 발생시킵니다. 개인정보 보호를 위해 오류 메세지는 하나로 통일하였습니다. */ + const inviterStatus = + req.body.inviter && + (await eventStatusModel.findOne({ _id: req.body.inviter }).lean()); if ( req.body.inviter && - (await eventStatusModel.findOne({ _id: req.body.inviter }).lean()) + (!inviterStatus || + inviterStatus.isBanned || + !inviterStatus.isEnabledInviteUrl) ) return res.status(400).json({ error: "GlobalState/Create : inviter did not participate in the event", @@ -117,8 +127,15 @@ const createUserGlobalStateHandler = async (req, res) => { await eventStatus.save(); await contracts.completeFirstLoginQuest(req.userOid, req.timestamp); + await contracts.completeEventSharingQuest(req.userOid, req.timestamp); + + if (req.body.inviter) + await contracts.completeEventSharingQuest( + inviterStatus.userId, + req.timestamp + ); - res.json({ result: true }); + return res.json({ result: true }); } catch (err) { logger.error(err); res From e08dc79ce6ccaec724cbf6e136f741dfdfe49bb9 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 21 Feb 2024 00:30:48 +0900 Subject: [PATCH 6/6] Fix: eventSharing and eventSharing5 were always completed at the same time --- src/lottery/modules/contracts.js | 18 ++++++++++++++---- src/lottery/modules/quests.js | 1 + src/lottery/services/globalState.js | 5 +++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/lottery/modules/contracts.js b/src/lottery/modules/contracts.js index 0ea00f14..6fe9f0fa 100644 --- a/src/lottery/modules/contracts.js +++ b/src/lottery/modules/contracts.js @@ -267,10 +267,20 @@ const completeAdPushAgreementQuest = async ( * @description 초대 링크를 통해 사용자가 이벤트에 참여할 때마다, 초대한 사용자 및 초대받은 사용자에 대해 각각 호출해 주세요. */ const completeEventSharingQuest = async (userId, timestamp) => { - return [ - await completeQuest(userId, timestamp, quests.eventSharing), - await completeQuest(userId, timestamp, quests.eventSharing5), - ]; + const eventSharingResult = await completeQuest( + userId, + timestamp, + quests.eventSharing + ); + if (!eventSharingResult || eventSharingResult.questCount % 5 !== 0) + return [eventSharingResult, null]; + + const eventSharing5Result = await completeQuest( + userId, + timestamp, + quests.eventSharing5 + ); + return [eventSharingResult, eventSharing5Result]; }; module.exports = { diff --git a/src/lottery/modules/quests.js b/src/lottery/modules/quests.js index 84f75d1d..04c6cd4c 100644 --- a/src/lottery/modules/quests.js +++ b/src/lottery/modules/quests.js @@ -154,6 +154,7 @@ const completeQuest = async (userId, timestamp, quest) => { logger.info(`User ${userId} successfully completed ${quest.id}Quest`); return { quest, + questCount: questCount + 1, transactionsId, }; } catch (err) { diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index 3771663d..d121350c 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -127,13 +127,14 @@ const createUserGlobalStateHandler = async (req, res) => { await eventStatus.save(); await contracts.completeFirstLoginQuest(req.userOid, req.timestamp); - await contracts.completeEventSharingQuest(req.userOid, req.timestamp); - if (req.body.inviter) + if (req.body.inviter) { + await contracts.completeEventSharingQuest(req.userOid, req.timestamp); await contracts.completeEventSharingQuest( inviterStatus.userId, req.timestamp ); + } return res.json({ result: true }); } catch (err) {