├── 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 |
2 |
6 |
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 |
2 |
3 |
4 |
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 |
2 |
8 | Upload
9 |
10 |
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 |
2 |
3 |
4 |
5 | © Rijn, 2017
6 |
7 |
8 | Homepage
9 |
10 |
11 | Blog
12 |
13 |
14 | GitHub
15 |
16 |
17 |
18 |
19 |
20 |
21 |
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 |
2 |
19 |
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 [](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 | 
12 | 
13 | 
14 | 
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 |
2 |
8 |
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 |
2 |
9 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
16 |
17 |
18 | NO MORE POST
19 |
20 |
21 |
22 |
27 | PREV
28 |
29 |
34 | NEXT
35 |
36 |
37 |
38 |
39 |
40 |
91 |
92 |
108 |
--------------------------------------------------------------------------------
/src/components/Auth.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Request Token
12 |
13 |
14 |
15 | Logout
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | Request Token
25 |
26 |
27 |
28 |
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 |
2 |
3 |
4 | Refresh
5 | New Post
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
105 |
106 |
113 |
--------------------------------------------------------------------------------
/src/components/Header.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Source Code on Github
14 |
15 |
16 |
17 |
18 |
27 |
28 |
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 |
2 |
3 |
4 |
5 |
6 |
11 |
12 | {{ message }}
13 |
16 |
17 |
18 |
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 |
2 |
3 |
15 |
16 | {{ placeholder }}
17 |
18 |
19 |
20 |
21 |
84 |
85 |
204 |
--------------------------------------------------------------------------------
/src/components/Button.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
46 |
47 |
212 |
--------------------------------------------------------------------------------
/src/components/Content.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
17 |
18 |
19 | {{ info.location }}
20 |
21 |
22 |
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 |
2 |
3 |
4 | Submit
5 | {{ $route.params.id === 'new' ? 'cancel' : 'delete' }}
6 | View
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
55 |
56 |
57 |
58 |
59 |
60 |
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 |
--------------------------------------------------------------------------------