diff --git a/package-lock.json b/package-lock.json index 31fbf3b..b9727ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -951,7 +951,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, "requires": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -1214,6 +1213,15 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "cron-parser": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-3.5.0.tgz", + "integrity": "sha512-wyVZtbRs6qDfFd8ap457w3XVntdvqcwBGxBoTvJQH9KGVKL/fB+h2k3C8AqiVxvUQKN1Ps/Ns46CNViOpVDhfQ==", + "requires": { + "is-nan": "^1.3.2", + "luxon": "^1.26.0" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1292,7 +1300,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, "requires": { "object-keys": "^1.0.12" } @@ -2189,7 +2196,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -2372,8 +2378,7 @@ "has-symbols": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" }, "has-tostringtag": { "version": "1.0.0", @@ -2623,6 +2628,15 @@ "is-path-inside": "^3.0.2" } }, + "is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + } + }, "is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -3062,6 +3076,11 @@ } } }, + "long-timeout": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", + "integrity": "sha1-lyHXiLR+C8taJMLivuGg2lXatRQ=" + }, "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", @@ -3084,6 +3103,11 @@ "es5-ext": "~0.10.2" } }, + "luxon": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz", + "integrity": "sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ==" + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -3561,6 +3585,16 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" }, + "node-schedule": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.1.0.tgz", + "integrity": "sha512-nl4JTiZ7ZQDc97MmpTq9BQjYhq7gOtoh7SiPH069gBFBj0PzD8HI7zyFs6rzqL8Y5tTiEEYLxgtbx034YPrbyQ==", + "requires": { + "cron-parser": "^3.5.0", + "long-timeout": "0.1.1", + "sorted-array-functions": "^1.3.0" + } + }, "nodemon": { "version": "2.0.15", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.15.tgz", @@ -3636,8 +3670,7 @@ "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, "object.assign": { "version": "4.1.2", @@ -4615,6 +4648,11 @@ "smart-buffer": "^4.2.0" } }, + "sorted-array-functions": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz", + "integrity": "sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==" + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", diff --git a/package.json b/package.json index f8cfade..c893c66 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "mongoose-validator": "^2.1.0", "multer": "^1.4.2", "multer-s3": "^2.9.0", + "node-schedule": "^2.1.0", "pg": "^8.7.1", "pg-hstore": "^2.3.4", "reflect-metadata": "^0.1.13", diff --git a/src/controller/auth.ts b/src/controller/auth.ts index affdc39..a7e89e9 100644 --- a/src/controller/auth.ts +++ b/src/controller/auth.ts @@ -334,12 +334,50 @@ const getLoginFlagController = async (req: Request, res: Response) => { } }; +/** + * @회원탈퇴 + * @route Patch /auth/withdraw + * @access private + * @err + */ +const patchWithdrawController = async (req: Request, res: Response) => { + try { + const resData = await authService.patchWithdrawService(req.user.id); + + if (resData === constant.NON_EXISTENT_USER) { + return response.basicResponse( + res, + returnCode.BAD_REQUEST, + false, + "이미 삭제된 유저입니다." + ); + } + + return response.basicResponse( + res, + returnCode.OK, + true, + "삭제가 완료되었습니다." + ); + } catch (err) { + slack.slackWebhook(req, err.message); + console.error(err.message); + return response.basicResponse( + res, + returnCode.INTERNAL_SERVER_ERROR, + false, + "서버 오류" + ); + } +}; + const authController = { getEmailController, getNicknameController, postLoginController, postSignupController, getLoginFlagController, + patchWithdrawController, }; export default authController; diff --git a/src/index.ts b/src/index.ts index bfd1982..10f3c04 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,9 @@ import router from "./router"; import connectDB from "./loader/db"; import config from "./config"; +// scheduler +import { userScan } from "./scheduler/userScheduler"; + const app = express(); // Connect Database @@ -40,6 +43,9 @@ app.use(function (err, req, res, next) { // render the error page res.status(err.status || 500); res.json({ error: err }); + + // scheduler + userScan; }); const server = app diff --git a/src/interface/IUser.ts b/src/interface/IUser.ts index 9b07e09..9881777 100644 --- a/src/interface/IUser.ts +++ b/src/interface/IUser.ts @@ -8,6 +8,7 @@ export interface IUser { img: string; refresh_token: string; email_code: string; + expired_at: Date; created_at: Date; updated_at: Date; is_deleted: boolean; diff --git a/src/loader/db.ts b/src/loader/db.ts index 89b1d53..c76ab2f 100644 --- a/src/loader/db.ts +++ b/src/loader/db.ts @@ -20,7 +20,12 @@ const connectDB = async () => { console.log("Review Collection is created!"); }); - console.log("Mongoose Connected ..."); + const uri = config.mongoURI; + console.log( + "\nMongoose Connected... [" + + uri.substring(uri.lastIndexOf("/") + 1, uri.length) + + "]\n" + ); } catch (err) { console.error(err.message); process.exit(1); diff --git a/src/models/User.ts b/src/models/User.ts index 9bb6d87..7e43719 100644 --- a/src/models/User.ts +++ b/src/models/User.ts @@ -34,6 +34,13 @@ const UserSchema = new mongoose.Schema({ type: String, required: false, }, + + // 만료 일자 + expired_at: { + type: Date, + required: false, + }, + // 생성 일자 created_at: { type: Date, diff --git a/src/router/auth.ts b/src/router/auth.ts index 844ea66..a1e86d3 100644 --- a/src/router/auth.ts +++ b/src/router/auth.ts @@ -1,7 +1,7 @@ import express from "express"; // Middleware -import { isLogin } from "../middleware/authMiddleware"; +import { auth, isLogin } from "../middleware/authMiddleware"; // Controller import authController from "../controller/auth"; @@ -13,5 +13,6 @@ router.get("/nickname", authController.getNicknameController); router.post("/login", authController.postLoginController); router.post("/signup", authController.postSignupController); router.get("/check", isLogin, authController.getLoginFlagController); +router.patch("/withdraw", auth, authController.patchWithdrawController); module.exports = router; diff --git a/src/scheduler/userScheduler.ts b/src/scheduler/userScheduler.ts new file mode 100644 index 0000000..af781c0 --- /dev/null +++ b/src/scheduler/userScheduler.ts @@ -0,0 +1,28 @@ +import schedule from "node-schedule"; + +// models +import Review from "../models/Review"; +import User from "../models/User"; + +// library +import { keysToSnake, keysToCamel } from "../library/convertSnakeToCamel"; + +export const userScan = schedule.scheduleJob("0 0 0 * * *", async () => { + // 현재 시간 + const current = new Date(new Date(Date.now()).setUTCMinutes(0, 0, 0)); + console.log("Scanning users...[" + current + "]"); + + // 삭제 예정 유저 + const deletedUsers = await User.find(keysToSnake({ isDeleted: true })); + + deletedUsers.forEach(async (user) => { + // 현재 날짜가 만료 날짜 이후 + if (current.getTime() >= user.expired_at.getTime()) { + // 해당 유저가 가진 리뷰 모두 삭제 + await Review.deleteMany(keysToSnake({ userID: user._id })); + // 해당 유저 삭제 + await user.deleteOne(keysToSnake({ _id: user._id })); + } + }); + console.log("Complete scanning..."); +}); diff --git a/src/service/auth.ts b/src/service/auth.ts index 3334464..eccb3d2 100644 --- a/src/service/auth.ts +++ b/src/service/auth.ts @@ -1,4 +1,5 @@ import config from "../config"; +import mongoose from "mongoose"; // library import jwt from "jsonwebtoken"; @@ -9,7 +10,7 @@ import { checkNicknameValid, checkPasswordValid, } from "../library/checkValidation"; -import { keysToSnake } from "../library/convertSnakeToCamel"; +import { keysToSnake, keysToCamel } from "../library/convertSnakeToCamel"; // model import User from "../models/User"; @@ -208,12 +209,41 @@ const getLoginFlagService = async (isLogin: Boolean) => { return { isLogin }; }; +/** + * @회원탈퇴 + * @route Patch /auth/withdraw + * @access private + * @err + */ +const patchWithdrawService = async (userId: string) => { + const user = await User.findById(new mongoose.Types.ObjectId(userId)); + + // snake to camel + const originUser = keysToCamel(user); + const camelUser = keysToCamel(originUser.Doc); + + if (camelUser.isDeleted) { + return constant.NON_EXISTENT_USER; + } + + // 삭제 + await user.updateOne({ $set: keysToSnake({ isDeleted: true }) }); + + // 만료 날짜 + const date = new Date(Date.now() + 30 * 24 * 3600 * 1000); + + // 시간 아래는 0 으로 초기화 + const expiredAt = date.setUTCMinutes(0, 0, 0); + await user.updateOne({ $set: keysToSnake({ expiredAt }) }); +}; + const authService = { getEmailService, getNicknameService, postLoginService, postSignupService, getLoginFlagService, + patchWithdrawService, }; export default authService;