├── bs
├── fb
├── fs
├── .DS_Store
├── fe
├── .browserslistrc
├── public
│ ├── favicon.ico
│ └── index.html
├── src
│ ├── assets
│ │ └── logo.png
│ ├── locales
│ │ ├── ko.json
│ │ └── en.json
│ ├── plugins
│ │ └── vuetify.js
│ ├── views
│ │ ├── test
│ │ │ ├── lv0.vue
│ │ │ ├── lv1.vue
│ │ │ ├── lv2.vue
│ │ │ └── lv3.vue
│ │ ├── e404.vue
│ │ ├── block.vue
│ │ ├── user.vue
│ │ ├── manage
│ │ │ ├── users.vue
│ │ │ ├── boards.vue
│ │ │ ├── sites.vue
│ │ │ ├── pages.vue
│ │ │ └── user.vue
│ │ ├── sign.vue
│ │ ├── dashboard
│ │ │ └── index.vue
│ │ ├── register.vue
│ │ └── board
│ │ │ └── index.vue
│ ├── components
│ │ ├── dashboard
│ │ │ ├── trendCard.vue
│ │ │ ├── smallCard.vue
│ │ │ ├── boardCard.vue
│ │ │ └── linkCard.vue
│ │ ├── user.vue
│ │ ├── imgUpload.vue
│ │ ├── HelloWorld.vue
│ │ └── manage
│ │ │ └── boardCard.vue
│ ├── i18n.js
│ ├── store.js
│ ├── main.js
│ ├── router.js
│ └── App.vue
├── postcss.config.js
├── babel.config.js
├── vue.config.js
├── .gitignore
├── README.md
├── .eslintrc.js
└── package.json
├── yarn.lock
├── be
├── public
│ └── stylesheets
│ │ └── style.css
├── routes
│ └── api
│ │ ├── notFound.js
│ │ ├── lb
│ │ └── index.js
│ │ ├── manage
│ │ ├── index.js
│ │ ├── page
│ │ │ └── index.js
│ │ ├── site
│ │ │ └── index.js
│ │ ├── user
│ │ │ └── index.js
│ │ └── board
│ │ │ └── index.js
│ │ ├── site
│ │ └── index.js
│ │ ├── register
│ │ └── index.js
│ │ ├── page
│ │ └── index.js
│ │ ├── board
│ │ └── index.js
│ │ ├── user
│ │ └── index.js
│ │ ├── test
│ │ └── index.js
│ │ ├── index.js
│ │ ├── sign
│ │ └── index.js
│ │ ├── comment
│ │ └── index.js
│ │ └── article
│ │ └── index.js
├── models
│ ├── pages.js
│ ├── boards.js
│ ├── comments.js
│ ├── sites.js
│ ├── articles.js
│ └── users.js
├── package.json
├── bin
│ └── www
└── app.js
├── ecosystem.config.js
├── package.json
├── .gitignore
└── README.md
/bs:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd be && npm start
3 |
--------------------------------------------------------------------------------
/fb:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd fe && yarn serve
3 |
--------------------------------------------------------------------------------
/fs:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd fe && yarn serve
3 |
--------------------------------------------------------------------------------
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fkkmemi/nemv3/HEAD/.DS_Store
--------------------------------------------------------------------------------
/fe/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 | not ie <= 10
4 |
--------------------------------------------------------------------------------
/fe/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fkkmemi/nemv3/HEAD/fe/public/favicon.ico
--------------------------------------------------------------------------------
/fe/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fkkmemi/nemv3/HEAD/fe/src/assets/logo.png
--------------------------------------------------------------------------------
/fe/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {}
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/fe/src/locales/ko.json:
--------------------------------------------------------------------------------
1 | {
2 | "message": "안녕 i18n !!",
3 | "dashboard": "현황",
4 | "today": "오늘"
5 | }
6 |
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 |
--------------------------------------------------------------------------------
/fe/src/locales/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "message": "hello i18n !!",
3 | "dashboard": "Dashboard",
4 | "today": "Today"
5 | }
6 |
--------------------------------------------------------------------------------
/be/public/stylesheets/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 50px;
3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
4 | }
5 |
6 | a {
7 | color: #00B7FF;
8 | }
9 |
--------------------------------------------------------------------------------
/be/routes/api/notFound.js:
--------------------------------------------------------------------------------
1 | const createError = require('http-errors')
2 |
3 | module.exports = (req, res, next) => {
4 | next(createError(404, `${req.path} not found`))
5 | }
6 |
--------------------------------------------------------------------------------
/fe/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "presets": [
3 | [
4 | "@vue/app",
5 | {
6 | "useBuiltIns": "entry"
7 | }
8 | ]
9 | ]
10 | }
--------------------------------------------------------------------------------
/fe/src/plugins/vuetify.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuetify from 'vuetify'
3 | import 'vuetify/dist/vuetify.min.css'
4 |
5 | Vue.use(Vuetify, {
6 | iconfont: 'md'
7 | })
8 |
--------------------------------------------------------------------------------
/fe/src/views/test/lv0.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 레벨 0 관리자
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/fe/src/views/test/lv1.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 레벨 1 중간관리자
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/fe/src/views/test/lv2.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 레벨 2 일반 가입자
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/be/routes/api/lb/index.js:
--------------------------------------------------------------------------------
1 | const router = require('express').Router()
2 |
3 | router.get('/', (req, res, next) => {
4 | res.send({ success: true })
5 | })
6 |
7 | module.exports = router
8 |
--------------------------------------------------------------------------------
/fe/src/views/e404.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{$route.path}} 그런 페이지 없어요~
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/fe/vue.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | pluginOptions: {
3 | i18n: {
4 | locale: 'en',
5 | fallbackLocale: 'en',
6 | localeDir: 'locales',
7 | enableInSFC: false
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/ecosystem.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | apps : [{
3 | name: 'nemv',
4 | script: './be/bin/www',
5 | max_memory_restart: '1G',
6 | env: {
7 | NODE_ENV: 'production',
8 | PORT: 80
9 | },
10 | env_dev: {
11 | NODE_ENV: 'development'
12 | }
13 | }]
14 | }
15 |
--------------------------------------------------------------------------------
/fe/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Editor directories and files
15 | .idea
16 | .vscode
17 | *.suo
18 | *.ntvs*
19 | *.njsproj
20 | *.sln
21 | *.sw*
22 |
--------------------------------------------------------------------------------
/fe/README.md:
--------------------------------------------------------------------------------
1 | # fe
2 |
3 | ## Project setup
4 | ```
5 | yarn install
6 | ```
7 |
8 | ### Compiles and hot-reloads for development
9 | ```
10 | yarn run serve
11 | ```
12 |
13 | ### Compiles and minifies for production
14 | ```
15 | yarn run build
16 | ```
17 |
18 | ### Run your tests
19 | ```
20 | yarn run test
21 | ```
22 |
23 | ### Lints and fixes files
24 | ```
25 | yarn run lint
26 | ```
27 |
--------------------------------------------------------------------------------
/fe/src/views/block.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 로그인이 필요한 페이지 입니다. {{$route.params.msg}}
5 |
6 |
7 |
8 |
16 |
--------------------------------------------------------------------------------
/be/models/pages.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const cfg = require('../../config')
3 |
4 | mongoose.set('useCreateIndex', true)
5 | const pageSchema = new mongoose.Schema({
6 | name: { type: String, default: '', index: true },
7 | title: { type: String, default: '' },
8 | inCnt: { type: Number, default: 0 },
9 | lv: { type: Number, default: 0 }
10 | })
11 |
12 | module.exports = mongoose.model('Page', pageSchema)
13 |
--------------------------------------------------------------------------------
/fe/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true
5 | },
6 | 'extends': [
7 | 'plugin:vue/essential',
8 | '@vue/standard'
9 | ],
10 | rules: {
11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
13 | },
14 | parserOptions: {
15 | parser: 'babel-eslint'
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nemv",
3 | "version": "0.0.1",
4 | "scripts": {
5 | "dev": "DEBUG=be:* NODE_ENV=development node ./be/bin/www",
6 | "serve": "cd fe && yarn serve",
7 | "pr": "cd fe && yarn && yarn build && cd ../be && yarn && cd .. && NODE_ENV=production PORT=80 node ./be/bin/www",
8 | "pm2": "cd fe && yarn && yarn build && cd ../be && yarn && cd .. && pm2 start --env pr"
9 | },
10 | "dependencies": {}
11 | }
12 |
--------------------------------------------------------------------------------
/be/models/boards.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const cfg = require('../../config')
3 |
4 | mongoose.set('useCreateIndex', true)
5 | const boardSchema = new mongoose.Schema({
6 | name: { type: String, default: '', index: true, unique: true },
7 | title: { type: String, default: '' },
8 | lv: { type: Number, default: 0 },
9 | rmk: { type: String, default: '' }
10 | })
11 |
12 | module.exports = mongoose.model('Board', boardSchema)
13 |
--------------------------------------------------------------------------------
/be/routes/api/manage/index.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var createError = require('http-errors');
3 | var router = express.Router();
4 |
5 | router.all('*', function(req, res, next) {
6 | if (req.user.lv) throw createError(403, '권한이 없습니다.')
7 | next()
8 | })
9 |
10 | router.use('/user', require('./user'))
11 | router.use('/page', require('./page'))
12 | router.use('/site', require('./site'))
13 | router.use('/board', require('./board'))
14 |
15 | module.exports = router;
16 |
--------------------------------------------------------------------------------
/fe/src/components/dashboard/trendCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{title}}
5 |
6 |
7 |
12 |
13 |
14 |
15 |
16 |
21 |
--------------------------------------------------------------------------------
/be/routes/api/site/index.js:
--------------------------------------------------------------------------------
1 | const router = require('express').Router()
2 | const createError = require('http-errors')
3 | const Site = require('../../../models/sites')
4 |
5 | router.get('/', (req, res, next) => {
6 | // return res.send({ success: true, d: req.user })
7 | Site.findOne({})
8 | .then(r => {
9 | res.send({ success: true, d: r, token: req.token })
10 | })
11 | .catch(e => {
12 | res.send({ success: false, msg: e.message })
13 | })
14 | })
15 |
16 | module.exports = router;
17 |
--------------------------------------------------------------------------------
/be/models/comments.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 |
3 | mongoose.set('useCreateIndex', true)
4 | const commentSchema = new mongoose.Schema({
5 | content: { type: String, default: '' },
6 | cnt: {
7 | view: { type: Number, default: 0 },
8 | like: { type: Number, default: 0 }
9 | },
10 | ip: { type: String, default: '' },
11 | _user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', index: true, default: null },
12 | _article: { type: mongoose.Schema.Types.ObjectId, ref: 'Article', index: true }
13 | })
14 |
15 | module.exports = mongoose.model('Comment', commentSchema)
16 |
--------------------------------------------------------------------------------
/fe/src/views/user.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 회원 정보 수정
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
29 |
--------------------------------------------------------------------------------
/be/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "be",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "node ./bin/www"
7 | },
8 | "dependencies": {
9 | "axios": "^0.18.0",
10 | "connect-history-api-fallback": "^1.5.0",
11 | "cookie-parser": "~1.4.3",
12 | "cors": "^2.8.4",
13 | "debug": "~2.6.9",
14 | "express": "~4.16.0",
15 | "http-errors": "~1.6.2",
16 | "image-data-uri": "^2.0.0",
17 | "jsonwebtoken": "^8.3.0",
18 | "moment": "^2.22.2",
19 | "mongoose": "^5.3.2",
20 | "morgan": "~1.9.0",
21 | "multer": "^1.4.1",
22 | "pug": "2.0.0-beta11",
23 | "request": "^2.88.0",
24 | "sharp": "^0.21.1"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/be/models/sites.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const cfg = require('../../config')
3 |
4 | mongoose.set('useCreateIndex', true)
5 | const siteSchema = new mongoose.Schema({
6 | title: { type: String, default: 'NEMV', index: true },
7 | copyright: { type: String, default: '© 2018 nemv' },
8 | dark: { type: Boolean, default: false }
9 | })
10 |
11 | const Site = mongoose.model('Site', siteSchema)
12 |
13 | Site.findOne()
14 | .then(r => {
15 | if (!r) return Site.create({})
16 | return Promise.resolve(null)
17 | })
18 | .then(r => {
19 | if (r) console.log(`${r.title} site is created`)
20 | })
21 | .catch(e => console.error(e.message))
22 |
23 | module.exports = Site
24 |
--------------------------------------------------------------------------------
/fe/src/i18n.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueI18n from 'vue-i18n'
3 |
4 | Vue.use(VueI18n)
5 |
6 | function loadLocaleMessages () {
7 | const locales = require.context('./locales', true, /[A-Za-z0-9-_,\s]+\.json$/i)
8 | const messages = {}
9 | locales.keys().forEach(key => {
10 | const matched = key.match(/([A-Za-z0-9-_]+)\./i)
11 | if (matched && matched.length > 1) {
12 | const locale = matched[1]
13 | messages[locale] = locales(key)
14 | }
15 | })
16 | return messages
17 | }
18 |
19 | export default new VueI18n({
20 | locale: process.env.VUE_APP_I18N_LOCALE || 'en',
21 | fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
22 | messages: loadLocaleMessages()
23 | })
24 |
--------------------------------------------------------------------------------
/be/models/articles.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 |
3 | mongoose.set('useCreateIndex', true)
4 | const articleSchema = new mongoose.Schema({
5 | title: { type: String, default: '', index: true },
6 | content: { type: String, default: '' },
7 | cnt: {
8 | view: { type: Number, default: 0 },
9 | like: { type: Number, default: 0 }
10 | },
11 | ip: { type: String, default: '' },
12 | // _comments: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Comment', default: null }],
13 | _user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', index: true, default: null },
14 | _board: { type: mongoose.Schema.Types.ObjectId, ref: 'Board', index: true }
15 | })
16 |
17 | module.exports = mongoose.model('Article', articleSchema)
18 |
--------------------------------------------------------------------------------
/be/routes/api/register/index.js:
--------------------------------------------------------------------------------
1 | const router = require('express').Router()
2 | const createError = require('http-errors')
3 | const crypto = require('crypto')
4 | const User = require('../../../models/users')
5 |
6 | router.post('/', (req, res) => {
7 | const u = req.body
8 | if (!u.id) throw createError(400, '아이디가 없습니다')
9 | if (!u.pwd) throw createError(400, '비밀번호가 없습니다')
10 | if (!u.name) throw createError(400, '이름이 없습니다.')
11 |
12 | User.findOne({ id: u.id })
13 | .then((r) => {
14 | if (r) throw new Error('이미 등록되어 있는 아이디입니다.')
15 | return User.create(u)
16 | })
17 | .then((r) => {
18 | const pwd = crypto.scryptSync(r.pwd, r._id.toString(), 64, { N: 1024 }).toString('hex')
19 | return User.updateOne({ _id: r._id }, { $set: { pwd } })
20 | })
21 | .then((r) => {
22 | res.send({ success: true })
23 | })
24 | .catch((e) => {
25 | res.send({ success: false, msg: e.message })
26 | })
27 | })
28 |
29 | module.exports = router;
30 |
--------------------------------------------------------------------------------
/be/routes/api/page/index.js:
--------------------------------------------------------------------------------
1 | const router = require('express').Router()
2 | const createError = require('http-errors')
3 | const Page = require('../../../models/pages')
4 |
5 | router.post('/', function(req, res, next) {
6 | // return res.send({ success: true, d: req.user })
7 | // throw createError(403, 'hhhh')
8 | const { name } = req.body
9 | Page.findOne({ name })
10 | .then((r) => {
11 | if (!r) {
12 | if (req.user.lv) throw new Error(`페이지 ${name} 생성이 안되었습니다`)
13 | return Page.create({ name, title: name })
14 | }
15 | if (r.lv < req.user.lv) throw new Error(`페이지 ${name} 이용 자격이 없습니다.`)
16 | return Page.updateOne({ _id: r._id }, { $inc: { inCnt: 1 } })
17 | })
18 | .then(() => {
19 | // return Page.find()
20 | // })
21 | // .then((rs) => {
22 | // console.log(rs)
23 | res.send({ success: true, d: req.user, token: req.token })
24 | })
25 | .catch(e => next(createError(403, e.message)))
26 | });
27 |
28 | module.exports = router;
29 |
--------------------------------------------------------------------------------
/fe/src/components/user.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{user.id}}
5 |
6 |
7 |
8 | 이름: {{user.name}}
9 | 권한: {{user.lv}}
10 | 나이: {{user.age}}
11 | 로그인 횟수: {{user.inCnt}}
12 |
13 |
14 | 자식용
15 | 부모용
16 |
17 |
18 | 자식 혼자 떠들기
19 |
20 |
21 |
22 |
41 |
--------------------------------------------------------------------------------
/fe/src/store.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 |
4 | Vue.use(Vuex)
5 |
6 | export default new Vuex.Store({
7 | state: {
8 | token: localStorage.getItem('token'),
9 | sb: {
10 | act: false,
11 | msg: '',
12 | color: 'error'
13 | },
14 | user: {
15 | name: '일반사용자',
16 | id: '없음',
17 | img: 'https://randomuser.me/api/portraits/men/85.jpg'
18 | }
19 | },
20 | mutations: {
21 | getToken (state, user) {
22 | state.token = localStorage.getItem('token')
23 | state.user = user
24 | },
25 | delToken (state) {
26 | localStorage.removeItem('token')
27 | state.token = null
28 | state.user = {
29 | name: '일반사용자',
30 | id: '없음',
31 | img: 'https://randomuser.me/api/portraits/men/85.jpg'
32 | }
33 | },
34 | pop (state, d) {
35 | state.sb.msg = d.msg
36 | state.sb.color = d.color
37 | state.sb.act = false
38 | if (d.act === undefined) state.sb.act = true
39 | }
40 | },
41 | actions: {
42 |
43 | }
44 | })
45 |
--------------------------------------------------------------------------------
/be/routes/api/board/index.js:
--------------------------------------------------------------------------------
1 | const router = require('express').Router()
2 | const createError = require('http-errors')
3 | const Board = require('../../../models/boards')
4 |
5 | router.all('*', function(req, res, next) {
6 | // if (req.user.lv) return res.send({ success: false, msg: '권한이 없습니다.' })
7 | next()
8 | })
9 |
10 | router.get('/read/:name', (req, res, next) => {
11 | const name = req.params.name
12 | Board.findOne({ name })
13 | .then(r => {
14 | // console.log(r)
15 | // 권한으로 못보게 하려면..
16 | // if (r.lv < req.lv) throw new Error(`${name} 게시판을 볼 수 있는 자격이 없습니다.`)
17 | res.send({ success: true, d: r, token: req.token })
18 | })
19 | .catch(e => {
20 | res.send({ success: false, msg: e.message })
21 | })
22 | })
23 |
24 | router.get('/list', (req, res, next) => {
25 | Board.find().sort({ lv: -1 })
26 | .then(rs => {
27 | res.send({ success: true, ds: rs, token: req.token })
28 | })
29 | .catch(e => {
30 | res.send({ success: false, msg: e.message })
31 | })
32 | })
33 |
34 | module.exports = router;
35 |
--------------------------------------------------------------------------------
/be/routes/api/manage/page/index.js:
--------------------------------------------------------------------------------
1 | const router = require('express').Router()
2 | const createError = require('http-errors')
3 | const Page = require('../../../../models/pages')
4 |
5 | router.get('/', function(req, res, next) {
6 | Page.find()
7 | .then(r => {
8 | res.send({ success: true, pages: r, token: req.token })
9 | })
10 | .catch(e => {
11 | res.send({ success: false, msg: e.message })
12 | })
13 | })
14 |
15 | router.put('/:_id', (req, res, next) => {
16 | const _id = req.params._id
17 | Page.updateOne({ _id }, { $set: req.body})
18 | .then(r => {
19 | res.send({ success: true, msg: r, token: req.token })
20 | })
21 | .catch(e => {
22 | res.send({ success: false, msg: e.message })
23 | })
24 | })
25 |
26 | router.delete('/:_id', (req, res, next) => {
27 | const _id = req.params._id
28 | Page.deleteOne({ _id })
29 | .then(r => {
30 | res.send({ success: true, msg: r, token: req.token })
31 | })
32 | .catch(e => {
33 | res.send({ success: false, msg: e.message })
34 | })
35 | })
36 |
37 | module.exports = router;
38 |
--------------------------------------------------------------------------------
/be/routes/api/manage/site/index.js:
--------------------------------------------------------------------------------
1 | const router = require('express').Router()
2 | const createError = require('http-errors')
3 | const Site = require('../../../../models/sites')
4 |
5 | router.get('/', function(req, res, next) {
6 | Site.find()
7 | .then(r => {
8 | res.send({ success: true, sites: r, token: req.token })
9 | })
10 | .catch(e => {
11 | res.send({ success: false, msg: e.message })
12 | })
13 | });
14 |
15 | router.put('/:_id', (req, res, next) => {
16 | const _id = req.params._id
17 | Site.updateOne({ _id }, { $set: req.body})
18 | .then(r => {
19 | res.send({ success: true, msg: r, token: req.token })
20 | })
21 | .catch(e => {
22 | res.send({ success: false, msg: e.message })
23 | })
24 | })
25 |
26 | router.delete('/:_id', (req, res, next) => {
27 | const _id = req.params._id
28 | Site.deleteOne({ _id })
29 | .then(r => {
30 | res.send({ success: true, msg: r, token: req.token })
31 | })
32 | .catch(e => {
33 | res.send({ success: false, msg: e.message })
34 | })
35 | })
36 |
37 | module.exports = router;
38 |
--------------------------------------------------------------------------------
/fe/src/views/manage/users.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{sb.msg}}
9 |
10 |
11 |
45 |
--------------------------------------------------------------------------------
/be/routes/api/manage/user/index.js:
--------------------------------------------------------------------------------
1 | const router = require('express').Router()
2 | const createError = require('http-errors')
3 | const User = require('../../../../models/users')
4 |
5 | router.get('/', function(req, res, next) {
6 | User.find().select('-pwd')
7 | .then(r => {
8 | res.send({ success: true, users: r, token: req.token })
9 | })
10 | .catch(e => {
11 | res.send({ success: false, msg: e.message })
12 | })
13 | });
14 |
15 | router.put('/:_id', (req, res, next) => {
16 | const _id = req.params._id
17 | User.updateOne({ _id }, { $set: req.body})
18 | .then(r => {
19 | res.send({ success: true, msg: r, token: req.token })
20 | })
21 | .catch(e => {
22 | res.send({ success: false, msg: e.message })
23 | })
24 | })
25 |
26 | router.delete('/:_id', (req, res, next) => {
27 | const _id = req.params._id
28 | User.deleteOne({ _id })
29 | .then(r => {
30 | res.send({ success: true, msg: r, token: req.token })
31 | })
32 | .catch(e => {
33 | res.send({ success: false, msg: e.message })
34 | })
35 | })
36 |
37 | module.exports = router;
38 |
--------------------------------------------------------------------------------
/fe/src/components/dashboard/smallCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
{{title}}
9 |
{{number}}
10 |
11 |
12 |
13 |
14 |
15 |
16 | {{tIcon}}
17 |
18 |
19 |
20 |
21 |
22 |
23 | {{bIcon}}
24 | {{bText}}
25 |
26 |
27 |
28 |
29 |
30 |
35 |
--------------------------------------------------------------------------------
/fe/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | fe
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
63 | # config
64 | config/
65 |
66 | # public
67 | public/
68 |
--------------------------------------------------------------------------------
/be/routes/api/user/index.js:
--------------------------------------------------------------------------------
1 | const router = require('express').Router()
2 | const createError = require('http-errors')
3 | const multer = require('multer')
4 | const sharp = require('sharp')
5 | const imageDataURI = require('image-data-uri')
6 | const fs = require('fs')
7 | const User = require('../../../models/users')
8 |
9 | router.post('/', multer({ dest: 'public/' }).single('bin') ,(req, res, next) => {
10 | // console.log(req.body)
11 | // console.log(req.file)
12 | // console.log(req.user)
13 | if (!req.user._id) throw createError(401, 'xxx')
14 | sharp(req.file.path).resize({
15 | width: 200,
16 | height: 200,
17 | fit: sharp.fit.cover,
18 | position: sharp.strategy.entropy
19 | }).toBuffer()
20 | .then(bf => {
21 | fs.unlinkSync(req.file.path)
22 | const img = imageDataURI.encode(bf, 'png')
23 | return User.findByIdAndUpdate(req.user._id, { $set: { img } }, { new: true }).select('-img')
24 | // res.send(imageDataURI.encode(bf, 'png'))
25 | })
26 | .then(r => {
27 | res.setHeader('Content-Type', 'text/plain')
28 | res.send(r._id.toString())
29 | })
30 | .catch(e => next(e))
31 | })
32 | router.delete('/', (req, res, next) => {
33 | res.status(204).send()
34 | })
35 |
36 | module.exports = router
37 |
--------------------------------------------------------------------------------
/be/routes/api/manage/board/index.js:
--------------------------------------------------------------------------------
1 | const router = require('express').Router()
2 | const createError = require('http-errors')
3 | const Board = require('../../../../models/boards')
4 |
5 | router.post('/', (req, res, next) => {
6 | Board.create(req.body)
7 | .then(r => {
8 | res.send({ success: true, msg: r, token: req.token })
9 | })
10 | .catch(e => {
11 | res.send({ success: false, msg: e.message })
12 | })
13 | })
14 |
15 | router.get('/', function(req, res, next) {
16 | Board.find()
17 | .then(rs => {
18 | res.send({ success: true, ds: rs, token: req.token })
19 | })
20 | .catch(e => {
21 | res.send({ success: false })
22 | })
23 | });
24 |
25 | router.put('/:_id', (req, res, next) => {
26 | const _id = req.params._id
27 | Board.updateOne({ _id }, { $set: req.body})
28 | .then(r => {
29 | res.send({ success: true, msg: r, token: req.token })
30 | })
31 | .catch(e => {
32 | res.send({ success: false, msg: e.message })
33 | })
34 | })
35 |
36 | router.delete('/:_id', (req, res, next) => {
37 | const _id = req.params._id
38 | Board.deleteOne({ _id })
39 | .then(r => {
40 | res.send({ success: true, msg: r, token: req.token })
41 | })
42 | .catch(e => {
43 | res.send({ success: false, msg: e.message })
44 | })
45 | })
46 |
47 | module.exports = router;
48 |
--------------------------------------------------------------------------------
/fe/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fe",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint",
9 | "i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'"
10 | },
11 | "dependencies": {
12 | "@babel/polyfill": "^7.0.0-rc.1",
13 | "@toast-ui/vue-editor": "^1.0.2",
14 | "axios": "^0.18.0",
15 | "filepond": "^3.5.0",
16 | "filepond-plugin-file-validate-type": "^1.2.0",
17 | "filepond-plugin-image-preview": "^3.1.4",
18 | "moment": "^2.22.2",
19 | "vee-validate": "^2.1.2",
20 | "vue": "^2.5.17",
21 | "vue-analytics": "^5.16.0",
22 | "vue-filepond": "^4.0.2",
23 | "vue-i18n": "^8.0.0",
24 | "vue-plugin-load-script": "^1.2.0",
25 | "vue-recaptcha": "^1.1.1",
26 | "vue-router": "^3.0.1",
27 | "vuetify": "^1.3.0",
28 | "vuetify-dialog": "^0.3.4",
29 | "vuetrend": "^0.3.2",
30 | "vuex": "^3.0.1"
31 | },
32 | "devDependencies": {
33 | "@vue/cli-plugin-babel": "^3.0.4",
34 | "@vue/cli-plugin-eslint": "^3.0.4",
35 | "@vue/cli-service": "^3.0.4",
36 | "@vue/eslint-config-standard": "^3.0.4",
37 | "vue-cli-plugin-i18n": "^0.6.0",
38 | "vue-cli-plugin-vuetify": "^0.2.1",
39 | "vue-template-compiler": "^2.5.17"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/be/routes/api/test/index.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var createError = require('http-errors');
3 | var router = express.Router();
4 | const User = require('../../../models/users')
5 | const Page = require('../../../models/pages')
6 |
7 | router.get('/', function(req, res, next) {
8 | res.send({ msg: 'hello', a: '괜찮아' })
9 | });
10 |
11 | router.get('/hello', function(req, res, next) {
12 | res.send({ msg: 'hello', a: 2222 })
13 | });
14 |
15 | router.delete('/delAll', function(req, res, next) {
16 | User.deleteMany({})
17 | .then(r => {
18 | console.log(r)
19 | return Page.deleteMany({})
20 | })
21 | .then(r => {
22 | console.log(r)
23 | res.send({ success: true, msg: '싹다지움..', token: req.token })
24 | })
25 | .catch(e => {
26 | console.log(e.message)
27 | res.send({ success: false, msg: e.message})
28 | })
29 | });
30 |
31 | router.put('/pageAuth', function(req, res, next) {
32 | Page.updateOne({
33 |
34 | })
35 | User.deleteMany({})
36 | .then(r => {
37 | console.log(r)
38 | return Page.deleteMany({})
39 | })
40 | .then(r => {
41 | console.log(r)
42 | res.send({ success: true, msg: '싹다지움..', token: req.token })
43 | })
44 | .catch(e => {
45 | console.log(e.message)
46 | res.send({ success: false, msg: e.message})
47 | })
48 | });
49 |
50 | router.all('*', function(req, res, next) {
51 | next(createError(404, '그런 api 없어'));
52 | });
53 |
54 | module.exports = router;
55 |
--------------------------------------------------------------------------------
/fe/src/views/test/lv3.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | vuetify-dialog시험
7 |
8 |
9 | 누르지마세요
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | {{editorText}}
30 |
31 |
32 |
33 |
34 |
54 |
--------------------------------------------------------------------------------
/be/models/users.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const crypto = require('crypto')
3 | const cfg = require('../../config')
4 |
5 | mongoose.set('useCreateIndex', true)
6 | mongoose.set('useFindAndModify', false)
7 | const userSchema = new mongoose.Schema({
8 | name: { type: String, default: '' },
9 | age: { type: Number, default: 1 },
10 | id: { type: String, default: '', unique: true, index: true },
11 | pwd: { type: String, default: '' },
12 | lv: { type: Number, default: 2 }, //add
13 | inCnt: { type: Number, default: 0 }, //add
14 | retry: { type: Number, default: 0 },
15 | img: { type: String, default: '' }
16 | })
17 |
18 | const User = mongoose.model('User', userSchema)
19 | // User.collection.dropIndexes({ name: 1})
20 |
21 | User.findOne({ id: cfg.admin.id })
22 | .then((r) => {
23 | // console.log(r)
24 | if (!r) return User.create({ id: cfg.admin.id, pwd: cfg.admin.pwd, name: cfg.admin.name, lv: 0 })
25 | // if (r.lv === undefined) return User.updateOne({ _id: r._id }, { $set: { lv: 0, inCnt: 0 } }) // 임시.. 관리자 계정 레벨 0으로..
26 | return Promise.resolve(r)
27 | })
28 | .then((r) => {
29 | if (r.pwd !== cfg.admin.pwd) return Promise.resolve(null)
30 | console.log(`admin:${r.id} created!`)
31 | const pwd = crypto.scryptSync(r.pwd, r._id.toString(), 64, { N: 1024 }).toString('hex')
32 | return User.updateOne({ _id: r._id }, { $set: { pwd } })
33 | })
34 | .then(r => {
35 | if (r) console.log('pwd changed!')
36 | })
37 | .catch((e) => {
38 | console.error(e.message)
39 | })
40 |
41 | module.exports = User
42 |
--------------------------------------------------------------------------------
/fe/src/main.js:
--------------------------------------------------------------------------------
1 | import '@babel/polyfill'
2 | import Vue from 'vue'
3 | import VeeValidate from 'vee-validate'
4 | import LoadScript from 'vue-plugin-load-script'
5 | import VueRecaptcha from 'vue-recaptcha'
6 | import VueAnalytics from 'vue-analytics'
7 | import Trend from 'vuetrend'
8 | import VuetifyDialog from 'vuetify-dialog'
9 | import 'tui-editor/dist/tui-editor.css'
10 | import 'tui-editor/dist/tui-editor-contents.css'
11 | import 'codemirror/lib/codemirror.css'
12 | import './plugins/vuetify'
13 | import App from './App.vue'
14 | import router from './router'
15 | import store from './store'
16 | import cfg from '../config'
17 | import { Editor, Viewer } from '@toast-ui/vue-editor'
18 | import i18n from './i18n'
19 |
20 | Vue.config.productionTip = false
21 |
22 | Vue.prototype.$cfg = cfg
23 | if (process.env.NODE_ENV === 'production' && location.protocol === 'http:' && cfg.httpsOnly) location.replace(`https://${location.hostname}`)
24 | Vue.use(VeeValidate)
25 | Vue.use(LoadScript)
26 | Vue.use(VueAnalytics, {
27 | id: cfg.analyticsID,
28 | router,
29 | autoTracking: {
30 | pageviewOnLoad: false
31 | }
32 | })
33 | Vue.use(Trend)
34 | Vue.use(VuetifyDialog)
35 |
36 | Vue.loadScript('https://www.google.com/recaptcha/api.js?onload=vueRecaptchaApiLoaded&render=explicit')
37 | .then(() => {
38 | Vue.component('vue-recaptcha', VueRecaptcha)
39 | })
40 | .catch((e) => {
41 | console.error(`google api load failed: ${e.message}`)
42 | })
43 | Vue.component('editor', Editor)
44 | Vue.component('viewer', Viewer)
45 |
46 | new Vue({
47 | router,
48 | store,
49 | i18n,
50 | render: h => h(App)
51 | }).$mount('#app')
52 |
--------------------------------------------------------------------------------
/fe/src/components/imgUpload.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
15 |
16 |
17 |
66 |
--------------------------------------------------------------------------------
/fe/src/views/sign.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 로그인 하기
8 |
9 |
10 |
11 |
12 |
13 |
14 |
18 |
19 |
20 |
21 |
22 | 로그인
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
57 |
--------------------------------------------------------------------------------
/fe/src/components/HelloWorld.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ msg }}
4 |
5 | For guide and recipes on how to configure / customize this project,
6 | check out the
7 | vue-cli documentation.
8 |
9 |
Installed CLI Plugins
10 |
14 |
Essential Links
15 |
22 |
Ecosystem
23 |
30 |
31 |
32 |
33 |
41 |
42 |
43 |
59 |
--------------------------------------------------------------------------------
/fe/src/components/dashboard/boardCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 최근 게시물
5 |
6 |
7 |
8 | search
9 |
10 |
11 |
12 |
13 |
17 | {{ item.header }}
18 |
19 |
20 |
25 |
26 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
72 |
--------------------------------------------------------------------------------
/be/routes/api/index.js:
--------------------------------------------------------------------------------
1 | const router = require('express').Router()
2 | const createError = require('http-errors')
3 | const jwt = require('jsonwebtoken')
4 | const moment = require('moment')
5 | const cfg = require('../../../config')
6 |
7 | router.use('/lb', require('./lb'))
8 | router.use('/sign', require('./sign'))
9 | router.use('/site', require('./site'))
10 | router.use('/board', require('./board'))
11 |
12 | const verifyToken = (t) => {
13 | return new Promise((resolve, reject) => {
14 | if (!t) resolve({ id: 'guest', name: '손님', lv: 3 })
15 | if ((typeof t) !== 'string') reject(new Error('문자가 아닌 토큰 입니다.'))
16 | if (t.length < 10) resolve({ id: 'guest', name: '손님', lv: 3 })
17 | jwt.verify(t, cfg.jwt.secretKey, (err, v) => {
18 | if (err) reject(err)
19 | resolve(v)
20 | })
21 | })
22 | }
23 |
24 | const signToken = (_id, id, lv, name, exp) => {
25 | return new Promise((resolve, reject) => {
26 | const o = {
27 | issuer: cfg.jwt.issuer,
28 | subject: cfg.jwt.subject,
29 | expiresIn: cfg.jwt.expiresIn, // 3분
30 | algorithm: cfg.jwt.algorithm,
31 | expiresIn: exp
32 | }
33 | jwt.sign({ _id, id, lv, name }, cfg.jwt.secretKey, o, (err, token) => {
34 | if (err) reject(err)
35 | resolve(token)
36 | })
37 | })
38 | }
39 |
40 | const getToken = async(t) => {
41 | let vt = await verifyToken(t)
42 | if (vt.lv > 2) return { user: vt, token: null }
43 | const diff = moment(vt.exp * 1000).diff(moment(), 'seconds')
44 | // return vt
45 | // console.log(diff)
46 | const expSec = (vt.exp - vt.iat)
47 | if (diff > expSec / cfg.jwt.expiresInDiv) return { user: vt, token: null }
48 |
49 | const nt = await signToken(vt._id, vt.id, vt.lv, vt.name, expSec)
50 | vt = await verifyToken(nt)
51 | return { user: vt, token: nt }
52 | }
53 | router.all('*', function(req, res, next) {
54 | // 토큰 검사
55 | getToken(req.headers.authorization)
56 | .then((v) => {
57 | // console.log(v)
58 | req.user = v.user
59 | req.token = v.token
60 | next()
61 | })
62 | // .catch(e => res.send({ success: false, msg: e.message }))
63 | .catch(e => next(createError(401, e.message)))
64 | })
65 |
66 | router.use('/page', require('./page'))
67 | router.use('/article', require('./article'))
68 | router.use('/comment', require('./comment'))
69 | router.use('/manage', require('./manage'))
70 | router.use('/user', require('./user'))
71 |
72 | router.all('*', require('./notFound'))
73 |
74 | module.exports = router
75 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # nemv3
2 | node express mongo vue
3 |
4 | ## 백엔드 config 파일 세팅 방법
5 |
6 | **config/index.js**
7 | ```javascript
8 | module.exports = {
9 | dbUrl: 'mongodb://localhost:27017/nemv',
10 | admin: {
11 | id: 'admin',
12 | pwd: '1234',
13 | name: '관리자'
14 | },
15 | jwt: {
16 | secretKey: '아주 어려운 토큰 발급용 키',
17 | issuer: 'xxx.com',
18 | subject: 'user-token',
19 | algorithm: 'HS256',
20 | expiresIn: 60 * 3, // 기본 3분
21 | expiresInRemember: 60 * 60 * 24 * 7 // 기억하기 눌렀을 때 7일
22 | expiresInDiv: 3, // 토큰시간 나누는 기준
23 | },
24 | recaptchaSecret: '6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe' // google testkey
25 | }
26 | ```
27 |
28 | - dbUrl: 데이터베이스 주소
29 | - admin: 최초 웹 구동시 생성될 관리자
30 |
31 | 토큰 만들 때 필요한 정보를 추가합니다.
32 |
33 | - jwt.secretKey: secretKey -> jwt.secretKey
34 | - jwt.issuer: 만든 사람, 서버
35 | - jwt.subject: 토큰 이름
36 | - jwt.algorithm: 암호화 10개중 고르면 됨
37 |
38 | | alg Parameter Value | Digital Signature or MAC Algorithm |
39 | | --- | --- |
40 | |HS256 | HMAC using SHA-256 hash algorithm |
41 | |HS384 | HMAC using SHA-384 hash algorithm|
42 | |HS512 | HMAC using SHA-512 hash algorithm|
43 | |RS256 | RSASSA using SHA-256 hash algorithm|
44 | |RS384 | RSASSA using SHA-384 hash algorithm|
45 | |RS512 | RSASSA using SHA-512 hash algorithm|
46 | |ES256 | ECDSA using P-256 curve and SHA-256 hash algorithm|
47 | |ES384 | ECDSA using P-384 curve and SHA-384 hash algorithm|
48 | |ES512 | ECDSA using P-521 curve and SHA-512 hash algorithm|
49 | |none | No digital signature or MAC value included|
50 |
51 | 참고: [https://www.npmjs.com/package/jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken)
52 |
53 | 토큰을 만들때 다른 정보를 추가했으니 해독이 더 어려워 지겠죠?
54 |
55 | - jwt.expiresIn: 기억히기 안 누른 토큰 시간(3분)
56 | - jwt.expiresInRemember: 기억하기 누른 토큰 시간(6일)
57 | - jwt.expiresInDiv: 토큰 시간 나눈 시간(1분 혹은 2일), 토큰 재발급 할때 쓰일 시간입니다.
58 |
59 | - recaptchaSecretKey: 구글 로봇 감지 비밀 키입니다.
60 | 참고: [https://developers.google.com/recaptcha/docs/faq](https://developers.google.com/recaptcha/docs/faq)
61 |
62 | ## 프론트엔드 config 파일 세팅 방법
63 |
64 | **fe/config/index.js**
65 | ```javascript
66 | module.exports = {
67 | recaptchaSiteKey: '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI',
68 | analyticsID: 'UA-93372301-1',
69 | httpsOnly: false
70 | }
71 | ```
72 |
73 | - recaptchaSiteKey: 구글 로봇 감지 사이트 키입니다.
74 | - analyticsID: 구글 애널리틱스 아이디입니다.
75 | - httpsOnly: http로 접속할 경우 https로 다시 로드합니다.
76 |
77 | ## ssl 서버 작동
78 |
79 | 3개의 파일 필요
80 |
81 | - config/keys/site.key
82 | - config/keys/cert.crt
83 | - config/keys/ca.crt
84 |
85 | site key는 로컬 머신에서 제작합니다.
86 |
87 | **site.key 제작 방법**
88 | ```bash
89 | $ mkdir config/keys && cd config/keys
90 | $ openssl req -new -newkey rsa:2048 -nodes -keyout site.key
91 | ```
92 |
93 | cert, ca는 ssl공급사로부터 받습니다.
94 |
--------------------------------------------------------------------------------
/be/routes/api/sign/index.js:
--------------------------------------------------------------------------------
1 | const router = require('express').Router()
2 | const createError = require('http-errors')
3 | const jwt = require('jsonwebtoken')
4 | const crypto = require('crypto')
5 | const request = require('request')
6 | const cfg = require('../../../../config')
7 | const User = require('../../../models/users')
8 |
9 | const signToken = (_id, id, lv, name, rmb) => {
10 | return new Promise((resolve, reject) => {
11 | const o = {
12 | issuer: cfg.jwt.issuer,
13 | subject: cfg.jwt.subject,
14 | expiresIn: cfg.jwt.expiresIn, // 3분
15 | algorithm: cfg.jwt.algorithm
16 | }
17 | if (rmb) o.expiresIn = cfg.jwt.expiresInRemember // 6일
18 | jwt.sign({ _id, id, lv, name }, cfg.jwt.secretKey, o, (err, token) => {
19 | if (err) reject(err)
20 | resolve(token)
21 | })
22 | })
23 | }
24 |
25 | router.post('/in', (req, res, next) => {
26 | const { id, pwd, remember } = req.body
27 | if (!id) throw createError(400, '아이디가 없습니다')
28 | if (!pwd) throw createError(400, '비밀번호가 없습니다')
29 | if (remember === undefined) throw createError(400, '기억하기가 없습니다.')
30 |
31 | let u = {}
32 | User.findOne({ id }).lean()
33 | .then((r) => {
34 | if (!r) throw new Error('존재하지 않는 아이디입니다.')
35 | const p = crypto.scryptSync(pwd, r._id.toString(), 64, { N: 1024 }).toString('hex')
36 | if (r.pwd !== p) throw new Error('비밀번호가 틀립니다.')
37 | delete r.pwd
38 | u = r
39 | return signToken(r._id, r.id, r.lv, r.name, remember)
40 | })
41 | .then((r) => {
42 | res.send({ success: true, token: r, user: u })
43 | })
44 | .catch((e) => {
45 | res.send({ success: false, msg: e.message })
46 | // next(createError(401, e.massage))
47 | })
48 | })
49 |
50 | router.post('/up', (req, res, next) => {
51 | const u = req.body
52 | if (!u.id) throw createError(400, '아이디가 없습니다')
53 | if (!u.pwd) throw createError(400, '비밀번호가 없습니다')
54 | if (!u.name) throw createError(400, '이름이 없습니다')
55 | if (!u.response) throw createError(400, '로봇 검증이 없습니다')
56 |
57 | const ro = {
58 | uri: 'https://www.google.com/recaptcha/api/siteverify',
59 | json: true,
60 | form: {
61 | secret: cfg.recaptchaSecretKey,
62 | response: u.response,
63 | remoteip: req.ip
64 | }
65 | }
66 | request.post(ro, (err, response, body) => {
67 | if (err) throw createError(401, '로봇 검증 실패입니다')
68 |
69 | User.findOne({ id: u.id })
70 | .then((r) => {
71 | if (r) throw new Error('이미 등록되어 있는 아이디입니다')
72 | return User.create(u)
73 | })
74 | .then((r) => {
75 | const pwd = crypto.scryptSync(r.pwd, r._id.toString(), 64, { N: 1024 }).toString('hex')
76 | return User.updateOne({ _id: r._id }, { $set: { pwd } })
77 | })
78 | .then((r) => {
79 | res.send({ success: true })
80 | })
81 | .catch((e) => {
82 | res.send({ success: false, msg: e.message })
83 | })
84 | })
85 | })
86 |
87 | module.exports = router;
88 |
--------------------------------------------------------------------------------
/fe/src/views/dashboard/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 |
15 |
16 |
25 |
26 |
27 |
28 |
37 |
38 |
39 |
40 |
49 |
50 |
51 |
52 |
57 |
58 |
59 |
64 |
65 |
66 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
108 |
--------------------------------------------------------------------------------
/fe/src/components/dashboard/linkCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{board.title}}
5 |
10 |
11 |
12 |
13 | {{ article.title }}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
106 |
--------------------------------------------------------------------------------
/be/routes/api/comment/index.js:
--------------------------------------------------------------------------------
1 | const router = require('express').Router()
2 | const createError = require('http-errors')
3 | const request = require('request')
4 | const Board = require('../../../models/boards')
5 | const Article = require('../../../models/articles')
6 | const Comment = require('../../../models/comments')
7 | const cfg = require('../../../../config')
8 |
9 | router.post('/:_article', (req, res, next) => {
10 | const _article = req.params._article
11 | if (!_article) throw createError(400, '게시물이 선택되지 않았습니다')
12 | const { content, response } = req.body
13 |
14 | if (!content) throw createError(400, '내용이 없습니다')
15 | if (!response) throw createError(400, '로봇 검증이 없습니다')
16 |
17 | const ro = {
18 | uri: 'https://www.google.com/recaptcha/api/siteverify',
19 | json: true,
20 | form: {
21 | secret: cfg.recaptchaSecretKey,
22 | response,
23 | remoteip: req.ip
24 | }
25 | }
26 | request.post(ro, (err, response, body) => {
27 | if (err) throw createError(401, '로봇 검증 실패입니다')
28 |
29 | Article.findById(_article)
30 | .then(r => {
31 | if (!r) throw createError(400, '잘못된 게시물입니다')
32 | // if (r.lv < req.user.lv) throw createError(403, '권한이 없습니다')
33 | const cmt = {
34 | content,
35 | _article,
36 | ip: '1.1.1.1',//req.ip,
37 | _user: null
38 | }
39 | if (req.user._id) cmt._user = req.user._id
40 | return Comment.create(cmt)
41 | })
42 | .then(r => {
43 | if (!r) throw new Error('게시물이 생성되지 않았습니다')
44 | res.send({ success: true, d: r, token: req.token })
45 | })
46 | .catch(e => {
47 | res.send({ success: false, msg: e.message })
48 | })
49 | })
50 | })
51 |
52 |
53 | router.put('/:_id', (req, res, next) => {
54 | if (!req.user._id) throw createError(403, '댓글 수정 권한이 없습니다')
55 | const _id = req.params._id
56 |
57 | const { content } = req.body
58 |
59 | if (!content) throw createError(400, '내용이 없습니다')
60 |
61 | Comment.findById(_id)
62 | .then(r => {
63 | if (!r) throw new Error('댓글 존재하지 않습니다')
64 | if (!r._user) throw new Error('손님 댓글 수정이 안됩니다')
65 | if (r._user.toString() !== req.user._id) throw new Error('본인이 작성한 댓글이 아닙니다')
66 | return Comment.findByIdAndUpdate(_id, { $set: { content } }, { new: true }).populate('_user')
67 | })
68 | .then(r => {
69 | res.send({ success: true, d: r, token: req.token })
70 | })
71 | .catch(e => {
72 | res.send({ success: false, msg: e.message })
73 | })
74 | })
75 |
76 | router.delete('/:_id', (req, res, next) => {
77 | if (!req.user._id) throw createError(403, '댓글 삭제 권한이 없습니다')
78 | const _id = req.params._id
79 |
80 | Comment.findById(_id).populate('_user', 'lv')
81 | .then(r => {
82 | if (!r) throw new Error('댓글이 존재하지 않습니다')
83 | if (!r._user) {
84 | if (req.user.lv > 0) throw new Error('손님 댓글은 삭제가 안됩니다')
85 | }
86 | else {
87 | if (r._user._id.toString() !== req.user._id) {
88 | if (r._user.lv < req.user.lv) throw new Error('본인이 작성한 댓글이 아닙니다')
89 | }
90 | }
91 | return Comment.deleteOne({ _id })
92 | })
93 | .then(r => {
94 | res.send({ success: true, d: r, token: req.token })
95 | })
96 | .catch(e => {
97 | res.send({ success: false, msg: e.message })
98 | })
99 | })
100 |
101 | module.exports = router;
102 |
--------------------------------------------------------------------------------
/be/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Module dependencies.
5 | */
6 |
7 | var app = require('../app');
8 | var debug = require('debug')('be:server');
9 | var http = require('http');
10 |
11 | /**
12 | * Get port from environment and store in Express.
13 | */
14 |
15 | var port = normalizePort(process.env.PORT || '3000');
16 | app.set('port', port);
17 |
18 | /**
19 | * Create HTTP server.
20 | */
21 |
22 | var server = http.createServer(app);
23 |
24 | /**
25 | * Listen on provided port, on all network interfaces.
26 | */
27 |
28 | server.listen(port);
29 | server.on('error', onError);
30 | server.on('listening', onListening);
31 |
32 | /**
33 | * Normalize a port into a number, string, or false.
34 | */
35 |
36 | function normalizePort(val) {
37 | var port = parseInt(val, 10);
38 |
39 | if (isNaN(port)) {
40 | // named pipe
41 | return val;
42 | }
43 |
44 | if (port >= 0) {
45 | // port number
46 | return port;
47 | }
48 |
49 | return false;
50 | }
51 |
52 | /**
53 | * Event listener for HTTP server "error" event.
54 | */
55 |
56 | function onError(error) {
57 | if (error.syscall !== 'listen') {
58 | throw error;
59 | }
60 |
61 | var bind = typeof port === 'string'
62 | ? 'Pipe ' + port
63 | : 'Port ' + port;
64 |
65 | // handle specific listen errors with friendly messages
66 | switch (error.code) {
67 | case 'EACCES':
68 | console.error(bind + ' requires elevated privileges');
69 | process.exit(1);
70 | break;
71 | case 'EADDRINUSE':
72 | console.error(bind + ' is already in use');
73 | process.exit(1);
74 | break;
75 | default:
76 | throw error;
77 | }
78 | }
79 |
80 | /**
81 | * Event listener for HTTP server "listening" event.
82 | */
83 |
84 | function onListening() {
85 | var addr = server.address();
86 | var bind = typeof addr === 'string'
87 | ? 'pipe ' + addr
88 | : 'port ' + addr.port;
89 | debug('Listening on ' + bind);
90 | }
91 |
92 | // ssl added
93 |
94 | const fs = require('fs')
95 | if (fs.existsSync('./config/keys/site.key') && fs.existsSync('./config/keys/cert.crt') && fs.existsSync('./config/keys/ca.crt')) {
96 | const https = require('https')
97 | const httpsPort = process.env.PORT ? 443 : 3001
98 | const o = {
99 | key: fs.readFileSync('./config/keys/site.key'),
100 | cert: fs.readFileSync('./config/keys/cert.crt'),
101 | ca: [fs.readFileSync('./config/keys/ca.crt')]
102 | }
103 | const serverSSL = https.createServer(o, app).listen(httpsPort)
104 |
105 | serverSSL.on('error', (error) => {
106 | if (error.syscall !== 'listen') {
107 | throw error
108 | }
109 | const bind = `port ${httpsPort}`
110 |
111 | // handle specific listen errors with friendly messages
112 | switch (error.code) {
113 | case 'EACCES':
114 | console.error(bind + ' requires elevated privileges')
115 | process.exit(1)
116 | break
117 | case 'EADDRINUSE':
118 | console.error(bind + ' is already in use')
119 | process.exit(1)
120 | break
121 | default:
122 | throw error
123 | }
124 | })
125 |
126 | serverSSL.on('listening', () => {
127 | {
128 | const addr = serverSSL.address()
129 | const bind = typeof addr === 'string'
130 | ? 'pipe ' + addr
131 | : 'port ' + addr.port
132 | debug('HTTPS Listening on ' + bind)
133 | }
134 | })
135 | }
136 |
--------------------------------------------------------------------------------
/fe/src/router.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 | import axios from 'axios'
4 | import store from './store'
5 |
6 | Vue.use(Router)
7 |
8 | Vue.prototype.$axios = axios
9 | const apiRootPath = process.env.NODE_ENV !== 'production' ? 'http://localhost:3000/api/' : '/api/'
10 | Vue.prototype.$apiRootPath = apiRootPath
11 | axios.defaults.baseURL = apiRootPath
12 | // axios.defaults.headers.common['Authorization'] = localStorage.getItem('token')
13 |
14 | // Add a request interceptor
15 | axios.interceptors.request.use(function (config) {
16 | // Do something before request is sent
17 | config.headers.Authorization = localStorage.getItem('token')
18 | return config
19 | }, function (error) {
20 | // Do something with request error
21 | return Promise.reject(error)
22 | })
23 |
24 | axios.interceptors.response.use(function (response) {
25 | const token = response.data.token
26 | if (token) localStorage.setItem('token', token)
27 | return response
28 | }, function (error) {
29 | // console.log(error.response)
30 | switch (error.response.status) {
31 | case 400:
32 | store.commit('pop', { msg: `잘못된 요청입니다(${error.response.status}:${error.message})`, color: 'error' })
33 | break
34 | case 401:
35 | store.commit('delToken')
36 | store.commit('pop', { msg: `인증 오류입니다(${error.response.data.msg}:${error.message})`, color: 'error' })
37 | break
38 | case 403:
39 | store.commit('pop', { msg: `이용 권한이 없습니다(${error.response.data.msg}:${error.message})`, color: 'warning' })
40 | break
41 | default:
42 | store.commit('pop', { msg: `알수 없는 오류입니다(${error.response.data.msg}:${error.message})`, color: 'error' })
43 | break
44 | }
45 | return Promise.reject(error)
46 | })
47 |
48 | const pageCheck = (to, from, next) => {
49 | axios.post('page', { name: to.path })
50 | .then((r) => {
51 | if (!r.data.success) throw new Error(r.data.msg)
52 | next()
53 | })
54 | .catch((e) => {
55 | if (!e.response) store.commit('pop', { msg: e.message, color: 'warning' })
56 | next(false)
57 | })
58 | }
59 |
60 | export default new Router({
61 | mode: 'history',
62 | base: process.env.BASE_URL,
63 | routes: [
64 | {
65 | path: '/',
66 | name: 'dashboard',
67 | component: () => import('./views/dashboard'),
68 | beforeEnter: pageCheck
69 | },
70 | {
71 | path: '/board/:name',
72 | name: 'board',
73 | component: () => import('./views/board'),
74 | beforeEnter: pageCheck
75 | },
76 | {
77 | path: '/test/lv3',
78 | name: 'testLv3',
79 | component: () => import('./views/test/lv3'),
80 | beforeEnter: pageCheck
81 | },
82 | {
83 | path: '/test/lv2',
84 | name: 'testLv2',
85 | component: () => import('./views/test/lv2'),
86 | beforeEnter: pageCheck
87 | },
88 | {
89 | path: '/test/lv1',
90 | name: 'testLv1',
91 | component: () => import('./views/test/lv1'),
92 | beforeEnter: pageCheck
93 | },
94 | {
95 | path: '/test/lv0',
96 | name: 'testLv0',
97 | component: () => import('./views/test/lv0'),
98 | beforeEnter: pageCheck
99 | },
100 | {
101 | path: '/manage/users',
102 | name: 'manageUsers',
103 | component: () => import('./views/manage/user'),
104 | beforeEnter: pageCheck
105 | },
106 | {
107 | path: '/manage/pages',
108 | name: 'managePages',
109 | component: () => import('./views/manage/pages'),
110 | beforeEnter: pageCheck
111 | },
112 | {
113 | path: '/manage/sites',
114 | name: 'manageSites',
115 | component: () => import('./views/manage/sites'),
116 | beforeEnter: pageCheck
117 | },
118 | {
119 | path: '/manage/boards',
120 | name: 'manageBoards',
121 | component: () => import('./views/manage/boards'),
122 | beforeEnter: pageCheck
123 | },
124 | {
125 | path: '/user',
126 | name: 'user',
127 | component: () => import('./views/user'),
128 | beforeEnter: pageCheck
129 | },
130 | {
131 | path: '/sign',
132 | name: '로그인',
133 | component: () => import('./views/sign')
134 | },
135 | {
136 | path: '/register',
137 | name: '회원가입',
138 | component: () => import('./views/register')
139 | },
140 | {
141 | path: '*',
142 | name: 'e404',
143 | component: () => import('./views/e404')
144 | }
145 | ]
146 | })
147 |
--------------------------------------------------------------------------------
/fe/src/views/manage/boards.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 | 데이터가 없습니다
8 |
9 |
10 |
11 |
12 |
13 |
23 | add
24 |
25 |
26 |
27 |
28 |
29 | 게시판 추가
30 |
31 |
32 |
33 |
34 |
35 |
42 |
43 |
44 |
51 |
52 |
53 |
60 |
61 |
62 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | 확인
75 | 취소
76 |
77 |
78 |
79 |
82 | {{ sb.msg }}
83 |
88 | 닫기
89 |
90 |
91 |
92 |
93 |
158 |
--------------------------------------------------------------------------------
/fe/src/views/manage/sites.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 | 데이터가 없습니다
8 |
9 |
10 |
11 |
12 |
13 |
14 |
{{site.title}}
15 |
16 |
17 |
18 |
19 |
20 |
하단: {{site.copyright}}
21 |
색상: {{site.dark}}
22 |
23 |
24 |
25 |
26 | 수정
27 | 삭제
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | 사이트 수정
36 |
37 |
38 |
39 |
40 |
41 |
48 |
49 |
50 |
57 |
58 |
59 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | 수정
70 | Close
71 |
72 |
73 |
74 |
77 | {{ sbMsg }}
78 |
83 | Close
84 |
85 |
86 |
87 |
88 |
146 |
--------------------------------------------------------------------------------
/fe/src/components/manage/boardCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{board.name}}
6 |
7 |
8 |
9 | 제목: {{board.title}}
10 | 권한: {{board.lv}}
11 | 설명: {{board.rmk}}
12 |
13 |
14 |
15 |
16 | 수정
17 | 삭제
18 |
19 |
20 |
21 |
22 | 게시판 수정
23 |
24 |
25 |
26 |
33 |
34 |
41 |
42 |
49 |
50 |
56 |
57 |
58 |
59 |
60 |
61 | 확인
62 | 취소
63 |
64 |
65 |
66 |
67 |
68 | 정말 진행 하시겠습니까?
69 | 확인
70 | 취소
71 |
72 |
73 |
74 | {{ma.msg}}
75 |
76 |
77 |
78 |
149 |
--------------------------------------------------------------------------------
/fe/src/views/manage/pages.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 | 데이터가 없습니다
8 |
9 |
10 |
11 |
12 |
13 |
14 |
{{page.name}}
15 |
16 |
17 |
18 |
19 |
20 |
제목: {{page.title}}
21 |
권한: {{page.lv}}
22 |
진입 횟수: {{page.inCnt}}
23 |
24 |
25 |
26 |
27 | 수정
28 | 삭제
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | 페이지 수정
37 |
38 |
39 |
40 |
41 |
42 |
49 |
56 |
57 |
58 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | 수정
71 | Close
72 |
73 |
74 |
75 |
78 | {{ sbMsg }}
79 |
84 | Close
85 |
86 |
87 |
88 |
89 |
156 |
--------------------------------------------------------------------------------
/fe/src/views/manage/user.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
{{user.id}}
10 |
11 |
12 |
13 |
14 |
15 |
이름: {{user.name}}
16 |
권한: {{user.lv}}
17 |
나이: {{user.age}}
18 |
로그인 횟수: {{user.inCnt}}
19 |
소금(_id): {{user._id}}
20 |
비밀번호: {{user.pwd}}
21 |
22 |
23 |
24 |
25 | 수정
26 | 삭제
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | User Profile
35 |
36 |
37 |
38 |
39 |
40 |
47 |
48 |
49 |
55 |
56 |
57 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | 수정
70 | Close
71 |
72 |
73 |
74 |
77 | {{ sbMsg }}
78 |
83 | Close
84 |
85 |
86 |
87 |
88 |
157 |
--------------------------------------------------------------------------------
/fe/src/views/register.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 회원 가입
8 |
9 |
10 |
11 |
12 |
21 |
31 |
40 |
41 |
51 |
58 |
59 |
60 |
61 | 가입
62 | 초기화
63 |
64 |
65 |
66 |
67 |
68 |
71 | {{ sb.msg }}
72 |
77 | 닫기
78 |
79 |
80 |
81 |
82 |
83 |
177 |
--------------------------------------------------------------------------------
/be/routes/api/article/index.js:
--------------------------------------------------------------------------------
1 | const router = require('express').Router()
2 | const createError = require('http-errors')
3 | const request = require('request')
4 | const Board = require('../../../models/boards')
5 | const Article = require('../../../models/articles')
6 | const Comment = require('../../../models/comments')
7 | const cfg = require('../../../../config')
8 |
9 | router.get('/list/:_board', (req, res, next) => {
10 | const _board = req.params._board
11 | let { search, sort, order, skip, limit } = req.query
12 | if (!(sort && order && skip && limit)) throw createError(400, '잘못된 요청입니다')
13 | if (!search) search = ''
14 | order = parseInt(order)
15 | limit = parseInt(limit)
16 | skip = parseInt(skip)
17 | const s = {}
18 | s[sort] = order
19 |
20 | const f = {}
21 | if (_board) f._board = _board
22 | let total = 0
23 |
24 | Article.countDocuments(f)
25 | .where('title').regex(search)
26 | .then(r => {
27 | total = r
28 | return Article.find(f)
29 | .where('title').regex(search)
30 | .sort(s)
31 | .skip(skip)
32 | .limit(limit)
33 | .select('-content')
34 | .populate('_user', '-pwd')
35 | })
36 | .then(rs => {
37 | res.send({ success: true, t: total, ds: rs, token: req.token })
38 | })
39 | .catch(e => {
40 | res.send({ success: false, msg: e.message })
41 | })
42 | })
43 |
44 | router.get('/read/:_id', (req, res, next) => {
45 | const _id = req.params._id
46 |
47 | let atc = {}
48 |
49 | Article.findByIdAndUpdate(_id, { $inc: { 'cnt.view': 1 } }, { new: true }).lean()
50 | .select('content cnt')
51 | .then(r => {
52 | if (!r) throw new Error('잘못된 게시판입니다')
53 | atc = r
54 | atc._comments = []
55 | return Comment.find({ _article: atc._id }).populate({ path: '_user', select: 'id name'}).sort({ _id: 1 }).limit(5)
56 | })
57 | .then(rs => {
58 | if (rs) atc._comments = rs
59 | res.send({ success: true, d: atc, token: req.token })
60 | })
61 | .catch(e => {
62 | res.send({ success: false, msg: e.message })
63 | })
64 | })
65 |
66 | // Article.deleteMany({}).then(r => console.log(r))
67 |
68 | router.post('/:_board', (req, res, next) => {
69 | const _board = req.params._board
70 | if (!_board) throw createError(400, '게시판이 선택되지 않았습니다')
71 | const { title, content, response } = req.body
72 | console.log(req.body)
73 |
74 | if (!title) throw createError(400, '제목이 없습니다')
75 | if (!content) throw createError(400, '내용이 없습니다')
76 | if (!response) throw createError(400, '로봇 검증이 없습니다')
77 |
78 | const ro = {
79 | uri: 'https://www.google.com/recaptcha/api/siteverify',
80 | json: true,
81 | form: {
82 | secret: cfg.recaptchaSecretKey,
83 | response,
84 | remoteip: req.ip
85 | }
86 | }
87 | request.post(ro, (err, response, body) => {
88 | if (err) throw createError(401, '로봇 검증 실패입니다')
89 |
90 | Board.findById(_board)
91 | .then(r => {
92 | if (!r) throw createError(400, '잘못된 게시판입니다')
93 | if (r.lv < req.user.lv) throw createError(403, '권한이 없습니다')
94 | const atc = {
95 | title,
96 | content,
97 | _board,
98 | ip: '1.1.1.1',//req.ip,
99 | _user: null
100 | }
101 | if (req.user._id) atc._user = req.user._id
102 | return Article.create(atc)
103 | })
104 | .then(r => {
105 | if (!r) throw new Error('게시물이 생성되지 않았습니다')
106 | res.send({ success: true, d: r, token: req.token })
107 | })
108 | .catch(e => {
109 | res.send({ success: false, msg: e.message })
110 | })
111 | })
112 | })
113 |
114 |
115 | router.put('/:_id', (req, res, next) => {
116 | if (!req.user._id) throw createError(403, '게시물 수정 권한이 없습니다')
117 | const _id = req.params._id
118 |
119 | const { title, content, response } = req.body
120 |
121 | if (!title) throw createError(400, '제목이 없습니다')
122 | if (!content) throw createError(400, '내용이 없습니다')
123 | if (!response) throw createError(400, '로봇 검증이 없습니다')
124 |
125 | const ro = {
126 | uri: 'https://www.google.com/recaptcha/api/siteverify',
127 | json: true,
128 | form: {
129 | secret: cfg.recaptchaSecretKey,
130 | response,
131 | remoteip: req.ip
132 | }
133 | }
134 | request.post(ro, (err, response, body) => {
135 | if (err) throw createError(401, '로봇 검증 실패입니다')
136 |
137 | Article.findById(_id)
138 | .then(r => {
139 | if (!r) throw new Error('게시물이 존재하지 않습니다')
140 | if (!r._user) throw new Error('손님 게시물은 수정이 안됩니다')
141 | if (r._user.toString() !== req.user._id) throw new Error('본인이 작성한 게시물이 아닙니다')
142 | return Article.findByIdAndUpdate(_id, { $set: { title, content } }, { new: true })
143 | })
144 | .then(r => {
145 | res.send({ success: true, d: r, token: req.token })
146 | })
147 | .catch(e => {
148 | res.send({ success: false, msg: e.message })
149 | })
150 | })
151 | })
152 |
153 | router.delete('/:_id', (req, res, next) => {
154 | if (!req.user._id) throw createError(403, '게시물 삭제 권한이 없습니다')
155 | const _id = req.params._id
156 |
157 | Article.findById(_id).populate('_user', 'lv')
158 | .then(r => {
159 | if (!r) throw new Error('게시물이 존재하지 않습니다')
160 | if (!r._user) {
161 | if (req.user.lv > 0) throw new Error('손님 게시물은 삭제가 안됩니다')
162 | }
163 | else {
164 | if (r._user._id.toString() !== req.user._id) {
165 | if (r._user.lv < req.user.lv) throw new Error('본인이 작성한 게시물이 아닙니다')
166 | }
167 | }
168 | return Article.deleteOne({ _id })
169 | })
170 | .then(r => {
171 | res.send({ success: true, d: r, token: req.token })
172 | })
173 | .catch(e => {
174 | res.send({ success: false, msg: e.message })
175 | })
176 | })
177 |
178 | module.exports = router;
179 |
--------------------------------------------------------------------------------
/fe/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | {{$store.state.user.name}}
20 |
21 |
22 |
23 |
24 | chevron_left
25 |
26 |
27 |
28 |
29 |
30 |
31 |
38 |
39 |
40 |
41 | {{ item.title }}
42 |
43 |
44 |
49 |
50 | {{ subItem.title }}
51 |
52 |
53 |
54 |
55 |
56 |
59 |
60 | {{siteTitle}} {{$t('message')}}
61 |
62 |
63 |
64 |
65 | more_vert
66 |
67 |
68 |
69 | {{$i18n.locale === 'en' ? '한글' : 'English'}}
70 |
71 |
72 |
73 | 로그인
74 |
75 |
76 | 회원가입
77 |
78 |
79 |
80 |
81 | 사용자 정보
82 |
83 |
84 | 로그아웃
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | {{siteCopyright}}
97 |
98 |
102 | {{ $store.state.sb.msg }}
103 |
107 | 닫기
108 |
109 |
110 |
111 |
112 |
113 |
284 |
289 |
--------------------------------------------------------------------------------
/be/app.js:
--------------------------------------------------------------------------------
1 | var createError = require('http-errors');
2 | var express = require('express');
3 | var path = require('path');
4 | var cookieParser = require('cookie-parser');
5 | var logger = require('morgan');
6 | const history = require('connect-history-api-fallback')
7 | const cors = require('cors')
8 |
9 | var app = express();
10 |
11 | app.use(logger('dev'));
12 | app.use(express.json());
13 | app.use(express.urlencoded({ extended: false }));
14 | app.use(cookieParser());
15 | // console.log(path.join(__dirname, '../', 'fe', 'dist'))
16 | if (process.env.NODE_ENV !== 'production') app.use(cors())
17 | app.use('/api', require('./routes/api'))
18 | app.use(history())
19 | app.use(express.static(path.join(__dirname, '../', 'fe', 'dist')));
20 |
21 |
22 | // catch 404 and forward to error handler
23 | app.use(function(req, res, next) {
24 | next(createError(404));
25 | });
26 |
27 | // error handler
28 | app.use(function(err, req, res, next) {
29 | // set locals, only providing error in development
30 | res.locals.message = err.message;
31 | res.locals.error = req.app.get('env') === 'development' ? err : {};
32 |
33 | // render the error page
34 | res.status(err.status || 500);
35 | res.send({ msg: err.message })
36 | console.error(err.message)
37 | });
38 |
39 | const mongoose = require('mongoose')
40 |
41 | console.log(`${process.env.NODE_ENV} started!`)
42 |
43 |
44 | const cfg = require('../config')
45 |
46 | mongoose.connect(cfg.dbUrl, { useNewUrlParser: true }, (err) => {
47 | if (err) return console.error(err)
48 | console.log('mongoose connected!')
49 | // User.deleteMany()
50 | // .then(r => console.log(r))
51 | // .catch(e => console.error(e))
52 |
53 | // User.create({ name: '하하' })
54 | // .then(r => console.log(r))
55 | // .catch(e => console.error(e))
56 | // User.find()
57 | // .then(r => console.log(r))
58 | // .catch(e => console.error(e))
59 |
60 | // User.updateOne({ _id: '5bbe038d7588bf14393d08bf' }, { $set: { age: 34 } })
61 | // .then(r => {
62 | // console.log(r)
63 | // console.log('updated')
64 | // return User.find()
65 | // })
66 | // .then(r => console.log(r))
67 | // .catch(e => console.error(e))
68 | // User.deleteOne({ name: '하하' })
69 | // .then(r => console.log(r))
70 | // .catch(e => console.error(e))
71 |
72 | })
73 |
74 |
75 | module.exports = app;
76 |
77 | // var jwt = require('jsonwebtoken');
78 | // const key = '베리베리어려운키'
79 | // var token = jwt.sign({ id: 'memi', email: 'memi@xxx.com' }, key);
80 | // console.log(token)
81 | //
82 | // jwt.verify(token, 'ㄴㄹㅈㄷㄹ', (err, r) => {
83 | // if (err) return console.error(err.message)
84 | // console.log(r) // bar
85 | // console.log(new Date(r.iat * 1000).toLocaleString())
86 | // })
87 |
88 |
89 | // console.log(jwt.sign({ id: 'memi' }, 'abcd'))
90 | // console.log('헬로?')
91 | // jwt.sign({ id: 'memi'}, 'key', (err, token) => {
92 | // console.log(token)
93 | // })
94 | // console.log('헬로?')
95 |
96 | // User.findOne({}, (err, r) => {
97 | // if (err) return console.error(err)
98 | // console.log(r)
99 | // })
100 | // User.findOne({})
101 | // .then(r => console.log(r))
102 | // .catch(err => console.error(err))
103 |
104 |
105 | // 콜백
106 | // User.findOne({ name: 'aaa'}, (err, u) => {
107 | // if (err) return console.error(err.message)
108 | // if (!u) {
109 | // console.log(u) // null
110 | // User.create({ name: 'aaa', age: 10 }, (err, cu) => {
111 | // if (err) return console.error(err.message)
112 | // console.log(cu) // { name: 'aaa', age: 10, _id: 5bd81974ad66cf3832db0838, __v: 0 }
113 | // jwt.sign({ name: cu.name, age: cu.age}, key, (err, token) => {
114 | // if (err) return console.error(err.message)
115 | // console.log(token) // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
116 | // jwt.verify(token, key, (err, v) => {
117 | // if (err) return console.error(err.message)
118 | // console.log(v) // { name: 'aaa', age: 10, iat: 1540888948 }
119 | // })
120 | // })
121 | // })
122 | // }
123 | // else {
124 | // console.log(u) // { name: 'aaa', age: 10, _id: 5bd81974ad66cf3832db0838, __v: 0 }
125 | // const user = u
126 | // User.updateOne({ _id: u._id }, { $inc: { age: 1 }}, (err, ur) => {
127 | // if (err) return console.error(err.message)
128 | // console.log(ur) // { n: 1, nModified: 1, ... }
129 | // jwt.sign({ name: user.name, age: user.age + 1 }, key, (err, token) => {
130 | // if (err) return console.error(err.message)
131 | // console.log(token) // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
132 | // jwt.verify(token, key, (err, v) => {
133 | // if (err) return console.error(err.message)
134 | // console.log(v) // { name: 'aaa', age: 11, iat: 1540889007 }
135 | // })
136 | // })
137 | // })
138 | // }
139 | // })
140 |
141 | // User.deleteMany({})
142 | // .then(r => console.log(r))
143 | // .catch(err => console.error(err))
144 |
145 | // const signToken = (u, k) => {
146 | // return new Promise((resolve, reject) => {
147 | // jwt.sign({ name: u.name, age: u.age }, k, (err, token) => {
148 | // if (err) reject(err)
149 | // resolve(token)
150 | // })
151 | // })
152 | // }
153 | // const verifyToken = (t, k) => {
154 | // return new Promise((resolve, reject) => {
155 | // jwt.verify(t, k, (err, v) => {
156 | // if (err) reject(err)
157 | // resolve(v)
158 | // })
159 | // })
160 | // }
161 |
162 | // 프라미스
163 | // let user
164 | // User.findOne({ name: 'aaa' })
165 | // .then((u) => {
166 | // if (!u) return User.create({ name: 'aaa', age: 10 })
167 | // return Promise.resolve(u)
168 | // })
169 | // .then((u) => {
170 | // user = u
171 | // return User.updateOne({ _id: u._id }, { $inc: { age: 1 }})
172 | // })
173 | // .then((r) => {
174 | // if (!r.nModified) throw new Error('수정된 것이 없네요..')
175 | // user.age++
176 | // return signToken(user, key)
177 | // })
178 | // .then((token) => {
179 | // return verifyToken(token, key)
180 | // })
181 | // .then(v => console.log(v))
182 | // .catch((err) => {
183 | // console.error(err.message)
184 | // })
185 |
186 | // 어씽크 테스트
187 |
188 | // const asyncTest = async (i) => {
189 | // if (i > 10) throw new Error('10보다 큰건 싫어요')
190 | // return i + 2
191 | // }
192 | //
193 | // asyncTest(1)
194 | // .then(r => console.log(`2 더해져서 ${r} 입니다.`))
195 | // .catch(e => console.error(`에러났네요: ${e.message}`))
196 | //
197 | // asyncTest(11)
198 | // .then(r => console.log(`2 더해져서 ${r} 입니다.`))
199 | // .catch(e => console.error(`에러났네요: ${e.message}`))
200 |
201 | // 어씽크
202 |
203 | // const getToken = async (name) => {
204 | // let u = await User.findOne({ name }) // name: name을 축약하고 await로 기다려 줍니다.
205 | // if (!u) u = await User.create({ name , age: 10 }) // 만들어주고 u를 갱신 합니다.
206 | // if (u.age > 12) throw new Error(`${u.age}는 나이가 너무 많습니다.`)
207 | // const ur = await User.updateOne({ _id: u._id }, { $inc: { age: 1 }}) // age를 증가시키고 ur(user result)에 결과값을 담아놓습니다.
208 | // if (!ur.nModified) throw new Error('수정된 것이 없네요..') // 수정된 값이 없다면 에러와 함께 내보냅니다.
209 | // u = await User.findOne({ _id: u._id }) // age가 증가 된 것으로 갱신해줍니다.
210 | // const token = await signToken(u, key) // 받아 두었던 u로 토큰을 만듭니다.
211 | // const v = await verifyToken(token, key)
212 | // return v
213 | // }
214 | //
215 | // getToken('aaa')
216 | // .then(v => console.log(v))
217 | // .catch(err => console.error(err.message))
218 |
219 | // const crypto = require('crypto');
220 | // const bf = Buffer.alloc(64)
221 | // const s = crypto.randomFillSync(bf)
222 | // console.log(s.toString('hex'))
223 |
224 | // const moment = require('moment')
225 |
226 | // moment.locale('ko')
227 | //
228 | // console.log(moment().fromNow())
229 | // console.log(moment().add(-5, 'hours').fromNow())
230 | // console.log(moment().add(5, 'hours').fromNow())
231 | //
232 | // moment.locale('ko')
233 | // console.log(moment().fromNow())
234 | // console.log(moment().add(-40, 'hours').fromNow())
235 | // console.log(moment().add(10, 'hours').fromNow())
236 |
237 | // const ct = moment() // 현재시간
238 | // const bt = moment().add(-1, 'hours') // 한시간전
239 | //
240 | // console.log(ct.diff(bt))
241 | // console.log(ct.diff(bt,'seconds'))
242 | //
243 | // console.log(bt.diff(ct))
244 | // console.log(bt.diff(ct,'seconds'))
245 |
246 |
247 | // const User = require('./models/users')
248 | // const Board = require('./models/boards')
249 | // const Article = require('./models/articles')
250 |
251 | // User.findOne()
252 | // .then(r => console.log(r.id, r._id)) // 5be1c7eb0ff40640c81ecc0d
253 | //
254 | //
255 | // Board.findOne()
256 | // .then(r => console.log(r.name, r._id)) // 5be97f5f8fb2da704ad95273
257 |
258 | // Article.create({ title: 'aaa', content: 'kkfjf', _user: '5be1c7eb0ff40640c81ecc0d', _board: '5be97f5f8fb2da704ad95273' })
259 | // .then(r => console.log(r))
260 |
261 |
262 | // Article.find({ _board: '5be97f5f8fb2da704ad95273'})
263 | // .populate('_user', 'name')
264 | // .populate('_board')
265 | // .then(r => console.log(r))
266 |
267 | // const net = require('net')
268 | // net.createServer((sock) => {
269 | //
270 | // console.log('connected')
271 | // sock.on('data', (data) => {
272 | // console.log(data)
273 | // })
274 | // sock.on('error', (err) => {
275 | // console.log(err)
276 | // })
277 | // sock.on('close', () => {
278 | // console.log('close')
279 | // })
280 | // }).listen(cfgNet.port)
281 |
--------------------------------------------------------------------------------
/fe/src/views/board/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{board.title}}
9 | {{board.rmk}}
10 |
11 |
12 |
13 |
20 |
21 |
31 |
32 | {{ id2date(props.item._id)}} |
33 | {{ props.item.title }} |
34 | {{ props.item._user ? props.item._user.id : '손님' }} |
35 | {{ props.item.cnt.view }} |
36 | {{ props.item.cnt.like }} |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
47 |
48 |
49 |
59 | add
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | 제목: {{selArticle.title}}
68 |
69 |
73 | clear
74 |
75 |
76 |
77 |
78 | 내용
79 |
80 |
81 |
82 |
83 |
84 | 수정
85 | 삭제
86 | 닫기
87 |
88 |
89 |
90 |
91 | 정말 진행 하시겠습니까?
92 | 확인
93 | 취소
94 |
95 |
96 |
97 |
98 |
99 |
100 | {{comment.content}}
101 | {{comment._user ? comment._user.id : '손님'}}
102 |
103 |
104 |
109 |
110 | create
111 |
112 |
113 |
114 |
115 |
116 |
121 |
122 | clear
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 | 글 {{(dlMode === 1) ? '작성' : '수정'}}
145 |
146 |
150 | clear
151 |
152 |
153 |
154 |
155 |
156 |
162 |
168 |
169 |
170 |
171 |
172 |
173 | 확인
174 | 취소
175 |
176 |
177 |
178 |
179 |
180 |
181 |
186 |
187 |
188 |
189 |
190 |
191 |
192 | 수정
193 |
194 | 닫기
195 |
196 |
197 |
198 |
199 |
200 |
209 |
216 |
217 |
218 |
219 |
489 |
494 |
--------------------------------------------------------------------------------