├── static └── .gitkeep ├── config ├── prod.env.js ├── dev.env.js ├── test.env.js ├── test.conf.js ├── dev.conf.js └── index.js ├── src ├── store │ ├── mutation-types.js │ ├── index.js │ └── modules │ │ └── token.js ├── assets │ ├── logo@2x.gif │ ├── logo@2x.png │ ├── logo-invert@2x.gif │ ├── logo-invert@2x.png │ └── cat-sakura-road-japan.jpg ├── styles │ ├── fonts │ │ ├── NotoSans-Regular-webfont.eot │ │ ├── NotoSans-Regular-webfont.ttf │ │ └── NotoSans-Regular-webfont.woff │ ├── index.less │ └── custom.less ├── components │ ├── AdminHome.vue │ ├── Logo.vue │ ├── Upload.vue │ ├── Footer.vue │ ├── Admin.vue │ ├── Loading.vue │ ├── Hello.vue │ ├── Auth.vue │ ├── AdminPost.vue │ ├── Header.vue │ ├── Post.vue │ ├── Input.vue │ ├── Button.vue │ ├── Content.vue │ └── AdminEdit.vue ├── main.js ├── resource.js ├── router │ └── index.js └── App.vue ├── screenshots ├── 1.jpg ├── 2.jpg ├── 3.jpg └── 4.jpg ├── .eslintignore ├── test ├── server │ ├── files │ │ ├── images │ │ │ ├── test.gif │ │ │ ├── test.ini │ │ │ ├── test.jpg │ │ │ └── test.png │ │ ├── post.spec.js │ │ └── list.spec.js │ ├── basic │ │ └── test.spec.js │ ├── users │ │ ├── basic.spec.js │ │ └── register.spec.js │ ├── mocha.conf.js │ ├── helpers │ │ ├── inject.js │ │ └── authorize.js │ ├── tokens │ │ └── basic.spec.js │ ├── posts │ │ ├── list.spec.js │ │ ├── modify.spec.js │ │ ├── post.spec.js │ │ └── get.spec.js │ └── tags │ │ └── basic.spec.js ├── unit │ ├── .eslintrc │ ├── specs │ │ └── Hello.spec.js │ ├── index.js │ └── karma.conf.js └── e2e │ ├── specs │ └── test.js │ ├── custom-assertions │ └── elementCount.js │ ├── runner.js │ └── nightwatch.conf.js ├── server ├── controllers │ ├── tokens │ │ ├── index.js │ │ └── post.js │ ├── users │ │ ├── index.js │ │ └── post.js │ ├── tags │ │ ├── -id │ │ │ ├── index.js │ │ │ └── delete.js │ │ ├── index.js │ │ ├── get.js │ │ └── post.js │ ├── files │ │ ├── index.js │ │ ├── get.js │ │ └── post.js │ └── posts │ │ ├── index.js │ │ ├── -id │ │ ├── index.js │ │ ├── delete.js │ │ ├── put.js │ │ └── get.js │ │ ├── post.js │ │ └── get.js ├── utils │ ├── pass.js │ ├── random.js │ ├── idt.js │ ├── error-handler.js │ ├── orm-schema.js │ ├── compose.js │ ├── authority.js │ └── router-loader.js ├── models │ ├── log.js │ ├── file.js │ ├── token.js │ ├── tag.js │ ├── item-tag.js │ ├── index.js │ ├── user.js │ └── post.js └── index.js ├── .editorconfig ├── .postcssrc.js ├── .gitignore ├── .babelrc ├── .travis.yml ├── .eslintrc.json ├── index.html ├── README.md ├── Gruntfile.js ├── package.json └── LICENSE /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"prod"' 3 | } 4 | -------------------------------------------------------------------------------- /src/store/mutation-types.js: -------------------------------------------------------------------------------- 1 | export const SET_TOKEN = 'SET_TOKEN'; 2 | -------------------------------------------------------------------------------- /screenshots/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/nodecho/master/screenshots/1.jpg -------------------------------------------------------------------------------- /screenshots/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/nodecho/master/screenshots/2.jpg -------------------------------------------------------------------------------- /screenshots/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/nodecho/master/screenshots/3.jpg -------------------------------------------------------------------------------- /screenshots/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/nodecho/master/screenshots/4.jpg -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | test/e2e/reports/ 4 | test/unit/coverage/ 5 | -------------------------------------------------------------------------------- /src/assets/logo@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/nodecho/master/src/assets/logo@2x.gif -------------------------------------------------------------------------------- /src/assets/logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/nodecho/master/src/assets/logo@2x.png -------------------------------------------------------------------------------- /src/assets/logo-invert@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/nodecho/master/src/assets/logo-invert@2x.gif -------------------------------------------------------------------------------- /src/assets/logo-invert@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/nodecho/master/src/assets/logo-invert@2x.png -------------------------------------------------------------------------------- /test/server/files/images/test.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/nodecho/master/test/server/files/images/test.gif -------------------------------------------------------------------------------- /test/server/files/images/test.ini: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/nodecho/master/test/server/files/images/test.ini -------------------------------------------------------------------------------- /test/server/files/images/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/nodecho/master/test/server/files/images/test.jpg -------------------------------------------------------------------------------- /test/server/files/images/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/nodecho/master/test/server/files/images/test.png -------------------------------------------------------------------------------- /src/assets/cat-sakura-road-japan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/nodecho/master/src/assets/cat-sakura-road-japan.jpg -------------------------------------------------------------------------------- /src/styles/fonts/NotoSans-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/nodecho/master/src/styles/fonts/NotoSans-Regular-webfont.eot -------------------------------------------------------------------------------- /src/styles/fonts/NotoSans-Regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/nodecho/master/src/styles/fonts/NotoSans-Regular-webfont.ttf -------------------------------------------------------------------------------- /src/styles/fonts/NotoSans-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/nodecho/master/src/styles/fonts/NotoSans-Regular-webfont.woff -------------------------------------------------------------------------------- /server/controllers/tokens/index.js: -------------------------------------------------------------------------------- 1 | exports.index = [ 2 | { 3 | method: 'POST', 4 | handler: require('./post') 5 | } 6 | ]; 7 | -------------------------------------------------------------------------------- /server/controllers/users/index.js: -------------------------------------------------------------------------------- 1 | exports.index = [ 2 | { 3 | method: 'POST', 4 | handler: require('./post') 5 | } 6 | ]; 7 | -------------------------------------------------------------------------------- /server/controllers/tags/-id/index.js: -------------------------------------------------------------------------------- 1 | exports.index = [ 2 | { 3 | method: 'DELETE', 4 | handler: require('./delete') 5 | } 6 | ]; 7 | -------------------------------------------------------------------------------- /test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "sinon": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"dev"' 6 | }) 7 | -------------------------------------------------------------------------------- /config/test.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var devEnv = require('./dev.env') 3 | 4 | module.exports = merge(devEnv, { 5 | NODE_ENV: '"test"' 6 | }) 7 | -------------------------------------------------------------------------------- /server/utils/pass.js: -------------------------------------------------------------------------------- 1 | const Q = require('q'); 2 | 3 | module.exports = function (_s) { 4 | var deferred = Q.defer(); 5 | deferred.resolve(_s || {}); 6 | return deferred.promise; 7 | }; 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | // to edit target browsers: use "browserlist" field in package.json 6 | "autoprefixer": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /server/controllers/files/index.js: -------------------------------------------------------------------------------- 1 | exports.index = [ 2 | { 3 | method: 'POST', 4 | handler: require('./post') 5 | }, 6 | { 7 | method: 'GET', 8 | handler: require('./get') 9 | } 10 | ]; 11 | -------------------------------------------------------------------------------- /server/controllers/posts/index.js: -------------------------------------------------------------------------------- 1 | exports.index = [ 2 | { 3 | method: 'POST', 4 | handler: require('./post') 5 | }, 6 | { 7 | method: 'GET', 8 | handler: require('./get') 9 | } 10 | ]; 11 | -------------------------------------------------------------------------------- /server/controllers/tags/index.js: -------------------------------------------------------------------------------- 1 | exports.index = [ 2 | { 3 | method: 'POST', 4 | handler: require('./post') 5 | }, 6 | { 7 | method: 'GET', 8 | handler: require('./get') 9 | } 10 | ]; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | test/unit/coverage 8 | test/e2e/reports 9 | selenium-debug.log 10 | config/prod.conf.js 11 | dev.db 12 | tmp/ 13 | files/ 14 | font.ttf 15 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "modules": false }], 4 | "stage-2" 5 | ], 6 | "plugins": ["transform-runtime"], 7 | "env": { 8 | "test": { 9 | "presets": ["env", "stage-2"], 10 | "plugins": [ "istanbul" ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import token from './modules/token'; 4 | 5 | Vue.use(Vuex); 6 | 7 | const debug = process.env.NODE_ENV !== 'prod'; 8 | 9 | export default new Vuex.Store({ 10 | modules: { 11 | token 12 | }, 13 | strict: debug 14 | }); 15 | -------------------------------------------------------------------------------- /server/controllers/posts/-id/index.js: -------------------------------------------------------------------------------- 1 | exports.index = [ 2 | { 3 | method: 'PUT', 4 | handler: require('./put') 5 | }, 6 | { 7 | method: 'GET', 8 | handler: require('./get') 9 | }, 10 | { 11 | method: 'DELETE', 12 | handler: require('./delete') 13 | } 14 | ]; 15 | -------------------------------------------------------------------------------- /server/utils/random.js: -------------------------------------------------------------------------------- 1 | module.exports = function (len, _pos) { 2 | var text = ''; 3 | var possible = _pos || 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 4 | 5 | for (var i = 0; i < len; i++) { 6 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 7 | } 8 | 9 | return text; 10 | }; 11 | -------------------------------------------------------------------------------- /config/test.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const merge = require('lodash').merge; 3 | 4 | var devConf = require('./dev.conf'); 5 | 6 | module.exports = merge(devConf, { 7 | 'file': { 8 | path: path.join(__dirname, '../tmp'), 9 | mimetype: ['image/gif', 'image/x-png', 'image/pjpeg', 'image/jpg', 'image/jpeg', 'image/png'] 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/AdminHome.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 21 | -------------------------------------------------------------------------------- /server/utils/idt.js: -------------------------------------------------------------------------------- 1 | const Hashids = require('hashids'); 2 | 3 | const comps = { 4 | 'User': new Hashids('user', 16), 5 | 'Post': new Hashids('post', 16) 6 | }; 7 | 8 | module.exports = { 9 | encode (type, code) { 10 | return comps[type].encode(code); 11 | }, 12 | decode (type, code) { 13 | return comps[type].decode(code)[0]; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /test/server/basic/test.spec.js: -------------------------------------------------------------------------------- 1 | describe('connectivity', function () { 2 | it('should response Undefined API', function () { 3 | return request(_server_) 4 | .get('/api') 5 | .expect('Content-Type', /json/) 6 | .expect(404) 7 | .then(response => { 8 | assert(response.body.error, 'Undefined API'); 9 | }); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/unit/specs/Hello.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Hello from '@/components/Hello'; 3 | 4 | describe('Hello.vue', () => { 5 | it('should render correct contents', () => { 6 | const Constructor = Vue.extend(Hello); 7 | const vm = new Constructor().$mount(); 8 | expect(vm.$el.querySelector('.hello h1').textContent) 9 | .to.equal('Welcome to Your Vue.js App'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App'; 3 | import router from './router'; 4 | import resource from './resource'; 5 | import store from './store'; 6 | 7 | import './styles/index.less'; 8 | import 'normalize.css/normalize.css'; 9 | 10 | Vue.config.productionTip = false; 11 | 12 | Vue.use(resource); 13 | 14 | /* eslint-disable no-new */ 15 | new Vue({ 16 | el: '#app', 17 | router, 18 | store, 19 | template: '', 20 | components: { App } 21 | }); 22 | -------------------------------------------------------------------------------- /server/models/log.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | var Log = sequelize.define('Log', { 5 | ip: { 6 | type: DataTypes.STRING(15), 7 | allowNull: false 8 | }, 9 | description: { 10 | type: DataTypes.STRING 11 | } 12 | }, { 13 | paranoid: true, 14 | updatedAt: false 15 | }); 16 | 17 | Log.associate = models => { 18 | Log.belongsTo(models.Post); 19 | }; 20 | 21 | return Log; 22 | }; 23 | -------------------------------------------------------------------------------- /server/models/file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | var File = sequelize.define('File', { 5 | key: { 6 | type: DataTypes.STRING(64), 7 | allowNull: false 8 | }, 9 | size: { 10 | type: DataTypes.INTEGER, 11 | allowNull: false 12 | } 13 | }, { 14 | paranoid: true 15 | }); 16 | 17 | File.associate = models => { 18 | File.belongsTo(models.User); 19 | }; 20 | 21 | return File; 22 | }; 23 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | Vue.config.productionTip = false; 4 | 5 | // require all test files (files that ends with .spec.js) 6 | const testsContext = require.context('./specs', true, /\.spec$/); 7 | testsContext.keys().forEach(testsContext); 8 | 9 | // require all src files except main.js for coverage. 10 | // you can also change this to match only the subset of files that 11 | // you want coverage for. 12 | const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/); 13 | srcContext.keys().forEach(srcContext); 14 | -------------------------------------------------------------------------------- /server/models/token.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-useless-escape */ 2 | 3 | 'use strict'; 4 | 5 | module.exports = (sequelize, DataTypes) => { 6 | var Token = sequelize.define('Token', { 7 | token: { 8 | type: DataTypes.STRING, 9 | allowNull: false, 10 | validate: { 11 | notEmpty: true, 12 | is: ['^([a-zA-Z0-9]){32}$', 'gi'] 13 | } 14 | } 15 | }); 16 | 17 | Token.associate = models => { 18 | Token.belongsTo(models.User); 19 | }; 20 | 21 | return Token; 22 | }; 23 | -------------------------------------------------------------------------------- /src/styles/index.less: -------------------------------------------------------------------------------- 1 | @import '~iview/src/styles/index.less'; 2 | 3 | @charset "UTF-8"; 4 | @font-face { 5 | font-family: 'NotoSans-Regular'; 6 | src: url('fonts/NotoSans-Regular-webfont.eot'); 7 | src: url('fonts/NotoSans-Regular-webfont.eot?#iefix') format('embedded-opentype'), url('fonts/NotoSans-Regular-webfont.woff') format('woff'), url('fonts/NotoSans-Regular-webfont.ttf') format('truetype'), url('fonts/NotoSans-Regular-webfont.svg#noto_sansregular') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | @import './custom.less'; 13 | -------------------------------------------------------------------------------- /src/components/Logo.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 22 | 23 | 28 | -------------------------------------------------------------------------------- /test/server/users/basic.spec.js: -------------------------------------------------------------------------------- 1 | describe('users', function () { 2 | it('should only has POST method', function () { 3 | return Promise.all([ 4 | request(_server_) 5 | .get('/api/user') 6 | .expect(404), 7 | request(_server_) 8 | .put('/api/user') 9 | .expect(404), 10 | request(_server_) 11 | .patch('/api/user') 12 | .expect(404), 13 | request(_server_) 14 | .delete('/api/user') 15 | .expect(404) 16 | ]); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/resource.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueResource from 'vue-resource'; 3 | 4 | Vue.use(VueResource); 5 | Vue.http.options.root = '/api'; 6 | 7 | export default class Resource { 8 | static models; 9 | 10 | constructor (props) { 11 | this.models = {}; 12 | } 13 | 14 | static install (Vue, options) { 15 | this.models = { 16 | files: Vue.resource('files'), 17 | posts: Vue.resource('posts{/id}'), 18 | tokens: Vue.resource('tokens', {}, { 19 | generate: { method: 'POST' } 20 | }) 21 | }; 22 | 23 | Vue.prototype.$api = this.models; 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /test/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // For authoring Nightwatch tests, see 2 | // http://nightwatchjs.org/guide#usage 3 | 4 | module.exports = { 5 | 'default e2e tests': function (browser) { 6 | // automatically uses dev Server port from /config.index.js 7 | // default: http://localhost:8080 8 | // see nightwatch.conf.js 9 | const devServer = browser.globals.devServerURL; 10 | 11 | browser 12 | .url(devServer) 13 | .waitForElementVisible('#app', 5000) 14 | .assert.elementPresent('.hello') 15 | .assert.containsText('h1', 'Welcome to Your Vue.js App') 16 | .assert.elementCount('img', 1) 17 | .end(); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /server/models/tag.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | var Tag = sequelize.define('Tag', { 5 | name: { 6 | type: DataTypes.STRING, 7 | allowNull: false, 8 | validate: { notEmpty: true } 9 | } 10 | }, { 11 | timestamps: false, 12 | underscored: true 13 | }); 14 | 15 | Tag.associate = models => { 16 | Tag.belongsToMany(models.Post, { 17 | through: { 18 | model: models.ItemTag, 19 | unique: false 20 | }, 21 | foreignKey: 'tag_id', 22 | constraints: false 23 | }); 24 | }; 25 | 26 | return Tag; 27 | }; 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: true 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - "6.11.0" 7 | 8 | python: 9 | - "2.7" 10 | 11 | compiler: 12 | - gcc 13 | 14 | addons: 15 | apt: 16 | sources: 17 | - ubuntu-toolchain-r-test 18 | packages: 19 | - gcc-5 20 | - g++-5 21 | 22 | install: 23 | - npm install -g node-gyp 24 | - sudo unlink /usr/bin/gcc && sudo ln -s /usr/bin/gcc-5 /usr/bin/gcc 25 | - sudo unlink /usr/bin/g++ && sudo ln -s /usr/bin/g++-5 /usr/bin/g++ 26 | - gcc --version 27 | - g++ --version 28 | - make --version 29 | - npm install -g apidoc bower grunt grunt-cli 30 | - npm install 31 | 32 | # command to run tests 33 | script: 34 | - grunt travis 35 | # - bash ./deploy/deploy.sh 36 | -------------------------------------------------------------------------------- /server/controllers/tags/get.js: -------------------------------------------------------------------------------- 1 | const Q = require('q'); 2 | const models = require('../../models'); 3 | const errorHandler = require('../../utils/error-handler'); 4 | const pass = require('../../utils/pass'); 5 | 6 | module.exports = (req, res) => { 7 | return Q 8 | .fcall(pass) 9 | 10 | .then(_s => { 11 | let deferred = Q.defer(); 12 | models.Tag 13 | .all() 14 | .then(projects => { 15 | _s.result = projects || []; 16 | deferred.resolve(_s); 17 | }); 18 | return deferred.promise; 19 | }) 20 | 21 | .done(_s => { 22 | res.status(200).send(_s.result); 23 | }, errorHandler(res)); 24 | }; 25 | -------------------------------------------------------------------------------- /server/models/item-tag.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | var ItemTag = sequelize.define('ItemTag', { 5 | id: { 6 | type: DataTypes.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true 9 | }, 10 | tag_id: { 11 | type: DataTypes.INTEGER, 12 | unique: 'item_tag_taggable' 13 | }, 14 | taggable: { 15 | type: DataTypes.STRING(191), 16 | unique: 'item_tag_taggable' 17 | }, 18 | taggable_id: { 19 | type: DataTypes.INTEGER, 20 | unique: 'item_tag_taggable', 21 | references: null 22 | } 23 | }, { 24 | timestamps: false, 25 | underscored: true 26 | }); 27 | 28 | return ItemTag; 29 | }; 30 | -------------------------------------------------------------------------------- /server/models/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const Sequelize = require('sequelize'); 6 | const config = require('../../config').db; 7 | const sequelize = new Sequelize(config.database, config.username, config.password, config); 8 | const db = {}; 9 | 10 | fs 11 | .readdirSync(__dirname) 12 | .filter(file => { 13 | return (file.indexOf('.') !== 0) && (file !== 'index.js'); 14 | }) 15 | .forEach(file => { 16 | var model = sequelize.import(path.join(__dirname, file)); 17 | db[model.name] = model; 18 | }); 19 | 20 | Object.keys(db).forEach(modelName => { 21 | if ('associate' in db[modelName]) { 22 | db[modelName].associate(db); 23 | } 24 | }); 25 | 26 | db.sequelize = sequelize; 27 | db.Sequelize = Sequelize; 28 | 29 | module.exports = db; 30 | -------------------------------------------------------------------------------- /config/dev.conf.js: -------------------------------------------------------------------------------- 1 | const tryRequire = require('tryrequire'); 2 | const path = require('path'); 3 | const merge = require('lodash').merge; 4 | 5 | let prodConf = {}; 6 | try { 7 | prodConf = require('./prod.conf'); 8 | } catch (e) { 9 | if (e.code !== 'MODULE_NOT_FOUND') { 10 | throw e; 11 | } 12 | }; 13 | 14 | module.exports = merge(prodConf, { 15 | 'db': { 16 | dialect: 'sqlite', 17 | storage: './dev.db', 18 | host: 'localhost', 19 | user: 'root', 20 | password: 'root', 21 | database: 'nodecho', 22 | logging: false, 23 | define: { 24 | underscored: true 25 | } 26 | }, 27 | 'file': { 28 | path: path.join(__dirname, '../files'), 29 | mimetype: ['image/gif', 'image/x-png', 'image/pjpeg', 'image/jpg', 'image/jpeg', 'image/png'] 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /server/utils/error-handler.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | module.exports = function (res) { 4 | return function (obj) { 5 | let err = { 6 | message: 'Undefined', 7 | _s: null, 8 | statusCode: 400, 9 | extra: {} 10 | }; 11 | if (_.isArray(obj)) { 12 | err.message = obj[0] || 'Undefined error'; 13 | err._s = obj[1]; 14 | err.statusCode = obj[2] || 400; 15 | err.extra = obj[3]; 16 | } else { 17 | err = _.assign(err, obj); 18 | } 19 | try { 20 | err._s.connection.release(); 21 | } catch (e) {} 22 | if (process.env.NODE_ENV === 'dev') console.log(obj); 23 | res.status(err.statusCode).send(_.defaults(err.extra, { 24 | 'error': (err.message).toString() 25 | })); 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /server/utils/orm-schema.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | module.exports = function (orm, exclude, extra) { 4 | return _.merge( 5 | _.omit( 6 | _.mapValues( 7 | _.pickBy( 8 | orm.rawAttributes, 9 | (p) => _.some(['validate', '_validate'], key => _.has(p, key)) 10 | ), 11 | (param, key) => _.assign( 12 | param.schema || {}, 13 | _.omit(param.validate, 'is'), 14 | { 15 | errorMessage: 'Invalid ' + key 16 | }, 17 | _.has(param.validate, 'is') 18 | ? { matches: { options: param.validate.is } } 19 | : {} 20 | ) 21 | ), 22 | exclude 23 | ), 24 | extra 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /server/utils/compose.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Compose middlewares 3 | * 4 | * @param middlewares 5 | * @param path 6 | * 7 | * @returns {Function} 8 | */ 9 | function compose (middlewares, path) { 10 | if (!Array.isArray(middlewares)) { 11 | throw new Error(`middlewares ${JSON.stringify(middlewares)} should be an Array of functions.`); 12 | }; 13 | 14 | if (middlewares.length) { 15 | for (const fn of middlewares) { 16 | if (typeof fn !== 'function') { 17 | throw new Error(`middleware ${path} - ${JSON.stringify(fn)} should be a function, ignored.`); 18 | }; 19 | }; 20 | }; 21 | 22 | return (req, res, next) => { 23 | (function iterate (i, max) { 24 | if (i === max) return next(); 25 | middlewares[i](req, res, iterate.bind(this, i + 1, max)); 26 | })(0, middlewares.length); 27 | }; 28 | }; 29 | 30 | module.exports = compose; 31 | -------------------------------------------------------------------------------- /test/e2e/custom-assertions/elementCount.js: -------------------------------------------------------------------------------- 1 | // A custom Nightwatch assertion. 2 | // the name of the method is the filename. 3 | // can be used in tests like this: 4 | // 5 | // browser.assert.elementCount(selector, count) 6 | // 7 | // for how to write custom assertions see 8 | // http://nightwatchjs.org/guide#writing-custom-assertions 9 | exports.assertion = function (selector, count) { 10 | this.message = 'Testing if element <' + selector + '> has count: ' + count; 11 | this.expected = count; 12 | this.pass = function (val) { 13 | return val === this.expected; 14 | }; 15 | this.value = function (res) { 16 | return res.value; 17 | }; 18 | this.command = function (cb) { 19 | var self = this; 20 | return this.api.execute(function (selector) { 21 | return document.querySelectorAll(selector).length; 22 | }, [selector], function (res) { 23 | cb.call(self, res); 24 | }); 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/Upload.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 37 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "babel-eslint", 4 | "parserOptions": { 5 | "sourceType": "module" 6 | }, 7 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 8 | "extends": "standard", 9 | // required to lint *.vue files 10 | "plugins": [ 11 | "html" 12 | ], 13 | // add your custom rules here 14 | "rules": { 15 | "indent": ["error", 4], 16 | "space-before-function-paren": ["error", "always"], 17 | // "space-in-parens": ["error", "always", { "exceptions": ["()", "empty", "{}"] }], 18 | // "space-in-parens": ["error", "never", { "exceptions": ["[]"] }], 19 | "space-before-blocks": ["error", "always"], 20 | "semi": ["error", "always"], 21 | // allow paren-less arrow functions 22 | "arrow-parens": 0, 23 | // allow async-await 24 | "generator-star-spacing": 0, 25 | "quotes": ["error", "single"], 26 | "no-undef": "off" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var merge = require('lodash').merge; 3 | 4 | let conf = {}; 5 | try { 6 | conf = require('./' + (process.env.NODE_ENV || 'test') + '.conf'); 7 | } catch (e) { 8 | if (e.code !== 'MODULE_NOT_FOUND') { 9 | throw e; 10 | } 11 | }; 12 | 13 | module.exports = merge(conf, { 14 | build: { 15 | env: require('./prod.env'), 16 | index: path.resolve(__dirname, '../dist/index.html'), 17 | assetsRoot: path.resolve(__dirname, '../dist'), 18 | assetsSubDirectory: 'static', 19 | assetsPublicPath: '/', 20 | productionSourceMap: true, 21 | productionGzip: true, 22 | productionGzipExtensions: ['js', 'css'], 23 | bundleAnalyzerReport: process.env.npm_config_report 24 | }, 25 | dev: { 26 | env: require('./dev.env'), 27 | port: 8080, 28 | autoOpenBrowser: false, 29 | assetsSubDirectory: 'static', 30 | assetsPublicPath: '/', 31 | proxyTable: {}, 32 | cssSourceMap: false 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /test/server/mocha.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var fs = require('fs'); 5 | 6 | module.exports = function (grunt) { 7 | var mochaTest = { 8 | options: { 9 | reporter: 'spec', 10 | quiet: false, 11 | noFail: false, 12 | clearRequireCache: false, 13 | require: [] 14 | // reporter: 'list' 15 | }, 16 | sharedFiles: [ 17 | 'test/server/helper/**/*.js' 18 | ], 19 | all: { 20 | src: [ 21 | 'test/server/**/*.spec.js' 22 | ] 23 | } 24 | }; 25 | 26 | var helperDir = 'test/server/helpers'; 27 | 28 | var helpers = fs.readdirSync( 29 | helperDir 30 | ).map(file => helperDir + '/' + file); 31 | 32 | _.forEach(mochaTest, test => { 33 | if (test.src) { 34 | test.src = helpers.concat(test.src); 35 | } 36 | }); 37 | 38 | grunt.config.merge({ mochaTest: mochaTest }); 39 | 40 | grunt.registerTask('mocha', [ 'env:test', 'mochaTest' ]); 41 | }; 42 | -------------------------------------------------------------------------------- /server/controllers/tags/-id/delete.js: -------------------------------------------------------------------------------- 1 | const Q = require('q'); 2 | const models = require('../../../models'); 3 | 4 | const errorHandler = require('../../../utils/error-handler'); 5 | const authority = require('../../../utils/authority'); 6 | 7 | module.exports = (req, res) => { 8 | return Q 9 | .fcall(() => { return { raw: req.body }; }) 10 | .then(authority) 11 | 12 | .then(_s => { 13 | let deferred = Q.defer(); 14 | models.Tag 15 | .destroy({ 16 | where: { 17 | id: req.params.id 18 | } 19 | }) 20 | .then(affectedRows => { 21 | if (affectedRows) { 22 | deferred.resolve(_s); 23 | } else { 24 | deferred.reject(['Not found', _s, 404]); 25 | } 26 | }); 27 | return deferred.promise; 28 | }) 29 | 30 | .done(_s => { 31 | res.status(200).send(_s.result); 32 | }, errorHandler(res)); 33 | }; 34 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 「秋桜」 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 25 | 26 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /server/utils/authority.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Q = require('q'); 3 | const models = require('../models'); 4 | const idt = require('../utils/idt'); 5 | 6 | module.exports = _s => { 7 | var deferred = Q.defer(); 8 | 9 | var data = _.pick(_s.raw, 'userid', 'token'); 10 | models.Token 11 | .destroy({ where: { 12 | created_at: { 13 | $lt: new Date(new Date() - 24 * 60 * 60 * 1000) 14 | } 15 | } }) 16 | .then(() => { 17 | return models.Token 18 | .findOne({ 19 | where: { 20 | token: data.token 21 | }, 22 | include: [{ 23 | model: models.User, 24 | where: { id: idt.decode('User', data.userid) } 25 | }] 26 | }); 27 | }) 28 | .then(token => { 29 | if (token) { 30 | deferred.resolve(_.extend(_s, { token, user_id: idt.decode('User', data.userid) })); 31 | } else { 32 | deferred.reject(['Unauthorized', _s, 401]); 33 | } 34 | }); 35 | 36 | return deferred.promise; 37 | }; 38 | -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | // This is a karma config file. For more details see 2 | // http://karma-runner.github.io/0.13/config/configuration-file.html 3 | // we are also using it with karma-webpack 4 | // https://github.com/webpack/karma-webpack 5 | 6 | var webpackConfig = require('../../build/webpack.test.conf'); 7 | 8 | module.exports = function (config) { 9 | config.set({ 10 | // to run in additional browsers: 11 | // 1. install corresponding karma launcher 12 | // http://karma-runner.github.io/0.13/config/browsers.html 13 | // 2. add it to the `browsers` array below. 14 | browsers: ['PhantomJS'], 15 | frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'], 16 | reporters: ['spec', 'coverage'], 17 | files: ['./index.js'], 18 | preprocessors: { 19 | './index.js': ['webpack', 'sourcemap'] 20 | }, 21 | webpack: webpackConfig, 22 | webpackMiddleware: { 23 | noInfo: true 24 | }, 25 | coverageReporter: { 26 | dir: './coverage', 27 | reporters: [ 28 | { type: 'lcov', subdir: '.' }, 29 | { type: 'text-summary' } 30 | ] 31 | } 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /test/e2e/runner.js: -------------------------------------------------------------------------------- 1 | // 1. start the dev server using production config 2 | process.env.NODE_ENV = 'test'; 3 | var server = require('../../build/dev-server.js'); 4 | 5 | server.ready.then(() => { 6 | // 2. run the nightwatch test suite against it 7 | // to run in additional browsers: 8 | // 1. add an entry in test/e2e/nightwatch.conf.json under "test_settings" 9 | // 2. add it to the --env flag below 10 | // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox` 11 | // For more information on Nightwatch's config file, see 12 | // http://nightwatchjs.org/guide#settings-file 13 | var opts = process.argv.slice(2); 14 | if (opts.indexOf('--config') === -1) { 15 | opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js']); 16 | } 17 | if (opts.indexOf('--env') === -1) { 18 | opts = opts.concat(['--env', 'chrome']); 19 | } 20 | 21 | var spawn = require('cross-spawn'); 22 | var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' }); 23 | 24 | runner.on('exit', function (code) { 25 | server.close(); 26 | process.exit(code); 27 | }); 28 | 29 | runner.on('error', function (err) { 30 | server.close(); 31 | throw err; 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/store/modules/token.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | import * as types from '../mutation-types'; 4 | 5 | import store from 'store'; 6 | import expirePlugin from 'store/plugins/expire'; 7 | store.addPlugin(expirePlugin); 8 | 9 | const state = { 10 | userid: null, 11 | token: null 12 | }; 13 | 14 | const getters = { 15 | token: state => { return { userid: state.userid, token: state.token }; }, 16 | isLogin: state => !!state.token 17 | }; 18 | 19 | const actions = { 20 | set ({ commit, state }, data) { 21 | commit(types.SET_TOKEN, data); 22 | }, 23 | reset ({ commit, state }) { 24 | commit(types.SET_TOKEN); 25 | } 26 | }; 27 | 28 | const mutations = { 29 | [types.SET_TOKEN] (state, { userid = null, token = null } = {}) { 30 | state.userid = userid; 31 | state.token = token; 32 | 33 | let expiration = new Date().getTime() + 1 * 24 * 60 * 60 * 1000; 34 | store.set('token', { userid, token }, expiration); 35 | console.log(store.get('token')); 36 | } 37 | }; 38 | 39 | if (store.get('token')) { 40 | Vue.set(state, 'userid', store.get('token').userid); 41 | Vue.set(state, 'token', store.get('token').token); 42 | }; 43 | 44 | export default { 45 | namespaced: true, 46 | state, 47 | getters, 48 | actions, 49 | mutations 50 | }; 51 | -------------------------------------------------------------------------------- /test/server/helpers/inject.js: -------------------------------------------------------------------------------- 1 | /* jslint no-unuse-var: "off" */ 2 | 3 | const Q = require('q'); 4 | 5 | global.request = require('supertest'); 6 | global.assert = require('assert'); 7 | global._ = require('lodash'); 8 | 9 | global._server_ = null; 10 | global._db_ = null; 11 | 12 | before(done => { 13 | require('../../../server') 14 | .then(({ db, server }) => { 15 | var deferred = Q.defer(); 16 | if (!(process.env.NODE_ENV === 'test' && db.sequelize.config.host === 'localhost')) { 17 | deferred.resolve({ db, server }); 18 | } else { 19 | db.sequelize 20 | .sync({ force: true, logging: false }) 21 | .then(() => { 22 | console.log('Sync successfully.'); 23 | deferred.resolve({ db, server }); 24 | }) 25 | .catch(err => { 26 | deferred.reject(err); 27 | }); 28 | } 29 | return deferred.promise; 30 | }) 31 | .done(({ db, server }) => { 32 | _db_ = db; 33 | _server_ = server; 34 | done(); 35 | }, (err) => { 36 | console.error(err); 37 | }); 38 | }); 39 | 40 | after(function () { 41 | _server_.close(); 42 | }); 43 | -------------------------------------------------------------------------------- /src/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 40 | 41 | 66 | -------------------------------------------------------------------------------- /test/e2e/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | var config = require('../../config'); 3 | 4 | // http://nightwatchjs.org/gettingstarted#settings-file 5 | module.exports = { 6 | src_folders: ['test/e2e/specs'], 7 | output_folder: 'test/e2e/reports', 8 | custom_assertions_path: ['test/e2e/custom-assertions'], 9 | 10 | selenium: { 11 | start_process: true, 12 | server_path: require('selenium-server').path, 13 | host: '127.0.0.1', 14 | port: 4444, 15 | cli_args: { 16 | 'webdriver.chrome.driver': require('chromedriver').path 17 | } 18 | }, 19 | 20 | test_settings: { 21 | default: { 22 | selenium_port: 4444, 23 | selenium_host: 'localhost', 24 | silent: true, 25 | globals: { 26 | devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port) 27 | } 28 | }, 29 | 30 | chrome: { 31 | desiredCapabilities: { 32 | browserName: 'chrome', 33 | javascriptEnabled: true, 34 | acceptSslCerts: true 35 | } 36 | }, 37 | 38 | firefox: { 39 | desiredCapabilities: { 40 | browserName: 'firefox', 41 | javascriptEnabled: true, 42 | acceptSslCerts: true 43 | } 44 | } 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-useless-escape */ 2 | 3 | 'use strict'; 4 | 5 | module.exports = (sequelize, DataTypes) => { 6 | var User = sequelize.define('User', { 7 | username: { 8 | type: DataTypes.STRING, 9 | allowNull: false, 10 | validate: { 11 | notEmpty: true, 12 | is: ['^([a-zA-Z0-9]|[_]){4,16}$', 'gi'] 13 | } 14 | }, 15 | password: { 16 | type: DataTypes.STRING, 17 | allowNull: false, 18 | validate: { 19 | notEmpty: true, 20 | is: ['^([0-9a-zA-Z_\\\-/+!@#$%^&*]){6,255}$', 'gi'] 21 | } 22 | }, 23 | salt: { 24 | type: DataTypes.STRING, 25 | allowNull: false 26 | }, 27 | email: { 28 | type: DataTypes.STRING, 29 | validate: { 30 | notEmpty: true, 31 | isEmail: true 32 | } 33 | }, 34 | authority: { 35 | type: DataTypes.INTEGER, 36 | allowNull: false, 37 | defaultValue: 0 38 | }, 39 | avatar: { 40 | type: DataTypes.STRING(64) 41 | } 42 | }, { 43 | paranoid: true, 44 | getterMethods: { 45 | _id_ () { return require('../utils/idt').encode('User', this.id); } 46 | } 47 | }); 48 | 49 | return User; 50 | }; 51 | -------------------------------------------------------------------------------- /test/server/helpers/authorize.js: -------------------------------------------------------------------------------- 1 | const Q = require('q'); 2 | 3 | global._authorize_ = (token, data = {}) => { return _.assign(_.clone(data), token); }; 4 | 5 | const defaultUserInfo = { 6 | username: 'test_user', 7 | password: '123456', 8 | email: 'test@test.edu' 9 | }; 10 | 11 | global.dropAndRegisterAndLogin = (userInfo = defaultUserInfo) => { 12 | return _db_.User 13 | .sync({ force: true }) 14 | .then(() => { 15 | return request(_server_) 16 | .post('/api/users') 17 | .send(userInfo) 18 | .expect(201); 19 | }) 20 | .then(() => { 21 | return request(_server_) 22 | .post('/api/tokens') 23 | .send(userInfo) 24 | .expect(201); 25 | }) 26 | .then(response => { 27 | return response.body; 28 | }); 29 | }; 30 | 31 | global.registerAndLogin = (userInfo = defaultUserInfo) => { 32 | return Q(null) 33 | .then(() => { 34 | return request(_server_) 35 | .post('/api/users') 36 | .send(userInfo) 37 | .expect(201); 38 | }) 39 | .then(() => { 40 | return request(_server_) 41 | .post('/api/tokens') 42 | .send(userInfo) 43 | .expect(201); 44 | }) 45 | .then(response => { 46 | return response.body; 47 | }); 48 | }; 49 | 50 | module.exports = { 51 | userInfo: defaultUserInfo 52 | }; 53 | -------------------------------------------------------------------------------- /server/controllers/tags/post.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Q = require('q'); 3 | const models = require('../../models'); 4 | 5 | const errorHandler = require('../../utils/error-handler'); 6 | const authority = require('../../utils/authority'); 7 | 8 | const schema = require('../../utils/orm-schema')(models.Tag, [], {}); 9 | 10 | module.exports = (req, res) => { 11 | return Q 12 | .fcall(() => { return { raw: req.body }; }) 13 | .then(authority) 14 | 15 | .then(_s => { 16 | let deferred = Q.defer(); 17 | req.checkBody(schema); 18 | req.getValidationResult().then(result => { 19 | if (!result.isEmpty()) { 20 | deferred.reject([result.useFirstErrorOnly().array()[0]]); 21 | } else { 22 | deferred.resolve(_.assign(_s, 23 | { data: _.pick(req.body, _.keys(schema)) } 24 | )); 25 | } 26 | }); 27 | return deferred.promise; 28 | }) 29 | 30 | .then(_s => { 31 | let deferred = Q.defer(); 32 | _s.assembly = _.assign( 33 | _.pick(_s.data, _.keys(models.Tag.rawAttributes)), {} 34 | ); 35 | models.Tag 36 | .create(_s.assembly) 37 | .then(() => { 38 | deferred.resolve(_s); 39 | }); 40 | return deferred.promise; 41 | }) 42 | 43 | .done(_s => { 44 | res.status(201).send(); 45 | }, errorHandler(res)); 46 | }; 47 | -------------------------------------------------------------------------------- /src/components/Admin.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 30 | 31 | 70 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | const Hello = resolve => require(['@/components/Hello'], resolve); 4 | const Post = resolve => require(['@/components/Post'], resolve); 5 | 6 | const Admin = resolve => require(['@/components/Admin'], resolve); 7 | const AdminHome = resolve => require(['@/components/AdminHome'], resolve); 8 | const AdminPost = resolve => require(['@/components/AdminPost'], resolve); 9 | const AdminEdit = resolve => require(['@/components/AdminEdit'], resolve); 10 | 11 | Vue.use(Router); 12 | 13 | export default new Router({ 14 | mode: 'history', 15 | routes: [ 16 | { 17 | path: '/', 18 | name: 'Hello', 19 | component: Hello 20 | }, 21 | { 22 | path: '/post/:id', 23 | name: 'Post', 24 | component: Post 25 | }, 26 | { 27 | path: '/admin', 28 | component: Admin, 29 | children: [ 30 | { 31 | path: '', 32 | name: 'AdminHome', 33 | component: AdminHome 34 | }, 35 | { 36 | path: 'post', 37 | name: 'AdminPost', 38 | component: AdminPost 39 | }, 40 | { 41 | path: 'edit/:id', 42 | name: 'AdminEdit', 43 | component: AdminEdit 44 | } 45 | ] 46 | } 47 | ], 48 | scrollBehavior (to, from, savedPosition) { 49 | if (savedPosition) { 50 | return savedPosition; 51 | } else { 52 | return { x: 0, y: 0 }; 53 | } 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /server/models/post.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | var Post = sequelize.define('Post', { 5 | title: { 6 | type: DataTypes.STRING, 7 | allowNull: false, 8 | validate: { notEmpty: true } 9 | }, 10 | summary: { 11 | type: DataTypes.TEXT, 12 | allowNull: false, 13 | validate: { notEmpty: true } 14 | }, 15 | content: { 16 | type: DataTypes.TEXT, 17 | allowNull: false, 18 | validate: { notEmpty: true } 19 | }, 20 | location: { 21 | type: DataTypes.STRING, 22 | _validate: { optional: true } 23 | }, 24 | password: { 25 | type: DataTypes.STRING, 26 | _validate: { optional: true } 27 | }, 28 | private: { 29 | type: DataTypes.BOOLEAN, 30 | allowNull: false, 31 | defaultValue: true, 32 | _validate: { optional: true } 33 | }, 34 | poem: { 35 | type: DataTypes.BOOLEAN, 36 | allowNull: false, 37 | defaultValue: false, 38 | _validate: { optional: true } 39 | } 40 | }, { 41 | paranoid: true, 42 | getterMethods: { 43 | _id_ () { return require('../utils/idt').encode('Post', this.id); } 44 | } 45 | }); 46 | 47 | Post.associate = models => { 48 | Post.belongsTo(models.User); 49 | Post.belongsToMany(models.Tag, { 50 | through: { 51 | model: models.ItemTag, 52 | unique: false, 53 | scope: { 54 | taggable: 'post' 55 | } 56 | }, 57 | foreignKey: 'taggable_id', 58 | constraints: false 59 | }); 60 | }; 61 | 62 | return Post; 63 | }; 64 | -------------------------------------------------------------------------------- /test/server/files/post.spec.js: -------------------------------------------------------------------------------- 1 | const Q = require('q'); 2 | const rimraf = require('rimraf'); 3 | 4 | describe('post files', function () { 5 | let token = { }; 6 | before(() => { return dropAndRegisterAndLogin().then(_token => { token = _token; }); }); 7 | 8 | let path = require('path').join(__dirname, 'images'); 9 | 10 | before((done) => { 11 | let path = require('../../../config').file.path; 12 | try { 13 | rimraf(path); 14 | } finally { 15 | done(); 16 | } 17 | }); 18 | 19 | describe('should return Unauthorized 401', () => { 20 | it('if did provide correct token', () => { 21 | return request(_server_) 22 | .post('/api/files') 23 | .attach('file', path + '/test.png') 24 | .expect(401); 25 | }); 26 | }); 27 | 28 | describe('should return BR 400', () => { 29 | xit('if file is too large', () => { }); 30 | 31 | it('if file has invalid mimetype', () => { 32 | return request(_server_) 33 | .post('/api/files') 34 | .field('userid', token.userid) 35 | .field('token', token.token) 36 | .attach('file', path + '/test.ini') 37 | .expect(400); 38 | }); 39 | }); 40 | 41 | describe('if call post and success', () => { 42 | let testFiles = ['test.gif', 'test.jpg', 'test.png']; 43 | 44 | it('should return Created 201 and the key', () => { 45 | return Q.all(testFiles.map(file => { 46 | return request(_server_) 47 | .post('/api/files') 48 | .field('userid', token.userid) 49 | .field('token', token.token) 50 | .attach('file', path + '/' + file) 51 | .expect(201); 52 | })); 53 | }); 54 | 55 | xit('should save the file', () => { }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/server/tokens/basic.spec.js: -------------------------------------------------------------------------------- 1 | describe('tokens', function () { 2 | let form = { 3 | username: 'test_user', 4 | password: '123456', 5 | email: 'test@test.edu' 6 | }; 7 | 8 | before((done) => { 9 | _db_.User 10 | .sync({ force: true }) 11 | .then(() => { 12 | request(_server_) 13 | .post('/api/users') 14 | .send(form) 15 | .expect(201) 16 | .then(() => { 17 | done(); 18 | }); 19 | }); 20 | }); 21 | 22 | describe('should response br 400', () => { 23 | it('if user nonexist', () => { 24 | return request(_server_) 25 | .post('/api/tokens') 26 | .send(_.assign(_.clone(form), { username: 'test_user_' })) 27 | .expect(400) 28 | .then(response => { 29 | assert(response.body.error === 'User nonexist'); 30 | }); 31 | }); 32 | }); 33 | 34 | describe('should response Unauthorized 401', () => { 35 | it('if password incorrect', () => { 36 | return request(_server_) 37 | .post('/api/tokens') 38 | .send(_.assign(_.clone(form), { password: '1234567' })) 39 | .expect(401) 40 | .then(response => { 41 | assert(response.body.error === 'Authority failed'); 42 | }); 43 | }); 44 | }); 45 | 46 | it('should get a valid token if provide correct info', () => { 47 | return request(_server_) 48 | .post('/api/tokens') 49 | .send(form) 50 | .expect(201) 51 | .then(response => { 52 | assert(response.body.token.match(/^([a-zA-Z0-9]){32}$/)); 53 | }); 54 | }); 55 | 56 | xit('should be random during multiple login', () => { }); 57 | 58 | xit('should be removed if expire', () => { }); 59 | }); 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nodecho [![Build Status](https://travis-ci.org/rijn/nodecho.svg?branch=master)](https://travis-ci.org/rijn/nodecho) 2 | 3 | > Just a blog 4 | 5 | ## Demo 6 | 7 | [「秋桜」](https://blog.rijnx.com/) 8 | 9 | ## Screenshots 10 | 11 | ![Screenshot#1](https://github.com/rijn/nodecho/raw/master/screenshots/1.jpg) 12 | ![Screenshot#2](https://github.com/rijn/nodecho/raw/master/screenshots/2.jpg) 13 | ![Screenshot#3](https://github.com/rijn/nodecho/raw/master/screenshots/3.jpg) 14 | ![Screenshot#4](https://github.com/rijn/nodecho/raw/master/screenshots/4.jpg) 15 | 16 | ## Install 17 | 18 | 1. Clone this repo 19 | 20 | ``` 21 | git clone https://github.com/rijn/nodecho.git blog 22 | ``` 23 | 24 | 2. Put the following config into `config/prod.conf.js`. The project is using [sequelize.js](http://docs.sequelizejs.com/manual/installation/getting-started.html#installation) which support `PG`, `MySQL`, `MsSQL` and `Sqlite`. 25 | 26 | ``` 27 | const path = require('path'); 28 | 29 | module.exports = { 30 | 'db': { 31 | dialect: 'mysql', 32 | host: '', 33 | port: 3306, 34 | username: '', 35 | password: '', 36 | database: '', 37 | pool: { 38 | max: 5, 39 | min: 0, 40 | idle: 10000 41 | }, 42 | logging: false, 43 | define: { 44 | underscored: true 45 | } 46 | }, 47 | 'file': { 48 | path: path.join(__dirname, '../files'), 49 | mimetype: ['image/gif', 'image/x-png', 'image/pjpeg', 'image/jpg', 'image/jpeg', 'image/png'] 50 | } 51 | }; 52 | ``` 53 | 54 | 3. Install dependencies and build the vendor. 55 | 56 | ``` 57 | npm i -g grunt grunt-cli 58 | npm i 59 | 60 | grunt build 61 | ``` 62 | 63 | 4. Install `PM2` and start the app 64 | 65 | ``` 66 | npm i -g pm2 67 | pm2 start ./server --name="blog" 68 | ``` 69 | 70 | ## Dev 71 | 72 | ``` 73 | # running dev server 74 | grunt dev 75 | 76 | # running api server spec test 77 | grunt mocha 78 | ``` 79 | 80 | -------------------------------------------------------------------------------- /src/styles/custom.less: -------------------------------------------------------------------------------- 1 | // Color 2 | @primary-color : #ffa4be; 3 | @link-color : shade(@primary-color, 10%); 4 | @link-hover-color : tint(@link-color, 30%); 5 | @link-active-color : shade(@link-color, 10%); 6 | @selected-color : fade(@primary-color, 90%); 7 | 8 | // Base 9 | @body-background : #fff; 10 | @font-family : 'NotoSans-Regular', 'Helvetica Neue', Helvetica, 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; 11 | @code-family : Consolas,Menlo,Courier,monospace; 12 | @title-color : #1c2438; 13 | @text-color : #495060; 14 | @font-size-base : 14px; 15 | @font-size-small : 12px; 16 | @line-height-base : 1.5; 17 | @line-height-computed : floor((@font-size-base * @line-height-base)); 18 | @border-radius-base : 6px; 19 | @border-radius-small : 4px; 20 | @cursor-disabled : not-allowed; 21 | 22 | @border-color-base : #dddee1; // outside 23 | 24 | @background-color-base : #f5f5f5; // base 25 | 26 | // Button 27 | @btn-font-weight : normal; 28 | @btn-padding-base : 0.7em 1.4em; 29 | @btn-font-size : 1rem; 30 | @btn-border-radius : 0px; 31 | 32 | @btn-disable-color : #bbbec4; 33 | @btn-disable-bg : @background-color-base; 34 | @btn-disable-border : @border-color-base; 35 | 36 | @btn-default-color : @text-color; 37 | @btn-default-bg : @background-color-base; 38 | @btn-default-border : @border-color-base; 39 | 40 | @btn-primary-color : #fff; 41 | @btn-primary-bg : @primary-color; 42 | 43 | @btn-ghost-color : #fff; 44 | @btn-ghost-bg : fade(#fff, 20%); 45 | @btn-ghost-border : @border-color-base; 46 | 47 | // Animation 48 | @animation-time : .3s; 49 | @transition-time : .2s; 50 | @ease-in-out : ease-in-out; 51 | 52 | // Input 53 | @input-height-base : 3rem; 54 | @input-height-large : 4px; 55 | @input-height-small : 2px; 56 | 57 | @input-padding-horizontal : 7px; 58 | @input-padding-vertical-base : 4px; 59 | @input-padding-vertical-small: 1px; 60 | @input-padding-vertical-large: 6px; -------------------------------------------------------------------------------- /server/controllers/files/get.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Q = require('q'); 3 | const models = require('../../models'); 4 | const errorHandler = require('../../utils/error-handler'); 5 | const authority = require('../../utils/authority'); 6 | 7 | const schema = { 8 | 'limit': { 9 | optional: true, 10 | isInt: true, 11 | errorMessage: 'Invalid limit' 12 | }, 13 | 'offset': { 14 | optional: true, 15 | isInt: true, 16 | errorMessage: 'Invalid offset' 17 | }, 18 | 'sort': { 19 | optional: true, 20 | matches: { 21 | options: ['^(created_at|view)@(DESC|ASC)$', 'g'] 22 | }, 23 | errorMessage: 'Invalid offset' 24 | } 25 | }; 26 | 27 | module.exports = (req, res) => { 28 | return Q 29 | .fcall(() => { return { raw: req.query }; }) 30 | .then(authority) 31 | 32 | .then(_s => { 33 | let deferred = Q.defer(); 34 | req.checkQuery(schema); 35 | req.getValidationResult().then(result => { 36 | if (!result.isEmpty()) { 37 | deferred.reject([result.useFirstErrorOnly().array()[0]]); 38 | } else { 39 | deferred.resolve(_.assign(_s, { data: _.pick(req.query, _.keys(schema)) })); 40 | } 41 | }); 42 | return deferred.promise; 43 | }) 44 | 45 | .then(_s => { 46 | let deferred = Q.defer(); 47 | models.File 48 | .findAll(_.assign( 49 | { 50 | where: { deleted_at: null, user_id: _s.user_id }, 51 | attributes: ['id', 'key', 'size', 'created_at'] 52 | }, 53 | _.pick(_s.data, ['offset', 'limit']), 54 | _s.data.sort ? { order: [_s.data.sort.split('@')] } : {} 55 | )) 56 | .then(files => { 57 | _s.files = _.map(files || [], file => file.dataValues); 58 | deferred.resolve(_s); 59 | }); 60 | return deferred.promise; 61 | }) 62 | 63 | .done(_s => { 64 | res.status(200).send(_s.files); 65 | }, errorHandler(res)); 66 | }; 67 | -------------------------------------------------------------------------------- /test/server/posts/list.spec.js: -------------------------------------------------------------------------------- 1 | const Q = require('q'); 2 | 3 | describe('list posts', function () { 4 | let posts = [ 5 | { title: 'test_title_1', summary: 'test_summary_1', content: 'test_content_1' }, 6 | { title: 'test_title_2', summary: 'test_summary_2', content: 'test_content_2' }, 7 | { title: 'test_title_3', summary: 'test_summary_3', content: 'test_content_3' } 8 | ]; 9 | 10 | beforeEach(() => { 11 | return [] 12 | .concat([ () => { return _db_.Post.sync({ force: true }); } ]) 13 | .concat(posts.map(post => { 14 | return () => { return Q.delay(10).then(() => { _db_.Post.create(post); }); }; 15 | })) 16 | .reduce(Q.when, Q(null)); 17 | }); 18 | 19 | it('should be able to list all posts', () => { 20 | return request(_server_) 21 | .get('/api/posts') 22 | .expect(200) 23 | .then(response => { 24 | assert.deepEqual(response.body.map(post => post.title), posts.map(post => post.title)); 25 | }); 26 | }); 27 | 28 | describe('should be able to', () => { 29 | xit('filter using offset and limit', () => { }); 30 | xit('filter blur name', () => { }); 31 | xit('filter use tags', () => { }); 32 | xit('filter use date', () => { }); 33 | xit('sort by view', () => { }); 34 | 35 | it('sort by date', () => { 36 | return request(_server_) 37 | .get('/api/posts') 38 | .query({ sort: 'created_at@DESC' }) 39 | .expect(200) 40 | .then(response => { 41 | assert.deepEqual( 42 | response.body.map(post => post.title), 43 | posts.map(post => post.title).reverse() 44 | ); 45 | }); 46 | }); 47 | }); 48 | 49 | it('should not list posts that have been deleted', () => { 50 | return _db_.Post 51 | .destroy({ where: { id: 1 } }) 52 | .then(() => { 53 | return request(_server_) 54 | .get('/api/posts'); 55 | }) 56 | .then(response => { 57 | assert(response.body.length === 2); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/components/Loading.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | 23 | 94 | -------------------------------------------------------------------------------- /server/controllers/files/post.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const fs = require('fs'); 3 | const Q = require('q'); 4 | const models = require('../../models'); 5 | const errorHandler = require('../../utils/error-handler'); 6 | const authority = require('../../utils/authority'); 7 | const random = require('../../utils/random'); 8 | 9 | const conf = require('../../../config').file; 10 | 11 | module.exports = (req, res) => { 12 | return Q 13 | .fcall(() => { return { raw: req.body }; }) 14 | .then(authority) 15 | 16 | .then(_s => { 17 | var deferred = Q.defer(); 18 | _s.file = req.files[0]; 19 | if (_.indexOf(conf.mimetype, _s.file.mimetype.toLowerCase()) === -1) { 20 | deferred.reject(['Invalid MIMETYPE']); 21 | } else { 22 | deferred.resolve(_s); 23 | } 24 | return deferred.promise; 25 | }) 26 | 27 | .then(_s => { 28 | var deferred = Q.defer(); 29 | try { 30 | if (!fs.existsSync(conf.path)) { 31 | fs.mkdirSync(conf.path); 32 | } 33 | } catch (e) {}; 34 | deferred.resolve(_s); 35 | return deferred.promise; 36 | }) 37 | 38 | .then(_s => { 39 | var deferred = Q.defer(); 40 | _s.key = random(32); 41 | fs.writeFile(conf.path + '/' + _s.key, _s.file.buffer, err => { 42 | if (err) { 43 | deferred.reject(['Internal error', _s, 500]); 44 | } else { 45 | deferred.resolve(_s); 46 | } 47 | }); 48 | return deferred.promise; 49 | }) 50 | 51 | .then(_s => { 52 | var deferred = Q.defer(); 53 | models.File 54 | .create( 55 | { 56 | key: _s.key, 57 | size: _s.file.size 58 | }, 59 | { include: [{ 60 | model: models.User 61 | }]} 62 | ) 63 | .then(() => { 64 | deferred.resolve(_s); 65 | }); 66 | return deferred.promise; 67 | }) 68 | 69 | .done(_s => { 70 | res.status(201).send(_.pick(_s, 'key')); 71 | }, errorHandler(res)); 72 | }; 73 | -------------------------------------------------------------------------------- /test/server/files/list.spec.js: -------------------------------------------------------------------------------- 1 | const Q = require('q'); 2 | 3 | describe('list files', function () { 4 | let token = { }; 5 | before(() => { 6 | return dropAndRegisterAndLogin().then(_token => { token = _token; }); 7 | }); 8 | 9 | before(() => { 10 | return registerAndLogin({ 11 | username: 'test2', 12 | password: 'test_password', 13 | email: 'test2@t.com' 14 | }); 15 | }); 16 | 17 | let files = [ 18 | { key: 'test_file_1', size: 1, user_id: 1 }, 19 | { key: 'test_file_2', size: 2, user_id: 1 }, 20 | { key: 'test_file_3', size: 3, user_id: 2 } 21 | ]; 22 | 23 | beforeEach(() => { 24 | return [] 25 | .concat([ () => { return _db_.File.sync({ force: true }); } ]) 26 | .concat(files.map(file => { 27 | return () => { 28 | return Q.delay(10).then(() => { 29 | _db_.File.create(file, { include: _db_.User }); 30 | }); 31 | }; 32 | })) 33 | .reduce(Q.when, Q(null)); 34 | }); 35 | 36 | it('should respond Unauthorized 401 if didn\'t pass token', () => { 37 | return request(_server_) 38 | .get('/api/files') 39 | .expect(401); 40 | }); 41 | 42 | it('should be able to list all files uploaded', () => { 43 | return request(_server_) 44 | .get('/api/files') 45 | .query(token) 46 | .expect(200) 47 | .then(response => { 48 | assert.deepEqual( 49 | response.body.map(file => file.key), 50 | files 51 | .filter(file => file.user_id === 1) 52 | .map(file => file.key) 53 | ); 54 | }); 55 | }); 56 | 57 | describe('should be able to', () => { 58 | it('sort by date', () => { 59 | return request(_server_) 60 | .get('/api/files') 61 | .query(_authorize_(token, { sort: 'created_at@DESC' })) 62 | .expect(200) 63 | .then(response => { 64 | assert.deepEqual( 65 | response.body.map(file => file.key), 66 | files 67 | .filter(file => file.user_id === 1) 68 | .map(file => file.key).reverse() 69 | ); 70 | }); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/server/tags/basic.spec.js: -------------------------------------------------------------------------------- 1 | describe('tags', function () { 2 | let token = { }; 3 | before(() => { return dropAndRegisterAndLogin().then(_token => { token = _token; }); }); 4 | 5 | beforeEach(() => { 6 | return _db_.Tag 7 | .sync({ force: true }) 8 | .then(() => { return _db_.Tag.create({ name: 'test1' }); }); 9 | }); 10 | 11 | it('should be able to list all tags', () => { 12 | return request(_server_) 13 | .get('/api/tags') 14 | .expect(200) 15 | .then(response => { 16 | assert.deepStrictEqual(response.body, [ { id: 1, name: 'test1' } ]); 17 | }); 18 | }); 19 | 20 | it('should be able to delete tag', () => { 21 | return request(_server_) 22 | .delete('/api/tags/1') 23 | .send(_authorize_(token)) 24 | .expect(200) 25 | .then(response => { 26 | }); 27 | }); 28 | 29 | it('should be able add a tag', () => { 30 | return request(_server_) 31 | .post('/api/tags') 32 | .send(_authorize_(token, { name: 'test2' })) 33 | .expect(201) 34 | .then(() => { 35 | return request(_server_) 36 | .get('/api/tags') 37 | .expect(200) 38 | .then(response => { 39 | assert.deepStrictEqual(response.body, [ 40 | { id: 1, name: 'test1' }, 41 | { id: 2, name: 'test2' } 42 | ]); 43 | }); 44 | }); 45 | }); 46 | 47 | describe('should return Unauthorized 401', () => { 48 | let fakeToken = _.set(token, 'token', 'fakeToken'); 49 | it('if using wrong token to delete a tag', () => { 50 | return request(_server_) 51 | .delete('/api/tags/1') 52 | .send(_authorize_(fakeToken)) 53 | .expect(401); 54 | }); 55 | 56 | it('if using wrong token to add a tag', () => { 57 | return request(_server_) 58 | .post('/api/tags') 59 | .send(_authorize_(fakeToken, { name: 'test2' })) 60 | .expect(401); 61 | }); 62 | 63 | it('db should not be modified', () => { 64 | return request(_server_) 65 | .get('/api/tags') 66 | .expect(200) 67 | .then(response => { 68 | assert.deepStrictEqual(response.body, [ { id: 1, name: 'test1' } ]); 69 | }); 70 | }); 71 | }); 72 | 73 | xit('should response Not Found 404 if delete a nonexist tag'); 74 | }); 75 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | 22 | 122 | -------------------------------------------------------------------------------- /server/controllers/posts/post.js: -------------------------------------------------------------------------------- 1 | const models = require('../../models'); 2 | const Q = require('q'); 3 | const _ = require('lodash'); 4 | const errorHandler = require('../../utils/error-handler'); 5 | const authority = require('../../utils/authority'); 6 | const pass = require('../../utils/pass'); 7 | 8 | var schema = require('../../utils/orm-schema')( 9 | models.Post, 10 | [], 11 | { 12 | 'tags': { 13 | optional: true, 14 | matches: { 15 | options: ['^[0-9,]*$', 'gi'] 16 | }, 17 | errorMessage: 'Invalid tags' 18 | } 19 | } 20 | ); 21 | 22 | module.exports = (req, res) => { 23 | return Q 24 | .fcall(() => { return { raw: req.body }; }) 25 | .then(authority) 26 | 27 | .then(_s => { 28 | var deferred = Q.defer(); 29 | req.checkBody(schema); 30 | req.getValidationResult().then(result => { 31 | if (!result.isEmpty()) { 32 | deferred.reject([result.useFirstErrorOnly().array()[0]]); 33 | } else { 34 | deferred.resolve(_.assign(_s, 35 | { data: _.pick(req.body, _.keys(schema)) } 36 | )); 37 | } 38 | }); 39 | return deferred.promise; 40 | }) 41 | 42 | // TODO: check tags 43 | .then(pass) 44 | 45 | .then(_s => { 46 | var deferred = Q.defer(); 47 | models.Post 48 | .create( 49 | _.assign( 50 | _.pick(_s.data, _.keys(models.Post.rawAttributes)), 51 | { user_id: _s.user_id } 52 | ), 53 | { 54 | include: [{ 55 | model: models.Tag, 56 | through: { 57 | model: models.ItemTag, 58 | scope: { 59 | taggable: 'post' 60 | } 61 | } 62 | }, { 63 | model: models.User 64 | }] 65 | } 66 | ) 67 | .then(post => { 68 | return models.Tag 69 | .findAll({ where: { id: { $in: 70 | (_s.data.tags || '').split(',') 71 | } } }) 72 | .then(tags => { 73 | return post 74 | .addTags(tags) 75 | .then(() => { 76 | _s.id = post._id_; 77 | deferred.resolve(_s); 78 | }); 79 | }); 80 | }); 81 | return deferred.promise; 82 | }) 83 | 84 | .done(_s => { 85 | res.status(201).send(_.pick(_s, 'id')); 86 | }, errorHandler(res)); 87 | }; 88 | -------------------------------------------------------------------------------- /server/controllers/posts/-id/delete.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Q = require('q'); 3 | const errorHandler = require('../../../utils/error-handler'); 4 | const models = require('../../../models'); 5 | const idt = require('../../../utils/idt'); 6 | const authority = require('../../../utils/authority'); 7 | 8 | const schema = { 9 | 'id': { 10 | notEmpty: true, 11 | errorMessage: 'Invalid id' 12 | } 13 | }; 14 | 15 | module.exports = (req, res) => { 16 | return Q 17 | .fcall(() => { return { raw: req.body }; }) 18 | .then(authority) 19 | 20 | .then(_s => { 21 | let deferred = Q.defer(); 22 | req.checkParams(schema); 23 | req.getValidationResult().then(result => { 24 | if (!result.isEmpty()) { 25 | deferred.reject([result.useFirstErrorOnly().array()[0]]); 26 | } else { 27 | deferred.resolve(_.assign(_s, { data: _.pick(req.params, _.keys(schema)) })); 28 | } 29 | }); 30 | return deferred.promise; 31 | }) 32 | 33 | .then(_s => { 34 | let deferred = Q.defer(); 35 | models.Post 36 | .findOne({ 37 | where: { 38 | id: idt.decode('Post', _s.data.id) 39 | }, 40 | include: [ models.User ] 41 | }) 42 | .then(post => { 43 | if (!post) { 44 | deferred.reject(['Not fount', _s, 404]); 45 | } else if (!post.User || post.User.id !== _s.user_id) { 46 | deferred.reject(['Unauthorized', _s, 401]); 47 | } else { 48 | deferred.resolve(_s); 49 | } 50 | }); 51 | return deferred.promise; 52 | }) 53 | 54 | .then(_s => { 55 | let deferred = Q.defer(); 56 | models.Post 57 | .destroy({ 58 | where: { 59 | id: idt.decode('Post', _s.data.id) 60 | } 61 | }) 62 | .then(affectedRows => { 63 | if (affectedRows) { 64 | deferred.resolve(_s); 65 | } else { 66 | deferred.reject('Tag id invalid', _s, 404); 67 | } 68 | }); 69 | return deferred.promise; 70 | }) 71 | 72 | .then(_s => { 73 | let deferred = Q.defer(); 74 | models.Log 75 | .create({ 76 | ip: req.ip, 77 | post_id: idt.decode('Post', req.params.id), 78 | description: 'delete' 79 | }) 80 | .then(() => { 81 | deferred.resolve(_s); 82 | }); 83 | return deferred.promise; 84 | }) 85 | 86 | .done(_s => { 87 | res.status(200).send(); 88 | }, errorHandler(res)); 89 | }; 90 | -------------------------------------------------------------------------------- /server/utils/router-loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Module dependencies 4 | const glob = require('glob'); 5 | const os = require('os'); 6 | 7 | const compose = require('./compose'); 8 | 9 | const METHOD_ENUM = ['get', 'post', 'put', 'delete', 'patch']; 10 | 11 | function loadRouter (app, root, options) { 12 | const opt = options || {}; 13 | 14 | glob.sync(`${root}/**/*.js`).forEach((file) => { 15 | const realRoot = os.platform() === 'win32' ? root.replace(/\\/ig, '/') : root; 16 | const filePath = file.replace(/\.[^.]*$/, ''); 17 | const controller = require(filePath); 18 | const constPrefix = opt.constPrefix || '/api'; 19 | const urlPrefix = filePath.replace(realRoot, '').replace(/\/index$/, ''); 20 | const methods = Object.keys(controller); 21 | 22 | // Handle options 23 | // const excludeRules = opt.excludeRules || null; 24 | const rewriteRules = opt.rewriteRules || new Map(); 25 | 26 | function applyMethod (name, methodBody) { 27 | const body = methodBody; 28 | let modifiedUrl = `${constPrefix}${urlPrefix}${name === 'index' ? '' : `/${name}`}`; 29 | let middlewares = []; 30 | let method = 'get'; 31 | let handler; 32 | let params; 33 | 34 | switch (typeof body) { 35 | case 'object': 36 | params = body.params || []; 37 | middlewares = body.middlewares || []; 38 | modifiedUrl += `/${params.join('/')}`; 39 | modifiedUrl = modifiedUrl.replace(/-/gi, ':'); 40 | handler = body.handler; 41 | method = (body.method || 'get').toLowerCase(); 42 | break; 43 | case 'function': 44 | handler = body; 45 | break; 46 | default: return; 47 | } 48 | 49 | if (METHOD_ENUM.indexOf(method) !== -1) { 50 | if (!handler) throw Error('[express-load-router]: no handler for method: ', method); 51 | 52 | if (process.env.NODE_ENV !== 'prod') console.log('[router-loader]: Register ' + modifiedUrl + ' - ' + method); 53 | 54 | app[method]( 55 | rewriteRules.has(modifiedUrl) 56 | ? rewriteRules.get(modifiedUrl) 57 | : modifiedUrl, 58 | compose(middlewares, modifiedUrl), 59 | handler 60 | ); 61 | } else { 62 | throw Error('[load-router]: invalid method: ', method); 63 | } 64 | } 65 | 66 | methods.forEach((method) => { 67 | const methodName = method; 68 | const methodBody = controller[method]; 69 | 70 | if (Array.isArray(methodBody)) { 71 | methodBody.forEach((m) => { 72 | applyMethod(methodName, m); 73 | }); 74 | // } else { 75 | // applyMethod(methodName, methodBody); 76 | } 77 | }); 78 | }); 79 | } 80 | 81 | module.exports = loadRouter; 82 | -------------------------------------------------------------------------------- /server/controllers/users/post.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Q = require('q'); 3 | const models = require('../../models'); 4 | const scrypt = require('scrypt'); 5 | 6 | const errorHandler = require('../../utils/error-handler'); 7 | const pass = require('../../utils/pass'); 8 | const random = require('../../utils/random'); 9 | 10 | const schema = require('../../utils/orm-schema')( 11 | models.User, 12 | ['salt', 'authority'], {} 13 | ); 14 | 15 | module.exports = (req, res) => { 16 | return Q 17 | .fcall(pass) 18 | 19 | .then(_s => { 20 | let deferred = Q.defer(); 21 | req.checkBody(schema); 22 | req.getValidationResult().then(result => { 23 | if (!result.isEmpty()) { 24 | deferred.reject([result.useFirstErrorOnly().array()[0]]); 25 | } else { 26 | deferred.resolve(_.assign(_s, 27 | { data: _.pick(req.body, _.keys(schema)) } 28 | )); 29 | } 30 | }); 31 | return deferred.promise; 32 | }) 33 | 34 | .then(_s => { 35 | let deferred = Q.defer(); 36 | models.User 37 | .findOne({ 38 | where: { 39 | $or: [ 40 | { username: _s.data.username }, 41 | { email: _s.data.email } 42 | ] 43 | } 44 | }) 45 | .then(user => { 46 | if (!user) { 47 | deferred.resolve(_s); 48 | } else { 49 | deferred.reject(['User exists', _s, 409]); 50 | } 51 | }); 52 | return deferred.promise; 53 | }) 54 | 55 | .then(_s => { 56 | let deferred = Q.defer(); 57 | _s.salt = random(16); 58 | scrypt 59 | .kdf(_s.data.password + _s.salt, { N: 2, r: 1, p: 1 }) 60 | .then(result => { 61 | deferred.resolve(_.assign(_s, { 62 | 'hashedPassword': result.toString('base64') 63 | })); 64 | }, err => { 65 | deferred.reject([err, _s]); 66 | }); 67 | return deferred.promise; 68 | }) 69 | 70 | .then(_s => { 71 | let deferred = Q.defer(); 72 | _s.assembly = _.assign( 73 | _.pick(_s.data, _.keys(models.User.rawAttributes)), 74 | { 75 | salt: _s.salt, 76 | password: _s.hashedPassword 77 | } 78 | ); 79 | models.User 80 | .create(_s.assembly) 81 | .then(user => { 82 | _s.id = user._id_; 83 | deferred.resolve(_s); 84 | }); 85 | return deferred.promise; 86 | }) 87 | 88 | .done(_s => { 89 | res.status(201).send(_.pick(_s, 'id')); 90 | }, errorHandler(res)); 91 | }; 92 | -------------------------------------------------------------------------------- /src/components/Hello.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 91 | 92 | 108 | -------------------------------------------------------------------------------- /src/components/Auth.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 88 | 89 | 109 | -------------------------------------------------------------------------------- /server/controllers/posts/get.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Q = require('q'); 3 | const models = require('../../models'); 4 | const errorHandler = require('../../utils/error-handler'); 5 | const pass = require('../../utils/pass'); 6 | 7 | const schema = { 8 | 'limit': { 9 | optional: true, 10 | isInt: true, 11 | errorMessage: 'Invalid limit' 12 | }, 13 | 'offset': { 14 | optional: true, 15 | isInt: true, 16 | errorMessage: 'Invalid offset' 17 | }, 18 | 'sort': { 19 | optional: true, 20 | matches: { 21 | options: ['^(created_at|view)@(DESC|ASC)$', 'g'] 22 | }, 23 | errorMessage: 'Invalid offset' 24 | } 25 | }; 26 | 27 | module.exports = (req, res) => { 28 | return Q 29 | .fcall(pass) 30 | 31 | .then(_s => { 32 | let deferred = Q.defer(); 33 | req.checkQuery(schema); 34 | req.getValidationResult().then(result => { 35 | if (!result.isEmpty()) { 36 | deferred.reject([result.useFirstErrorOnly().array()[0]]); 37 | } else { 38 | deferred.resolve(_.assign(_s, { data: _.pick(req.query, _.keys(schema)) })); 39 | } 40 | }); 41 | return deferred.promise; 42 | }) 43 | 44 | .then(_s => { 45 | let deferred = Q.defer(); 46 | models.Post 47 | .findAll(_.assign( 48 | { 49 | where: { deleted_at: null }, 50 | attributes: ['id', 'title', 'summary', 'created_at'], 51 | include: [{ 52 | duplicating: false, 53 | model: models.Tag, 54 | through: { 55 | model: models.ItemTag, 56 | attributes: [] 57 | }, 58 | attributes: ['id', 'name'] 59 | }, { 60 | model: models.User, 61 | attributes: ['id', 'username'] 62 | }] 63 | }, 64 | _.mapValues(_.pick(_s.data, ['offset', 'limit']), v => parseInt(v)), 65 | _s.data.sort ? { order: [_s.data.sort.split('@')] } : {} 66 | ), { 67 | subQuery: false 68 | }) 69 | .then(posts => { 70 | _s.result = _.map( 71 | posts || [], 72 | post => _.defaultsDeep( 73 | { 74 | id: post._id_, 75 | user: { 76 | id: _.get(post.User, '_id_') 77 | } 78 | }, 79 | _.assign( 80 | { 81 | user: _.get(post.dataValues.User, 'dataValues'), 82 | tags: _.map(post.dataValues.Tags, tag => _.get(tag, 'dataValues')) 83 | }, 84 | _.omit(post.dataValues, 'User', 'Tags') 85 | ) 86 | ) 87 | ); 88 | deferred.resolve(_s); 89 | }); 90 | return deferred.promise; 91 | }) 92 | 93 | .done(_s => { 94 | res.status(200).send(_s.result); 95 | }, errorHandler(res)); 96 | }; 97 | -------------------------------------------------------------------------------- /src/components/AdminPost.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 105 | 106 | 113 | -------------------------------------------------------------------------------- /src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 74 | 75 | 134 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* jshint esversion: 5 */ 2 | 3 | 'use strict'; 4 | 5 | // var _ = require('lodash'); 6 | // var webpack = require('webpack'); 7 | // var fs = require('fs'); 8 | var serveStatic = require('serve-static'); 9 | var args = process.argv.slice(2); 10 | 11 | module.exports = function (grunt) { 12 | require('load-grunt-tasks')(grunt); 13 | 14 | require('time-grunt')(grunt); 15 | 16 | var lrPort = 35729; 17 | var lrSnippet = require('connect-livereload')({ port: lrPort }); 18 | var lrMiddleware = function (connect, options) { 19 | return [ 20 | lrSnippet, 21 | serveStatic(options.base[0]) 22 | ]; 23 | }; 24 | 25 | grunt.initConfig({ 26 | shell: { 27 | build: { 28 | command: 'node ./build/build.js' 29 | } 30 | }, 31 | watch: { 32 | server: { 33 | files: [ 34 | 'server/**/*.js' 35 | ], 36 | tasks: [ 'eslint', 'express:dev' ], 37 | options: { 38 | spawn: false, 39 | livereload: true 40 | } 41 | }, 42 | doc: { 43 | files: [ 44 | 'controllers/**/*.js' 45 | ], 46 | tasks: [ 'apidoc' ], 47 | options: { 48 | livereload: true 49 | } 50 | } 51 | }, 52 | env: { 53 | options: { 54 | }, 55 | dev: { 56 | NODE_ENV: 'dev' 57 | }, 58 | test: { 59 | NODE_ENV: 'test' 60 | } 61 | }, 62 | express: { 63 | options: { 64 | }, 65 | dev: { 66 | options: { 67 | script: 'server', 68 | port: 8080, 69 | args: [] 70 | } 71 | } 72 | }, 73 | clean: { 74 | options: {}, 75 | dist: [ 'dist' ] 76 | }, 77 | eslint: { 78 | target: [ 79 | '*.js', 80 | 'server/**/*.js', 81 | 'src/**/*.js', 82 | 'test/**/*.js' 83 | // 'build/**/*.js', 84 | // 'config/**/*.js' 85 | ] 86 | }, 87 | connect: { 88 | options: { 89 | }, 90 | doc: { 91 | options: { 92 | port: 8080, 93 | base: 'doc', 94 | middleware: lrMiddleware 95 | } 96 | } 97 | } 98 | }); 99 | 100 | require('./test/server/mocha.conf')(grunt); 101 | 102 | grunt.registerTask('build', [ 'clean', 'shell:build' ]); 103 | 104 | grunt.registerTask('dev', function (target) { 105 | if (target !== 'server') { 106 | grunt.config('express.dev.options.args', args.concat([ 107 | '--vue' 108 | ])); 109 | } 110 | grunt.task.run([ 111 | 'eslint', 112 | 'env:dev', 113 | 'express:dev', 114 | target === 'client' ? 'keepalive' : 'watch:server' 115 | ]); 116 | }); 117 | 118 | grunt.registerTask('travis', function () { 119 | grunt.task.run([ 120 | 'continue:on', 121 | 'clean', 122 | 'eslint', 123 | 'mocha', 124 | 'build', 125 | 'continue:off', 126 | 'continue:fail-on-warning' 127 | ]); 128 | }); 129 | }; 130 | -------------------------------------------------------------------------------- /src/components/Post.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 104 | 105 | 122 | -------------------------------------------------------------------------------- /server/controllers/tokens/post.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Q = require('q'); 3 | const scrypt = require('scrypt'); 4 | 5 | const models = require('../../models'); 6 | const random = require('../../utils/random'); 7 | const pass = require('../../utils/pass'); 8 | const errorHandler = require('../../utils/error-handler'); 9 | 10 | const schema = { 11 | 'username': { 12 | notEmpty: true, 13 | errorMessage: 'Invalid username' 14 | }, 15 | 'password': { 16 | notEmpty: true, 17 | errorMessage: 'Invalid password' 18 | } 19 | }; 20 | 21 | module.exports = (req, res) => { 22 | return Q 23 | .fcall(pass) 24 | 25 | .then(_s => { 26 | let deferred = Q.defer(); 27 | let data = _.extend( 28 | { 29 | username: null, 30 | password: null 31 | }, 32 | req.body 33 | ); 34 | deferred.resolve({ data }); 35 | return deferred.promise; 36 | }) 37 | .then(_s => { 38 | let deferred = Q.defer(); 39 | req.checkBody(schema); 40 | req.getValidationResult().then(result => { 41 | if (!result.isEmpty()) { 42 | deferred.reject([result.useFirstErrorOnly().array()[0]]); 43 | } else { 44 | deferred.resolve(_s); 45 | } 46 | }); 47 | return deferred.promise; 48 | }) 49 | 50 | .then(_s => { 51 | let deferred = Q.defer(); 52 | models.User 53 | .findOne({ 54 | where: { 55 | $or: [ 56 | { username: _s.data.username }, 57 | { email: _s.data.username } 58 | ] 59 | } 60 | }) 61 | .then(user => { 62 | if (user) { 63 | deferred.resolve(_.extend(_s, { user })); 64 | } else { 65 | deferred.reject(['User nonexist', _s, 400]); 66 | } 67 | }); 68 | return deferred.promise; 69 | }) 70 | 71 | .then(_s => { 72 | let deferred = Q.defer(); 73 | scrypt 74 | .verifyKdf(Buffer.from(_s.user.password, 'base64'), _s.data.password + _s.user.salt) 75 | .then(result => { 76 | if (result) { 77 | deferred.resolve(_s); 78 | } else { 79 | deferred.reject(['Authority failed', _s, 401]); 80 | } 81 | }, err => { 82 | deferred.reject([err, _s]); 83 | }); 84 | return deferred.promise; 85 | }) 86 | 87 | .then(_s => { 88 | let deferred = Q.defer(); 89 | _s.token = random(32); 90 | deferred.resolve(_s); 91 | return deferred.promise; 92 | }) 93 | 94 | .then(_s => { 95 | let deferred = Q.defer(); 96 | models.Token 97 | .create({ 98 | token: _s.token, 99 | user_id: _s.user.id 100 | }) 101 | .then(() => { 102 | deferred.resolve(_s); 103 | }); 104 | return deferred.promise; 105 | }) 106 | 107 | .done(_s => { 108 | res.status(201).send({ 109 | userid: _s.user._id_, 110 | token: _s.token 111 | }); 112 | }, errorHandler(res)); 113 | }; 114 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | console.log('Running under NODE_ENV=' + process.env.NODE_ENV); 2 | 3 | var path = require('path'); 4 | var express = require('express'); 5 | var bodyParser = require('body-parser'); 6 | var expressValidator = require('express-validator'); 7 | var _ = require('underscore'); 8 | var multer = require('multer'); 9 | var storage = multer.memoryStorage(); 10 | var args = process.argv.slice(2); 11 | 12 | const app = express(); 13 | const http = require('http').Server(app); 14 | 15 | app.use(bodyParser.urlencoded({ extended: true })); 16 | app.use(bodyParser.json()); 17 | 18 | app.use(multer({ 19 | dest: path.join(__dirname, '../files'), 20 | limits: { 21 | fieldNameSize: 100, 22 | files: 1, 23 | fileSize: 1 * 1024 * 1024 24 | }, 25 | storage: storage 26 | }).any()); 27 | 28 | // inject validator 29 | app.use(expressValidator({ 30 | errorFormatter: function (param, msg, value) { 31 | return msg; 32 | } 33 | })); 34 | 35 | // enable CORS 36 | app.use(function (req, res, next) { 37 | res.header('Access-Control-Allow-Origin', '*'); 38 | res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); 39 | next(); 40 | }); 41 | 42 | require('./utils/router-loader')(app, path.join(__dirname, 'controllers'), { 43 | constPrefix: '/api', 44 | excludeRules: /get|post|put|delete/gi 45 | }); 46 | 47 | app.use(function (req, res, next) { 48 | if (_.first(_.without(req.path.split('/'), '')) === 'api') { 49 | res.status(404).send({ 50 | 'error': 'Undefined API' 51 | }); 52 | } else { 53 | next(); 54 | } 55 | }); 56 | 57 | app.use('/files', express.static(path.join(__dirname, '../files'))); 58 | 59 | const history = require('connect-history-api-fallback'); 60 | app.use(history({ 61 | rewrites: [{ 62 | from: /^[a-zA_Z0-9,/]*$/, 63 | to: '/' 64 | }] 65 | })); 66 | 67 | if (args.indexOf('--vue') > -1) { 68 | console.log('Injecting Vue Middleware...'); 69 | require('../build/dev-middleware')(app); 70 | } else { 71 | app.use('/', express.static(path.join(__dirname, '../dist'))); 72 | } 73 | 74 | if (args.indexOf('--livereload') > -1) { 75 | console.log('Enabling Livereload Service...'); 76 | app.use(require('connect-livereload')({ 77 | port: 35729 78 | })); 79 | } 80 | 81 | var port = process.env.PORT || 8080; 82 | 83 | const Q = require('q'); 84 | 85 | var db = require('./models'); 86 | 87 | module.exports = Q 88 | .fcall(() => { 89 | var deferred = Q.defer(); 90 | db.sequelize 91 | .authenticate() 92 | .then(() => { 93 | console.log('Connection has been established successfully.'); 94 | deferred.resolve({ db }); 95 | }) 96 | .catch(err => { 97 | console.error(err); 98 | deferred.resolve({ db }); 99 | }); 100 | return deferred.promise; 101 | }) 102 | .then(({ db }) => { 103 | var deferred = Q.defer(); 104 | db.sequelize 105 | .sync({ force: false, logging: false }) 106 | .then(() => { 107 | console.log('Sync successfully.'); 108 | deferred.resolve({ db }); 109 | }) 110 | .catch(err => { 111 | console.error(err); 112 | deferred.resolve({ db }); 113 | }); 114 | return deferred.promise; 115 | }) 116 | .then(({ db }) => { 117 | var deferred = Q.defer(); 118 | var server = http.listen(port, () => { 119 | var port = server.address().port; 120 | console.log('Server is listening on ' + port); 121 | deferred.resolve({ db, server }); 122 | }); 123 | return deferred.promise; 124 | }); 125 | -------------------------------------------------------------------------------- /server/controllers/posts/-id/put.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Q = require('q'); 3 | const errorHandler = require('../../../utils/error-handler'); 4 | const models = require('../../../models'); 5 | const idt = require('../../../utils/idt'); 6 | const authority = require('../../../utils/authority'); 7 | 8 | var schema = require('../../../utils/orm-schema')( 9 | models.Post, 10 | [], 11 | { 12 | 'tags': { 13 | optional: true, 14 | matches: { 15 | options: ['^[0-9,]*$', 'gi'] 16 | }, 17 | errorMessage: 'Invalid tags' 18 | } 19 | } 20 | ); 21 | 22 | module.exports = (req, res) => { 23 | return Q 24 | .fcall(() => { return { raw: req.body }; }) 25 | .then(authority) 26 | 27 | .then(_s => { 28 | var deferred = Q.defer(); 29 | req.checkBody(schema); 30 | req.getValidationResult().then(result => { 31 | if (!result.isEmpty()) { 32 | deferred.reject([result.useFirstErrorOnly().array()[0]]); 33 | } else { 34 | deferred.resolve(_.assign(_s, 35 | { data: _.pick(req.body, _.keys(schema)) } 36 | )); 37 | } 38 | }); 39 | return deferred.promise; 40 | }) 41 | 42 | .then(_s => { 43 | let deferred = Q.defer(); 44 | models.Post 45 | .findOne({ 46 | where: { 47 | id: idt.decode('Post', req.params.id) 48 | }, 49 | include: [ models.User ] 50 | }) 51 | .then(post => { 52 | if (!post) { 53 | deferred.reject(['Not fount', _s, 404]); 54 | } else if (!post.User || post.User.id !== _s.user_id) { 55 | deferred.reject(['Unauthorized', _s, 401]); 56 | } else { 57 | deferred.resolve(_s); 58 | } 59 | }); 60 | return deferred.promise; 61 | }) 62 | 63 | .then(_s => { 64 | var deferred = Q.defer(); 65 | models.Post 66 | .findOne({ 67 | where: { 68 | id: idt.decode('Post', req.params.id) 69 | }, 70 | include: [ models.User ] 71 | }) 72 | .then(post => { 73 | return post.update(_.pick(_s.data, _.keys(models.Post.rawAttributes))) 74 | .then(() => post); 75 | }) 76 | .then(post => { 77 | return models.Tag 78 | .findAll({ where: { id: { $in: 79 | (_s.data.tags || '').split(',') 80 | } } }) 81 | .then(tags => { 82 | return post 83 | .setTags(tags) 84 | .then(() => { 85 | _s.id = post._id_; 86 | deferred.resolve(_s); 87 | }); 88 | }); 89 | }); 90 | return deferred.promise; 91 | }) 92 | 93 | .then(_s => { 94 | let deferred = Q.defer(); 95 | models.Log 96 | .create({ 97 | ip: req.ip, 98 | post_id: idt.decode('Post', req.params.id), 99 | description: 'put' 100 | }) 101 | .then(() => { 102 | deferred.resolve(_s); 103 | }); 104 | return deferred.promise; 105 | }) 106 | 107 | .done(_s => { 108 | res.status(200).send(); 109 | }, errorHandler(res)); 110 | }; 111 | -------------------------------------------------------------------------------- /test/server/users/register.spec.js: -------------------------------------------------------------------------------- 1 | describe('users register', function () { 2 | let form = { 3 | username: 'test_user', 4 | password: '123456', 5 | email: 'test@test.edu' 6 | }; 7 | 8 | describe('should response br 400', () => { 9 | it('if username is invalid', () => { 10 | return request(_server_) 11 | .post('/api/users') 12 | .send(_.assign(_.clone(form), { username: '123' })) 13 | .expect('Content-Type', /json/) 14 | .expect(400) 15 | .then(response => { 16 | assert(response.body.error === 'Invalid username'); 17 | }); 18 | }); 19 | 20 | it('if password is invalid', () => { 21 | return request(_server_) 22 | .post('/api/users') 23 | .send(_.assign(_.clone(form), { password: '123' })) 24 | .expect('Content-Type', /json/) 25 | .expect(400) 26 | .then(response => { 27 | assert(response.body.error === 'Invalid password'); 28 | }); 29 | }); 30 | 31 | it('if email is invalid', () => { 32 | return request(_server_) 33 | .post('/api/users') 34 | .send(_.assign(_.clone(form), { email: 'test@com' })) 35 | .expect('Content-Type', /json/) 36 | .expect(400) 37 | .then(response => { 38 | assert(response.body.error === 'Invalid email'); 39 | }); 40 | }); 41 | }); 42 | 43 | describe('regular registration', () => { 44 | let userId; 45 | 46 | before((done) => { 47 | _db_.User 48 | .sync({ force: true }) 49 | .then(() => { 50 | request(_server_) 51 | .post('/api/users') 52 | .send(form) 53 | .expect(201) 54 | .then(response => { 55 | assert(response.body.id !== ''); 56 | userId = response.body.id; 57 | done(); 58 | }); 59 | }); 60 | }); 61 | 62 | it('should insert into db after post', () => { 63 | return _db_.User 64 | .findById(require('../../../server/utils/idt').decode('User', userId)) 65 | .then(user => { 66 | assert(user); 67 | 68 | let value = user.dataValues; 69 | _.forEach(['username, email, school'], (key) => { 70 | assert(value[key] === form[key]); 71 | }); 72 | }); 73 | }); 74 | }); 75 | 76 | describe('should response conflict 409', () => { 77 | before((done) => { 78 | request(_server_) 79 | .post('/api/users') 80 | .send(_.assign(_.clone(form), { 81 | username: 'test_user_2', 82 | email: 'test2@test.edu' 83 | })) 84 | .expect('Content-Type', /json/) 85 | .expect(201, done); 86 | }); 87 | 88 | it('if username exists', (done) => { 89 | request(_server_) 90 | .post('/api/users') 91 | .send(_.assign(_.clone(form), { 92 | email: 'test3@test.edu' 93 | })) 94 | .expect('Content-Type', /json/) 95 | .expect(409, done); 96 | }); 97 | 98 | it('if email exists', (done) => { 99 | request(_server_) 100 | .post('/api/users') 101 | .send(_.assign(_.clone(form), { 102 | username: 'test_user_3', 103 | email: 'test2@test.edu' 104 | })) 105 | .expect('Content-Type', /json/) 106 | .expect(409, done); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /test/server/posts/modify.spec.js: -------------------------------------------------------------------------------- 1 | describe('modify posts', function () { 2 | let token = { }; 3 | 4 | let post = { 5 | title: 'test_title', 6 | summary: 'test_summary', 7 | content: 'test_content', 8 | location: 'test_location', 9 | private: false, 10 | user_id: null 11 | }; 12 | 13 | let postId; 14 | 15 | before(() => { 16 | return dropAndRegisterAndLogin() 17 | .then(_token => { token = _token; }) 18 | .then(() => { 19 | return _db_.User.findOne(); 20 | }) 21 | .then(user => { 22 | post.user_id = user.id; 23 | }) 24 | .then(() => { 25 | return _db_.Post.sync({ force: true }); 26 | }) 27 | .then(() => { 28 | return _db_.Post.create(post); 29 | }) 30 | .then(post => { 31 | postId = post._id_; 32 | }); 33 | }); 34 | 35 | describe('should return Unauthorized 401 if didn\'t provide correct token', () => { 36 | let anotherToken = { }; 37 | before(() => { 38 | return registerAndLogin({ username: 'test2', password: 'test_password', email: 'test2@t.com' }) 39 | .then(_token => { anotherToken = _token; }); 40 | }); 41 | 42 | it('when calling put', () => { 43 | return request(_server_) 44 | .put(`/api/posts/${postId}`) 45 | .send(_authorize_(anotherToken, post)) 46 | .expect(401); 47 | }); 48 | 49 | it('when calling delete', () => { 50 | return request(_server_) 51 | .delete(`/api/posts/${postId}`) 52 | .send(_authorize_(anotherToken, {})) 53 | .expect(401); 54 | }); 55 | }); 56 | 57 | describe('should return Not Found 404 if id was incorrect', () => { 58 | it('when calling put', () => { 59 | return request(_server_) 60 | .put('/api/posts/123') 61 | .send(_authorize_(token, post)) 62 | .expect(404); 63 | }); 64 | 65 | it('when calling delete', () => { 66 | return request(_server_) 67 | .delete('/api/posts/123') 68 | .send(_authorize_(token, {})) 69 | .expect(404); 70 | }); 71 | }); 72 | 73 | describe('should return OK 200 and success', () => { 74 | before(() => { 75 | return _db_.Log 76 | .sync({ force: true }); 77 | }); 78 | 79 | let anotherPost = _.merge(post, { 80 | title: 'test_title_other', 81 | summary: 'test_summary_other', 82 | content: 'test_content_other', 83 | location: 'test_location_other', 84 | private: true 85 | }); 86 | 87 | it('when calling put', () => { 88 | return request(_server_) 89 | .put(`/api/posts/${postId}`) 90 | .send(_authorize_(token, anotherPost)) 91 | .expect(200); 92 | }); 93 | 94 | it('data should be changed', () => { 95 | return _db_.Post 96 | .findOne() 97 | .then(post => { 98 | assert.deepStrictEqual(anotherPost, _.pick(post.dataValues, _.keys(anotherPost))); 99 | }); 100 | }); 101 | 102 | it('when calling delte', () => { 103 | return request(_server_) 104 | .delete(`/api/posts/${postId}`) 105 | .send(_authorize_(token, {})) 106 | .expect(200); 107 | }); 108 | 109 | it('entry should be removed', () => { 110 | return _db_.Post 111 | .findOne({ 112 | where: { deleted_at: null } 113 | }) 114 | .then(post => { 115 | assert(!post); 116 | }); 117 | }); 118 | }); 119 | 120 | it('should log ip if post was modified', () => { 121 | return _db_.Log 122 | .findAndCountAll() 123 | .then(({ count }) => { 124 | console.log(count === 6); 125 | }); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /test/server/posts/post.spec.js: -------------------------------------------------------------------------------- 1 | const Q = require('q'); 2 | 3 | describe('post posts', function () { 4 | let token = { }; 5 | before(() => { return dropAndRegisterAndLogin().then(_token => { token = _token; }); }); 6 | 7 | let tags = [ 8 | { 9 | name: 'test_tag_1' 10 | }, 11 | { 12 | name: 'test_tag_2' 13 | } 14 | ]; 15 | 16 | let post = { 17 | title: 'test_title', 18 | summary: 'test_summary', 19 | content: 'test_content', 20 | password: 'test_password', 21 | private: false, 22 | tags: null 23 | }; 24 | 25 | before(() => { 26 | return Q 27 | .all(_.map(tags, tag => { 28 | return request(_server_) 29 | .post('/api/tags') 30 | .send(_authorize_(token, tag)) 31 | .expect(201); 32 | })) 33 | .then(() => { 34 | return request(_server_) 35 | .get('/api/tags') 36 | .expect(200) 37 | .then(response => { 38 | post.tags = _.map(response.body, tag => tag.id).join(','); 39 | }); 40 | }); 41 | }); 42 | 43 | describe('should return Unauthorized 401', () => { 44 | it('if did provide correct token', () => { 45 | return request(_server_) 46 | .post('/api/posts') 47 | .send(post) 48 | .expect(401); 49 | }); 50 | }); 51 | 52 | describe('should return BR 400', () => { 53 | it('if title is not valid', () => { 54 | return request(_server_) 55 | .post('/api/posts') 56 | .send(_authorize_(token, _.set(_.clone(post), 'title', null))) 57 | .expect(400); 58 | }); 59 | 60 | it('if summary is not valid', () => { 61 | return request(_server_) 62 | .post('/api/posts') 63 | .send(_authorize_(token, _.set(_.clone(post), 'summary', null))) 64 | .expect(400); 65 | }); 66 | 67 | it('if content is not valid', () => { 68 | return request(_server_) 69 | .post('/api/posts') 70 | .send(_authorize_(token, _.set(_.clone(post), 'content', null))) 71 | .expect(400); 72 | }); 73 | }); 74 | 75 | describe('if call post and success', () => { 76 | let callPost = () => { 77 | return request(_server_) 78 | .post('/api/posts') 79 | .send(_authorize_(token, post)); 80 | }; 81 | 82 | let fetchDb = (id) => { 83 | return _db_.Post 84 | .findOne({ 85 | where: { id }, 86 | include: [{ 87 | model: _db_.Tag, 88 | through: { 89 | model: _db_.ItemTag 90 | } 91 | }, { 92 | model: _db_.User 93 | }] 94 | }); 95 | }; 96 | 97 | it('should return Created 201', () => { 98 | return callPost() 99 | .expect(201); 100 | }); 101 | 102 | describe('should save correct data into db', () => { 103 | let data; 104 | 105 | before(() => { 106 | return callPost() 107 | .then(response => { 108 | let id = require('../../../server/utils/idt').decode('Post', response.body.id); 109 | return fetchDb(id) 110 | .then(post => { 111 | data = post; 112 | }); 113 | }); 114 | }); 115 | 116 | it('post content', () => { 117 | assert.deepStrictEqual(_.omit(post, 'tags'), _.pick(data.dataValues, _.keys(_.omit(post, 'tags')))); 118 | }); 119 | 120 | it('user', () => { 121 | assert(data.User.username === require('../helpers/authorize').userInfo.username); 122 | }); 123 | 124 | it('tags', () => { 125 | let _tags = _.map(tags, tag => tag.name); 126 | _.each(data.Tags, Tag => { assert(_.includes(_tags, Tag.name)); }); 127 | }); 128 | }); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodecho", 3 | "version": "1.0.0", 4 | "description": "Just a blog", 5 | "author": "Rijn ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "start": "node build/dev-server.js", 10 | "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run", 11 | "e2e": "node test/e2e/runner.js", 12 | "test": "npm run unit && npm run e2e" 13 | }, 14 | "dependencies": { 15 | "express-validator": "^3.2.0", 16 | "fontmin": "^0.9.6", 17 | "hashids": "^1.1.1", 18 | "lodash": "^4.17.4", 19 | "multer": "^1.3.0", 20 | "mysql2": "^1.3.5", 21 | "q": "^1.5.0", 22 | "rimraf": "^2.6.1", 23 | "scrypt": "^6.0.3", 24 | "sequelize": "^4.2.1", 25 | "sqlite3": "^3.1.8", 26 | "tryrequire": "^1.1.5", 27 | "vue": "^2.3.3", 28 | "vue-router": "^2.3.1" 29 | }, 30 | "devDependencies": { 31 | "autoprefixer": "^6.7.2", 32 | "babel-core": "^6.22.1", 33 | "babel-eslint": "^7.1.1", 34 | "babel-loader": "^6.2.10", 35 | "babel-plugin-istanbul": "^4.1.1", 36 | "babel-plugin-transform-runtime": "^6.22.0", 37 | "babel-preset-env": "^1.3.2", 38 | "babel-preset-stage-2": "^6.22.0", 39 | "babel-register": "^6.22.0", 40 | "chai": "^3.5.0", 41 | "chalk": "^1.1.3", 42 | "chromedriver": "^2.27.2", 43 | "compression-webpack-plugin": "^0.4.0", 44 | "connect-history-api-fallback": "^1.3.0", 45 | "connect-livereload": "^0.6.0", 46 | "copy-webpack-plugin": "^4.0.1", 47 | "cross-env": "^4.0.0", 48 | "cross-spawn": "^5.0.1", 49 | "css-loader": "^0.28.0", 50 | "eslint": "^3.19.0", 51 | "eslint-config-standard": "^10.2.1", 52 | "eslint-friendly-formatter": "^2.0.7", 53 | "eslint-loader": "^1.7.1", 54 | "eslint-plugin-html": "^1.7.0", 55 | "eslint-plugin-import": "^2.6.0", 56 | "eslint-plugin-node": "^5.1.0", 57 | "eslint-plugin-promise": "^3.5.0", 58 | "eslint-plugin-standard": "^3.0.1", 59 | "eventsource-polyfill": "^0.9.6", 60 | "express": "^4.14.1", 61 | "extract-text-webpack-plugin": "^2.0.0", 62 | "file-loader": "^0.11.1", 63 | "friendly-errors-webpack-plugin": "^1.1.3", 64 | "grunt": "^1.0.1", 65 | "grunt-apidoc": "^0.11.0", 66 | "grunt-config": "^1.0.0", 67 | "grunt-continue": "^0.1.0", 68 | "grunt-contrib-clean": "^1.1.0", 69 | "grunt-contrib-connect": "^1.0.2", 70 | "grunt-contrib-watch": "^1.0.0", 71 | "grunt-env": "^0.4.4", 72 | "grunt-eslint": "^20.0.0", 73 | "grunt-express-server": "^0.5.3", 74 | "grunt-karma": "^2.0.0", 75 | "grunt-keepalive": "^1.0.0", 76 | "grunt-mocha": "^1.0.4", 77 | "grunt-mocha-test": "^0.13.2", 78 | "grunt-newer": "^1.3.0", 79 | "grunt-shell": "^2.1.0", 80 | "grunt-webpack": "^1.0.18", 81 | "html-webpack-plugin": "^2.28.0", 82 | "http-proxy-middleware": "^0.17.3", 83 | "inject-loader": "^3.0.0", 84 | "iview": "^2.0.0-rc.18", 85 | "karma": "^1.4.1", 86 | "karma-coverage": "^1.1.1", 87 | "karma-mocha": "^1.3.0", 88 | "karma-phantomjs-launcher": "^1.0.2", 89 | "karma-phantomjs-shim": "^1.4.0", 90 | "karma-sinon-chai": "^1.3.1", 91 | "karma-sourcemap-loader": "^0.3.7", 92 | "karma-spec-reporter": "0.0.30", 93 | "karma-webpack": "^2.0.2", 94 | "less": "^2.7.2", 95 | "less-loader": "^4.0.4", 96 | "load-grunt-tasks": "^3.5.2", 97 | "lolex": "^1.5.2", 98 | "markdown": "^0.5.0", 99 | "mocha": "^3.2.0", 100 | "nightwatch": "^0.9.12", 101 | "normalize.css": "^7.0.0", 102 | "opn": "^4.0.2", 103 | "optimize-css-assets-webpack-plugin": "^1.3.0", 104 | "ora": "^1.2.0", 105 | "phantomjs-prebuilt": "^2.1.14", 106 | "rimraf": "^2.6.0", 107 | "selenium-server": "^3.0.1", 108 | "semver": "^5.3.0", 109 | "shelljs": "^0.7.6", 110 | "sinon": "^2.1.0", 111 | "sinon-chai": "^2.8.0", 112 | "store": "^2.0.12", 113 | "supertest": "^3.0.0", 114 | "time-grunt": "^1.4.0", 115 | "url-loader": "^0.5.8", 116 | "vue-loader": "^12.1.0", 117 | "vue-resource": "^1.3.4", 118 | "vue-style-loader": "^3.0.1", 119 | "vue-template-compiler": "^2.3.3", 120 | "vuex": "^2.3.1", 121 | "webpack": "^2.6.1", 122 | "webpack-bundle-analyzer": "^2.2.1", 123 | "webpack-dev-middleware": "^1.10.0", 124 | "webpack-dev-server": "^2.5.0", 125 | "webpack-hot-middleware": "^2.18.0", 126 | "webpack-merge": "^4.1.0" 127 | }, 128 | "engines": { 129 | "node": ">= 4.0.0", 130 | "npm": ">= 3.0.0" 131 | }, 132 | "browserslist": [ 133 | "> 1%", 134 | "last 2 versions", 135 | "not ie <= 8" 136 | ] 137 | } 138 | -------------------------------------------------------------------------------- /src/components/Input.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 84 | 85 | 204 | -------------------------------------------------------------------------------- /src/components/Button.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 46 | 47 | 212 | -------------------------------------------------------------------------------- /src/components/Content.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 71 | 72 | 140 | 141 | 224 | -------------------------------------------------------------------------------- /server/controllers/posts/-id/get.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Q = require('q'); 3 | const models = require('../../../models'); 4 | const errorHandler = require('../../../utils/error-handler'); 5 | const idt = require('../../../utils/idt'); 6 | const authority = require('../../../utils/authority'); 7 | 8 | const Fontmin = require('fontmin'); 9 | const path = require('path'); 10 | const fontPath = path.join(__dirname, '../../../../font.ttf'); 11 | 12 | const schema = { 13 | 'id': { 14 | notEmpty: true, 15 | errorMessage: 'Invalid id' 16 | }, 17 | 'password': { 18 | optional: true 19 | } 20 | }; 21 | 22 | module.exports = (req, res) => { 23 | return Q 24 | .fcall(() => { return { raw: req.query }; }) 25 | 26 | .then(_s => { 27 | let deferred = Q.defer(); 28 | req.checkParams(schema); 29 | req.getValidationResult().then(result => { 30 | if (!result.isEmpty()) { 31 | deferred.reject([result.useFirstErrorOnly().array()[0]]); 32 | } else { 33 | deferred.resolve(_.assign(_s, { data: _.pick(req.params, _.keys(schema)) })); 34 | } 35 | }); 36 | return deferred.promise; 37 | }) 38 | 39 | .then(_s => { 40 | let deferred = Q.defer(); 41 | models.Post 42 | .findOne({ 43 | where: { 44 | id: idt.decode('Post', _s.data.id) 45 | }, 46 | include: [{ 47 | model: models.Tag, 48 | through: { 49 | model: models.ItemTag, 50 | attributes: [] 51 | }, 52 | attributes: { 53 | include: ['id', 'name'] 54 | } 55 | }, { 56 | model: models.User, 57 | attributes: ['id', 'username'] 58 | }] 59 | }) 60 | .then(post => { 61 | if (!post) { 62 | deferred.reject(['Not found', _s, 404]); 63 | } 64 | return { post }; 65 | }) 66 | .then(({ post = null }) => { 67 | if (!post) return { post }; 68 | if (!_.has(post, 'User') || !_.has(_s.raw, 'token')) return { post }; 69 | return [ 70 | authority, 71 | _s => { return { post, p: _s.user_id === post.User.id }; } 72 | ].reduce(Q.when, Q(_s)).catch(() => { 73 | deferred.reject({ 74 | message: 'Invalid token', 75 | _s, 76 | statusCode: 400 77 | }); 78 | }); 79 | }) 80 | .then(({ post = null, p = false }) => { 81 | if (p || !post) return { post }; 82 | if (post.password && _s.raw.password === post.password) return { post }; 83 | if (!post.private && !post.password) return { post }; 84 | 85 | deferred.reject({ 86 | message: 'Unauthorized', 87 | _s, 88 | statusCode: 401, 89 | extra: { 90 | passwordRequired: !post.private, 91 | private: post.private 92 | } 93 | }); 94 | return {}; 95 | }) 96 | .then(({ post = null }) => { 97 | if (!post) return { post }; 98 | deferred.resolve(_.assign(_s, { 99 | post: post ? _.defaultsDeep( 100 | { 101 | id: post._id_, 102 | user: { 103 | id: _.get(post.User, '_id_') 104 | } 105 | }, 106 | _.assign( 107 | { 108 | user: _.get(post.dataValues.User, 'dataValues'), 109 | tags: _.map(post.dataValues.Tags, tag => _.get(tag, 'dataValues')) 110 | }, 111 | _.omit(post.dataValues, 'User', 'Tags', 'user_id') 112 | ) 113 | ) : null, 114 | post_id: post.id 115 | })); 116 | }); 117 | return deferred.promise; 118 | }) 119 | 120 | .then(_s => { 121 | let deferred = Q.defer(); 122 | models.Log 123 | .create({ 124 | ip: req.ip, 125 | post_id: _s.post_id, 126 | description: 'get' 127 | }) 128 | .then(() => { 129 | deferred.resolve(_s); 130 | }); 131 | return deferred.promise; 132 | }) 133 | 134 | .then(_s => { 135 | let deferred = Q.defer(); 136 | if (_s.post.poem) { 137 | new Fontmin() 138 | .src(fontPath) 139 | .use(Fontmin.glyph({ 140 | text: _s.post.content 141 | })) 142 | .use(Fontmin.css({ 143 | fontFamily: 'poem-font', 144 | base64: true 145 | })) 146 | .run((err, files) => { 147 | if (err) { 148 | console.error(err); 149 | } 150 | _s.post.font = files[1]._contents.toString(); 151 | deferred.resolve(_s); 152 | }); 153 | } else { 154 | deferred.resolve(_s); 155 | } 156 | return deferred.promise; 157 | }) 158 | 159 | .done(_s => { 160 | res.status(200).send(_s.post); 161 | }, errorHandler(res)); 162 | }; 163 | -------------------------------------------------------------------------------- /test/server/posts/get.spec.js: -------------------------------------------------------------------------------- 1 | describe('get posts', function () { 2 | let token = { }; 3 | before(() => { return dropAndRegisterAndLogin().then(_token => { token = _token; }); }); 4 | 5 | let anotherToken = { }; 6 | before(() => { 7 | return registerAndLogin({ username: 'test2', password: 'test_password', email: 'test2@t.com' }) 8 | .then(_token => { anotherToken = _token; }); 9 | }); 10 | 11 | let post = { 12 | title: 'test_title', 13 | summary: 'test_summary', 14 | content: 'test_content', 15 | location: 'test_location', 16 | private: false, 17 | user_id: null 18 | }; 19 | 20 | before(() => { 21 | return _db_.User 22 | .findOne() 23 | .then(user => { 24 | post.user_id = user.id; 25 | }); 26 | }); 27 | 28 | describe('should return Unauthorized 401', () => { 29 | it('if post is private', () => { 30 | return _db_.Post 31 | .create(_.set(_.clone(post), 'private', true)) 32 | .then(post => { 33 | return request(_server_) 34 | .get(`/api/posts/${post._id_}`) 35 | .expect(401); 36 | }); 37 | }); 38 | 39 | it('if post is locked and provide wrong password', () => { 40 | return _db_.Post 41 | .create(_.set(_.clone(post), 'password', 'test_password')) 42 | .then(post => { 43 | return request(_server_) 44 | .get(`/api/posts/${post._id_}`) 45 | .query({ password: 'hhh' }) 46 | .expect(401); 47 | }); 48 | }); 49 | 50 | it('if post is private then privide incorrect token', () => { 51 | return _db_.Post 52 | .create(_.set(_.clone(post), 'private', true), { 53 | include: [{ 54 | model: _db_.User 55 | }] 56 | }) 57 | .then(post => { 58 | return request(_server_) 59 | .get(`/api/posts/${post._id_}`) 60 | .query(_authorize_(anotherToken, {})) 61 | .expect(401); 62 | }); 63 | }); 64 | 65 | it('if post is locked then privide incorrect token and incorrect password', () => { 66 | return _db_.Post 67 | .create(_.set(_.clone(post), 'password', '1234'), { 68 | include: [{ 69 | model: _db_.User 70 | }] 71 | }) 72 | .then(post => { 73 | return request(_server_) 74 | .get(`/api/posts/${post._id_}`) 75 | .query(_authorize_(anotherToken, { password: '123' })) 76 | .expect(401); 77 | }); 78 | }); 79 | }); 80 | 81 | describe('should return OK 200 and correct content', () => { 82 | it('if post is open', () => { 83 | return _db_.Post 84 | .create(post) 85 | .then(post => { 86 | return request(_server_) 87 | .get(`/api/posts/${post._id_}`) 88 | .expect(200) 89 | .then(response => { 90 | ['title', 'summary', 'content', 'location'].forEach(key => { 91 | assert(response.body[key] === post[key]); 92 | }); 93 | assert(response.body.user.id === token.userid); 94 | }); 95 | }); 96 | }); 97 | 98 | it('if post is locked but provide correct password', () => { 99 | let password = 'test_password'; 100 | 101 | return _db_.Post 102 | .create(_.set(_.clone(post), 'password', password)) 103 | .then(post => { 104 | return request(_server_) 105 | .get(`/api/posts/${post._id_}`) 106 | .query({ password }) 107 | .expect(200); 108 | }); 109 | }); 110 | 111 | it('if post is locked but privide correct token', () => { 112 | return _db_.Post 113 | .create(_.set(_.clone(post), 'password', '123'), { 114 | include: [{ 115 | model: _db_.User 116 | }] 117 | }) 118 | .then(post => { 119 | return request(_server_) 120 | .get(`/api/posts/${post._id_}`) 121 | .query(_authorize_(token, {})) 122 | .expect(200); 123 | }); 124 | }); 125 | 126 | it('if post is private but provide correct token', () => { 127 | return _db_.Post 128 | .create(_.set(_.clone(post), 'private', true), { 129 | include: [{ 130 | model: _db_.User 131 | }] 132 | }) 133 | .then(post => { 134 | return request(_server_) 135 | .get(`/api/posts/${post._id_}`) 136 | .query(_authorize_(token, {})) 137 | .expect(200); 138 | }); 139 | }); 140 | 141 | it('if post is locked then privide incorrect token and correct password', () => { 142 | let password = 'test_password'; 143 | 144 | return _db_.Post 145 | .create(_.set(_.clone(post), 'password', password), { 146 | include: [{ 147 | model: _db_.User 148 | }] 149 | }) 150 | .then(post => { 151 | return request(_server_) 152 | .get(`/api/posts/${post._id_}`) 153 | .query(_authorize_(anotherToken, { password })) 154 | .expect(200); 155 | }); 156 | }); 157 | }); 158 | 159 | it('should return Not Found 404 if id is incorrect', () => { 160 | return _db_.Post 161 | .create(post) 162 | .then(post => { 163 | return request(_server_) 164 | .get('/api/posts/123') 165 | .expect(404); 166 | }); 167 | }); 168 | 169 | it('should log ip if post was readed', () => { 170 | return _db_.Log 171 | .sync({ force: true }) 172 | .then(() => { 173 | return _db_.Post 174 | .create(post); 175 | }) 176 | .then(post => { 177 | return request(_server_) 178 | .get(`/api/posts/${post._id_}`); 179 | }) 180 | .then(() => { 181 | return _db_.Log 182 | .findAndCountAll(); 183 | }) 184 | .then(({ count }) => { 185 | assert(count === 1); 186 | }); 187 | }); 188 | }); 189 | -------------------------------------------------------------------------------- /src/components/AdminEdit.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 191 | 192 | 204 | 205 | 210 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-present iView 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | MIT LICENSE 24 | 25 | Copyright (c) 2015-present Alipay.com, https://www.alipay.com/ 26 | 27 | Permission is hereby granted, free of charge, to any person obtaining 28 | a copy of this software and associated documentation files (the 29 | "Software"), to deal in the Software without restriction, including 30 | without limitation the rights to use, copy, modify, merge, publish, 31 | distribute, sublicense, and/or sell copies of the Software, and to 32 | permit persons to whom the Software is furnished to do so, subject to 33 | the following conditions: 34 | 35 | The above copyright notice and this permission notice shall be 36 | included in all copies or substantial portions of the Software. 37 | 38 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 39 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 40 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 41 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 42 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 43 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 44 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 45 | 46 | The MIT License (MIT) 47 | 48 | Copyright (c) 2016 ElemeFE 49 | 50 | Permission is hereby granted, free of charge, to any person obtaining a copy 51 | of this software and associated documentation files (the "Software"), to deal 52 | in the Software without restriction, including without limitation the rights 53 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 54 | copies of the Software, and to permit persons to whom the Software is 55 | furnished to do so, subject to the following conditions: 56 | 57 | The above copyright notice and this permission notice shall be included in all 58 | copies or substantial portions of the Software. 59 | 60 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 61 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 62 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 63 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 64 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 65 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 66 | SOFTWARE. 67 | 68 | The MIT License (MIT) 69 | 70 | Copyright (c) 2015 Koala 71 | 72 | Permission is hereby granted, free of charge, to any person obtaining a copy 73 | of this software and associated documentation files (the "Software"), to deal 74 | in the Software without restriction, including without limitation the rights 75 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 76 | copies of the Software, and to permit persons to whom the Software is 77 | furnished to do so, subject to the following conditions: 78 | 79 | The above copyright notice and this permission notice shall be included in all 80 | copies or substantial portions of the Software. 81 | 82 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 83 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 84 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 85 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 86 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 87 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 88 | SOFTWARE. 89 | 90 | The MIT License (MIT) 91 | 92 | Copyright (c) 2016 vue-beauty 93 | 94 | Permission is hereby granted, free of charge, to any person obtaining a copy 95 | of this software and associated documentation files (the "Software"), to deal 96 | in the Software without restriction, including without limitation the rights 97 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 98 | copies of the Software, and to permit persons to whom the Software is 99 | furnished to do so, subject to the following conditions: 100 | 101 | The above copyright notice and this permission notice shall be included in all 102 | copies or substantial portions of the Software. 103 | 104 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 105 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 106 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 107 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 108 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 109 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 110 | SOFTWARE. 111 | 112 | MIT License 113 | 114 | Copyright (c) 2016-present, Airyland 115 | 116 | Permission is hereby granted, free of charge, to any person obtaining a copy 117 | of this software and associated documentation files (the "Software"), to deal 118 | in the Software without restriction, including without limitation the rights 119 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 120 | copies of the Software, and to permit persons to whom the Software is 121 | furnished to do so, subject to the following conditions: 122 | 123 | The above copyright notice and this permission notice shall be included in all 124 | copies or substantial portions of the Software. 125 | 126 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 127 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 128 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 129 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 130 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 131 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 132 | SOFTWARE. 133 | 134 | The MIT License (MIT) 135 | 136 | Copyright (c) 2016 Drifty (http://drifty.com/) 137 | 138 | Permission is hereby granted, free of charge, to any person obtaining a copy 139 | of this software and associated documentation files (the "Software"), to deal 140 | in the Software without restriction, including without limitation the rights 141 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 142 | copies of the Software, and to permit persons to whom the Software is 143 | furnished to do so, subject to the following conditions: 144 | 145 | The above copyright notice and this permission notice shall be included in 146 | all copies or substantial portions of the Software. 147 | 148 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 149 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 150 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 151 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 152 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 153 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 154 | THE SOFTWARE. 155 | 156 | The MIT License (MIT) 157 | 158 | Copyright (c) 2015 Tobias Ahlin 159 | 160 | Permission is hereby granted, free of charge, to any person obtaining a copy of 161 | this software and associated documentation files (the "Software"), to deal in 162 | the Software without restriction, including without limitation the rights to 163 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 164 | the Software, and to permit persons to whom the Software is furnished to do so, 165 | subject to the following conditions: 166 | 167 | The above copyright notice and this permission notice shall be included in all 168 | copies or substantial portions of the Software. 169 | 170 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 171 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 172 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 173 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 174 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 175 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 176 | --------------------------------------------------------------------------------