diff --git a/.github/workflows/deploy-main.yml b/.github/workflows/deploy-main.yml new file mode 100644 index 0000000..b94ae27 --- /dev/null +++ b/.github/workflows/deploy-main.yml @@ -0,0 +1,69 @@ +name: deploy-main + +on: + push: + branches: + - "feature/mission-10/소리" + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure SSH + run: | + mkdir -p ~/.ssh + echo "$EC2_SSH_KEY_SORI" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + + # 접속 별명 설정 + cat >>~/.ssh/config <=6.0.0" + } + }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -214,6 +227,12 @@ "concat-map": "0.0.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -499,6 +518,15 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -924,6 +952,91 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -1132,6 +1245,12 @@ "node": "^14.16.0 || >=16.10.0" } }, + "node_modules/oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1198,6 +1317,74 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "license": "MIT", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "license": "MIT", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -1223,6 +1410,11 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -1398,6 +1590,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", @@ -1604,6 +1808,12 @@ "node": ">= 0.6" } }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1613,6 +1823,15 @@ "node": ">= 0.8" } }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index b17807e..7d91d43 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,12 @@ "dotenv": "^17.2.3", "express": "^5.1.0", "http-status-codes": "^2.3.0", + "jsonwebtoken": "^9.0.2", "morgan": "^1.10.1", "mysql2": "^3.15.3", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", + "passport-jwt": "^4.0.1", "prisma": "^6.19.0", "swagger-autogen": "^2.23.7", "swagger-ui-express": "^5.0.1" diff --git a/prisma/migrations/20251203163625_make_fields_optional/migration.sql b/prisma/migrations/20251203163625_make_fields_optional/migration.sql new file mode 100644 index 0000000..6ae8c7d --- /dev/null +++ b/prisma/migrations/20251203163625_make_fields_optional/migration.sql @@ -0,0 +1,115 @@ +-- CreateTable +CREATE TABLE `user` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `email` VARCHAR(255) NOT NULL, + `name` VARCHAR(100) NOT NULL, + `gender` VARCHAR(15) NULL, + `birth` DATE NULL, + `address` VARCHAR(255) NULL, + `detail_address` VARCHAR(255) NULL, + `phone_number` VARCHAR(15) NULL, + `point` INTEGER NOT NULL DEFAULT 0, + + UNIQUE INDEX `email`(`email`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `restaurant` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(100) NOT NULL, + `address` VARCHAR(255) NOT NULL, + `detail_address` VARCHAR(255) NULL, + `phone_number` VARCHAR(15) NOT NULL, + `region_id` INTEGER NOT NULL, + `food_category_id` INTEGER NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `region` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(100) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `review` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `content` VARCHAR(100) NOT NULL, + `rating` FLOAT NOT NULL, + `user_id` INTEGER NOT NULL, + `restaurant_id` INTEGER NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `food_category` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(100) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `user_favor_category` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `food_category_id` INTEGER NOT NULL, + + INDEX `f_category_id`(`food_category_id`), + INDEX `user_id`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `mission` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `point` INTEGER NOT NULL, + `content` TEXT NOT NULL, + `deadline` DATE NULL, + `restaurant_id` INTEGER NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `user_mission` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `status` VARCHAR(20) NOT NULL DEFAULT '진행중', + `user_id` INTEGER NOT NULL, + `mission_id` INTEGER NOT NULL, + + UNIQUE INDEX `user_mission_user_id_mission_id_key`(`user_id`, `mission_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `restaurant` ADD CONSTRAINT `restaurant_region_id_fkey` FOREIGN KEY (`region_id`) REFERENCES `region`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `restaurant` ADD CONSTRAINT `restaurant_food_category_id_fkey` FOREIGN KEY (`food_category_id`) REFERENCES `food_category`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `review` ADD CONSTRAINT `review_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `review` ADD CONSTRAINT `review_restaurant_id_fkey` FOREIGN KEY (`restaurant_id`) REFERENCES `restaurant`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `user_favor_category` ADD CONSTRAINT `user_favor_category_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `user_favor_category` ADD CONSTRAINT `user_favor_category_food_category_id_fkey` FOREIGN KEY (`food_category_id`) REFERENCES `food_category`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `mission` ADD CONSTRAINT `mission_restaurant_id_fkey` FOREIGN KEY (`restaurant_id`) REFERENCES `restaurant`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `user_mission` ADD CONSTRAINT `user_mission_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `user_mission` ADD CONSTRAINT `user_mission_mission_id_fkey` FOREIGN KEY (`mission_id`) REFERENCES `mission`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..592fc0b --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "mysql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c3e13c3..ef54c4e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -17,11 +17,11 @@ model User { id Int @id @default(autoincrement()) email String @unique(map: "email") @db.VarChar(255) name String @db.VarChar(100) - gender String @db.VarChar(15) - birth DateTime @db.Date - address String @db.VarChar(255) + gender String? @db.VarChar(15) + birth DateTime? @db.Date + address String? @db.VarChar(255) detailAddress String? @map("detail_address") @db.VarChar(255) - phoneNumber String @map("phone_number") @db.VarChar(15) + phoneNumber String? @map("phone_number") @db.VarChar(15) point Int @default(0) userFavorCategories UserFavorCategory[] diff --git a/src/auth.config.js b/src/auth.config.js new file mode 100644 index 0000000..874c61a --- /dev/null +++ b/src/auth.config.js @@ -0,0 +1,98 @@ +import dotenv from 'dotenv'; +import {Strategy as GoogleStrategy} from 'passport-google-oauth20'; +import {prisma} from "./db.config.js"; +import jwt from 'jsonwebtoken'; //json 생성을 위해 import +import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt'; + +dotenv.config(); +const secret = process.env.JWT_SECRET; // JWT 비밀 키 + +export const generateAccessToken = (user) => { + return jwt.sign( + {id: user.id, email: user.email}, + secret, + {expiresIn: '1h'} // 토큰 유효 기간 설정 + ); +}; + +export const generateRefreshToken = (user) => { + return jwt.sign( + {id: user.id, email: user.email}, + secret, + {expiresIn: '14d'} // 리프레시 토큰 유효 기간 설정 + ); +}; + +// Google Verify +const googleVerify = async (profile) => { + const email = profile.emails[0].value; + if (!email) { + throw new Error(`profile.email was not found: ${profile}`); + } + + const user = await prisma.user.findFirst({where: {email }}); + if (user) { + return {id: user.id, email: user.email, name: user.name}; + } + + const created = await prisma.user.create({ + data: { + email, + name: profile.displayName, + gender: null, + birth: null, + address: null, + detailAddress: null, + phoneNumber: null, + }, + }); + + return {id: created.id, email: created.email, name: created.name}; +}; + + +// Google Strategy +export const googleStrategy = new GoogleStrategy( + { + clientID: process.env.PASSPORT_GOOGLE_CLIENT_ID, + clientSecret: process.env.PASSPORT_GOOGLE_CLIENT_SECRET, + callbackURL: "/oauth2/callback/google", + scope: ["email", "profile"], + }, + + async (accessToken, refreshToken, profile, cd) => { + try { + const user = await googleVerify(profile); + + const jwtAccessToken = generateAccessToken(user); + const jwtRefreshToken = generateRefreshToken(user); + + return cd(null, { + accessToken: jwtAccessToken, + refreshToken: jwtRefreshToken, + }); + } catch (err) { + return cd(err); + } + } + +) + +const jwtOptions = { + // 요청 헤더의 'Autiorization'에서 'Bearer ' 토큰을 추출 + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: process.env.JWT_SECRET, +}; + +export const jwtStrategy = new JwtStrategy(jwtOptions, async (payload, done) => { + try { + const user = await prisma.user.findFirst({where: {id: payload.id}}); + if (user) { + return done(null, user); + } else { + return done(null, false); + } + }catch (err) { + return done(err, false); + } +}); \ No newline at end of file diff --git a/src/controllers/mission.controller.js b/src/controllers/mission.controller.js index aa47a9f..6d89aca 100644 --- a/src/controllers/mission.controller.js +++ b/src/controllers/mission.controller.js @@ -59,6 +59,7 @@ export const handleAddMission = async (req, res, next) => { export const handleChallengeMission = async (req, res, next) => { /* #swagger.summary = '미션 도전하기 API'; + #swagger.security = [{ "bearerAuth": [] }] #swagger.parameters['missionId'] = { description: '미션 ID', type: 'number' }; #swagger.requestBody = { required: true, @@ -67,7 +68,6 @@ export const handleChallengeMission = async (req, res, next) => { schema: { type: "object", properties: { - userId: { type: "number", example: 1 } } } } @@ -95,7 +95,12 @@ export const handleChallengeMission = async (req, res, next) => { }; */ console.log("미션 도전하기를 요청했습니다"); - const challengeData = bodyToChallenge(req.body, req.params); + console.log("params (missionId):", req.params); + console.log("user (from jwt):", req.user); // 토큰 정보 확인용 + + // req.user.id를 DTO의 3번째 인자로 전달 + const challengeData = bodyToChallenge(req.body, req.params, req.user.id); + const newChallenge = await challengeMission(challengeData); res.status(StatusCodes.CREATED).success(newChallenge); }; diff --git a/src/controllers/review.controller.js b/src/controllers/review.controller.js index e645bf0..2f4ceb8 100644 --- a/src/controllers/review.controller.js +++ b/src/controllers/review.controller.js @@ -5,6 +5,7 @@ import { createReview } from "../services/review.service.js"; export const handleAddReview = async (req, res, next) => { /* #swagger.summary = '리뷰 작성 API'; + #swagger.security = [{ "bearerAuth": [] }]; #swagger.parameters['restaurantId'] = { description: '가게 ID', type: 'number' }; #swagger.requestBody = { required: true, @@ -13,8 +14,7 @@ export const handleAddReview = async (req, res, next) => { schema: { type: "object", properties: { - userId: { type: "number", example: 1 }, - content: { type: "string", example: "맛있어요!" }, + content: { type: "string", example: "정말 맛있어요!" }, rating: { type: "number", example: 4.5 } } } @@ -43,12 +43,37 @@ export const handleAddReview = async (req, res, next) => { } } }; + #swagger.responses[400] = { + description: "가게가 존재하지 않음", + content: { + "application/json": { + schema: { + type: "object", + properties: { + resultType: { type: "string", example: "FAIL" }, + error: { + type: "object", + properties: { + errorCode: { type: "string", example: "R001" }, + reason: { type: "string", example: "존재하지 않는 가게입니다." }, + data: { type: "object" } + } + }, + success: { type: "object", nullable: true, example: null } + } + } + } + } + }; */ console.log("리뷰 추가를 요청했습니다"); console.log("params (restaurantId):", req.params); - console.log("body (userId, content, rating):", req.body); + console.log("body (content, rating):", req.body); + console.log("user (from jwt):", req.user); // 토큰에서 추출한 유저 정보 확인 + + // req.user.id를 DTO의 3번째 인자로 전달 + const reviewData = bodyToReview(req.body, req.params, req.user.id); - const reviewData = bodyToReview(req.body, req.params); const newReview = await createReview(reviewData); res.status(StatusCodes.CREATED).success(newReview); }; \ No newline at end of file diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js index d2331b8..de3071b 100644 --- a/src/controllers/user.controller.js +++ b/src/controllers/user.controller.js @@ -4,6 +4,7 @@ import { userSignUp, listUserReviews, listUserMissions, + updateMyInfo, } from "../services/user.service.js"; export const handleUserSignUp = async (req, res, next) => { @@ -174,4 +175,36 @@ export const handleListUserMissions = async (req, res, next) => { typeof req.query.cursor === "string" ? parseInt(req.query.cursor) : 0 ); res.status(StatusCodes.OK).success(missions); +}; + +// 내 정보 수정 컨트롤러 +export const handleUpdateMyInfo = async (req, res, next) => { + /* + #swagger.summary = '내 정보 수정 API'; + #swagger.security = [{ "bearerAuth": [] }] + #swagger.requestBody = { + required: true, + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { type: "string", example: "홍길동" }, + gender: { type: "string", example: "MALE" }, + birth: { type: "string", example: "1999-01-01" }, + address: { type: "string", example: "서울시 강남구" }, + detailAddress: { type: "string", example: "101호" }, + phoneNumber: { type: "string", example: "010-1234-5678" } + } + } + } + } + }; + */ + console.log("내 정보 수정을 요청했습니다!"); + console.log("body:", req.body); + + // 🚨 중요: userId는 req.body가 아니라 req.user.id (토큰)에서 가져옵니다. + const updatedUser = await updateMyInfo(req.user.id, req.body); + res.status(StatusCodes.OK).success(updatedUser); }; \ No newline at end of file diff --git a/src/dtos/mission.dto.js b/src/dtos/mission.dto.js index 5126b79..940f9d7 100644 --- a/src/dtos/mission.dto.js +++ b/src/dtos/mission.dto.js @@ -19,9 +19,9 @@ export const responseFromMission = (data) => { }; // 미션 도전하기 요청 DTO -export const bodyToChallenge = (body, params) => { +export const bodyToChallenge = (body, params, userId) => { return { - userId: body.userId, + userId: userId, missionId: params.missionId, }; }; diff --git a/src/dtos/review.dto.js b/src/dtos/review.dto.js index 83e39b0..1071b20 100644 --- a/src/dtos/review.dto.js +++ b/src/dtos/review.dto.js @@ -1,10 +1,10 @@ -export const bodyToReview = (body, params) => { +export const bodyToReview = (body, params, userId) => { return { - userId: parseInt(body.userId), - restaurantId: parseInt(params.restaurantId), + userId: userId, // 컨트롤러에서 넘겨준 토큰 ID 사용 + restaurantId: parseInt(params.restaurantId), content: body.content, - rating: body.rating, + rating: body.rating, }; }; diff --git a/src/dtos/user.dto.js b/src/dtos/user.dto.js index 6f7332d..5c1a88c 100644 --- a/src/dtos/user.dto.js +++ b/src/dtos/user.dto.js @@ -42,4 +42,17 @@ export const responseFromUserMissions = (missions) => { cursor: missions.length ? missions[missions.length - 1].id : null, }, }; +}; + +// 사용자 정보 수정 요청 +export const bodyToUserUpdate = (body) => { + return { + name: body.name || undefined, + gender: body.gender || undefined, + birth: body.birth ? new Date(body.birth) : undefined, + address: body.address || undefined, + detailAddress: body.detailAddress || undefined, + phoneNumber: body.phoneNumber || undefined, + preferences: body.preferences || [], + }; }; \ No newline at end of file diff --git a/src/error.js b/src/error.js index 183a9f2..0007284 100644 --- a/src/error.js +++ b/src/error.js @@ -1,6 +1,7 @@ // 이메일 중복 export class DuplicateUserEmailError extends Error { errorCode = "U001"; + statusCode = 400; constructor(reason, data) { super(reason); @@ -12,6 +13,7 @@ export class DuplicateUserEmailError extends Error { // 존재하지 않는 가게 export class RestaurantNotFoundError extends Error { errorCode = "R001"; + statusCode = 404; constructor(reason, data) { super(reason); @@ -23,6 +25,7 @@ export class RestaurantNotFoundError extends Error { // 존재하지 않는 미션 export class MissionNotFoundError extends Error { errorCode = "M001"; + statusCode = 404; constructor(reason, data) { super(reason); @@ -34,6 +37,8 @@ export class MissionNotFoundError extends Error { // 존재하지 않는 유저 미션 export class MissionUserNotFoundError extends Error { errorCode = "M002"; + statusCode = 404; + constructor(reason, data) { super(reason); this.reason = reason; @@ -46,6 +51,7 @@ export class MissionUserNotFoundError extends Error { // 존재하지 않는 유저 export class UserNotFoundError extends Error { errorCode = "U002"; + statusCode = 404; constructor(reason, data) { super(reason); @@ -70,6 +76,7 @@ export class InternalServerError extends Error { // 이미 도전중인 미션 export class MissionAlreadyChallengedError extends Error { errorCode = "M003"; + statusCode = 400; constructor(reason, data) { super(reason); @@ -81,6 +88,8 @@ export class MissionAlreadyChallengedError extends Error { // 이미 완료한 미션 export class MissionAlreadyCompletedError extends Error { errorCode = "M004"; + statusCode = 400; + constructor(reason, data) { super(reason); this.reason = reason; diff --git a/src/index.js b/src/index.js index 4154391..f194c98 100644 --- a/src/index.js +++ b/src/index.js @@ -5,12 +5,15 @@ import morgan from "morgan"; import cookieParser from "cookie-parser"; import swaggerAutogen from "swagger-autogen"; import swaggerUiExpress from "swagger-ui-express"; - +import passport from "passport"; +import { googleStrategy, jwtStrategy } from "./auth.config.js"; +import {prisma} from "./db.config.js"; import { handleUserSignUp, handleListUserReviews, handleListUserMissions, + handleUpdateMyInfo, } from "./controllers/user.controller.js"; import { handleAddRestaurant, @@ -25,6 +28,9 @@ import { dotenv.config(); +passport.use(googleStrategy); +passport.use(jwtStrategy); + const app = express(); const port = process.env.PORT; @@ -54,6 +60,7 @@ app.use(express.static("public")); // 정적 파일 접근 app.use(express.json()); // request의 본문을 json으로 해석할 수 있도록 함 (JSON 형태의 요청 body를 파싱하기 위함) app.use(express.urlencoded({ extended: false })); // 단순 객체 문자열 형태로 본문 데이터 해석 app.use(cookieParser()); +app.use(passport.initialize()); app.use( "/docs", @@ -86,6 +93,8 @@ app.get("/openapi.json", async (req, res, next) => { res.json(result ? result.data : null); }); +const isLogin = passport.authenticate("jwt", { session: false }); + // ... app.get("/", (req, res) => { @@ -93,14 +102,44 @@ app.get("/", (req, res) => { }); app.post("/api/v1/users/signup", handleUserSignUp); -app.post("/api/v1/restaurants", handleAddRestaurant); -app.post("/api/v1/restaurants/:restaurantId/reviews", handleAddReview); -app.post("/api/v1/restaurants/:restaurantId/missions", handleAddMission); -app.post("/api/v1/missions/:missionId/challenge", handleChallengeMission); +app.post("/api/v1/restaurants", isLogin, handleAddRestaurant); +app.post("/api/v1/restaurants/:restaurantId/reviews", isLogin, handleAddReview); +app.post("/api/v1/restaurants/:restaurantId/missions",isLogin, handleAddMission); +app.post("/api/v1/missions/:missionId/challenge", isLogin, handleChallengeMission); app.get("/api/v1/restaurants/:restaurantId/reviews", handleListRestaurantReviews); app.get("/api/v1/users/:userId/reviews", handleListUserReviews); app.get("/api/v1/users/:userId/missions", handleListUserMissions) -app.patch("/api/v1/missions/:userMissionId/complete", handleCompleteMission); +app.patch("/api/v1/missions/:userMissionId/complete", isLogin, handleCompleteMission); +app.patch("/api/v1/users/me", isLogin, handleUpdateMyInfo); + +app.get("/oauth2/login/google", + passport.authenticate("google", { session: false } + ) +); +app.get("/oauth2/callback/google", + passport.authenticate("google", { session: false, failureRedirect: "/login-failed" }), + (req, res) => { + const tokens = req.user; + + res.status(200).json({ + resultType: "SUCCESS", + error: null, + success: { + message: "Google 로그인 성공!", + tokens: tokens, + } + }); + + } +); + + +app.get('/mypage', isLogin, (req, res) => { + res.status(200).success({ + message: `인증 성공! ${req.user.name}님의 마이페이지입니다.`, + user: req.user, + }); +}); /** * 전역 오류를 처리하기 위한 미들웨어 diff --git a/src/repositories/mission.repository.js b/src/repositories/mission.repository.js index e53c348..a915714 100644 --- a/src/repositories/mission.repository.js +++ b/src/repositories/mission.repository.js @@ -40,7 +40,7 @@ export const getMissionById = async (missionId) => { // 사용자가 특정 미션에 도전 중인지 확인 (검증용) export const checkUserMissionExists = async (userId, missionId) => { - const existingMission = await prisma.user_mission.findUnique({ + const existingMission = await prisma.userMission.findUnique({ where: { userId_missionId: { userId: userId, @@ -83,15 +83,20 @@ export const getUserMissionById = async (userMissionId) => { try { const userMission = await prisma.userMission.findUnique({ where: { id: userMissionId }, - include: { - mission: true, // 포인트 정보를 알기 위해 - user: true, // 유저 정보를 알기 위해 - }, + select: { + id: true, + status: true, + userId: true, + missionId: true, + user: true, + mission: true, + }, }); + return userMission; + } catch (err) { - console.error(err); - throw new InternalServerError(`DB 오류가 발생했습니다: ${err.message}`); + throw new InternalServerError(`DB 오류가 발생했습니다: ${err.message}`, err); } }; diff --git a/src/repositories/user.repository.js b/src/repositories/user.repository.js index 39c50a1..23aa69c 100644 --- a/src/repositories/user.repository.js +++ b/src/repositories/user.repository.js @@ -76,4 +76,20 @@ export const getAllUserMissions = async (userId, cursor) => { take: 5, }); return missions; +}; + +// 유저 정보 수정 +export const updateUserInfo = async (userId, data) => { + const updatedUser = await prisma.user.update({ + where: { id: userId }, + data: { + name: data.name, + gender: data.gender, + birth: data.birth, + address: data.address, + detailAddress: data.detailAddress, + phoneNumber: data.phoneNumber, + }, + }); + return updatedUser; }; \ No newline at end of file diff --git a/src/services/user.service.js b/src/services/user.service.js index eba9076..00e3986 100644 --- a/src/services/user.service.js +++ b/src/services/user.service.js @@ -2,6 +2,7 @@ import { responseFromUser, responseFromReviews, responseFromUserMissions, + bodyToUserUpdate } from '../dtos/user.dto.js'; import { addUser, @@ -10,7 +11,9 @@ import { setPreference, getAllUserReviews, getAllUserMissions, + updateUserInfo, } from "../repositories/user.repository.js"; +import { DuplicateUserEmailError } from "../error.js"; export const userSignUp = async (data) => { const joinUserId = await addUser({ @@ -46,4 +49,15 @@ export const listUserMissions = async (userId, cursor) => { console.log(`[Service] Got userId: ${userId}, Got cursor: ${cursor}`); const missions = await getAllUserMissions(userId, cursor);   return responseFromUserMissions(missions); +}; + +// 사용자 정보 수정 서비스 +export const updateMyInfo = async (userId, body) => { + const updateData = bodyToUserUpdate(body); + + const updatedUser = await updateUserInfo(userId, updateData); + + const preferences = await getUserPreferencesByUserId(userId); + + return responseFromUser({ user: updatedUser, preferences }); }; \ No newline at end of file