Skip to content

Commit

Permalink
Merge pull request #528 from sparcs-kaist/#527-account-delete
Browse files Browse the repository at this point in the history
#527 회원 탈퇴 구현
  • Loading branch information
kmc7468 authored Feb 5, 2025
2 parents 7dd683f + b3d5071 commit 749614e
Show file tree
Hide file tree
Showing 38 changed files with 454 additions and 157 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/node_modules
/dist
.tsbuildinfo
/dump
.env
.env.test
Expand Down
41 changes: 41 additions & 0 deletions scripts/chatContentUserIdUpdater.js
Original file line number Diff line number Diff line change
@@ -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!");
});
2 changes: 1 addition & 1 deletion src/lottery/services/globalState.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion src/lottery/services/invites.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion src/lottery/services/items.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down
11 changes: 8 additions & 3 deletions src/lottery/services/publicNotice.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions src/middlewares/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
};

Expand Down
4 changes: 2 additions & 2 deletions src/middlewares/authAdmin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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가 화이트리스트에 있는지 확인
Expand Down
9 changes: 8 additions & 1 deletion src/modules/auths/login.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/modules/ban.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
22 changes: 22 additions & 0 deletions src/modules/fcm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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입니다.
Expand Down
10 changes: 8 additions & 2 deletions src/modules/populates/chats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Chat, "authorId"> {
authorId?: Pick<User, "_id" | "nickname" | "profileImageUrl">;
authorId: Pick<
User,
"_id" | "nickname" | "profileImageUrl" | "withdraw"
> | null;
}
6 changes: 3 additions & 3 deletions src/modules/populates/reports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Report, "reportedId"> {
reportedId: Pick<
User,
"_id" | "id" | "name" | "nickname" | "profileImageUrl"
>;
"_id" | "id" | "name" | "nickname" | "profileImageUrl" | "withdraw"
> | null;
}
63 changes: 47 additions & 16 deletions src/modules/populates/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Participant, "settlementStatus" | "readAt"> {
user: Pick<User, "_id" | "id" | "name" | "nickname" | "profileImageUrl">;
user: Pick<
User,
"_id" | "id" | "name" | "nickname" | "profileImageUrl" | "withdraw"
> | null;
}

export interface PopulatedRoom extends Omit<Room, "from" | "to" | "part"> {
from: Pick<Location, "_id" | "koName" | "enName">;
to: Pick<Location, "_id" | "koName" | "enName">;
part?: PopulatedParticipant[];
from: Pick<Location, "_id" | "koName" | "enName"> | null;
to: Pick<Location, "_id" | "koName" | "enName"> | null;
part: PopulatedParticipant[];
}

export interface FormattedRoom
extends Omit<PopulatedRoom, "part" | "settlementTotal"> {
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;
}[];
Expand All @@ -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,
};
Expand All @@ -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을 반환합니다.
Expand Down
Loading

0 comments on commit 749614e

Please sign in to comment.