diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..ae495ad0 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,8 @@ +# This is a comment. +# Each line is a file pattern followed by one or more owners. + +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, +# @global-owner1 and @global-owner2 will be requested for +# review when someone opens a pull request. +* @boostcamp-2020/project12-c diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE new file mode 100644 index 00000000..dae0896e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE @@ -0,0 +1,8 @@ +## Linked Issue +close # + +## 공유할 사항 +- + +## 논의할 사항 +- 없습니다. ❌ diff --git a/backend/app.js b/backend/app.js index 2278f175..ee549fe3 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1,3 +1,4 @@ +require('dotenv').config() import express from 'express' import path from 'path' import cookieParser from 'cookie-parser' @@ -6,7 +7,10 @@ import mongoose from 'mongoose' import controller from './controller' import statusCode from './util/statusCode' import resMessage from './util/resMessage' -require('dotenv').config() +import passport from 'passport' +import passportConfig from './config/passport' +import './chatServer' +import cors from 'cors' const app = express() @@ -21,10 +25,14 @@ mongoose .catch(err => console.error(err)) 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/chatServer.js b/backend/chatServer.js new file mode 100644 index 00000000..da78cfcd --- /dev/null +++ b/backend/chatServer.js @@ -0,0 +1,31 @@ +import { config as dotenv } from 'dotenv' +import express from 'express' +import { createServer } from 'http' +import createChatServer from 'socket.io' +dotenv() + +const server = createServer(express()) +const io = createChatServer(server, { + cors: { origin: process.env.FRONTEND_HOST, credentials: true }, +}) + +const namespace = io.of('chat') +namespace.use((socket, next) => { + // TODO jwt 검증 로직 필요 + next() +}) + +namespace.on('connection', socket => { + socket.on('new message', data => { + // TODO 특정 채널로 전송하도록 변경, db에 저장 필요 (현재는 자신 제외 전체 전송) + socket.broadcast.emit('new message', { + message: data, + }) + }) +}) + +server.listen(process.env.CHAT_PORT, () => { + console.log('chat server created 4000') +}) + +export default server 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/channel/channel.js b/backend/controller/channel/channel.js index 000468cb..712f6f6b 100644 --- a/backend/controller/channel/channel.js +++ b/backend/controller/channel/channel.js @@ -1,9 +1,11 @@ +import { asyncWrapper } from '../../util' +import service from '../../service/channel' + const { WorkspaceUserInfo } = require('../../model/WorkspaceUserInfo') const { Channel } = require('../../model/Channel') const { ChannelConfig } = require('../../model/ChannelConfig') const { Chat } = require('../../model/Chat') -/* GET /api/channle get channel list */ const getChannelList = async (req, res, next) => { try { const workspaceUserInfoId = req.query.workspaceUserInfoId @@ -116,9 +118,20 @@ const muteChannel = async (req, res, next) => { } } + +const createChannel = asyncWrapper(async (req, res) => { + const { code, success, data } = await service.createChannel({ + ...req.body, + creator: req.user, + }) + return res.status(code).json({ success, data }) +}) + module.exports = { getChannelList, getChannelHeaderInfo, inviteUser, muteChannel, + createChannel } + diff --git a/backend/controller/channel/index.js b/backend/controller/channel/index.js index a173dc40..a14f0c96 100644 --- a/backend/controller/channel/index.js +++ b/backend/controller/channel/index.js @@ -1,17 +1,20 @@ const express = require('express') const router = express.Router() -const channelController = require('./channel') +const controller = require('./channel') /* GET /api/channle get channel list */ -router.get('/', channelController.getChannelList) +router.get('/', controller.getChannelList) + + +router.post('/', controller.createChannel) /* GET /api/channle/{channelId}/info get channel header info */ -router.get('/:channelId/info', channelController.getChannelHeaderInfo) +router.get('/:channelId/info', controller.getChannelHeaderInfo) /* POST /api/channle/invite invite user to channel */ -router.post('/invite', channelController.inviteUser) +router.post('/invite', controller.inviteUser) /* PATCH /api/channle/mute mute channel */ -router.patch('/mute', channelController.muteChannel) +router.patch('/mute', controller.muteChannel) module.exports = router diff --git a/backend/controller/index.js b/backend/controller/index.js index 73c74a46..5c6e880e 100644 --- a/backend/controller/index.js +++ b/backend/controller/index.js @@ -1,10 +1,13 @@ import express from 'express' + import channelCotroller from './channel' import searchCotroller from './search' +import userController from './user' const router = express.Router() router.use('/channel', channelCotroller) router.use('/search', searchCotroller) +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 be368874..0912c14c 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -1,122 +1,424 @@ ---- openapi: 3.0.0 info: - description: This is a simple API - version: 1.0.0 - title: Simple Inventory API - contact: - email: you@your-company.com + title: Project12-C-Slack-clone API + description: Project12-C-Slack-clone API license: name: Apache 2.0 - url: http://www.apache.org/licenses/LICENSE-2.0.html -host: virtserver.swaggerhub.com -basePath: /solo295/test/1.0.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' + version: 1.0.0-oas3 +servers: + - url: 'https://virtserver.swaggerhub.com/solo295/test/1.0.0' tags: - name: admins description: Secured Admin-only calls - name: developers description: Operations available to regular developers -schemes: - - https paths: - /inventory: + /api/workspace: get: - tags: - - developers - summary: searches inventory - description: | - By passing in the appropriate options, you can search for - available inventory in the system - operationId: searchInventory - produces: - - application/json + summary: select user workspace + description: select user workspace userID 미들웨어에서 추가 parameters: - - name: searchString + - name: userID in: query - description: pass an optional search string for looking up inventory - required: false - type: string - - name: skip + description: user id + required: true + explode: true + schema: + type: integer + responses: + '200': + content: + application/json: + schema: + type: object + properties: + workspaces: + type: array + items: + type: object + properties: + workspaceID: + type: integer + workspaceName: + type: string + workspaceURL: + type: string + description: invite url + description: success + '400': + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + description: bad request + default: + description: Default error sample response + post: + summary: create new workspace + description: Create new workspace + responses: + '200': + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + url: + type: string + description: workspace url + description: success response + '400': + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + description: bad request + default: + description: Default error sample response + parameters: + - name: userID in: query - description: number of records to skip for pagination - required: false - type: integer - minimum: 0 - format: int32 - - name: limit + description: user id + required: true + style: form + explode: true + schema: + type: string + - name: name + in: query + description: workspace name + required: true + style: form + explode: true + schema: + type: string + - name: channelName in: query - description: maximum number of records to return + description: Channel name created by default required: false - type: integer - maximum: 50 - minimum: 0 - format: int32 + style: form + explode: true + schema: + type: string + /api/workspace/invite: + summary: invite to workspace + description: invite to workspace + post: + summary: invite to workspace + description: invite to workspace + operationId: '' responses: '200': - description: search results matching criteria + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + url: + type: string + description: invite url + description: success response + '400': + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + description: bad request + default: + description: Default error sample response + parameters: + - name: workspaceID + in: query + description: workspace id + required: true + style: form + explode: true + schema: + type: integer + - name: userID + in: query + description: user id + required: true + style: form + explode: true + schema: + type: string + + /api/thread/follow: + summary: update follow config + description: follow 설정을 변경한다. + patch: + summary: update follow config + description: follow 설정을 변경한다. + parameters: + - name: workspaceUserInfoId + in: query + description: workspaceUserInfoId + required: true + explode: true + schema: + type: integer + - name: chatId + in: query + description: chatId + required: true + explode: true schema: - type: array - items: - $ref: '#/definitions/InventoryItem' + type: integer + responses: + '200': + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + description: success response '400': - description: bad input parameter + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + description: bad request + default: + description: Default error sample response + /api/channel/description: + summary: update channel description + description: channel description 변경 + patch: + summary: update channel description + description: channel description 변경 + parameters: + - name: channelId + in: query + description: channelId + required: true + explode: true + schema: + type: integer + - name: description + in: query + description: 변경 될 description + required: true + explode: true + schema: + type: string + responses: + '200': + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + description: success response + '400': + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + description: bad request + default: + description: Default error sample response + /api/channel/topic: + summary: update channel topic + description: channel topic 변경 + patch: + summary: update channel topic + description: channel topic 변경 + parameters: + - name: channelId + in: query + description: channelId + required: true + explode: true + schema: + type: integer + - name: topic + in: query + description: 변경 될 topic + required: true + explode: true + schema: + type: string + responses: + '200': + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + description: success + '400': + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + description: bad request + default: + description: Default error sample response + /api/channel: + summary: create channel + description: channel 생성 post: - tags: - - admins - summary: adds an inventory item - description: Adds an item to the system - operationId: addInventory - consumes: - - application/json - produces: - - application/json + summary: create channel + description: channel 생성 parameters: - - in: body - name: inventoryItem - description: Inventory item to add - required: false + - name: title + in: query + description: channel title + required: true + explode: true schema: - $ref: '#/definitions/InventoryItem' + type: string + - name: creator + in: query + description: channel creator, middleware로 userId 전달 + required: true + explode: true + schema: + type: string + - name: description + in: query + description: channel description + explode: true + schema: + type: string + - name: channelType + in: query + description: channel type, (private = 0, public = 1, DM = 2) + required: true + explode: true + schema: + type: integer responses: - '201': - description: item created + '200': + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + channelId: + type: integer + description: channelId + description: success response + '400': - description: invalid input, object invalid - '409': - description: an existing item already exists -definitions: - InventoryItem: - type: object - required: - - id - - manufacturer - - name - - releaseDate - properties: - id: - type: string - format: uuid - example: d290f1ee-6c54-4b01-90e6-d701748f0851 - name: - type: string - example: Widget Adapter - releaseDate: - type: string - format: date-time - example: 2016-08-29T09:12:33.001Z - manufacturer: - $ref: '#/definitions/Manufacturer' - Manufacturer: - required: - - name - properties: - name: - type: string - example: ACME Corporation - homePage: - type: string - format: url - example: https://www.acme-corp.com - phone: - type: string - example: 408-867-5309 + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + 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: + required: + - id + - manufacturer + - name + - releaseDate + type: object + properties: + id: + type: string + format: uuid + example: d290f1ee-6c54-4b01-90e6-d701748f0851 + name: + type: string + example: Widget Adapter + releaseDate: + type: string + format: date-time + example: '2016-08-29T09:12:33.001Z' + manufacturer: + $ref: '#/components/schemas/Manufacturer' + Manufacturer: + required: + - name + properties: + name: + type: string + example: ACME Corporation + homePage: + type: string + format: url + example: 'https://www.acme-corp.com' + phone: + type: string + example: 408-867-5309 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/model/Channel.js b/backend/model/Channel.js index 9fedafd0..975d447f 100644 --- a/backend/model/Channel.js +++ b/backend/model/Channel.js @@ -5,6 +5,7 @@ const channelSchema = mongoose.Schema( { title: { type: String, + required: true, }, description: { type: String, @@ -15,9 +16,11 @@ const channelSchema = mongoose.Schema( creator: { type: Schema.Types.ObjectId, ref: 'WorkspaceUserInfo', + required: true, }, channelType: { type: Number, + required: true, }, isDeleted: { type: Boolean, diff --git a/backend/package-lock.json b/backend/package-lock.json index a6353923..4a0e0752 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1142,12 +1142,92 @@ "defer-to-connect": "^1.0.1" } }, + "@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.33", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", + "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", + "requires": { + "@types/node": "*" + } + }, + "@types/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg==" + }, + "@types/cors": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.8.tgz", + "integrity": "sha512-fO3gf3DxU2Trcbr75O7obVndW/X5k8rJNZkLXlQWStTHhP71PkRqjwPIEI0yMnJdg9R9OasjU+Bsr+Hr1xy/0w==", + "requires": { + "@types/express": "*" + } + }, + "@types/express": { + "version": "4.17.9", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.9.tgz", + "integrity": "sha512-SDzEIZInC4sivGIFY4Sz1GG6J9UObPwCInYJjko2jzOf/Imx/dlpume6Xxwj1ORL82tBbmN4cPDIDkLbWHk9hw==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.14.tgz", + "integrity": "sha512-uFTLwu94TfUFMToXNgRZikwPuZdOtDgs3syBtAIr/OXorL1kJqUJT9qCLnRZ5KBOWfZQikQ2xKgR2tnDj1OgDA==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, "@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "@types/mime": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", + "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==" + }, + "@types/node": { + "version": "14.14.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.10.tgz", + "integrity": "sha512-J32dgx2hw8vXrSbu4ZlVhn1Nm3GbeCFNw2FWL8S5QKucHGY0cyNwjdQdO+KMBZ4wpmC7KhLCiNsdk1RFRIYUQQ==" + }, + "@types/qs": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz", + "integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==" + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, + "@types/serve-static": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.8.tgz", + "integrity": "sha512-MoJhSQreaVoL+/hurAZzIm8wafFR6ajiTM1m4A0kv6AGeVBl4r4pOV8bGFrjjq1sGxDTnCoF8i22o0/aE5XCyA==", + "requires": { + "@types/mime": "*", + "@types/node": "*" + } + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -1277,6 +1357,15 @@ "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", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, "basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -1449,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", @@ -1610,6 +1704,11 @@ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "dev": true }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1716,6 +1815,15 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1825,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", @@ -1854,6 +1970,45 @@ "once": "^1.4.0" } }, + "engine.io": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-4.0.4.tgz", + "integrity": "sha512-4ggUX5pICZU17OTZNFv5+uFE/ZyoK+TIXv2SvxWWX8lwStllQ6Lvvs4lDBqvKpV9EYXNcvlNOcjKChd/mo+8Tw==", + "requires": { + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.1.0", + "engine.io-parser": "~4.0.0", + "ws": "^7.1.2" + }, + "dependencies": { + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "engine.io-parser": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.1.tgz", + "integrity": "sha512-v5aZK1hlckcJDGmHz3W8xvI3NUHYc9t8QtTbqdR5OaH3S9iJZilPubauOm+vLWOMMWzpE3hiq92l9lTAHamRCg==" + }, "enquirer": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", @@ -2938,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", @@ -3012,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", @@ -3292,6 +3525,16 @@ "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", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, "object-inspect": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", @@ -3446,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", @@ -3490,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", @@ -4003,6 +4294,66 @@ "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" }, + "socket.io": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-3.0.3.tgz", + "integrity": "sha512-TC1GnSXhDVmd3bHji5aG7AgWB8UL7E6quACbKra8uFXBqlMwEDbrJFK+tjuIY5Pe9N0L+MAPPDv3pycnn0000A==", + "requires": { + "@types/cookie": "^0.4.0", + "@types/cors": "^2.8.8", + "@types/node": "^14.14.7", + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.1.0", + "engine.io": "~4.0.0", + "socket.io-adapter": "~2.0.3", + "socket.io-parser": "~4.0.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "socket.io-adapter": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.0.3.tgz", + "integrity": "sha512-2wo4EXgxOGSFueqvHAdnmi5JLZzWqMArjuP4nqC26AtLh5PoCPsaRbRdah2xhcwTAMooZfjYiNVNkkmmSMaxOQ==" + }, + "socket.io-parser": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.1.tgz", + "integrity": "sha512-5JfNykYptCwU2lkOI0ieoePWm+6stEhkZ2UnLDjqnE1YEjUlXXLd1lpxPZ+g+h3rtaytwWkWrLQCaJULlGqjOg==", + "requires": { + "component-emitter": "~1.3.0", + "debug": "~4.1.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -4303,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", @@ -4571,6 +4927,11 @@ "typedarray-to-buffer": "^3.1.5" } }, + "ws": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz", + "integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==" + }, "xdg-basedir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index 8c075d77..f17b0f58 100644 --- a/backend/package.json +++ b/backend/package.json @@ -19,14 +19,20 @@ "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" + "nodemon": "^2.0.4", + "passport": "^0.4.1", + "passport-github": "^1.1.0", + "passport-jwt": "^4.0.0" + "socket.io": "^3.0.3" }, "devDependencies": { "@babel/core": "^7.12.8", diff --git a/backend/service/channel.js b/backend/service/channel.js new file mode 100644 index 00000000..dc8527ba --- /dev/null +++ b/backend/service/channel.js @@ -0,0 +1,16 @@ +import { Channel } from '../model/Channel' +import statusCode from '../util/statusCode' +import { verifyRequiredParams, dbErrorHandler } from '../util' + +const createChannel = async params => { + verifyRequiredParams(params.creator, params.title, params.channelType) + + const result = await dbErrorHandler(() => Channel.create(params)) + return { + code: statusCode.CREATED, + data: result, + success: true, + } +} + +module.exports = { createChannel } diff --git a/backend/util/index.js b/backend/util/index.js new file mode 100644 index 00000000..56fd3b67 --- /dev/null +++ b/backend/util/index.js @@ -0,0 +1,28 @@ +import resMessage from './resMessage' +import statusCode from './statusCode' + +const asyncWrapper = callback => { + return (req, res, next) => { + callback(req, res, next).catch(next) + } +} + +const verifyRequiredParams = (...params) => { + for (const param of params) + if (!param) + throw { status: statusCode.BAD_REQUEST, message: resMessage.OUT_OF_VALUE } +} + +const dbErrorHandler = async callback => { + try { + return await callback() + } catch (err) { + console.log(err) + throw { + status: statusCode.DB_ERROR, + message: resMessage.DB_ERROR, + } + } +} + +module.exports = { asyncWrapper, verifyRequiredParams, dbErrorHandler } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b475e908..a0565901 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -4231,6 +4231,11 @@ "integrity": "sha512-TbH79tcyi9FHwbyboOKeRachRq63mSuWYXOflsNO9ZyE5ClQ/JaozNKl+aWUq87qPNsXasXxi2AbgfwIJ+8GQw==", "dev": true }, + "@types/component-emitter": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz", + "integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==" + }, "@types/eslint": { "version": "7.2.5", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.5.tgz", @@ -5484,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", @@ -6255,6 +6268,11 @@ "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, "bail": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", @@ -6316,6 +6334,11 @@ } } }, + "base64-arraybuffer": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", + "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=" + }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -7234,6 +7257,11 @@ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -8751,6 +8779,43 @@ "objectorarray": "^1.0.4" } }, + "engine.io-client": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-4.0.4.tgz", + "integrity": "sha512-and4JRvjv+BQ4WBLopYUFePxju3ms3aBRk0XjaLdh/t9TKv2LCKtKKWFRoRzIfUZsu3U38FcYqNLuXhfS16vqw==", + "requires": { + "base64-arraybuffer": "0.1.4", + "component-emitter": "~1.3.0", + "debug": "~4.1.0", + "engine.io-parser": "~4.0.1", + "has-cors": "1.1.0", + "parseqs": "0.0.6", + "parseuri": "0.0.6", + "ws": "~7.2.1", + "xmlhttprequest-ssl": "~1.5.4", + "yeast": "0.1.2" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ws": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.5.tgz", + "integrity": "sha512-C34cIU4+DB2vMyAbmEKossWq2ZQDr6QEyuuCzWrM9zfw1sGc0mYiJ0UnG9zzNykt49C2Fi34hvr2vssFQRS6EA==" + } + } + }, + "engine.io-parser": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.1.tgz", + "integrity": "sha512-v5aZK1hlckcJDGmHz3W8xvI3NUHYc9t8QtTbqdR5OaH3S9iJZilPubauOm+vLWOMMWzpE3hiq92l9lTAHamRCg==" + }, "enhanced-resolve": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz", @@ -10804,6 +10869,11 @@ } } }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -14693,6 +14763,16 @@ "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" }, + "parseqs": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", + "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==" + }, + "parseuri": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", + "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==" + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -18476,6 +18556,50 @@ } } }, + "socket.io-client": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-3.0.3.tgz", + "integrity": "sha512-kwCJAKb6JMqE9ZYXg78Dgt8rYLSwtJ/g/LJqpb/pOTFRZMSr1cKAsCaisHZ+IBwKHBY7DYOOkjtkHqseY3ZLpw==", + "requires": { + "@types/component-emitter": "^1.2.10", + "backo2": "1.0.2", + "component-bind": "1.0.0", + "component-emitter": "~1.3.0", + "debug": "~4.1.0", + "engine.io-client": "~4.0.0", + "parseuri": "0.0.6", + "socket.io-parser": "~4.0.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "socket.io-parser": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.1.tgz", + "integrity": "sha512-5JfNykYptCwU2lkOI0ieoePWm+6stEhkZ2UnLDjqnE1YEjUlXXLd1lpxPZ+g+h3rtaytwWkWrLQCaJULlGqjOg==", + "requires": { + "component-emitter": "~1.3.0", + "debug": "~4.1.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, "sockjs": { "version": "0.3.20", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.20.tgz", @@ -21815,6 +21939,11 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, + "xmlhttprequest-ssl": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", + "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -21891,6 +22020,11 @@ } } }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" + }, "zwitch": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8fd6d15f..6fa08c4e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,10 +7,12 @@ "@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", "react-scripts": "4.0.0", + "socket.io-client": "^3.0.3", "styled-components": "^5.2.1", "web-vitals": "^0.2.4" }, diff --git a/frontend/src/atom/input/Input.js b/frontend/src/atom/input/Input.js new file mode 100644 index 00000000..3a32719c --- /dev/null +++ b/frontend/src/atom/input/Input.js @@ -0,0 +1,14 @@ +import React from 'react' + +function Input({ placeholder, handleChange, handleKey, value }) { + return ( + + ) +} + +export default Input diff --git a/frontend/src/atom/input/Input.stories.js b/frontend/src/atom/input/Input.stories.js new file mode 100644 index 00000000..ce7871f5 --- /dev/null +++ b/frontend/src/atom/input/Input.stories.js @@ -0,0 +1,18 @@ +import React from 'react' +import Input from './Input' +import { action } from '@storybook/addon-actions' + +export default { + title: 'Example/Input', + component: Input, +} + +const Template = args => + +export const MessageInput = Template.bind({}) +MessageInput.args = { + placeholder: 'Send a message to #example', + handleChange: action(e => { + console.log(e.target.value) + }), +} 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 6fd1433a..ffab71bc 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -1,14 +1,19 @@ import React from 'react' import ReactDOM from 'react-dom' import './index.css' -import App from './page/App' +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( - + + \ + , document.getElementById('root'), diff --git a/frontend/src/organism/messageEditor/MessageEditor.js b/frontend/src/organism/messageEditor/MessageEditor.js new file mode 100644 index 00000000..ca03a9e4 --- /dev/null +++ b/frontend/src/organism/messageEditor/MessageEditor.js @@ -0,0 +1,29 @@ +import React, { useState } from 'react' + +import Input from '../../atom/input/Input' + +function MessageEditor({ channelTitle, sendMessage }) { + const [message, setMessage] = useState('') + const handleInput = e => { + setMessage(e.target.value) + } + const handleKey = e => { + if (e.key === 'Enter') { + sendMessage(message) + setMessage('') + } + } + return ( +
+ + {/* TODO markdown, chat action 적용 필요 */} +
+ ) +} + +export default MessageEditor diff --git a/frontend/src/page/App.css b/frontend/src/page/App.css deleted file mode 100644 index 74b5e053..00000000 --- a/frontend/src/page/App.css +++ /dev/null @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/frontend/src/page/App.js b/frontend/src/page/App.js deleted file mode 100644 index 44ee2c32..00000000 --- a/frontend/src/page/App.js +++ /dev/null @@ -1,25 +0,0 @@ -import logo from './logo.svg' -import './App.css' - -function App() { - return ( -
-
- logo -

- Edit src/App.js and save to reload. -

- - Learn React - -
-
- ) -} - -export default App diff --git a/frontend/src/page/App.test.js b/frontend/src/page/App.test.js deleted file mode 100644 index 4741580c..00000000 --- a/frontend/src/page/App.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import { render, screen } from '@testing-library/react' -import App from './App' - -test('renders learn react link', () => { - render() - const linkElement = screen.getByText(/learn react/i) - expect(linkElement).toBeInTheDocument() -}) 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/page/channel/Channel.js b/frontend/src/page/channel/Channel.js new file mode 100644 index 00000000..a1ccea42 --- /dev/null +++ b/frontend/src/page/channel/Channel.js @@ -0,0 +1,43 @@ +import React from 'react' +import MessageEditor from '../../organism/messageEditor/MessageEditor' +import io from 'socket.io-client' +import { useState, useEffect } from 'react' + +const socket = io('http://localhost:4000/chat', { + query: { username: 'test1' }, +}) + +function Channel() { + const [messages, setMessages] = useState([]) + // TODO message 전송 template, 추후 구현 + const sendMessage = message => socket.emit('new message', message) + + useEffect(() => { + socket.on('connect', () => { + console.log('connected') + }) + + socket.on('disconnect', () => { + console.log('disconnected') + }) + + socket.on('new message', data => { + setMessages(...messages, data) + }) + + socket.emit('add user', 'username') + return () => { + socket.off('connect') + socket.off('disconnect') + socket.off('new message') + } + }, []) + return ( +
+ {/* TODO messgae component channel header component 추가 필요 */} + +
+ ) +} + +export default Channel diff --git a/frontend/src/page/index.js b/frontend/src/page/index.js deleted file mode 100644 index e69de29b..00000000 diff --git a/frontend/src/page/logo.svg b/frontend/src/page/logo.svg deleted file mode 100644 index 6b60c104..00000000 --- a/frontend/src/page/logo.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - 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