├── logs └── .keep ├── .nvmrc ├── test ├── data │ └── .keep ├── lib │ └── .keep ├── app │ ├── services │ │ └── .keep │ ├── models │ │ └── user.spec.js │ ├── jobs │ │ ├── MyJob.spec.js │ │ └── Job.spec.js │ └── controllers │ │ └── usersController.spec.js ├── helpers │ └── httpMocks.js ├── init.js └── factories │ └── users.js ├── .ruby-version ├── src ├── app │ ├── services │ │ └── .keep │ ├── helpers │ │ └── wrap.js │ ├── jobs │ │ ├── index.js │ │ ├── MyJob.js │ │ └── Job.js │ ├── views │ │ ├── index.pug │ │ ├── error.pug │ │ ├── layout │ │ │ └── base.pug │ │ └── extras.pug │ ├── controllers │ │ ├── ApplicationController.js │ │ └── api │ │ │ └── UsersController.js │ ├── errors │ │ └── apiError.js │ ├── routes.js │ ├── middlewares │ │ └── jwtMiddleware.js │ ├── boots │ │ ├── JobHandler.js │ │ └── PassportHandler.js │ └── models │ │ └── user.js ├── assets │ ├── styles │ │ ├── error.scss │ │ ├── extras.scss │ │ └── main.scss │ ├── scripts │ │ └── fadeIn.js │ ├── images │ │ └── favicon.png │ └── fonts │ │ └── Raleway-Regular.ttf ├── lib │ ├── gocool │ │ ├── package.json │ │ ├── lib │ │ │ ├── index.js │ │ │ ├── Controller.js │ │ │ ├── Router.js │ │ │ └── Server.js │ │ └── test │ │ │ └── lib │ │ │ ├── Controller.spec.js │ │ │ ├── Router.spec.js │ │ │ └── Server.spec.js │ ├── Logable.js │ └── Logger.js ├── plugins │ └── gocool-github-demo-plugin │ │ ├── lib │ │ ├── views │ │ │ └── github-demo │ │ │ │ ├── index.pug │ │ │ │ └── closed-issues.pug │ │ ├── index.js │ │ ├── Plugin.js │ │ ├── controllers │ │ │ ├── api │ │ │ │ └── GithubController.js │ │ │ └── ApplicationController.js │ │ ├── routes.js │ │ └── services │ │ │ └── GithubService.js │ │ ├── package.json │ │ └── test │ │ ├── helpers │ │ └── httpMocks.js │ │ ├── services │ │ └── GithubService.spec.js │ │ ├── controllers │ │ └── githubController.spec.js │ │ └── data │ │ └── github │ │ └── issues │ │ └── closed.json ├── commands │ ├── job-my.js │ └── job.js ├── config │ ├── kueConfig.js │ ├── appConfig.js │ ├── redisConfig.js │ └── knexConfig.js ├── database │ ├── seeds │ │ ├── data │ │ │ └── users.js │ │ ├── test │ │ │ └── testSeeds.js │ │ ├── components │ │ │ ├── Seeder.js │ │ │ ├── Cleaner.js │ │ │ └── UsersSeeder.js │ │ ├── development │ │ │ └── developmentSeeds.js │ │ └── helpers │ │ │ └── SeedRunner.js │ ├── migrations │ │ └── 20161219201844_create_users.js │ ├── drop.js │ ├── index.js │ └── create.js └── server.js ├── tools ├── capistrano │ ├── tasks │ │ └── .keep │ ├── stages │ │ ├── localhost.rb │ │ ├── dev.rb │ │ ├── staging.rb │ │ └── production.rb │ └── deploy.rb └── gulp │ ├── tasks │ ├── clean.js │ ├── test.js │ ├── main.js │ ├── serve.js │ ├── watch.js │ ├── linter.js │ ├── transpile.js │ ├── bower.js │ └── build.js │ └── config.js ├── .eslintignore ├── gulpfile.babel.js ├── .gitignore ├── bower.json ├── bin ├── eslint-pr.sh ├── eslint-pre-commit.sh └── after_test.sh ├── Gemfile ├── .editorconfig ├── pm2.config.js ├── docker-compose.yml ├── .babelrc ├── Gemfile.lock ├── .eslintrc.json ├── .env.sample ├── circle.yml ├── Capfile ├── CHANGELOG.md ├── package.json └── README.md /logs/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 6.10.1 2 | -------------------------------------------------------------------------------- /test/data/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/lib/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.2.4 2 | -------------------------------------------------------------------------------- /src/app/services/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/app/services/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/helpers/httpMocks.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tools/capistrano/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/* 2 | build/* 3 | -------------------------------------------------------------------------------- /src/assets/styles/error.scss: -------------------------------------------------------------------------------- 1 | .red { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | require('require-dir')('tools/gulp/tasks'); 2 | -------------------------------------------------------------------------------- /src/assets/styles/extras.scss: -------------------------------------------------------------------------------- 1 | #wrapper { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/helpers/wrap.js: -------------------------------------------------------------------------------- 1 | export default fn => (...args) => fn(...args).catch(args[2]); 2 | -------------------------------------------------------------------------------- /src/lib/gocool/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gocool", 3 | "main": "lib/index.js" 4 | } 5 | -------------------------------------------------------------------------------- /src/app/jobs/index.js: -------------------------------------------------------------------------------- 1 | import MyJob from './MyJob'; 2 | 3 | export default [ 4 | MyJob, 5 | ]; 6 | -------------------------------------------------------------------------------- /src/assets/scripts/fadeIn.js: -------------------------------------------------------------------------------- 1 | $(document).ready(() => { 2 | $('#wrapper').fadeIn(500); 3 | }); 4 | -------------------------------------------------------------------------------- /src/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imheretw/imhere/HEAD/src/assets/images/favicon.png -------------------------------------------------------------------------------- /src/plugins/gocool-github-demo-plugin/lib/views/github-demo/index.pug: -------------------------------------------------------------------------------- 1 | h1= title 2 | p Welcome to Github Demo! 3 | -------------------------------------------------------------------------------- /src/assets/fonts/Raleway-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imheretw/imhere/HEAD/src/assets/fonts/Raleway-Regular.ttf -------------------------------------------------------------------------------- /src/commands/job-my.js: -------------------------------------------------------------------------------- 1 | import MyJob from 'jobs/MyJob'; 2 | 3 | const job = new MyJob(); 4 | job.runImmediate(); 5 | job.shutdown(); 6 | -------------------------------------------------------------------------------- /src/plugins/gocool-github-demo-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gocool-github-demo-plugin", 3 | "main": "lib/index.js" 4 | } 5 | -------------------------------------------------------------------------------- /src/app/views/index.pug: -------------------------------------------------------------------------------- 1 | extends layout/base 2 | 3 | block head 4 | title= title 5 | 6 | block body 7 | h1= title 8 | p Welcome to #{title}! 9 | -------------------------------------------------------------------------------- /tools/capistrano/stages/localhost.rb: -------------------------------------------------------------------------------- 1 | set :branch, ENV["CI_BRANCH"] 2 | 3 | role :app, %w{localhost} 4 | role :web, %w{localhost} 5 | role :db, %w{localhost} 6 | -------------------------------------------------------------------------------- /src/plugins/gocool-github-demo-plugin/lib/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | 3 | import Plugin from './Plugin'; 4 | 5 | export default Plugin; 6 | -------------------------------------------------------------------------------- /src/plugins/gocool-github-demo-plugin/lib/views/github-demo/closed-issues.pug: -------------------------------------------------------------------------------- 1 | h1= title 2 | - 3 | each issue in issues 4 | li 5 | a(href=issue.html_url) #{issue.title} 6 | -------------------------------------------------------------------------------- /src/config/kueConfig.js: -------------------------------------------------------------------------------- 1 | import redis from './redisConfig'; 2 | 3 | require('dotenv').config(); 4 | 5 | export default { 6 | prefix: process.env.KUE_PREFIX, 7 | redis, 8 | }; 9 | -------------------------------------------------------------------------------- /test/init.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import factory, { BookshelfAdapter } from 'factory-girl'; 3 | 4 | dotenv.config(); 5 | factory.setAdapter(new BookshelfAdapter()); 6 | -------------------------------------------------------------------------------- /src/lib/Logable.js: -------------------------------------------------------------------------------- 1 | import Logger from 'Logger'; 2 | 3 | export default class Logable { 4 | constructor() { 5 | this.logger = new Logger(this.constructor.name); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /logs 3 | 4 | /src/static 5 | 6 | /bower_components 7 | /node_modules 8 | /npm-debug.log 9 | /yarn-error.log 10 | /.env 11 | 12 | /.nyc_output 13 | /coverage 14 | -------------------------------------------------------------------------------- /src/commands/job.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import program from 'commander'; 3 | 4 | dotenv.config(); 5 | 6 | program 7 | .version('0.0.1') 8 | .command('my', 'run my job').alias('m') 9 | .parse(process.argv); 10 | -------------------------------------------------------------------------------- /src/app/views/error.pug: -------------------------------------------------------------------------------- 1 | extends layout/base 2 | 3 | block head 4 | title= error 5 | link(rel='stylesheet', href='/css/error.css') 6 | 7 | block body 8 | h1 9 | span.red= status + ' Error! ' 10 | = message 11 | pre= stack 12 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "imhere", 3 | "version": "0.1.0", 4 | "description": "imhere", 5 | "ignore": [ 6 | "**/.*", 7 | "node_modules", 8 | "components" 9 | ], 10 | "dependencies": { 11 | "jquery": "^2.2.2" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/controllers/ApplicationController.js: -------------------------------------------------------------------------------- 1 | import { Controller } from 'gocool'; 2 | 3 | export default class ApplicationController extends Controller { 4 | async index() { 5 | this.res.render('index', { 6 | title: 'imhere', 7 | }); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /bin/eslint-pr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | git fetch origin master:refs/remotes/origin/master 5 | git diff origin/master HEAD --name-only --diff-filter ACMR | egrep '.js$' | xargs $(npm bin)/eslint 6 | RESULT=$? 7 | 8 | [ $RESULT -ne 0 ] && exit 1 9 | exit 0 10 | -------------------------------------------------------------------------------- /bin/eslint-pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git stash -q --keep-index 4 | 5 | git diff-index --cached HEAD --name-only --diff-filter ACMR | egrep '.js$' | xargs $(npm bin)/eslint 6 | RESULT=$? 7 | git stash pop -q 8 | 9 | [ $RESULT -ne 0 ] && exit 1 10 | exit 0 11 | -------------------------------------------------------------------------------- /src/lib/gocool/lib/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | 3 | import Server from './Server'; 4 | import Controller from './Controller'; 5 | import Router from './Router'; 6 | 7 | export { 8 | Server, 9 | Controller, 10 | Router, 11 | }; 12 | -------------------------------------------------------------------------------- /src/database/seeds/data/users.js: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt'; 2 | import config from 'config/appConfig'; 3 | 4 | const users = [{ 5 | id: 1, 6 | name: 'Test', 7 | email: 'test@test.com', 8 | encrypted_password: bcrypt.hashSync('123456', config.auth.bcryptSalt), 9 | }]; 10 | 11 | export default users; 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # A sample Gemfile 3 | source "https://rubygems.org" 4 | 5 | gem 'capistrano', '3.8.0' 6 | gem 'capistrano-yarn' 7 | gem 'capistrano-nvm' 8 | gem 'capistrano-bower' 9 | gem 'capistrano-gulp' 10 | gem 'capistrano_pm2' 11 | gem 'capistrano-locally' 12 | gem 'capistrano-env-config' 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # 2 space indentation 12 | [**.*] 13 | indent_style = space 14 | indent_size = 2 -------------------------------------------------------------------------------- /pm2.config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | module.exports = { 4 | apps: [{ 5 | name: process.env.APP_NAME, 6 | script: './server.js', 7 | cwd: process.env.APP_PATH, 8 | error_file: './logs/server.err.log', 9 | out_file: './logs/server.out.log', 10 | exec_mode: 'fork_mode', 11 | }], 12 | }; 13 | -------------------------------------------------------------------------------- /src/plugins/gocool-github-demo-plugin/lib/Plugin.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import routes from './routes'; 3 | 4 | export default class Plugin { 5 | getRoutes() { 6 | return routes; 7 | } 8 | 9 | getViews() { 10 | const rootPath = path.normalize(__dirname); 11 | return [`${rootPath}/views`]; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/config/appConfig.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | export default { 4 | // environment 5 | env: process.env.NODE_ENV, 6 | 7 | // port on which to listen 8 | port: process.env.PORT, 9 | 10 | // authentication 11 | auth: { 12 | jwt: process.env.JWT_SECRET, 13 | bcryptSalt: process.env.BCRYPT_SALT, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/database/seeds/test/testSeeds.js: -------------------------------------------------------------------------------- 1 | import Cleaner from '../components/Cleaner'; 2 | import SeedRunner from '../helpers/SeedRunner'; 3 | 4 | exports.seed = async () => { 5 | const seedRunner = new SeedRunner(); 6 | const ComponentClasses = [ 7 | Cleaner, 8 | ]; 9 | 10 | seedRunner.addSeeds(ComponentClasses); 11 | await seedRunner.run(); 12 | }; 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | mysql: 2 | image: mysql:5.7.12 3 | environment: 4 | MYSQL_ROOT_PASSWORD: mysql 5 | ports: 6 | - 3306:3306 7 | phpmyadmin: 8 | image: phpmyadmin/phpmyadmin 9 | links: 10 | - mysql 11 | environment: 12 | PMA_HOST: mysql 13 | ports: 14 | - 8080:80 15 | redis: 16 | image: redis:3.0.7 17 | ports: 18 | - 6379:6379 19 | -------------------------------------------------------------------------------- /src/config/redisConfig.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | export default { 4 | host: process.env.REDIS_HOST, 5 | port: process.env.REDIS_PORT, 6 | auth: process.env.REDIS_PASSWORD, 7 | db: process.env.REDIS_DB, // if provided select a non-default redis db 8 | options: { 9 | // see https://github.com/mranney/node_redis#rediscreateclient 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/plugins/gocool-github-demo-plugin/test/helpers/httpMocks.js: -------------------------------------------------------------------------------- 1 | import nock from 'nock'; 2 | import Logger from 'Logger'; 3 | import { GITHUB_API_BASE_URL } from '../../lib/services/GithubService'; 4 | 5 | const logger = new Logger('httpMock'); 6 | 7 | const githubHttpMock = nock(GITHUB_API_BASE_URL).log(logger.debug); 8 | 9 | export default { 10 | githubHttpMock, 11 | }; 12 | -------------------------------------------------------------------------------- /src/database/seeds/components/Seeder.js: -------------------------------------------------------------------------------- 1 | import { bookshelf } from 'database'; 2 | import Logger from 'Logger'; 3 | 4 | export default class Seeder { 5 | constructor() { 6 | this.knex = bookshelf.knex; 7 | this.logger = new Logger(this.constructor.name); 8 | } 9 | 10 | async run() { 11 | this.logger.debug('run default method! Should override me'); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/gocool/test/lib/Controller.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import Controller from '../../lib/Controller'; 3 | 4 | describe('Test Controller', () => { 5 | describe('static action()', () => { 6 | it('should return a object', async () => { 7 | const obj = Controller.action('test'); 8 | expect(obj).to.be.defined; 9 | }); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tools/gulp/tasks/clean.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import del from 'del'; 3 | import { DEST, ALL } from '../config'; 4 | 5 | // create clean tasks 6 | ALL.forEach(task => gulp.task(`clean:${task.name}`, () => del([task.dest]))); 7 | 8 | // clean everything! 9 | gulp.task('clean', () => del([DEST])); 10 | 11 | // clear cache 12 | gulp.task('clear', done => $.cache.clearAll(done)); 13 | -------------------------------------------------------------------------------- /src/plugins/gocool-github-demo-plugin/lib/controllers/api/GithubController.js: -------------------------------------------------------------------------------- 1 | import { Controller } from 'gocool'; 2 | import GithubService from '../../services/GithubService'; 3 | 4 | export default class GithubController extends Controller { 5 | async closedIssues() { 6 | const issues = await GithubService.getClosedIssues(); 7 | 8 | this.res.json({ issues }); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/gocool/test/lib/Router.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import Router from '../../lib/Router'; 3 | 4 | describe('Test Router', () => { 5 | describe('constructor()', () => { 6 | it('should create a object', async () => { 7 | const obj = new Router(); 8 | expect(obj).to.be.defined; 9 | expect(obj._router).to.be.defined; 10 | }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tools/gulp/tasks/test.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import path from 'path'; 3 | import run from 'gulp-run'; 4 | import { SRC, TEST } from '../config'; 5 | 6 | // watch all js files for changes and run test 7 | gulp.task('watch:test', () => { 8 | gulp.watch([path.join(SRC, '**/*.js'), path.join(TEST, '**/*.js')], ['test']); 9 | }); 10 | 11 | gulp.task('test', () => run('npm run test').exec()); 12 | -------------------------------------------------------------------------------- /src/database/seeds/components/Cleaner.js: -------------------------------------------------------------------------------- 1 | import knexCleaner from 'knex-cleaner'; 2 | import Seeder from './Seeder'; 3 | 4 | export default class Cleaner extends Seeder { 5 | run() { 6 | const options = { 7 | mode: 'truncate', // Valid options 'truncate', 'delete' 8 | ignoreTables: ['knex_migrations', 'knex_migrations_lock'], 9 | }; 10 | 11 | return knexCleaner.clean(this.knex, options); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tools/capistrano/stages/dev.rb: -------------------------------------------------------------------------------- 1 | set :branch, ENV["CI_BRANCH"] 2 | set :user, ENV["DEV_USER"] 3 | 4 | role :app, ENV['DEV_HOST'] 5 | role :web, ENV['DEV_HOST'] 6 | 7 | set :deploy_to, ENV['DEV_DEPLOY_TO'] 8 | 9 | set :ssh_options, { 10 | user: fetch(:user), 11 | auth_methods: %w(publickey), 12 | keys: [ 13 | File.join(ENV['HOME'], '.ssh', 'id_rsa'), 14 | File.join(ENV['HOME'], '.ssh', ENV['DEV_PEM']) 15 | ], 16 | } 17 | -------------------------------------------------------------------------------- /src/app/jobs/MyJob.js: -------------------------------------------------------------------------------- 1 | import Job from './Job'; 2 | 3 | export default class MyJob extends Job { 4 | async run(job) { 5 | // you should implement the run method. 6 | this._log(); 7 | } 8 | 9 | _log() { 10 | this.logger.debug('run my job'); 11 | } 12 | 13 | // addtional required arguments for job handler 14 | /* 15 | getPayload() { 16 | return { 17 | x: 1, 18 | y: 2 19 | }; 20 | } 21 | */ 22 | } 23 | -------------------------------------------------------------------------------- /src/app/errors/apiError.js: -------------------------------------------------------------------------------- 1 | import ExtendableError from 'es6-error'; 2 | 3 | class ApiError extends ExtendableError { 4 | constructor(message = 'unknown api error', statusCode = 422) { 5 | super(message); 6 | this._statusCode = statusCode; 7 | } 8 | 9 | get statusCode() { 10 | return this._statusCode; 11 | } 12 | 13 | set statusCode(value) { 14 | this._statusCode = value; 15 | } 16 | } 17 | 18 | export default ApiError; 19 | -------------------------------------------------------------------------------- /src/database/seeds/development/developmentSeeds.js: -------------------------------------------------------------------------------- 1 | import Cleaner from '../components/Cleaner'; 2 | import UsersSeeder from '../components/UsersSeeder'; 3 | import SeedRunner from '../helpers/SeedRunner'; 4 | 5 | exports.seed = async () => { 6 | const seedRunner = new SeedRunner(); 7 | const ComponentClasses = [ 8 | Cleaner, 9 | UsersSeeder, 10 | ]; 11 | 12 | seedRunner.addSeeds(ComponentClasses); 13 | await seedRunner.run(); 14 | }; 15 | -------------------------------------------------------------------------------- /tools/capistrano/stages/staging.rb: -------------------------------------------------------------------------------- 1 | set :branch, ENV["CI_BRANCH"] 2 | set :user, ENV["STAGING_USER"] 3 | 4 | role :app, ENV['STAGING_HOST'] 5 | role :web, ENV['STAGING_HOST'] 6 | 7 | set :deploy_to, ENV['STAGING_DEPLOY_TO'] 8 | 9 | set :ssh_options, { 10 | user: fetch(:user), 11 | auth_methods: %w(publickey), 12 | keys: [ 13 | File.join(ENV['HOME'], '.ssh', 'id_rsa'), 14 | File.join(ENV['HOME'], '.ssh', ENV['STAGING_PEM']) 15 | ], 16 | } 17 | -------------------------------------------------------------------------------- /src/app/views/layout/base.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='en') 3 | head 4 | meta(charset='utf-8') 5 | meta(http-equiv='X-UA-Compatible', content='IE=edge') 6 | meta(name='description', content='kid guard') 7 | meta(name='viewport', content='width=device-width, initial-scale=1') 8 | link(rel='stylesheet', href='/css/main.css') 9 | block head 10 | body 11 | block body 12 | script(src='/js/vendor.min.js') 13 | block scripts 14 | -------------------------------------------------------------------------------- /tools/capistrano/stages/production.rb: -------------------------------------------------------------------------------- 1 | set :branch, ENV["CI_BRANCH"] 2 | set :user, ENV["PRODUCTION_USER"] 3 | 4 | role :app, ENV['PRODUCTION_HOST'] 5 | role :web, ENV['PRODUCTION_HOST'] 6 | 7 | set :deploy_to, ENV['PRODUCTION_DEPLOY_TO'] 8 | 9 | set :ssh_options, { 10 | user: fetch(:user), 11 | auth_methods: %w(publickey), 12 | keys: [ 13 | File.join(ENV['HOME'], '.ssh', 'id_rsa'), 14 | File.join(ENV['HOME'], '.ssh', ENV['PRODUCTION_PEM']) 15 | ], 16 | } 17 | -------------------------------------------------------------------------------- /src/database/migrations/20161219201844_create_users.js: -------------------------------------------------------------------------------- 1 | exports.up = function up(knex, Promise) { 2 | return knex.schema.createTable('users', (table) => { 3 | table.increments('id').primary(); 4 | table.string('name').notNullable(); 5 | table.string('email').unique(); 6 | table.string('encrypted_password'); 7 | table.timestamps(); 8 | }); 9 | }; 10 | 11 | exports.down = function down(knex, Promise) { 12 | return knex.schema.dropTable('users'); 13 | }; 14 | -------------------------------------------------------------------------------- /test/factories/users.js: -------------------------------------------------------------------------------- 1 | import factory from 'factory-girl'; 2 | import bcrypt from 'bcrypt'; 3 | import User from 'models/user'; 4 | import config from 'config/appConfig'; 5 | 6 | factory.define('user', User, { 7 | id: factory.seq('User.id', n => n), 8 | name: factory.seq('User.name', n => `name${n}`), 9 | email: factory.seq('User.email', n => `user${n}@test.com`), 10 | encrypted_password: bcrypt.hashSync('123', config.auth.bcryptSalt), 11 | }); 12 | 13 | export default factory; 14 | -------------------------------------------------------------------------------- /src/app/views/extras.pug: -------------------------------------------------------------------------------- 1 | extends layout/base 2 | 3 | block head 4 | title= 'extras' 5 | link(rel='stylesheet', href='/css/extras.css') 6 | 7 | block body 8 | div#wrapper 9 | h1= 'extras' 10 | p= message 11 | p this is just another example page. 12 | if base 13 | p= 'navigate to /extras/ and see what happens!' 14 | else 15 | p return to 16 | a(href='/extras')= 'extras' 17 | 18 | block scripts 19 | script(src='/js/fadeIn.js') 20 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "es2016", "es2017"], 3 | "env": { 4 | "development": { 5 | "plugins": [ 6 | ["module-resolver", { 7 | "root": ["./src/app", "./src/lib", "./src/plugins", "./src", "./test"], 8 | }], 9 | [ 10 | "transform-runtime", { 11 | "polyfill": true, 12 | "regenerator": true, 13 | }, 14 | ], 15 | "transform-object-rest-spread", 16 | ] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tools/gulp/tasks/main.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import plugins from 'gulp-load-plugins'; 3 | import { DEST } from '../config'; 4 | 5 | const $ = plugins({ 6 | pattern: ['gulp-*', 'main-bower-files'], 7 | }); 8 | 9 | // default task is the same as serve 10 | gulp.task('default', ['serve']); 11 | 12 | gulp.task('copy', () => 13 | gulp.src('pm2.config.js') 14 | .pipe(gulp.dest(DEST)) 15 | .pipe($.print(fp => `copy: ${fp}`)) 16 | ); 17 | 18 | // show all tasks 19 | gulp.task('tasks', $.taskListing); 20 | -------------------------------------------------------------------------------- /src/database/seeds/helpers/SeedRunner.js: -------------------------------------------------------------------------------- 1 | import Logger from 'Logger'; 2 | 3 | export default class SeedRunner { 4 | addSeeds(ComponentClasses) { 5 | this.ComponentClasses = ComponentClasses; 6 | this.logger = new Logger('SeedRunner'); 7 | } 8 | 9 | async run() { 10 | const ComponentClass = this.ComponentClasses.shift(); 11 | 12 | if (ComponentClass) { 13 | this.logger.debug(`Run seed: ${ComponentClass.name}`); 14 | await new ComponentClass().run(); 15 | await this.run(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'gocool'; 2 | 3 | import ApplicationController from 'controllers/ApplicationController'; 4 | import UsersController from 'controllers/api/UsersController'; 5 | 6 | const router = new Router(); 7 | 8 | router.route('get', '/', ApplicationController, 'index'); 9 | 10 | router.route('get', '/api/users/current', UsersController, 'currentUser'); 11 | router.route('post', '/api/users/login', UsersController, 'login'); 12 | router.resource('/api/users', UsersController); 13 | 14 | export default router.expressRouter; 15 | -------------------------------------------------------------------------------- /src/plugins/gocool-github-demo-plugin/lib/routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'gocool'; 2 | 3 | import ApplicationController from './controllers/ApplicationController'; 4 | import GithubController from './controllers/api/GithubController'; 5 | 6 | const router = new Router(); 7 | 8 | router.route('get', '/api/closed_issues', GithubController, 'closedIssues'); 9 | 10 | router.route('get', '/', ApplicationController, 'index'); 11 | router.route('get', '/closed_issues', ApplicationController, 'closedIssues'); 12 | 13 | export default [router.expressRouter]; 14 | -------------------------------------------------------------------------------- /tools/gulp/tasks/serve.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import path from 'path'; 3 | import plugins from 'gulp-load-plugins'; 4 | import { DEST } from '../config'; 5 | 6 | const $ = plugins({ 7 | pattern: ['gulp-*', 'main-bower-files'], 8 | }); 9 | 10 | // start express server and reload when server-side files change 11 | gulp.task('serve', ['watch'], () => 12 | $.nodemon({ 13 | exec: 'node-inspector & node --debug', 14 | script: path.join(DEST, 'server.js'), 15 | watch: path.join(DEST, '**/*.js'), 16 | ignore: path.join(DEST, 'static'), 17 | }) 18 | ); 19 | -------------------------------------------------------------------------------- /src/database/seeds/components/UsersSeeder.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import Seeder from './Seeder'; 3 | import users from '../data/users'; 4 | 5 | export default class UsersSeeder extends Seeder { 6 | run() { 7 | const promises = users.map((user) => { 8 | user = Object.assign(user, { 9 | created_at: new Date(), 10 | updated_at: new Date(), 11 | }); 12 | 13 | user = _.omitBy(user, (value, key) => _.startsWith(key, 'join')); 14 | return this.knex('users').insert(user); 15 | }); 16 | 17 | return Promise.all(promises); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/assets/styles/main.scss: -------------------------------------------------------------------------------- 1 | // fonts 2 | @import url('/fonts/Raleway-Regular.css'); 3 | 4 | // variables 5 | $font-fancy: Raleway-Regular; 6 | $font-base: "Courier New", Courier, monospace; 7 | $base-color: rgba(189, 195, 199, 1); 8 | 9 | // semi-sensible defaults 10 | html { 11 | font-size: 62.5%; 12 | height: 100%; 13 | width: 100%; 14 | margin: 0; 15 | padding: 0; 16 | background-color: $base-color; 17 | } 18 | 19 | body { 20 | font-size: 1.4rem; 21 | font-family: $font-base; 22 | padding: 3rem; 23 | } 24 | 25 | h1, h2, h3, h4, h5, h6 { 26 | font-family: $font-fancy; 27 | } 28 | -------------------------------------------------------------------------------- /src/plugins/gocool-github-demo-plugin/lib/controllers/ApplicationController.js: -------------------------------------------------------------------------------- 1 | import { Controller } from 'gocool'; 2 | import GithubService from '../services/GithubService'; 3 | 4 | export default class GithubController extends Controller { 5 | async index() { 6 | this.res.render('./github-demo/index', { 7 | title: 'Github', 8 | }); 9 | } 10 | 11 | async closedIssues() { 12 | const issues = await GithubService.getClosedIssues(); 13 | 14 | this.res.render('./github-demo/closed-issues', { 15 | title: 'Github Closed Issues', 16 | issues, 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tools/gulp/tasks/watch.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import { TRANSPILE, CLIENT } from '../config'; 3 | 4 | gulp.task('watch', ['build'], () => { 5 | TRANSPILE.forEach((task) => { 6 | gulp.watch(task.src, [`transpile:${task.name}`]); 7 | }); 8 | 9 | CLIENT.forEach((task) => { 10 | // add some delay for images 11 | if (task.name === 'images') { 12 | gulp.watch(task.src, { 13 | debounceDelay: 2500, 14 | }, ['images']); 15 | } else gulp.watch(task.src, [task.name]); 16 | }); 17 | 18 | // also lint this gulpfile on save 19 | gulp.watch('gulpfile.babel.js', ['lint:gulpfile']); 20 | }); 21 | -------------------------------------------------------------------------------- /src/database/drop.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | require('babel-register'); 4 | require('dotenv').config(); 5 | 6 | const dbConfig = require('../config/knexConfig'); 7 | 8 | const environment = process.env.NODE_ENV || 'development'; 9 | const config = dbConfig[environment]; 10 | 11 | const database = config.connection.database; 12 | config.connection.database = null; 13 | 14 | const knex = require('knex')(config); 15 | // connect without database selected 16 | 17 | knex.raw(`DROP DATABASE ${database}`) 18 | .catch((err) => { 19 | console.log(err); 20 | }) 21 | .finally(() => { 22 | knex.destroy(); 23 | }); 24 | -------------------------------------------------------------------------------- /src/database/index.js: -------------------------------------------------------------------------------- 1 | import Bookshelf from 'bookshelf'; 2 | import modelBase from 'bookshelf-modelbase'; 3 | import Schema from 'bookshelf-schema'; 4 | import knex from 'knex'; 5 | 6 | import dbConfig from 'config/knexConfig'; 7 | 8 | const environment = process.env.NODE_ENV || 'development'; 9 | const config = dbConfig[environment]; 10 | const connection = knex(config); 11 | const _bookshelf = Bookshelf(connection); 12 | 13 | _bookshelf.plugin('pagination'); 14 | _bookshelf.plugin(Schema()); 15 | _bookshelf.plugin(modelBase.pluggable); 16 | 17 | export const bookshelf = _bookshelf; 18 | export const ModelBase = modelBase(bookshelf); 19 | -------------------------------------------------------------------------------- /src/database/create.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | require('babel-register'); 4 | require('dotenv').config(); 5 | 6 | const dbConfig = require('../config/knexConfig'); 7 | 8 | const environment = process.env.NODE_ENV || 'development'; 9 | const config = dbConfig[environment]; 10 | 11 | const database = config.connection.database; 12 | config.connection.database = null; 13 | 14 | const knex = require('knex')(config); 15 | // connect without database selected 16 | 17 | knex.raw(`CREATE DATABASE ${database}`) 18 | .catch((err) => { 19 | console.log(err); 20 | }) 21 | .finally(() => { 22 | knex.destroy(); 23 | }); 24 | -------------------------------------------------------------------------------- /src/lib/gocool/test/lib/Server.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import path from 'path'; 3 | import Server from '../../lib/Server'; 4 | 5 | describe('Test Server', () => { 6 | describe('constructor()', () => { 7 | const config = {}; 8 | 9 | before(() => { 10 | // path to root directory of this app 11 | const rootPath = path.normalize(__dirname); 12 | 13 | config.path = { 14 | view: path.join(rootPath, 'app/views'), 15 | static: path.join(rootPath, 'static'), 16 | }; 17 | }); 18 | 19 | it('should create a object', async () => { 20 | const obj = new Server({ 21 | config, 22 | }); 23 | expect(obj).to.be.defined; 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tools/gulp/tasks/linter.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import plugins from 'gulp-load-plugins'; 3 | import { LINT } from '../config'; 4 | 5 | const $ = plugins({ 6 | pattern: ['gulp-*', 'main-bower-files'], 7 | }); 8 | 9 | // returns a function that lints the files in src 10 | const lintTask = src => 11 | () => 12 | gulp.src(src) 13 | .pipe($.eslint()) 14 | .pipe($.eslint.formatEach()) 15 | .pipe($.eslint.failAfterError()); 16 | 17 | // create lint tasks for client and server scripts 18 | LINT.forEach(task => gulp.task(`lint:${task.name}`, lintTask(task.src))); 19 | 20 | // lint this gulpfile 21 | gulp.task('lint:gulpfile', lintTask('gulpfile.babel.js')); 22 | 23 | // lint everything! 24 | gulp.task('lint', [...LINT].map(el => `lint:${el}`).concat('lint:gulpfile')); 25 | -------------------------------------------------------------------------------- /src/lib/gocool/lib/Controller.js: -------------------------------------------------------------------------------- 1 | export default class Controller { 2 | 3 | static action(name) { 4 | const controller = new this(); 5 | 6 | return { 7 | action: (req, res, next) => { 8 | controller.req = req; 9 | controller.res = res; 10 | controller.next = next; 11 | controller[name].call(controller); 12 | }, 13 | middlewares: (controller.beforeMiddlewares || {})[name], 14 | }; 15 | } 16 | 17 | before(actionName, middlewares) { 18 | this.beforeMiddlewares = this.beforeMiddlewares || {}; 19 | this.beforeMiddlewares[actionName] = this.beforeMiddlewares[actionName] || []; 20 | 21 | if (middlewares.slice) { 22 | this.beforeMiddlewares[actionName].concat(middlewares); 23 | } else { 24 | this.beforeMiddlewares[actionName].push(middlewares); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/plugins/gocool-github-demo-plugin/lib/services/GithubService.js: -------------------------------------------------------------------------------- 1 | import Logger from 'Logger'; 2 | import GithubApi from 'github'; 3 | 4 | const github = new GithubApi(); 5 | const logger = new Logger('GithubService'); 6 | 7 | export const GITHUB_API_BASE_URL = 'https://api.github.com'; 8 | export const GITHUB_API_ENDPOINTS = { 9 | closedIssues: (owner, repo) => `/repos/${owner}/${repo}/issues?state=closed`, 10 | }; 11 | 12 | export default class GithubService { 13 | static async getClosedIssues() { 14 | logger.debug('start getting closed issues'); 15 | const response = await github.issues.getForRepo({ 16 | owner: 'imheretw', 17 | repo: 'imhere', 18 | state: 'closed', 19 | }); 20 | 21 | const issues = response.data; 22 | 23 | logger.debug('issues', JSON.stringify(issues)); 24 | 25 | return issues; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/plugins/gocool-github-demo-plugin/test/services/GithubService.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import GithubService, { GITHUB_API_ENDPOINTS } from '../../lib/services/GithubService'; 3 | import httpMocks from '../helpers/httpMocks'; 4 | import githubClosedIssueData from '../data/github/issues/closed.json'; 5 | 6 | describe('Test GithubService', () => { 7 | describe('getClosedIssues()', () => { 8 | it('should return closed issues', async () => { 9 | const [owner, repo] = ['imheretw', 'imhere']; 10 | 11 | httpMocks.githubHttpMock 12 | .get(GITHUB_API_ENDPOINTS.closedIssues(owner, repo)) 13 | .reply(200, githubClosedIssueData); 14 | 15 | const issues = await GithubService.getClosedIssues(); 16 | const issue = issues[0]; 17 | 18 | expect(issues.length).be.gt(1); 19 | expect(issue.title).be.eq('Kue screenshot'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/app/models/user.spec.js: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt'; 2 | import { expect } from 'chai'; 3 | import User from 'models/user'; 4 | import config from 'config/appConfig'; 5 | 6 | describe('Test User Model', () => { 7 | describe('validatePassword', () => { 8 | let user; 9 | 10 | before(() => { 11 | const cryptedPassword = bcrypt.hashSync('123', config.auth.bcryptSalt); 12 | user = new User({ 13 | encrypted_password: cryptedPassword, 14 | }); 15 | }); 16 | 17 | it('should validate correct password', (done) => { 18 | user.validatePassword('123', (error, result) => { 19 | expect(error).be.falsy; 20 | done(); 21 | }); 22 | }); 23 | 24 | it('should fail to validate correct password', (done) => { 25 | user.validatePassword('111', (error, result) => { 26 | expect(error).be.not.null; 27 | done(); 28 | }); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /bin/after_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ## for CI after test hook 3 | set -e 4 | 5 | # CI_BRANCH="issue-10-testing" 6 | # CI_BRANCH="release-1.0.0" 7 | # CI_BRANCH="master" 8 | 9 | function checkWIP () { 10 | msg=`git log -1 --pretty=%B` 11 | # To compare for case insensitive 12 | shopt -s nocasematch 13 | 14 | case $msg in 15 | savepoint ) 16 | echo "SAVEPOINT commit, do nothing" 17 | exit; 18 | ;; 19 | WIP ) 20 | echo "WIP commit, do nothing" 21 | exit; 22 | ;; 23 | esac 24 | } 25 | 26 | function deploy() { 27 | case $CI_BRANCH in 28 | master ) 29 | echo "cap staging deploy" 30 | cap staging deploy 31 | ;; 32 | issue-* ) 33 | echo "cap dev deploy" 34 | cap dev deploy 35 | ;; 36 | * ) 37 | echo "do nothing" 38 | ;; 39 | esac 40 | } 41 | 42 | checkWIP 43 | deploy 44 | 45 | RESULT=$? 46 | 47 | [ $RESULT -ne 0 ] && exit 1 48 | exit 0 49 | -------------------------------------------------------------------------------- /tools/gulp/tasks/transpile.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import plugins from 'gulp-load-plugins'; 3 | import { TRANSPILE } from '../config'; 4 | 5 | const $ = plugins({ 6 | pattern: ['gulp-*', 'main-bower-files'], 7 | }); 8 | 9 | // create transpile tasks for server scripts 10 | TRANSPILE.forEach((task) => { 11 | gulp.task(`transpile:${task.name}`, [`lint:${task.name}`], () => 12 | gulp.src(task.src) 13 | .pipe($.changed(task.dest)) 14 | .pipe($.babel({ 15 | presets: ['es2015', 'es2016', 'es2017'], 16 | plugins: [ 17 | [ 18 | 'transform-runtime', { 19 | polyfill: false, 20 | regenerator: true, 21 | }, 22 | ], 23 | ], 24 | })) 25 | .pipe(gulp.dest(task.dest)) 26 | .pipe($.print(fp => `transpiled: ${fp}`)) 27 | ); 28 | }); 29 | 30 | // transpile everything! 31 | gulp.task('transpile', [...TRANSPILE].map(el => `transpile:${el.name}`)); 32 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import dotenv from 'dotenv'; 3 | import kue from 'kue'; 4 | 5 | import { Server } from 'gocool'; 6 | 7 | import JobHandler from 'app/boots/JobHandler'; 8 | import PassportHandler from 'app/boots/PassportHandler'; 9 | import config from 'config/appConfig'; 10 | import routes from 'app/routes'; 11 | 12 | import GithubDemoPlugin from 'gocool-github-demo-plugin'; 13 | 14 | dotenv.config(); 15 | 16 | // path to root directory of this app 17 | const rootPath = path.normalize(__dirname); 18 | 19 | config.path = { 20 | view: path.join(rootPath, 'app/views'), 21 | static: path.join(rootPath, 'static'), 22 | }; 23 | 24 | const server = new Server({ config }); 25 | 26 | server 27 | .setRoutes(routes) 28 | .addHandlers([ 29 | new JobHandler(), 30 | new PassportHandler(), 31 | ]) 32 | .addPlugin('/github-demo', GithubDemoPlugin) 33 | .addExpressPlugins([ 34 | { path: '/kue', content: kue.app }, 35 | ]) 36 | .start(); 37 | 38 | export default server; 39 | -------------------------------------------------------------------------------- /src/app/middlewares/jwtMiddleware.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import User from 'models/user'; 3 | import config from 'config/appConfig'; 4 | 5 | export default function jwtMiddleware(req, res, next) { 6 | const token = req.headers.authorization; 7 | if (!token) { 8 | res.status(401).send({ error: 'require jwt token' }); 9 | return; 10 | } 11 | 12 | jwt.verify(token, config.auth.jwt, (err, user) => { 13 | if (err) { 14 | res.status(401).json({ error: 'invalid jwt token' }); 15 | return; 16 | } 17 | 18 | // load user into req.user 19 | User 20 | .query({ where: { id: user.id } }) 21 | .fetch() 22 | .then((userModel) => { 23 | if (!userModel) { 24 | res.status(401).send({ error: 'invalid jwt token' }); 25 | return; 26 | } 27 | 28 | req.user = userModel; 29 | next(); 30 | }) 31 | .catch((error) => { 32 | res.status(500).send({ error }); 33 | }); 34 | }); // end of jwt verify 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/Logger.js: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | 3 | export default class Logger { 4 | constructor(prefix) { 5 | this._prefix = prefix; 6 | this._logger = new winston.Logger({ 7 | transports: [ 8 | new winston.transports.Console({ 9 | level: process.env.DEBUG_LEVEL, 10 | timestamp: true, 11 | stderrLevels: ['error'], 12 | colorize: true, 13 | }), 14 | new (winston.transports.File)({ 15 | name: 'kidguard.debug.log', 16 | filename: 'logs/debug.log', 17 | humanReadableUnhandledException: true, 18 | json: false, 19 | level: 'debug', 20 | }), 21 | ], 22 | }); 23 | 24 | ['debug', 'verbose', 'info', 'warn', 'error'].forEach((level) => { 25 | this[level] = (...args) => this._log('debug', args); 26 | }); 27 | } 28 | 29 | _log(level, args) { 30 | const newArgs = this._appendPrefix(args); 31 | this._logger[level](...newArgs); 32 | } 33 | 34 | _appendPrefix(args) { 35 | return this._prefix ? [`${this._prefix}:`, ...args] : [...args]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/boots/JobHandler.js: -------------------------------------------------------------------------------- 1 | import kue from 'kue'; 2 | import Logable from 'Logable'; 3 | import Jobs from 'jobs/index'; 4 | import kueConfig from 'config/kueConfig'; 5 | 6 | export default class JobHandler extends Logable { 7 | constructor() { 8 | super(); 9 | 10 | this.jobs = Jobs.map(Job => new Job()); 11 | this.queue = kue.createQueue(kueConfig); 12 | 13 | this.logger.info(`${this.constructor.name} created`); 14 | } 15 | 16 | start() { 17 | this.jobs.forEach((job) => { 18 | this.queue.process(job.JOB_NAME, job.CUNCURRENCY, async (jobInstance, done) => { 19 | try { 20 | this.logger.debug(`job ${jobInstance.id} in queue ${job.JOB_NAME} is processing now.`); 21 | 22 | await job.run(jobInstance); 23 | done(); 24 | } catch (error) { 25 | this.logger.error(`Error when runing job of ${job.constructor.name}:`, error); 26 | done(error); 27 | } 28 | }); 29 | 30 | this.logger.debug(`${job.constructor.name} registered.`); 31 | }); 32 | 33 | this.logger.info(`start ${this.constructor.name}`); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tools/gulp/tasks/bower.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import _ from 'lodash'; 3 | import plugins from 'gulp-load-plugins'; 4 | import { PATHS, CLIENT } from '../config'; 5 | 6 | const $ = plugins({ 7 | pattern: ['gulp-*', 'main-bower-files'], 8 | }); 9 | 10 | // move bower files to destination directory 11 | gulp.task('bower', ['bower:js', 'bower:css']); 12 | 13 | // concatenate and minify bower js 14 | gulp.task('bower:js', () => 15 | gulp.src($.mainBowerFiles()) 16 | .pipe($.filter('**/*.js')) 17 | .pipe($.print(fp => `bower: ${fp}`)) // ensure concat order 18 | .pipe($.concat('vendor.js')) 19 | .pipe(gulp.dest(PATHS.scripts.dest)) 20 | .pipe($.print(fp => `bower: ${fp}`)) 21 | .pipe($.cache($.uglify())) 22 | .pipe($.rename('vendor.min.js')) 23 | .pipe(gulp.dest(PATHS.scripts.dest)) 24 | .pipe($.print(fp => `bower: ${fp}`)) 25 | ); 26 | 27 | // move bower css 28 | gulp.task('bower:css', () => 29 | gulp.src($.mainBowerFiles()) 30 | .pipe($.changed(_.find(CLIENT, { name: 'styles' }).dest)) 31 | .pipe($.filter('**/*.css')) 32 | .pipe(gulp.dest(_.find(CLIENT, { name: 'styles' }).dest)) 33 | .pipe($.print(fp => `bower: ${fp}`)) 34 | ); 35 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | airbrussh (1.1.2) 5 | sshkit (>= 1.6.1, != 1.7.0) 6 | capistrano (3.8.0) 7 | airbrussh (>= 1.0.0) 8 | i18n 9 | rake (>= 10.0.0) 10 | sshkit (>= 1.9.0) 11 | capistrano-bower (1.1.0) 12 | capistrano (~> 3.0) 13 | capistrano-env-config (0.3.0) 14 | capistrano (~> 3.0) 15 | dotenv (~> 2.0) 16 | capistrano-gulp (0.0.2) 17 | capistrano (~> 3.0) 18 | capistrano-locally (0.2.4) 19 | capistrano (~> 3.0) 20 | capistrano-nvm (0.0.6) 21 | capistrano (~> 3.1) 22 | capistrano-yarn (2.0.2) 23 | capistrano (~> 3.0) 24 | capistrano_pm2 (0.0.4) 25 | capistrano (~> 3.0, >= 3.0.0) 26 | dotenv (2.2.0) 27 | i18n (0.8.1) 28 | net-scp (1.2.1) 29 | net-ssh (>= 2.6.5) 30 | net-ssh (4.1.0) 31 | rake (12.0.0) 32 | sshkit (1.13.1) 33 | net-scp (>= 1.1.2) 34 | net-ssh (>= 2.8.0) 35 | 36 | PLATFORMS 37 | ruby 38 | 39 | DEPENDENCIES 40 | capistrano (= 3.8.0) 41 | capistrano-bower 42 | capistrano-env-config 43 | capistrano-gulp 44 | capistrano-locally 45 | capistrano-nvm 46 | capistrano-yarn 47 | capistrano_pm2 48 | 49 | BUNDLED WITH 50 | 1.12.5 51 | -------------------------------------------------------------------------------- /src/plugins/gocool-github-demo-plugin/test/controllers/githubController.spec.js: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import chaiHttp from 'chai-http'; 3 | import server from 'server'; // TODO: need to fix dependency 4 | import { GITHUB_API_ENDPOINTS } from '../../lib/services/GithubService'; 5 | import httpMocks from '../helpers/httpMocks'; 6 | import githubClosedIssueData from '../data/github/issues/closed.json'; 7 | 8 | chai.use(chaiHttp); 9 | 10 | describe('Test github controller', () => { 11 | describe('GET /github-demo/api/closed_issues', () => { 12 | it('should return closed issues', async () => { 13 | const [owner, repo] = ['imheretw', 'imhere']; 14 | 15 | httpMocks.githubHttpMock 16 | .get(GITHUB_API_ENDPOINTS.closedIssues(owner, repo)) 17 | .reply(200, githubClosedIssueData); 18 | 19 | const res = await chai.request(server.expressServer) 20 | .get('/github-demo/api/closed_issues'); 21 | 22 | const issues = res.body.issues; 23 | expect(res.status).to.equal(200); 24 | expect(res).to.be.json; 25 | expect(issues).to.a('array'); 26 | expect(issues.length).to.gt(1); 27 | expect(issues[0]).to.have.property('title', 'Kue screenshot'); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "plugin:mocha/recommended", "airbnb"], 3 | "env": { 4 | "browser": true, 5 | "es6": true, 6 | "jquery": true, 7 | "node": true, 8 | "mocha": true 9 | }, 10 | "parser": "babel-eslint", 11 | "parserOptions": { 12 | "ecmaVersion": 6, 13 | "sourceType": "module" 14 | }, 15 | "rules": { 16 | "arrow-body-style": [2, "as-needed"], 17 | "class-methods-use-this": "off", 18 | "eqeqeq": 2, 19 | "no-console": 1, 20 | "no-param-reassign": "off", 21 | "no-undef-init": 2, 22 | "no-undefined": 2, 23 | "no-unused-vars": [1, { 24 | "vars": "all", 25 | "args": "none" 26 | }], 27 | "no-use-before-define": "off", 28 | "no-useless-constructor": 2, 29 | "no-underscore-dangle": "off", 30 | "semi": [2, "always"], 31 | "comma-dangle": [1, "always-multiline"], 32 | "import/no-unresolved": "off", 33 | "import/extensions": "off", 34 | "import/no-extraneous-dependencies": "off", 35 | "import/prefer-default-export": 1, 36 | 37 | "no-unused-expressions": 0, 38 | "chai-friendly/no-unused-expressions": 2 39 | }, 40 | "globals": { 41 | "logger": true 42 | }, 43 | "plugins": ["chai-expect", "chai-friendly", "mocha"] 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/gocool/lib/Router.js: -------------------------------------------------------------------------------- 1 | import { Router as ExpressRouter } from 'express'; 2 | import Logger from 'Logger'; 3 | 4 | const logger = new Logger('resource'); 5 | 6 | export default class Router { 7 | constructor() { 8 | this._router = new ExpressRouter({ mergeParams: true }); 9 | } 10 | 11 | get expressRouter() { 12 | return this._router; 13 | } 14 | 15 | resource(path, controller) { 16 | const settings = [ 17 | { method: 'get', action: 'index', url: '/' }, 18 | { method: 'get', action: 'show', url: '/:id' }, 19 | { method: 'post', action: 'store', url: '/' }, 20 | { method: 'delete', action: 'delete', url: '/' }, 21 | { method: 'put', action: 'update', url: '/update' }, 22 | { method: 'patch', action: 'update', url: '/update' }, 23 | ]; 24 | 25 | settings.forEach((setting) => { 26 | logger.debug('register route:', setting.method, `${path}${setting.url}`, controller.name, setting.action); 27 | this.route(setting.method, `${path}${setting.url}`, controller, setting.action); 28 | }); 29 | } 30 | 31 | route(method, path, controller, actionName) { 32 | const func = this._router[method]; 33 | const { action, middlewares } = controller.action(actionName); 34 | func.call(this._router, path, middlewares || [], action); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/app/jobs/MyJob.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import sinon from 'sinon'; 3 | 4 | import MyJob from 'jobs/MyJob'; 5 | 6 | describe('Test MyJob', () => { 7 | let sandbox; 8 | let myJob; 9 | 10 | const dummy = () => {}; 11 | 12 | beforeEach(async () => { 13 | sandbox = sinon.sandbox.create(); 14 | myJob = new MyJob(); 15 | myJob.queue.testMode.enter(); 16 | }); 17 | 18 | afterEach(() => { 19 | myJob.queue.testMode.clear(); 20 | sandbox.restore(); 21 | }); 22 | 23 | describe('constructor', () => { 24 | it('should create a queue', async () => { 25 | expect(myJob.queue).be.not.undefined; 26 | }); 27 | }); 28 | 29 | describe('when calling run', () => { 30 | it('should call _log', async () => { 31 | const stub = sandbox.stub(myJob, '_log') 32 | .returns({ 33 | then: dummy, 34 | }); 35 | myJob.run(); 36 | 37 | expect(stub.calledOnce).be.true; 38 | }); 39 | }); 40 | 41 | describe('when calling _log', () => { 42 | it('should calling logger.debug', async () => { 43 | const stub = sandbox.stub(myJob.logger, 'debug') 44 | .returns({ 45 | then: dummy, 46 | }); 47 | 48 | myJob._log(dummy); 49 | 50 | expect(stub.calledOnce).be.true; 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | # General 2 | NODE_ENV=development 3 | BABEL_ENV=development 4 | 5 | # pm2 6 | APP_NAME=imhere 7 | APP_PATH='/opt/www/imhere/current/build' 8 | 9 | # database 10 | DB_DEVELOPMEMT_HOST=localhost 11 | DB_DEVELOPMEMT_DATABASE=imhere 12 | DB_DEVELOPMEMT_USER=root 13 | DB_DEVELOPMEMT_PASSWORD=mysql 14 | 15 | DB_TEST_HOST=localhost 16 | DB_TEST_DATABASE=imhere_test 17 | DB_TEST_USER=root 18 | DB_TEST_PASSWORD=mysql 19 | 20 | # server 21 | PORT=5000 22 | 23 | # debugging & logging 24 | DEBUG_LEVEL=debug 25 | DEBUG_FD=1 26 | DEBUG=knex:query,knex:bindings 27 | 28 | # encryption 29 | JWT_SECRET="Imhere" 30 | BCRYPT_SALT=$2a$06$NkYh0RCM8pNWPaYvRLgN9.LbJw4gcnWCOQYIom0P08UEZRQQjbfpy 31 | 32 | # redis 33 | REDIS_HOST=127.0.0.1 34 | REDIS_PORT=6379 35 | REDIS_PASSWORD= 36 | REDIS_DB=1 37 | 38 | # Kue 39 | KUE_PREFIX=local 40 | 41 | # deployment 42 | APPLICATION=imhere 43 | REPO=git@github.com:imheretw/imhere.git 44 | DEPLOY_TO=/opt/www/imhere 45 | 46 | DEV_HOST=dev.test.com 47 | DEV_PEM=dev.pem 48 | DEV_USER=ubuntu 49 | DEV_DEPLOY_TO=/opt/www/imhere 50 | 51 | STAGING_HOST=staging.test.com 52 | STAGING_PEM=staging.pem 53 | STAGING_USER=ubuntu 54 | STAGING_DEPLOY_TO=/opt/www/imhere 55 | 56 | PRODUCTION_HOST=production.test.com 57 | PRODUCTION_PEM=production.pem 58 | PRODUCTION_USER=ubuntu 59 | PRODUCTION_DEPLOY_TO=/opt/www/imhere 60 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/imhere 5 | docker: 6 | - image: node:6.2.2 7 | cmd: ["/bin/bash"] 8 | - image: redis:3.0 9 | 10 | steps: 11 | - run: 12 | name: Install System Libs 13 | command: | 14 | apt-key adv --fetch-keys http://dl.yarnpkg.com/debian/pubkey.gpg 15 | echo "deb http://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list 16 | apt -qq update 17 | apt install -y -qq git yarn bcrypt 18 | 19 | - checkout 20 | 21 | - restore_cache: 22 | key: dependency-cache-yarn 23 | 24 | - run: 25 | name: Install Dependencies 26 | command: | 27 | yarn install 28 | cp .env.sample .env 29 | 30 | - save_cache: 31 | key: dependency-cache-yarn 32 | paths: 33 | - ~/.cache/yarn 34 | 35 | - run: 36 | name: Run eslint 37 | command: | 38 | yarn lint 39 | 40 | - run: 41 | name: Run Tests 42 | command: | 43 | yarn test:debug 44 | 45 | - store_artifacts: 46 | path: ~/imhere/coverage 47 | destination: prefix 48 | 49 | - store_test_results: 50 | path: ~/imhere/coverage 51 | -------------------------------------------------------------------------------- /src/app/models/user.js: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt'; 2 | 3 | import Logger from 'Logger'; 4 | import config from 'config/appConfig'; 5 | import { ModelBase } from 'database'; 6 | 7 | export const TYPE_DEMO_USER = 'DemoUser'; 8 | export const TYPE_LIMITED_ACCESS_USER = 'LimitedAccessUser'; 9 | export const FULL_ACCESS_USER = 'FullAccessUser'; 10 | 11 | const logger = new Logger('User'); 12 | 13 | const User = ModelBase.extend({ 14 | tableName: 'users', 15 | hasTimestamps: true, 16 | 17 | validatePassword(candidatePassword, cb) { 18 | const cryptedPassword = bcrypt.hashSync(candidatePassword, config.auth.bcryptSalt); 19 | if (cryptedPassword === this.get('encrypted_password')) { 20 | cb(null, true); 21 | } else { 22 | cb('password is invalid'); 23 | } 24 | }, 25 | }); 26 | 27 | User.findOrCreate = function findOrCreate(attributes, transacting) { 28 | return User 29 | .findOne({ email: attributes.email }, { transacting }) 30 | .then((user) => { 31 | logger.debug(attributes.email); 32 | logger.debug(`user found by email ${attributes.email}`); 33 | return user; 34 | }) 35 | .catch((err) => { 36 | logger.debug(`user does not exist by email ${attributes.email}`); 37 | return User.forge(attributes).save(null, { transacting }); 38 | }); 39 | }; 40 | 41 | export default User; 42 | -------------------------------------------------------------------------------- /Capfile: -------------------------------------------------------------------------------- 1 | # Load DSL and set up stages 2 | # default deploy_config_path is 'config/deploy.rb' 3 | set :deploy_config_path, 'tools/capistrano/deploy.rb' 4 | set :stage_config_path, 'tools/capistrano/stages' 5 | 6 | # Load DSL and set up stages 7 | require 'capistrano/setup' 8 | 9 | # Include default deployment tasks 10 | require 'capistrano/deploy' 11 | 12 | # Include tasks from other gems included in your Gemfile 13 | # 14 | # For documentation on these, see for example: 15 | # 16 | # https://github.com/capistrano/rvm 17 | # https://github.com/capistrano/rbenv 18 | # https://github.com/capistrano/chruby 19 | # https://github.com/capistrano/bundler 20 | # https://github.com/capistrano/rails 21 | # https://github.com/capistrano/passenger 22 | # 23 | # require 'capistrano/rvm' 24 | # require 'capistrano/rbenv' 25 | # require 'capistrano/chruby' 26 | # require 'capistrano/bundler' 27 | # require 'capistrano/rails/assets' 28 | # require 'capistrano/rails/migrations' 29 | # require 'capistrano/passenger' 30 | require 'capistrano/env-config' 31 | require 'capistrano/nvm' 32 | require 'capistrano/yarn' 33 | require 'capistrano/bower' 34 | require 'capistrano/gulp' 35 | require 'capistrano/pm2' 36 | require 'capistrano/locally' 37 | 38 | # require 'net/ssh/proxy/command' # live deploy use proxy 39 | # require 'cap-ec2/capistrano' 40 | 41 | # default deploy_config_path is 'config/deploy.rb' 42 | set :deploy_config_path, 'tools/capistrano/deploy.rb' 43 | set :stage_config_path, 'tools/capistrano/stages' 44 | 45 | # Load custom tasks from `lib/capistrano/tasks` if you have any defined 46 | Dir.glob('tools/capistrano/tasks/*.rb').each { |r| import r } 47 | -------------------------------------------------------------------------------- /src/config/knexConfig.js: -------------------------------------------------------------------------------- 1 | // Please enable innodatabase_large_prefix config if your mysql version is under 5.7.7 2 | // This application required larger mysql index length for store emoji characters. 3 | 4 | // Update with your config settings. 5 | require('babel-register'); 6 | require('dotenv').config(); 7 | 8 | module.exports = { 9 | development: { 10 | client: 'mysql2', 11 | connection: { 12 | host: process.env.DB_DEVELOPMEMT_HOST, 13 | database: process.env.DB_DEVELOPMEMT_DATABASE, 14 | user: process.env.DB_DEVELOPMEMT_USER, 15 | password: process.env.DB_DEVELOPMEMT_PASSWORD, 16 | charset: 'utf8mb4', 17 | }, 18 | debug: false, 19 | seeds: { 20 | directory: './src/database/seeds/development', 21 | }, 22 | migrations: { 23 | directory: './src//database/migrations', 24 | }, 25 | }, 26 | test: { 27 | client: 'sqlite3', 28 | connection: ':memory:', 29 | useNullAsDefault: true, 30 | debug: false, 31 | seeds: { 32 | directory: './src/database/seeds/test', 33 | }, 34 | migrations: { 35 | directory: './src/database/migrations', 36 | }, 37 | }, 38 | test_mysql: { 39 | client: 'mysql2', 40 | connection: { 41 | host: process.env.DB_DEVELOPMEMT_HOST, 42 | database: process.env.DB_DEVELOPMEMT_DATABASE, 43 | user: process.env.DB_DEVELOPMEMT_USER, 44 | password: process.env.DB_DEVELOPMEMT_PASSWORD, 45 | charset: 'utf8mb4', 46 | }, 47 | debug: false, 48 | seeds: { 49 | directory: './src/database/seeds/test', 50 | }, 51 | migrations: { 52 | directory: './src/database/migrations', 53 | }, 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /src/app/jobs/Job.js: -------------------------------------------------------------------------------- 1 | import kue from 'kue'; 2 | import moment from 'moment'; 3 | 4 | import Logable from 'Logable'; 5 | import kueConfig from '../../config/kueConfig'; 6 | 7 | export default class Job extends Logable { 8 | constructor() { 9 | super(); 10 | 11 | // you can customize below attributes of your jobs 12 | this.CUNCURRENCY = 10; 13 | this.ATTEMPTS = 3; 14 | this.PRIORITY = 'medium'; 15 | this.BACKOFF = null; 16 | 17 | this.queue = kue.createQueue(kueConfig); 18 | } 19 | 20 | run(job, done) { 21 | throw new Error('Should implement run method!'); 22 | } 23 | 24 | runImmediate() { 25 | const jobInstance = this._prepareJobData(); 26 | this.run(jobInstance); 27 | } 28 | 29 | runLater(delay) { 30 | return this._addJobToQueue(delay); 31 | } 32 | 33 | runUntil(until) { 34 | const delay = moment(until).diff(new Date()); 35 | return this.runLater(delay); 36 | } 37 | 38 | getPayload() { 39 | return {}; 40 | } 41 | 42 | shutdown() { 43 | this.logger.info('[ Shutting down Kue... ]'); 44 | this.queue.shutdown((err) => { 45 | if (err) { 46 | this.logger.error('[ Failed to shut down Kue. ]'); 47 | } 48 | 49 | this.logger.info('[ Kue is shut down. ]'); 50 | }); 51 | } 52 | 53 | _prepareJobData() { 54 | const payload = this.getPayload(); 55 | const data = { title: this.constructor.name, ...payload }; 56 | 57 | return { data }; 58 | } 59 | 60 | _addJobToQueue(delay = 0) { 61 | return new Promise((resolve, reject) => { 62 | const { data } = this._prepareJobData(); 63 | const job = this.queue.create(this.constructor.name, data) 64 | .attempts(this.ATTEMPTS) 65 | .delay(delay) 66 | .priority(this.PRIORITY) 67 | .backoff(this.BACKOFF) 68 | .save((error) => { 69 | if (error) { 70 | reject(error); 71 | } 72 | 73 | this.logger.info(`dispatch ${this.constructor.name}: ${job.id} with delay: ${delay}`, data); 74 | resolve(job); 75 | }); 76 | }); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [0.2.2](https://github.com/imheretw/imhere/compare/0.1.0...0.2.2) (2017-04-15) 3 | 4 | 5 | ### Bug Fixes 6 | 7 | * **gulp:** fix missing files to transpile ([9171e70](https://github.com/imheretw/imhere/commit/9171e70)) 8 | * **view:** rename *.jade files to *.pug ([fde4e1a](https://github.com/imheretw/imhere/commit/fde4e1a)) 9 | 10 | 11 | ### Features 12 | 13 | * **API:** [GET /api/github/closed_issues] new API ([07d31fd](https://github.com/imheretw/imhere/commit/07d31fd)) 14 | * **command:** new command yarn command:job:my v0.17.6 ([aaba4a0](https://github.com/imheretw/imhere/commit/aaba4a0)) 15 | * **controller:** [ApplicationController] new ([fe33095](https://github.com/imheretw/imhere/commit/fe33095)) 16 | * **job:** add runImmediate(), runLater(), runUntil() ([b511938](https://github.com/imheretw/imhere/commit/b511938)) 17 | * **route:** add resource helper function ([4fc12e5](https://github.com/imheretw/imhere/commit/4fc12e5)) 18 | * install babel-node and change command files extension to js ([d3f285e](https://github.com/imheretw/imhere/commit/d3f285e)) 19 | 20 | 21 | 22 | 23 | # [0.2.0](https://github.com/imheretw/imhere/compare/0.1.0...0.2.0) (2017-04-14) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * **gulp:** fix missing files to transpile ([9171e70](https://github.com/imheretw/imhere/commit/9171e70)) 29 | * **view:** rename *.jade files to *.pug ([fde4e1a](https://github.com/imheretw/imhere/commit/fde4e1a)) 30 | 31 | 32 | ### Features 33 | 34 | * **API:** [GET /api/github/closed_issues] new API ([07d31fd](https://github.com/imheretw/imhere/commit/07d31fd)) 35 | * **command:** new command yarn command:job:my v0.17.6 ([aaba4a0](https://github.com/imheretw/imhere/commit/aaba4a0)) 36 | * **controller:** [ApplicationController] new ([fe33095](https://github.com/imheretw/imhere/commit/fe33095)) 37 | * **job:** add runImmediate(), runLater(), runUntil() ([b511938](https://github.com/imheretw/imhere/commit/b511938)) 38 | * **route:** add resource helper function ([4fc12e5](https://github.com/imheretw/imhere/commit/4fc12e5)) 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/app/boots/PassportHandler.js: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | import { Strategy as LocalStrategy } from 'passport-local'; 3 | 4 | import Logable from 'Logable'; 5 | import User from 'app/models/user'; 6 | 7 | export default class PassportHandler extends Logable { 8 | constructor() { 9 | super(); 10 | 11 | this.logger.info(`${this.constructor.name} created`); 12 | } 13 | 14 | start() { 15 | passport.serializeUser((user, done) => { 16 | done(null, user.id); 17 | }); 18 | 19 | passport.deserializeUser((id, done) => { 20 | User 21 | .forge({ id }).fetch() 22 | .then((user) => { 23 | done(null, user.toJSON()); 24 | }); 25 | }); 26 | 27 | /** 28 | * Sign in using Email and Password. 29 | */ 30 | passport.use(new LocalStrategy({ usernameField: 'email' }, async (email, password, done) => { 31 | const ERROR_MSG = 'Account or password error.'; 32 | const errorCallback = cb => cb(null, false, { msg: ERROR_MSG }); 33 | const user = await User.forge({ email: email.toLowerCase() }).fetch(); 34 | 35 | if (!user) { 36 | errorCallback(done); 37 | return; 38 | } 39 | 40 | // Validate user password 41 | user.validatePassword(password, (err, isValid) => { 42 | if (err) { 43 | errorCallback(done); 44 | return; 45 | } 46 | 47 | // If the password was not valid 48 | if (!isValid) { 49 | errorCallback(done); 50 | return; 51 | } 52 | 53 | done(null, user.toJSON()); 54 | }); 55 | })); 56 | } 57 | } 58 | 59 | /** 60 | * Login Required middleware. 61 | */ 62 | export const isAuthenticated = (req, res, next) => { 63 | if (req.isAuthenticated()) { 64 | return next(); 65 | } 66 | 67 | return res.redirect('/login'); 68 | }; 69 | 70 | /** 71 | * Authorization Required middleware. 72 | */ 73 | export const isAuthorized = (req, res, next) => { 74 | // const provider = req.path.split('/').slice(-1)[0]; 75 | 76 | // if (_.find(req.user.tokens, { kind: provider })) { 77 | // next(); 78 | // } else { 79 | // res.redirect(`/auth/${provider}`); 80 | // } 81 | next(); 82 | }; 83 | -------------------------------------------------------------------------------- /tools/capistrano/deploy.rb: -------------------------------------------------------------------------------- 1 | # config/deploy.rb 2 | 3 | lock '3.8.0' 4 | 5 | # application settings 6 | set :application, ENV['APPLICATION'] 7 | set :repo_url, ENV['REPO'] 8 | set :deploy_to, ENV['DEPLOY_TO'] 9 | set :scm, :git 10 | 11 | # others settings 12 | set :format, :pretty 13 | set :log_level, :debug 14 | set :pty, true 15 | set :keep_releases, 5 16 | set :linked_dirs, %w{node_modules build/conf build/static/attachments build/logs} 17 | set :linked_files, %w(.env) 18 | 19 | # nvm settings 20 | set :nvm_type, :user # or :system, depends on your nvm setup 21 | set :nvm_node, 'v6.10.1' 22 | set :nvm_map_bins, %w{node yarn gulp bower pm2 knex} 23 | set :nvm_node_path, -> { 24 | if fetch(:nvm_type, :user) == :system 25 | '/usr/local/nvm/' 26 | else 27 | "$HOME/.nvm/" 28 | end 29 | } 30 | 31 | # yarn setting 32 | set :yarn_flags, '' # default 33 | set :yarn_roles, :all # default 34 | set :yarn_env_variables, {} # default 35 | 36 | # bower settings 37 | set :bower_flags, '--quiet --config.interactive=false --allow-root' 38 | 39 | # gulp tasks 40 | set :gulp_tasks, 'build' 41 | 42 | # pm2 settings 43 | set :pm2_config, 'build/pm2.config.js' # PM2 config path by default 44 | 45 | namespace :yarn do 46 | desc "build production" 47 | task :build do 48 | on roles fetch(:yarn_roles) do 49 | within fetch(:yarn_target_path, release_path) do 50 | with fetch(:yarn_env_variables, {}) do 51 | execute :yarn, 'build' 52 | end 53 | end 54 | end 55 | end 56 | end 57 | 58 | namespace :knex do 59 | desc 'Runs knex migrate:latest if migrations are set' 60 | task :migrate do 61 | print '[knex:migrate] Run migration' 62 | on roles(:db) do 63 | within current_path do 64 | execute :knex, 'migrate:latest' 65 | end 66 | end 67 | end 68 | 69 | desc 'Runs knex seed:run' 70 | task :seed do 71 | print '[knex:seed] Apply seed' 72 | on roles(:db) do 73 | within current_path do 74 | execute :knex, 'seed:run' 75 | end 76 | end 77 | end 78 | end 79 | 80 | # hooks 81 | namespace :deploy do 82 | before :updated, 'gulp' 83 | after :published, 'pm2:reload' 84 | after :published, 'pm2:dump' 85 | after :published, 'knex:migrate' 86 | end 87 | -------------------------------------------------------------------------------- /src/app/controllers/api/UsersController.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import passport from 'passport'; 3 | import bcrypt from 'bcrypt'; 4 | 5 | import { Controller } from 'gocool'; 6 | import User from 'models/user'; 7 | import jwtMiddleware from 'middlewares/jwtMiddleware'; 8 | import config from 'config/appConfig'; 9 | import Logger from 'Logger'; 10 | 11 | const logger = new Logger('UserController'); 12 | const expiresIn = 60 * 60 * 24 * 30; 13 | 14 | export default class UserController extends Controller { 15 | constructor() { 16 | super(); 17 | // TODO: support this.before(['action-1', 'action-2'], middleware); 18 | this.before('currentUser', jwtMiddleware); 19 | } 20 | 21 | async index() { 22 | const users = await User 23 | .forge() 24 | .orderBy('name') 25 | .fetchPage({ 26 | page: this.req.query.page || 1, 27 | pageSize: this.req.query.size || 50, 28 | }); 29 | 30 | this.res.json({ users, pagination: users.pagination }); 31 | } 32 | 33 | async show() { 34 | const user = await User.findOne({ id: this.req.params.id }); 35 | 36 | this.res.json({ user }); 37 | } 38 | 39 | async store() { 40 | try { 41 | const attributes = { 42 | email: this.req.body.email, 43 | name: this.req.body.name, 44 | encrypted_password: bcrypt.hashSync(this.req.body.password, config.auth.bcryptSalt), 45 | }; 46 | 47 | const userModel = await User.create(attributes); 48 | const userJSON = userModel.toJSON(); 49 | const token = jwt.sign(userJSON, config.auth.jwt, { expiresIn }); 50 | 51 | this.res.status(201).json({ user: userJSON, token }); 52 | } catch (error) { 53 | logger.error('create user error', error); 54 | this.res.status(error.statusCode || 500).json({ error: error.message }); 55 | } 56 | } 57 | 58 | async login() { 59 | passport.authenticate('local', (err, user, info) => { 60 | if (!user) { 61 | return this.res.status(401).json({ error: 'Email or password error.' }); 62 | } 63 | 64 | const token = jwt.sign(user, config.auth.jwt, { expiresIn }); 65 | return this.res.json({ token, user }); 66 | })(this.req, this.res, this.next); 67 | } 68 | 69 | async currentUser() { 70 | this.res.json(this.req.user); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test/app/jobs/Job.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import sinon from 'sinon'; 3 | 4 | import Job from 'jobs/Job'; 5 | 6 | describe('Test Job', () => { 7 | let sandbox; 8 | let job; 9 | 10 | before(() => { 11 | sandbox = sinon.sandbox.create(); 12 | job = new Job(); 13 | job.queue.testMode.enter(); 14 | }); 15 | 16 | afterEach(() => { 17 | job.queue.testMode.clear(); 18 | sandbox.restore(); 19 | }); 20 | 21 | describe('constructor', () => { 22 | it('should create a queue', async () => { 23 | expect(job).to.have.property('CUNCURRENCY'); 24 | expect(job).to.have.property('ATTEMPTS'); 25 | expect(job).to.have.property('PRIORITY'); 26 | expect(job).to.have.property('BACKOFF'); 27 | expect(job).to.have.property('queue'); 28 | }); 29 | }); 30 | 31 | describe('when calling _addJobToQueue', () => { 32 | it('should call queue.create', async () => { 33 | const stub = sandbox.stub(job.queue, 'create').returns({ 34 | save: () => {}, 35 | }); 36 | job._addJobToQueue(); 37 | 38 | expect(stub.calledWith(job.constructor.name)).be.true; 39 | }); 40 | }); 41 | 42 | describe('when calling run', () => { 43 | it('should throw error', async () => { 44 | expect(job.run).be.throw(Error); 45 | }); 46 | }); 47 | 48 | describe('when calling runImmediate', () => { 49 | it('should call run', async () => { 50 | const stub = sandbox.stub(job, 'run'); 51 | job.runImmediate(); 52 | 53 | expect(stub.calledOnce).be.true; 54 | }); 55 | }); 56 | 57 | describe('when calling runLater', () => { 58 | it('should call _addJobToQueue with delay', async () => { 59 | const stub = sandbox.stub(job, '_addJobToQueue'); 60 | const delay = 1000; 61 | job.runLater(delay); 62 | 63 | expect(stub.calledWith(delay)).be.true; 64 | }); 65 | }); 66 | 67 | describe('when calling runUntil', () => { 68 | it('should call runLater', async () => { 69 | const stub = sandbox.stub(job, 'runLater'); 70 | job.runUntil(); 71 | 72 | expect(stub.calledOnce).be.true; 73 | }); 74 | }); 75 | 76 | describe('when calling shutdown', () => { 77 | it('should call queue.shutdown', async () => { 78 | const stub = sandbox.stub(job.queue, 'shutdown'); 79 | job.shutdown(); 80 | 81 | expect(stub.calledOnce).be.true; 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /tools/gulp/config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | // base directories, paths, etc. 4 | export const SRC = 'src'; 5 | export const TEST = 'test'; 6 | export const DEST = 'build'; 7 | export const PATHS = { 8 | // client 9 | assets: [ 10 | { 11 | name: 'styles', 12 | src: path.join(SRC, 'assets/styles/**/*.scss'), 13 | dest: path.join(DEST, 'static/css'), 14 | }, 15 | { 16 | name: 'images', 17 | src: path.join(SRC, 'assets/images/**/*'), 18 | dest: path.join(DEST, 'static/images'), 19 | }, 20 | { 21 | name: 'attachments', 22 | src: path.join(SRC, 'assets/static/attachments/**/*'), 23 | dest: path.join(DEST, 'static/attachments'), 24 | }, 25 | { 26 | name: 'files', 27 | src: path.join(SRC, 'assets/files/**/*'), 28 | dest: path.join(DEST, 'static/files'), 29 | }, 30 | { 31 | name: 'fonts', 32 | src: path.join(SRC, 'assets/fonts/**/*.ttf'), 33 | dest: path.join(DEST, 'static/fonts'), 34 | }, 35 | ], 36 | 37 | scripts: { 38 | name: 'scripts', 39 | src: path.join(SRC, 'assets/scripts/**/*.js'), 40 | dest: path.join(DEST, 'static/js'), 41 | }, 42 | 43 | views: [{ 44 | name: 'views', 45 | src: path.join(SRC, 'app/views/**/*.pug'), 46 | dest: path.join(DEST, 'app/views'), 47 | }, { 48 | name: 'views', 49 | src: path.join(SRC, 'plugins/**/*.pug'), 50 | dest: path.join(DEST, 'plugins'), 51 | }], 52 | 53 | // server 54 | transpile: [ 55 | { 56 | name: 'app:entry', 57 | src: path.join(SRC, '*.js'), 58 | dest: DEST, 59 | }, 60 | { 61 | name: 'app', 62 | src: path.join(SRC, 'app/**/*.js'), 63 | dest: path.join(DEST, 'app'), 64 | }, 65 | { 66 | name: 'config', 67 | src: path.join(SRC, 'config/**/*.js'), 68 | dest: path.join(DEST, 'config'), 69 | }, 70 | { 71 | name: 'lib', 72 | src: path.join(SRC, 'lib/**/*.js'), 73 | dest: path.join(DEST, 'lib'), 74 | }, 75 | { 76 | name: 'plugins', 77 | src: path.join(SRC, 'plugins/**/*.js'), 78 | dest: path.join(DEST, 'plugins'), 79 | }, 80 | { 81 | name: 'database', 82 | src: path.join(SRC, 'database/**/*.js'), 83 | dest: path.join(DEST, 'database'), 84 | }, 85 | ], 86 | }; 87 | 88 | // commonly used sets pertaining to tasks 89 | // set of all keys of PATHS 90 | export const ALL = new Set([...PATHS.assets, PATHS.scripts, ...PATHS.transpile]); 91 | 92 | // client-related set 93 | export const CLIENT = [...PATHS.assets, PATHS.scripts, ...PATHS.views]; 94 | 95 | export const TRANSPILE = new Set(PATHS.transpile); 96 | 97 | // set of things that need to be linted, i.e. TRANSPILE ∪ {'scripts'} 98 | export const LINT = new Set([...TRANSPILE, PATHS.scripts]); 99 | -------------------------------------------------------------------------------- /test/app/controllers/usersController.spec.js: -------------------------------------------------------------------------------- 1 | import chai, { assert, expect } from 'chai'; 2 | import chaiHttp from 'chai-http'; 3 | import factory from 'factory-girl'; 4 | 5 | import server from 'server'; 6 | import { bookshelf } from 'database'; 7 | 8 | chai.use(chaiHttp); 9 | 10 | describe('Test users controller', () => { 11 | before(async () => { 12 | await bookshelf.knex.migrate.latest(); 13 | }); 14 | 15 | beforeEach(async () => { 16 | await bookshelf.knex.seed.run(); 17 | await factory.createMany('user', 3); 18 | }); 19 | 20 | afterEach(async () => { 21 | factory.cleanUp(); 22 | }); 23 | 24 | describe('GET /api/users', () => { 25 | it('should return all users', async () => { 26 | const res = await chai.request(server.expressServer) 27 | .get('/api/users'); 28 | const users = res.body.users; 29 | const pagination = res.body.pagination; 30 | expect(res.status).to.equal(200); 31 | expect(res).to.be.json; 32 | expect(users).to.a('array'); 33 | expect(users.length).to.equal(3); 34 | expect(users).to.have.property('length', 3); 35 | expect(users[0]).to.have.property('name', 'name1'); 36 | expect(pagination).to.have.property('page', 1); 37 | expect(pagination).to.have.property('pageSize', 50); 38 | }); 39 | }); 40 | 41 | describe('POST /api/users', () => { 42 | it('should create a user', async () => { 43 | const res = await chai.request(server.expressServer) 44 | .post('/api/users') 45 | .send({ 46 | email: 'user4@test.com', 47 | name: 'A', 48 | last_name: 'A', 49 | password: '123', 50 | }); 51 | expect(res.status).to.equal(201); 52 | }); 53 | 54 | it('should fail to create a user', async () => { 55 | try { 56 | await chai.request(server.expressServer) 57 | .post('/api/users') 58 | .send({ 59 | email: 'user1@test.com', 60 | name: 'A', 61 | last_name: 'A', 62 | password: '123', 63 | }); 64 | assert.fail(); 65 | } catch (error) { 66 | expect(error.status).to.equal(500); 67 | } 68 | }); 69 | }); 70 | 71 | describe('POST /api/users/login', () => { 72 | it('should login successfully', async () => { 73 | const res = await chai.request(server.expressServer) 74 | .post('/api/users/login') 75 | .send({ 76 | email: 'user1@test.com', 77 | password: '123', 78 | }); 79 | expect(res.status).to.equal(200); 80 | }); 81 | 82 | it('should login fail', async () => { 83 | try { 84 | await chai.request(server.expressServer) 85 | .post('/api/users/login') 86 | .send({ 87 | email: 'user1@test.com', 88 | password: 'wrong', 89 | }); 90 | assert.fail(); 91 | } catch (error) { 92 | expect(error.status).to.equal(401); 93 | } 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /tools/gulp/tasks/build.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import fs from 'fs'; 3 | import mkdirp from 'mkdirp'; 4 | import path from 'path'; 5 | import plugins from 'gulp-load-plugins'; 6 | import vfs from 'vinyl-fs'; 7 | import _ from 'lodash'; 8 | import { PATHS, DEST, CLIENT } from '../config'; 9 | 10 | const $ = plugins({ 11 | pattern: ['gulp-*', 'main-bower-files'], 12 | }); 13 | 14 | // build everything 15 | gulp.task('build', ['build:client', 'build:server']); 16 | 17 | // build client-side files 18 | gulp.task('build:client', _.map(CLIENT, 'name').concat('bower')); 19 | 20 | // build server-side files 21 | gulp.task('build:server', ['transpile', 'views', 'ln', 'copy']); 22 | 23 | // optimize images 24 | gulp.task('images', () => { 25 | const task = _.find(CLIENT, { name: 'images' }); 26 | 27 | gulp.src(task.src) 28 | .pipe($.changed(task.dest)) 29 | .pipe($.imagemin()) 30 | .pipe(gulp.dest(task.dest)) 31 | .pipe($.print(fp => `image: ${fp}`)); 32 | }); 33 | 34 | // optimize attachments 35 | gulp.task('attachments', () => { 36 | const task = _.find(CLIENT, { name: 'attachments' }); 37 | 38 | gulp.src(task.src) 39 | .pipe($.changed(task.dest)) 40 | .pipe($.imagemin()) 41 | .pipe(gulp.dest(task.dest)) 42 | .pipe($.print(fp => `attachment: ${fp}`)); 43 | }); 44 | 45 | // compile sass, sourcemaps, autoprefix, minify 46 | gulp.task('styles', () => { 47 | const task = _.find(CLIENT, { name: 'styles' }); 48 | 49 | gulp.src(task.src) 50 | .pipe($.changed(task.dest, { 51 | extension: '.css', 52 | })) 53 | .pipe($.plumber({ 54 | errorHandler: function errorHandler(err) { 55 | $.util.log(err); 56 | this.emit('end'); 57 | }, 58 | })) 59 | .pipe($.sourcemaps.init()) 60 | .pipe($.sass()) 61 | .pipe($.autoprefixer()) 62 | .pipe($.cssnano()) 63 | .pipe($.sourcemaps.write('.')) 64 | .pipe(gulp.dest(task.dest)) 65 | .pipe($.print(fp => `style: ${fp}`)); 66 | }); 67 | 68 | // transpile scripts, sourcemaps, minify 69 | gulp.task('scripts', ['lint:scripts'], () => { 70 | const task = PATHS.scripts; 71 | 72 | gulp.src(task.src) 73 | .pipe($.changed(task.dest)) 74 | .pipe($.sourcemaps.init()) 75 | .pipe($.babel({ 76 | presets: ['es2015', 'es2016', 'es2017'], 77 | plugins: [ 78 | [ 79 | 'transform-runtime', { 80 | polyfill: false, 81 | regenerator: true, 82 | }, 83 | ], 84 | ], 85 | })) 86 | .pipe($.uglify()) 87 | .pipe($.sourcemaps.write('.')) 88 | .pipe(gulp.dest(task.dest)) 89 | .pipe($.print(fp => `script: ${fp}`)); 90 | }); 91 | 92 | // copy files over to destination 93 | gulp.task('files', () => { 94 | const task = _.find(CLIENT, { name: 'files' }); 95 | 96 | gulp.src(task.src) 97 | .pipe($.changed(task.dest)) 98 | .pipe(gulp.dest(task.dest)) 99 | .pipe($.print(fp => `file: ${fp}`)); 100 | }); 101 | 102 | // generate webfonts and css from ttf fonts 103 | gulp.task('fonts', (done) => { 104 | const task = _.find(CLIENT, { name: 'fonts' }); 105 | // eot 106 | gulp.src(task.src) 107 | .pipe($.changed(task.dest)) 108 | .pipe($.ttf2eot()) 109 | .pipe(gulp.dest(task.dest)) 110 | .pipe($.print(fp => `font: ${fp}`)); 111 | 112 | // woff 113 | gulp.src(task.src) 114 | .pipe($.changed(task.dest)) 115 | .pipe($.ttf2woff()) 116 | .pipe(gulp.dest(task.dest)) 117 | .pipe($.print(fp => `font: ${fp}`)); 118 | 119 | // woff2 120 | gulp.src(task.src) 121 | .pipe($.changed(task.dest)) 122 | .pipe($.ttf2woff2()) 123 | .pipe(gulp.dest(task.dest)) 124 | .pipe($.print(fp => `font: ${fp}`)); 125 | 126 | // css 127 | gulp.src(task.src) 128 | .pipe($.changed(task.dest)) 129 | .pipe($.tap((file) => { 130 | mkdirp(task.dest, (err) => { 131 | if (err) $.util.log(err); 132 | const fname = path.basename(file.path, '.ttf'); 133 | const fp = path.join(task.dest, `${fname}.css`); 134 | const css = `@font-face { 135 | font-family: "${fname}"; 136 | src: url("${fname}.eot"); 137 | src: url("${fname}.eot?#iefix") format("embedded-opentype"), 138 | url("${fname}.woff2") format("woff2"), 139 | url("${fname}.woff") format("woff"), 140 | url("${fname}.ttf") format("truetype"); 141 | font-weight: 300; 142 | font-style: normal; 143 | }`; 144 | fs.writeFileSync(fp, css); 145 | }); 146 | })) 147 | .pipe(gulp.dest(task.dest)) 148 | .pipe($.print(fp => `font: ${fp}`)); 149 | done(); 150 | }); 151 | 152 | // copy views over to destination 153 | gulp.task('views', () => { 154 | PATHS.views.forEach((view) => { 155 | gulp.src(view.src) 156 | .pipe($.changed(view.dest)) 157 | .pipe(gulp.dest(view.dest)) 158 | .pipe($.print(fp => `view: ${fp}`)); 159 | }); 160 | }); 161 | 162 | // symlink package.json and node_modules to destination 163 | gulp.task('ln', () => 164 | vfs.src(['package.json', 'node_modules'], { 165 | followSymlinks: false, 166 | }) 167 | .pipe(vfs.symlink(DEST)) 168 | .pipe($.print(fp => `symlink: ${fp}`)) 169 | ); 170 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "imhere", 3 | "version": "0.4.1", 4 | "description": "A integrated node framework", 5 | "main": "src/server.js", 6 | "repository": "git@github.com:imheretw/imhere.git", 7 | "author": "ImHere ", 8 | "license": "MIT", 9 | "scripts": { 10 | "start": "rimraf build && gulp serve", 11 | "lint": "eslint .", 12 | "test:core": "PORT=4000 nyc mocha --compilers js:babel-core/register -r test/init --recursive test src/plugins -t 10000", 13 | "test:core:debug": "DEBUG_LEVEL=debug DEBUG=knex:query,knex:bindings,nock.* npm run test:core", 14 | "test": "NODE_ENV=test DEBUG_LEVEL=fatal npm run test:core", 15 | "test:debug": "NODE_ENV=test npm run test:core:debug", 16 | "test:mysql": "NODE_ENV=test_mysql npm run test:core", 17 | "test:mysql:debug": "NODE_ENV=test_mysql npm run test:core:debug", 18 | "db:create": "babel-node src/database/create.js", 19 | "db:drop": "babel-node src/database/drop.js", 20 | "db:migrate": "knex migrate:latest --knexfile ./src/config/knexConfig.js --cwd .", 21 | "db:rollback": "knex migrate:rollback --knexfile ./src/config/knexConfig.js --cwd .", 22 | "db:seed": "knex seed:run --knexfile ./src/config/knexConfig.js --cwd .", 23 | "command:job:my": "babel-node ./src/commands/job.js my", 24 | "eslint-pre-commit": "./bin/eslint-pre-commit.sh", 25 | "changelog": "standard-changelog" 26 | }, 27 | "pre-commit": [ 28 | "eslint-pre-commit" 29 | ], 30 | "dependencies": { 31 | "async": "^2.3.0", 32 | "bcrypt": "^1.0.2", 33 | "body-parser": "^1.17.1", 34 | "bookshelf": "^0.10.3", 35 | "bookshelf-modelbase": "^2.10.3", 36 | "bookshelf-schema": "^0.3.2", 37 | "bookshelf-validator": "^0.2.2", 38 | "commander": "^2.9.0", 39 | "compression": "^1.6.2", 40 | "cookie-parser": "^1.4.3", 41 | "crypto-js": "^3.1.9-1", 42 | "dotenv": "^4.0.0", 43 | "es6-error": "^4.0.2", 44 | "express": "^4.15.2", 45 | "express-jwt": "^5.1.0", 46 | "fermata": "^0.11.0-beta1", 47 | "fs-readfile-promise": "^3.0.0", 48 | "github": "^9.2.0", 49 | "google-libphonenumber": "^2.0.14", 50 | "helmet": "^3.5.0", 51 | "jsonwebtoken": "^7.3.0", 52 | "knex": "^0.12.9", 53 | "knex-cleaner": "^1.1.3", 54 | "kue": "^0.11.5", 55 | "lodash": "^4.17.4", 56 | "mkdirp": "^0.5.1", 57 | "mysql2": "^1.2.0", 58 | "passport": "^0.3.2", 59 | "passport-local": "^1.0.0", 60 | "password-hash": "^1.2.2", 61 | "promise": "^7.1.1", 62 | "pug": "^2.0.0-beta11", 63 | "require-dir": "^0.3.1", 64 | "serve-favicon": "^2.4.2", 65 | "strong-params": "^0.7.1", 66 | "time": "^0.12.0", 67 | "winston": "^2.3.1" 68 | }, 69 | "devDependencies": { 70 | "babel-cli": "^6.24.1", 71 | "babel-core": "^6.24.1", 72 | "babel-eslint": "^7.2.2", 73 | "babel-plugin-module-resolver": "^2.7.0", 74 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 75 | "babel-plugin-transform-runtime": "^6.23.0", 76 | "babel-polyfill": "^6.23.0", 77 | "babel-preset-es2015": "^6.24.1", 78 | "babel-preset-es2016": "^6.24.1", 79 | "babel-preset-es2017": "^6.24.1", 80 | "chai": "^3.5.0", 81 | "chai-http": "^3.0.0", 82 | "del": "^2.2.2", 83 | "eslint": "^3.19.0", 84 | "eslint-config-airbnb": "^14.1.0", 85 | "eslint-plugin-chai-expect": "^1.1.1", 86 | "eslint-plugin-chai-friendly": "^0.2.0", 87 | "eslint-plugin-import": "^2.2.0", 88 | "eslint-plugin-jsx-a11y": "^4.0.0", 89 | "eslint-plugin-mocha": "^4.9.0", 90 | "eslint-plugin-react": "^6.10.3", 91 | "factory-girl": "^4.2.2", 92 | "gulp": "^3.9.1", 93 | "gulp-autoprefixer": "^3.1.1", 94 | "gulp-babel": "^6.1.2", 95 | "gulp-cache": "^0.4.6", 96 | "gulp-changed": "^2.0.0", 97 | "gulp-concat": "^2.6.1", 98 | "gulp-cssnano": "^2.1.2", 99 | "gulp-eslint": "^3.0.1", 100 | "gulp-filter": "^5.0.0", 101 | "gulp-imagemin": "^3.2.0", 102 | "gulp-load-plugins": "^1.5.0", 103 | "gulp-nodemon": "^2.2.1", 104 | "gulp-plumber": "^1.1.0", 105 | "gulp-print": "^2.0.1", 106 | "gulp-rename": "^1.2.2", 107 | "gulp-run": "^1.7.1", 108 | "gulp-sass": "^3.1.0", 109 | "gulp-sourcemaps": "^2.6.0", 110 | "gulp-tap": "^0.4.2", 111 | "gulp-task-listing": "^1.0.1", 112 | "gulp-ttf2eot": "^1.1.1", 113 | "gulp-ttf2woff": "^1.1.0", 114 | "gulp-ttf2woff2": "^2.0.2", 115 | "gulp-uglify": "^2.1.2", 116 | "gulp-util": "^3.0.8", 117 | "main-bower-files": "^2.13.1", 118 | "mkdirp": "^0.5.1", 119 | "mocha": "^3.2.0", 120 | "nock": "^9.0.13", 121 | "nyc": "^10.2.0", 122 | "pre-commit": "^1.2.2", 123 | "rimraf": "^2.6.1", 124 | "sinon": "^2.1.0", 125 | "sqlite3": "^3.1.8", 126 | "standard-changelog": "^1.0.1", 127 | "vinyl-fs": "^2.4.4" 128 | }, 129 | "nyc": { 130 | "include": [ 131 | "src/**/*.js" 132 | ], 133 | "exclude": [ 134 | "**/*.spec.js" 135 | ], 136 | "reporter": [ 137 | "lcov", 138 | "text", 139 | "html" 140 | ] 141 | }, 142 | "eslint-pre-commit": "./bin/eslint-pre-commit.sh" 143 | } 144 | -------------------------------------------------------------------------------- /src/lib/gocool/lib/Server.js: -------------------------------------------------------------------------------- 1 | // external 2 | import bodyParser from 'body-parser'; 3 | import compress from 'compression'; 4 | import cookieParser from 'cookie-parser'; 5 | import express from 'express'; 6 | import helmet from 'helmet'; 7 | import passport from 'passport'; 8 | import params from 'strong-params'; 9 | 10 | // lib 11 | import Logger from 'Logger'; 12 | 13 | export default class Server { 14 | constructor({ config, routes }) { 15 | this._logger = new Logger('Server'); 16 | this._app = null; 17 | this._expressServer = null; 18 | this._views = []; 19 | this._expressPlugins = []; 20 | this._handlers = []; 21 | this._config = config; 22 | 23 | this.setRoutes(routes); 24 | 25 | this._initApp(); 26 | } 27 | 28 | start() { 29 | this._logger.debug('Starting Server...'); 30 | 31 | this._initExpressPlugins(); 32 | this._initRoutes(); 33 | this._initViews(); 34 | this._initExpressServer(); 35 | 36 | return this; 37 | } 38 | 39 | setRoutes(routes) { 40 | this._routes = routes || []; 41 | 42 | return this; 43 | } 44 | 45 | addHandlers(handlers) { 46 | this._handlers = [ 47 | ...this._handlers, 48 | ...handlers, 49 | ]; 50 | 51 | handlers.forEach(handler => handler.start()); 52 | 53 | return this; 54 | } 55 | 56 | addViews(views) { 57 | if (views && views.length) { 58 | this._views = [ 59 | ...this._views, 60 | ...views, 61 | ]; 62 | } 63 | 64 | return this; 65 | } 66 | 67 | addExpressPlugins(expressPlugins) { 68 | if (expressPlugins && expressPlugins.length) { 69 | this._expressPlugins = [ 70 | ...this._expressPlugins, 71 | ...expressPlugins, 72 | ]; 73 | } 74 | 75 | return this; 76 | } 77 | 78 | addPlugin(routePath, Plugin) { 79 | const plugin = new Plugin(); 80 | const routes = plugin.getRoutes(); 81 | const views = plugin.getViews(); 82 | 83 | this._app.use(routePath, routes); 84 | this.addViews(views); 85 | 86 | return this; 87 | } 88 | 89 | _initApp() { 90 | // EXPRESS SET-UP 91 | // create app 92 | this._app = express(); 93 | 94 | // use pug and set views and static directories 95 | this._app.set('view engine', 'pug'); 96 | 97 | this.addViews([this._config.path.view]); 98 | 99 | this._app.use(express.static(this._config.path.static)); 100 | 101 | // add middlewares 102 | this._app.use(bodyParser.json({ 103 | verify(req, res, buf) { 104 | req.rawBody = buf; 105 | }, 106 | })); 107 | this._app.use(bodyParser.urlencoded({ 108 | extended: true, 109 | verify(req, res, buf) { 110 | req.rawBody = buf; 111 | }, 112 | })); 113 | this._app.use(compress()); 114 | this._app.use(cookieParser()); 115 | this._app.use(helmet()); 116 | this._app.use(params.expressMiddleware()); 117 | 118 | // passport for authenticate 119 | this._app.use(passport.initialize()); 120 | this._app.use(passport.session()); 121 | } 122 | 123 | _initRoutes() { 124 | if (this._routes) { 125 | // routes 126 | this._app.use('/', this._routes); 127 | // catch 404 and forward to error handler 128 | this._app.use((req, res, next) => { 129 | const err = new Error('Route Not Found'); 130 | err.statusCode = 404; 131 | next(err); 132 | }); 133 | } 134 | 135 | // general errors 136 | this._app.use((err, req, res, next) => { 137 | const sc = err.statusCode || 500; 138 | res.status(sc); 139 | 140 | this._logger.error( 141 | 'Error on status', sc, err.stack 142 | ); 143 | 144 | if (sc === 500) { 145 | res.render('error', { 146 | status: sc, 147 | message: err.message, 148 | stack: this._config.env === 'development' ? err.stack : '', 149 | }); 150 | } else { 151 | res.json({ 152 | error: err.message, 153 | }); 154 | } 155 | }); 156 | 157 | return this; 158 | } 159 | 160 | _initViews() { 161 | this._app.set('views', this._views); 162 | } 163 | 164 | _initExpressPlugins() { 165 | this._expressPlugins.forEach((plugin) => { 166 | if (plugin.path) { 167 | this._app.use(plugin.path, plugin.content); 168 | } else { 169 | this._app.use(plugin.content); 170 | } 171 | }); 172 | } 173 | 174 | _initExpressServer() { 175 | this.setRoutes(); 176 | // START AND STOP 177 | this._expressServer = this._app.listen(this._config.port, () => { 178 | this._logger.info(`Started server and listening on port ${this._config.port}`); 179 | }); 180 | 181 | process.on('SIGINT', () => { 182 | this._logger.info('Server shutting down!'); 183 | this._expressServer.close(); 184 | process.exit(); 185 | }); 186 | 187 | process.on('uncaughtException', (error) => { 188 | this._logger.error(`uncaughtException: ${error.message}`); 189 | this._logger.error(error.stack); 190 | process.exit(1); 191 | }); 192 | } 193 | 194 | get expressServer() { 195 | return this._expressServer; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ImHere ![CircleCI Build Status](https://circleci.com/gh/imheretw/imhere.svg?style=shield&circle-token=86e04f476d21b9b2164053879588dc4e676fc520) 2 | 3 | We integrated most popular and widely used packages for web development. 4 | Node.js web developer is now easily to start developing web application in minutes just like other web frameworks such as Laravel, Rails ...etc. 5 | 6 | # Integrated Features 7 | 8 | * ES Lint with Airbnb code style checking 9 | * Support ES7 async/await (babel) 10 | * MVC architecture 11 | * router (express.js) 12 | * controllers (express.js) 13 | * ORM (Bookshelf.js) 14 | * Configuration Settings (dotenv) 15 | * User Authentication 16 | * jwt 17 | * passport 18 | * Logger (winston) 19 | * add prefix to make debug easier 20 | * Commands (commander) 21 | * Database schema migration (knex.js) 22 | * Unit Test Integrations 23 | * mocha: javascript testing framework 24 | * chai: BDD/TDD assertion 25 | * factory-girl: db data generation for test 26 | * sinon: stub & mock for unit test 27 | * nock: web mock 28 | * nyc: code coverage 29 | * Live Reload For Development 30 | * Debugger (node-inspector) 31 | * Support Background Jobs (Kue.js) 32 | * Deployment 33 | * Capistrano (Ruby gem) 34 | * pm2 (Production process manager for Node.js) 35 | 36 | # Prerequisite 37 | - redis 3.0+ 38 | - mysql 5.6+ 39 | - Node 6+ 40 | - Ruby 2.0+ (For deploy) 41 | 42 | # Install 43 | 44 | ## Environment 45 | 46 | Startup services 47 | ```shell 48 | > docker-compose up -d 49 | Creating imhere_redis_1 50 | Creating imhere_mysql_1 51 | Creating imhere_phpmyadmin_1 52 | ``` 53 | 54 | Install tools 55 | ```shell 56 | > npm install -g yarn 57 | > npm install -g bower 58 | > npm install -g bable-cli # for commands 59 | > npm install -g pm2 # for deployment environment 60 | > gem install bundler # for capistrano to deploy 61 | > bundle install # for capistrano to deploy 62 | ``` 63 | 64 | Install npm and bower packages 65 | ``` 66 | > yarn 67 | > bower i 68 | ``` 69 | 70 | ## Config 71 | 72 | Copy `.env.sample` to `.env` and change the setting 73 | ```shell 74 | > cp .env.sample .env 75 | ``` 76 | 77 | ## Prepare for database 78 | 79 | Migration 80 | ```shell 81 | > yarn db:create 82 | > yarn db:migrate 83 | ``` 84 | 85 | Seed 86 | ```shell 87 | > yarn db:seed 88 | ``` 89 | 90 | # Up and Running 91 | 92 | Start the service 93 | ``` 94 | > yarn start 95 | yarn start v0.18.1 96 | $ gulp serve 97 | [19:25:50] Requiring external module babel-register 98 | [19:25:50] Using gulpfile ~/projects/nodejs/imhere/gulpfile.babel.js 99 | [19:25:51] Starting 'styles'... 100 | [19:25:51] Starting 'images'... 101 | [19:25:51] Starting 'attachments'... 102 | [19:25:51] Starting 'lint:scripts'... 103 | [19:25:51] Starting 'files'... 104 | [19:25:51] Starting 'fonts'... 105 | .... 106 | 2017-01-08T11:25:54.207Z - info: App: listening on port 5000 107 | ``` 108 | 109 | Test users API 110 | ```json 111 | > curl http://localhost:5000/api/users 112 | 113 | { 114 | "users": [ 115 | { 116 | "id": 1, 117 | "name": "Test", 118 | "email": "test@test.com", 119 | "encrypted_password": "$2a$06$NkYh0RCM8pNWPaYvRLgN9.Tl30VHCXEDh66RKnuDJNBV0RLQSypWa", 120 | "created_at": "2017-02-04T08:33:18.000Z", 121 | "updated_at": "2017-02-04T08:33:18.000Z" 122 | } 123 | ], 124 | "pagination": { 125 | "page": 1, 126 | "pageSize": 50, 127 | "rowCount": 1, 128 | "pageCount": 1 129 | } 130 | } 131 | ``` 132 | 133 | Test github API 134 | ```json 135 | > curl http://localhost:5000/api/github/closed_issues 136 | { 137 | "issues": [{ 138 | "url": "https://api.github.com/repos/imheretw/imhere/issues/4", 139 | "repository_url": "https://api.github.com/repos/imheretw/imhere", 140 | "labels_url": "https://api.github.com/repos/imheretw/imhere/issues/4/labels{/name}", 141 | "comments_url": "https://api.github.com/repos/imheretw/imhere/issues/4/comments", 142 | "events_url": "https://api.github.com/repos/imheretw/imhere/issues/4/events", 143 | "html_url": "https://github.com/imheretw/imhere/issues/4", 144 | "id": 205337147, 145 | "number": 4, 146 | "title": "Kue screenshot", 147 | "user": { 148 | "login": "koshuang", 149 | "id": 1978357, 150 | "avatar_url": "https://avatars.githubusercontent.com/u/1978357?v=3", 151 | "gravatar_id": "", 152 | "url": "https://api.github.com/users/koshuang", 153 | "html_url": "https://github.com/koshuang", 154 | "followers_url": "https://api.github.com/users/koshuang/followers", 155 | "following_url": "https://api.github.com/users/koshuang/following{/other_user}", 156 | "gists_url": "https://api.github.com/users/koshuang/gists{/gist_id}", 157 | "starred_url": "https://api.github.com/users/koshuang/starred{/owner}{/repo}", 158 | "subscriptions_url": "https://api.github.com/users/koshuang/subscriptions", 159 | "organizations_url": "https://api.github.com/users/koshuang/orgs", 160 | "repos_url": "https://api.github.com/users/koshuang/repos", 161 | "events_url": "https://api.github.com/users/koshuang/events{/privacy}", 162 | "received_events_url": "https://api.github.com/users/koshuang/received_events", 163 | "type": "User", 164 | "site_admin": false 165 | }, 166 | "labels": [ 167 | 168 | ], 169 | "state": "closed", 170 | "locked": false, 171 | "assignee": null, 172 | "assignees": [ 173 | 174 | ], 175 | "milestone": null, 176 | "comments": 0, 177 | "created_at": "2017-02-04T09:56:19Z", 178 | "updated_at": "2017-02-04T10:05:02Z", 179 | "closed_at": "2017-02-04T10:05:02Z", 180 | "body": "![kue](https://cloud.githubusercontent.com/assets/1978357/22617555/34ee1780-eb03-11e6-998d-01557f517763.png)\r\n" 181 | }] 182 | } 183 | ``` 184 | 185 | Kue page: http://localhost:5000/kue 186 | 187 | ![kue](https://cloud.githubusercontent.com/assets/1978357/22617555/34ee1780-eb03-11e6-998d-01557f517763.png) 188 | 189 | # Available Commands 190 | * bootstrap 191 | * **`yarn start`** starting web server on local machine. 192 | * lint 193 | * **`yarn lint`** run eslint to check code style 194 | * testing 195 | * **`yarn test`** run testcases with sqlite 196 | * **`yarn test:debug`** run testcase with sqlite and more debug logs 197 | * **`yarn test:mysql`** run testcase with mysql 198 | * **`yarn test:mysql:debug`** run testcase with mysql and more debug logs 199 | * database 200 | * **`yarn db:create`** create database 201 | * **`yarn db:drop`** drop database 202 | * **`yarn db:migrate`** run database schema migration 203 | * **`yarn db:rollback`** rollback last schema migration 204 | * **`yarn db:seed`** generate seed data into database 205 | * custom commands 206 | * **`yarn command:job:my`** run sample job 207 | * deployment 208 | * **`cap localhost deploy`** deploy to localhost 209 | * **`cap dev deploy`** deploy to dev server 210 | * **`cap staging deploy`** deploy to staging server 211 | * **`cap production deploy`** deploy to production server 212 | -------------------------------------------------------------------------------- /src/plugins/gocool-github-demo-plugin/test/data/github/issues/closed.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "url": "https://api.github.com/repos/imheretw/imhere/issues/4", 3 | "repository_url": "https://api.github.com/repos/imheretw/imhere", 4 | "labels_url": "https://api.github.com/repos/imheretw/imhere/issues/4/labels{/name}", 5 | "comments_url": "https://api.github.com/repos/imheretw/imhere/issues/4/comments", 6 | "events_url": "https://api.github.com/repos/imheretw/imhere/issues/4/events", 7 | "html_url": "https://github.com/imheretw/imhere/issues/4", 8 | "id": 205337147, 9 | "number": 4, 10 | "title": "Kue screenshot", 11 | "user": { 12 | "login": "koshuang", 13 | "id": 1978357, 14 | "avatar_url": "https://avatars.githubusercontent.com/u/1978357?v=3", 15 | "gravatar_id": "", 16 | "url": "https://api.github.com/users/koshuang", 17 | "html_url": "https://github.com/koshuang", 18 | "followers_url": "https://api.github.com/users/koshuang/followers", 19 | "following_url": "https://api.github.com/users/koshuang/following{/other_user}", 20 | "gists_url": "https://api.github.com/users/koshuang/gists{/gist_id}", 21 | "starred_url": "https://api.github.com/users/koshuang/starred{/owner}{/repo}", 22 | "subscriptions_url": "https://api.github.com/users/koshuang/subscriptions", 23 | "organizations_url": "https://api.github.com/users/koshuang/orgs", 24 | "repos_url": "https://api.github.com/users/koshuang/repos", 25 | "events_url": "https://api.github.com/users/koshuang/events{/privacy}", 26 | "received_events_url": "https://api.github.com/users/koshuang/received_events", 27 | "type": "User", 28 | "site_admin": false 29 | }, 30 | "labels": [ 31 | 32 | ], 33 | "state": "closed", 34 | "locked": false, 35 | "assignee": null, 36 | "assignees": [ 37 | 38 | ], 39 | "milestone": null, 40 | "comments": 0, 41 | "created_at": "2017-02-04T09:56:19Z", 42 | "updated_at": "2017-02-04T10:05:02Z", 43 | "closed_at": "2017-02-04T10:05:02Z", 44 | "body": "![kue](https://cloud.githubusercontent.com/assets/1978357/22617555/34ee1780-eb03-11e6-998d-01557f517763.png)\r\n" 45 | }, { 46 | "url": "https://api.github.com/repos/imheretw/imhere/issues/3", 47 | "repository_url": "https://api.github.com/repos/imheretw/imhere", 48 | "labels_url": "https://api.github.com/repos/imheretw/imhere/issues/3/labels{/name}", 49 | "comments_url": "https://api.github.com/repos/imheretw/imhere/issues/3/comments", 50 | "events_url": "https://api.github.com/repos/imheretw/imhere/issues/3/events", 51 | "html_url": "https://github.com/imheretw/imhere/pull/3", 52 | "id": 199415153, 53 | "number": 3, 54 | "title": "update readme", 55 | "user": { 56 | "login": "chenghung", 57 | "id": 3082432, 58 | "avatar_url": "https://avatars.githubusercontent.com/u/3082432?v=3", 59 | "gravatar_id": "", 60 | "url": "https://api.github.com/users/chenghung", 61 | "html_url": "https://github.com/chenghung", 62 | "followers_url": "https://api.github.com/users/chenghung/followers", 63 | "following_url": "https://api.github.com/users/chenghung/following{/other_user}", 64 | "gists_url": "https://api.github.com/users/chenghung/gists{/gist_id}", 65 | "starred_url": "https://api.github.com/users/chenghung/starred{/owner}{/repo}", 66 | "subscriptions_url": "https://api.github.com/users/chenghung/subscriptions", 67 | "organizations_url": "https://api.github.com/users/chenghung/orgs", 68 | "repos_url": "https://api.github.com/users/chenghung/repos", 69 | "events_url": "https://api.github.com/users/chenghung/events{/privacy}", 70 | "received_events_url": "https://api.github.com/users/chenghung/received_events", 71 | "type": "User", 72 | "site_admin": false 73 | }, 74 | "labels": [ 75 | 76 | ], 77 | "state": "closed", 78 | "locked": false, 79 | "assignee": null, 80 | "assignees": [ 81 | 82 | ], 83 | "milestone": null, 84 | "comments": 3, 85 | "created_at": "2017-01-08T10:45:27Z", 86 | "updated_at": "2017-01-09T02:22:52Z", 87 | "closed_at": "2017-01-09T02:22:52Z", 88 | "pull_request": { 89 | "url": "https://api.github.com/repos/imheretw/imhere/pulls/3", 90 | "html_url": "https://github.com/imheretw/imhere/pull/3", 91 | "diff_url": "https://github.com/imheretw/imhere/pull/3.diff", 92 | "patch_url": "https://github.com/imheretw/imhere/pull/3.patch" 93 | }, 94 | "body": "" 95 | }, { 96 | "url": "https://api.github.com/repos/imheretw/imhere/issues/2", 97 | "repository_url": "https://api.github.com/repos/imheretw/imhere", 98 | "labels_url": "https://api.github.com/repos/imheretw/imhere/issues/2/labels{/name}", 99 | "comments_url": "https://api.github.com/repos/imheretw/imhere/issues/2/comments", 100 | "events_url": "https://api.github.com/repos/imheretw/imhere/issues/2/events", 101 | "html_url": "https://github.com/imheretw/imhere/pull/2", 102 | "id": 199413187, 103 | "number": 2, 104 | "title": "feat(BackgroundJob) add kue as background job handler #1", 105 | "user": { 106 | "login": "chenghung", 107 | "id": 3082432, 108 | "avatar_url": "https://avatars.githubusercontent.com/u/3082432?v=3", 109 | "gravatar_id": "", 110 | "url": "https://api.github.com/users/chenghung", 111 | "html_url": "https://github.com/chenghung", 112 | "followers_url": "https://api.github.com/users/chenghung/followers", 113 | "following_url": "https://api.github.com/users/chenghung/following{/other_user}", 114 | "gists_url": "https://api.github.com/users/chenghung/gists{/gist_id}", 115 | "starred_url": "https://api.github.com/users/chenghung/starred{/owner}{/repo}", 116 | "subscriptions_url": "https://api.github.com/users/chenghung/subscriptions", 117 | "organizations_url": "https://api.github.com/users/chenghung/orgs", 118 | "repos_url": "https://api.github.com/users/chenghung/repos", 119 | "events_url": "https://api.github.com/users/chenghung/events{/privacy}", 120 | "received_events_url": "https://api.github.com/users/chenghung/received_events", 121 | "type": "User", 122 | "site_admin": false 123 | }, 124 | "labels": [ 125 | 126 | ], 127 | "state": "closed", 128 | "locked": false, 129 | "assignee": null, 130 | "assignees": [ 131 | 132 | ], 133 | "milestone": null, 134 | "comments": 1, 135 | "created_at": "2017-01-08T09:49:37Z", 136 | "updated_at": "2017-01-08T10:55:20Z", 137 | "closed_at": "2017-01-08T10:55:20Z", 138 | "pull_request": { 139 | "url": "https://api.github.com/repos/imheretw/imhere/pulls/2", 140 | "html_url": "https://github.com/imheretw/imhere/pull/2", 141 | "diff_url": "https://github.com/imheretw/imhere/pull/2.diff", 142 | "patch_url": "https://github.com/imheretw/imhere/pull/2.patch" 143 | }, 144 | "body": "#1 \r\n\r\n@koshuang @imheretw r?, thanks." 145 | }, { 146 | "url": "https://api.github.com/repos/imheretw/imhere/issues/1", 147 | "repository_url": "https://api.github.com/repos/imheretw/imhere", 148 | "labels_url": "https://api.github.com/repos/imheretw/imhere/issues/1/labels{/name}", 149 | "comments_url": "https://api.github.com/repos/imheretw/imhere/issues/1/comments", 150 | "events_url": "https://api.github.com/repos/imheretw/imhere/issues/1/events", 151 | "html_url": "https://github.com/imheretw/imhere/issues/1", 152 | "id": 199412536, 153 | "number": 1, 154 | "title": "support background job (Kue.js)", 155 | "user": { 156 | "login": "chenghung", 157 | "id": 3082432, 158 | "avatar_url": "https://avatars.githubusercontent.com/u/3082432?v=3", 159 | "gravatar_id": "", 160 | "url": "https://api.github.com/users/chenghung", 161 | "html_url": "https://github.com/chenghung", 162 | "followers_url": "https://api.github.com/users/chenghung/followers", 163 | "following_url": "https://api.github.com/users/chenghung/following{/other_user}", 164 | "gists_url": "https://api.github.com/users/chenghung/gists{/gist_id}", 165 | "starred_url": "https://api.github.com/users/chenghung/starred{/owner}{/repo}", 166 | "subscriptions_url": "https://api.github.com/users/chenghung/subscriptions", 167 | "organizations_url": "https://api.github.com/users/chenghung/orgs", 168 | "repos_url": "https://api.github.com/users/chenghung/repos", 169 | "events_url": "https://api.github.com/users/chenghung/events{/privacy}", 170 | "received_events_url": "https://api.github.com/users/chenghung/received_events", 171 | "type": "User", 172 | "site_admin": false 173 | }, 174 | "labels": [ 175 | 176 | ], 177 | "state": "closed", 178 | "locked": false, 179 | "assignee": null, 180 | "assignees": [ 181 | 182 | ], 183 | "milestone": null, 184 | "comments": 0, 185 | "created_at": "2017-01-08T09:31:33Z", 186 | "updated_at": "2017-01-08T11:03:31Z", 187 | "closed_at": "2017-01-08T11:03:31Z", 188 | "body": "" 189 | }] 190 | --------------------------------------------------------------------------------