diff --git a/backend/app.js b/backend/app.js index cda30ae0..ee549fe3 100644 --- a/backend/app.js +++ b/backend/app.js @@ -7,6 +7,8 @@ import mongoose from 'mongoose' import controller from './controller' import statusCode from './util/statusCode' import resMessage from './util/resMessage' +import passport from 'passport' +import passportConfig from './config/passport' import './chatServer' import cors from 'cors' @@ -26,8 +28,11 @@ app.use(logger('dev')) app.use(cors({ origin: true, credentials: true })) app.use(express.json()) app.use(express.urlencoded({ extended: false })) -app.use(cookieParser()) app.use(express.static(path.join(__dirname, '../dist'))) +app.use(cookieParser(process.env.COOKIE_SECRET)) +app.use(passport.initialize()) +app.use(cors({ origin: true, credentials: true })) +passportConfig() app.use('/api', controller) app.use('/docs', express.static(path.join(__dirname, './docs'))) diff --git a/backend/config/passport.js b/backend/config/passport.js new file mode 100644 index 00000000..0e4fd4d9 --- /dev/null +++ b/backend/config/passport.js @@ -0,0 +1,87 @@ +const passport = require('passport') +const GitHubStrategy = require('passport-github').Strategy +const JWTStrategy = require('passport-jwt').Strategy +const { User } = require('../model/User') + +require('dotenv').config() + +const githubStrategyOption = { + clientID: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + callbackURL: process.env.GITHUB_CALLBACK_URL, +} + +async function gitStrategyLogin(profiles) { + try { + let user = await User.findOne({ OAuthId: profiles.id }) + if (user === null) { + const data = await User.create({ + OAuthId: profiles.id, + fullName: profiles.username, + isDeleted: false, + }) + return { + success: true, + id: data._id, + } + } + return { + success: true, + id: user._id, + } + } catch (err) { + return { success: false } + } +} + +async function githubVerify(accessToken, refreshToken, profile, done) { + try { + const result = await gitStrategyLogin(profile) + const user = { id: result.id } + + if (result.success) { + return done(null, user) + } + return done(null, false, { message: '깃허브 로그인에 실패했습니다.' }) + } catch (err) { + return done(null, false, { message: 'GitHub verify err 발생' }) + } +} + +const cookieExtractor = req => { + if (req.signedCookies) return req.signedCookies.token + if (req.cookies) return req.cookies +} + +const isExist = async userId => { + try { + let user = await User.findOne({ _id: userId }) + return { + success: true, + id: user._id, + } + } catch (err) { + return { success: false } + } +} + +const jwtStrategyOption = { + jwtFromRequest: cookieExtractor, + secretOrKey: process.env.JWT_SECRET, +} +async function jwtVerify(payload, done) { + try { + const result = await isExist(payload.id) + if (!result.success) { + return done(null, false, { message: 'JWT 토큰 인증에 실패했습니다.' }) + } + return done(null, result) + } catch (err) { + return done(null, false, { message: 'JWT verify err 발생' }) + } +} + +module.exports = () => { + passport.use(new GitHubStrategy(githubStrategyOption, githubVerify)) + passport.use(new JWTStrategy(jwtStrategyOption, jwtVerify)) +} diff --git a/backend/controller/index.js b/backend/controller/index.js index 1578f4fc..1e0ce416 100644 --- a/backend/controller/index.js +++ b/backend/controller/index.js @@ -1,8 +1,11 @@ import express from 'express' +import userController from './user' import channelController from './channel' const router = express.Router() router.use('/channel', channelController) +router.use('/user', userController) + module.exports = router diff --git a/backend/controller/user/index.js b/backend/controller/user/index.js new file mode 100644 index 00000000..8fc33cde --- /dev/null +++ b/backend/controller/user/index.js @@ -0,0 +1,9 @@ +import express from 'express' +const router = express.Router() +const controller = require('./userController') + +router.get('/sign-in/github', controller.githubLogin) +router.get('/sign-in/github/callback', controller.githubCallback) +router.get('/auth', controller.authCheck) + +module.exports = router diff --git a/backend/controller/user/userController.js b/backend/controller/user/userController.js new file mode 100644 index 00000000..b3e524ec --- /dev/null +++ b/backend/controller/user/userController.js @@ -0,0 +1,40 @@ +const passport = require('passport') +const jwt = require('jsonwebtoken') + +exports.githubLogin = passport.authenticate('github') + +exports.githubCallback = async (req, res, next) => { + const frontHost = process.env.FRONTEND_HOST + passport.authenticate('github', (err, id) => { + if (err || !id) { + return res.status(200).redirect(frontHost) + } + req.login(id, { session: false }, err => { + if (err) { + res.send(err) + } + + const token = jwt.sign(id, process.env.JWT_SECRET, { expiresIn: '1H' }) + res.cookie('token', token, { + maxAge: 1000 * 60 * 60, + httpOnly: true, + signed: true, + }) + return res.status(200).redirect(frontHost) + }) + })(req, res) +} + +exports.authCheck = (req, res) => { + let token = req.signedCookies.token + if (token) { + try { + let decoded = jwt.verify(token, process.env.JWT_SECRET) + return res.json({ verify: true }) + } catch (err) { + return res.json({ verify: false }) + } + } else { + return res.json({ verify: false, message: 'token does not exist' }) + } +} diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 441d1e98..0912c14c 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -343,6 +343,48 @@ paths: description: bad request default: description: Default error sample response + /api/user/sign-in/github: + summary: workspace user login + description: selectworkspace user login + post: + summary: workspace user login + description: workspace user login + operationId: '' + parameters: + - name: oauthId + in: query + description: workspace user id + required: true + explode: true + schema: + type: integer + responses: + '200': + description: signin success + '400': + description: bad input parameter + /api/user/sign-out: + summary: workspace user signout + description: selectworkspace user signout + delete: + summary: workspace user signout + description: workspace user signout + operationId: '' + parameters: + - name: workspaceUserInfoId + in: query + description: workspace user id + required: true + explode: true + schema: + type: integer + responses: + '200': + description: search results matching criteria + '400': + description: bad input parameter + + components: schemas: InventoryItem: diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js new file mode 100644 index 00000000..0dbf6edd --- /dev/null +++ b/backend/middleware/auth.js @@ -0,0 +1,12 @@ +const passport = require('passport') +require('dotenv').config() + +exports.Auth = (req, res, next) => { + passport.authenticate('jwt', { session: false }, (err, user) => { + if (err || !user || !user.success) { + next({ status: 403, message: 'auth error' }) + } + req.user = user + next() + })(req, res, next) +} diff --git a/backend/package-lock.json b/backend/package-lock.json index 91f92655..4a0e0752 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1357,6 +1357,10 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" "base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", @@ -1534,6 +1538,11 @@ "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz", "integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg==" }, + "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": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -1924,6 +1933,14 @@ "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" }, + "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==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -3076,6 +3093,49 @@ "minimist": "^1.2.5" } }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "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": "^5.6.0" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "kareem": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.1.tgz", @@ -3150,6 +3210,41 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", @@ -3430,6 +3525,11 @@ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" }, + "oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE=" + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3589,6 +3689,49 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "passport": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz", + "integrity": "sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1" + } + }, + "passport-github": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/passport-github/-/passport-github-1.1.0.tgz", + "integrity": "sha1-jOHj/NYa11eOsd9ZWDnkrqEjVdQ=", + "requires": { + "passport-oauth2": "1.x.x" + } + }, + "passport-jwt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.0.tgz", + "integrity": "sha512-BwC0n2GP/1hMVjR4QpnvqA61TxenUMlmfNjYNgK0ZAs0HK4SOQkHcSv4L328blNTLtHq7DbmvyNJiH+bn6C5Mg==", + "requires": { + "jsonwebtoken": "^8.2.0", + "passport-strategy": "^1.0.0" + } + }, + "passport-oauth2": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.5.0.tgz", + "integrity": "sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ==", + "requires": { + "base64url": "3.x.x", + "oauth": "0.9.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" + }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -3633,6 +3776,11 @@ } } }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" + }, "picomatch": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", @@ -4506,6 +4654,11 @@ "is-typedarray": "^1.0.0" } }, + "uid2": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", + "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=" + }, "undefsafe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", diff --git a/backend/package.json b/backend/package.json index 7644ffd5..f17b0f58 100644 --- a/backend/package.json +++ b/backend/package.json @@ -19,15 +19,19 @@ "homepage": "https://github.com/boostcamp-2020/Project12-C-Slack-Web", "dependencies": { "concurrently": "^5.3.0", - "cookie-parser": "~1.4.4", + "cookie-parser": "^1.4.5", "core-js": "^3.6.5", "cors": "^2.8.5", "debug": "~2.6.9", "dotenv": "^8.2.0", "express": "~4.16.1", + "jsonwebtoken": "^8.5.1", "mongoose": "^5.10.15", "morgan": "~1.9.1", "nodemon": "^2.0.4", + "passport": "^0.4.1", + "passport-github": "^1.1.0", + "passport-jwt": "^4.0.0" "socket.io": "^3.0.3" }, "devDependencies": { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6655d449..a0565901 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5489,6 +5489,14 @@ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.1.1.tgz", "integrity": "sha512-5Kgy8Cz6LPC9DJcNb3yjAXTu3XihQgEdnIg50c//zOC/MyLP0Clg+Y8Sh9ZjjnvBrDZU4DgXS9C3T9r4/scGZQ==" }, + "axios": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.0.tgz", + "integrity": "sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index beb00066..6fa08c4e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,6 +7,7 @@ "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", + "axios": "^0.21.0", "react": "^17.0.1", "react-dom": "^17.0.1", "react-router-dom": "^5.2.0", diff --git a/frontend/src/hooks/Auth.js b/frontend/src/hooks/Auth.js new file mode 100644 index 00000000..b9b3dd73 --- /dev/null +++ b/frontend/src/hooks/Auth.js @@ -0,0 +1,31 @@ +import React, { useState, useEffect } from 'react' +import request from '../util/request' + +export default function Auth(Component, loginRequired) { + function Authentication(props) { + const [loading, setloading] = useState(true) + useEffect(() => { + ;(async () => { + try { + const data = await request.GET('/api/user/auth') + if (!data.verify) { + // 로그인이 되어 있지 않을때 + if (loginRequired) { + props.history.push('/login') + } + } else { + if (!loginRequired) { + // 로그인 유저가 접근하면 안되는 페이지 + props.history.push('/') + } + } + setloading(false) + } catch (err) { + console.error(err) + } + })() + }, []) + return !loading && + } + return Authentication +} diff --git a/frontend/src/index.js b/frontend/src/index.js index 4742c73f..ffab71bc 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -1,14 +1,18 @@ import React from 'react' import ReactDOM from 'react-dom' import './index.css' - import Channel from './page/channel/Channel' import { BrowserRouter, Route } from 'react-router-dom' import reportWebVitals from './reportWebVitals' +import LoginPage from './page/LoginPage' +import WorkspaceSelectPage from './page/WorkspaceSelectPage' +import Auth from './hooks/Auth' ReactDOM.render( + + \ , diff --git a/frontend/src/page/LoginPage.js b/frontend/src/page/LoginPage.js new file mode 100644 index 00000000..2bcc368c --- /dev/null +++ b/frontend/src/page/LoginPage.js @@ -0,0 +1,37 @@ +import React from 'react' +import styled from 'styled-components' + +const LoginPage = () => { + return ( + <> +

Slack에 로그인

+
+ Login With github +
+ + ) +} + +const LoginButton = styled.button` + display: flex; + justify-content: center; + align-items: center; + border: 1px solid #1da1f2; + outline: none; + background-color: white; + width: 12rem; + height: 2rem; + border-radius: 20px; + & > * { + padding-right: 10px; + } + :hover { + background-color: #fcf7f7; + cursor: pointer; + } + :active { + background-color: #ebebeb; + } +` + +export default LoginPage diff --git a/frontend/src/page/WorkspaceSelectPage.js b/frontend/src/page/WorkspaceSelectPage.js new file mode 100644 index 00000000..4b35a21f --- /dev/null +++ b/frontend/src/page/WorkspaceSelectPage.js @@ -0,0 +1,12 @@ +import React from 'react' + +const WorkspaceSelectPage = () => { + return ( + <> +

내 워크스페이스

+

새 워크스페이스 생성

+ + ) +} + +export default WorkspaceSelectPage diff --git a/frontend/src/util/request.js b/frontend/src/util/request.js new file mode 100644 index 00000000..cb53d631 --- /dev/null +++ b/frontend/src/util/request.js @@ -0,0 +1,81 @@ +import axios from 'axios' + +const baseURL = /*process.env.API_URL | */ 'http://localhost:5000' + +const options = { + withCredentials: true, + baseURL, +} + +const GET = async (path, params = null) => { + try { + const res = await axios.get(baseURL + path, { params, ...options }) + return res.data + } catch (err) { + console.error(err) + } +} + +const POST = async (path, data, contentType = 'application/json') => { + try { + const response = await axios.post(baseURL + path, data, { + headers: { + 'Content-Type': contentType, + }, + ...options, + }) + return response.data + } catch (err) { + console.error(err) + } +} + +const DELETE = async (path, params = null) => { + try { + const response = await axios.delete(baseURL + path, { + params, + ...options, + }) + return response.data + } catch (err) { + console.error(err) + } +} + +const PATCH = async (path, data, contentType = 'application/json') => { + try { + const response = await axios.patch(baseURL + path, data, { + headers: { + 'Content-Type': contentType, + }, + ...options, + }) + return response.data + } catch (err) { + console.error(err) + } +} + +const PUT = async (path, data, contentType = 'application/json') => { + try { + const response = await axios.put(baseURL + path, data, { + headers: { + 'Content-Type': contentType, + }, + ...options, + }) + return response.data + } catch (err) { + console.error(err) + } +} + +const request = { + GET, + POST, + DELETE, + PATCH, + PUT, +} + +export default request