[init] server build up

This commit is contained in:
TommyXin
2020-09-11 14:00:50 +08:00
parent 719d2f3f73
commit 79abfdb2dd
35 changed files with 20589 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
{
"presets": [
["@babel/preset-env", {
"targets": {
"node": "current"
}
}]
],
"env": {
"test": {
"presets": [
["@babel/preset-env", {
"targets": {
"node": "current"
}
}]
]
}
},
"plugins": ["@babel/plugin-transform-runtime"]
}
+8
View File
@@ -0,0 +1,8 @@
dist
node_modules
.dockerignore
Dockerfile
npm-debug.log
.git
.hg
.svn
+9
View File
@@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
+3
View File
@@ -0,0 +1,3 @@
build/*.js
assets/*.js
test/**/*.js
+27
View File
@@ -0,0 +1,27 @@
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint',
ecmaVersion: 2017, //指定ECMAScript支持的版本,6为ES6,这里为了兼容async和await,设置为2017
sourceType: 'module'
},
extends: 'standard',
plugins: [
'html',
'promise'
],
env: {
'node': true
},
rules: {
// allow console
'no-console': 0,
// allow paren-less arrow functions
'arrow-parens': 0,
// allow async-await
'generator-star-spacing': 0,
// allow debugger during development
'no-debugger': 0
},
}
+8
View File
@@ -0,0 +1,8 @@
.DS_Store
node_modules/
dist/
npm-debug.log
test/unit/coverage
test/e2e/reports
selenium-debug.log
.idea/
+11
View File
@@ -0,0 +1,11 @@
{
"indent_size": 2,
"indent_char": " ",
"other": " ",
"indent_level": 0,
"indent_with_tabs": false,
"preserve_newlines": true,
"max_preserve_newlines": 2,
"jslint_happy": true,
"indent_handlebars": true
}
+19
View File
@@ -0,0 +1,19 @@
FROM node:10-alpine
ENV EXPOSE_PORT 3000
ENV TZ=Asia/Shanghai
WORKDIR /app
COPY package*.json ./
RUN npm install -g @babel/cli @babel/core \
&& npm install
COPY . .
RUN npm run build
EXPOSE ${EXPOSE_PORT}
ENTRYPOINT [ "node", "/app/dist/app.js" ]
+11
View File
@@ -0,0 +1,11 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
+1
View File
@@ -0,0 +1 @@
1
+2
View File
@@ -0,0 +1,2 @@
require('@babel/register')
require('../src/app')
+84
View File
@@ -0,0 +1,84 @@
const gulp = require('gulp')
const eslint = require('gulp-eslint')
const nodemon = require('gulp-nodemon')
const friendlyFormatter = require('eslint-friendly-formatter')
var jsScript = 'node'
if (process.env.npm_config_argv !== undefined && process.env.npm_config_argv.indexOf('debug') > 0) {
jsScript = 'node debug'
}
function lintOne (aims) {
console.log('ESlint:' + aims)
console.time('Finished eslint')
return gulp.src(aims)
.pipe(eslint({ configFile: './.eslintrc.js' }))
.pipe(eslint.format(friendlyFormatter))
.pipe(eslint.results(results => {
// Called once for all ESLint results.
console.log(`- Total Results: ${results.length}`)
console.log(`- Total Warnings: ${results.warningCount}`)
console.log(`- Total Errors: ${results.errorCount}`)
console.timeEnd('Finished eslint')
}))
}
gulp.task('ESlint', () => {
return gulp.src(['src/**/*.js', '!node_modules/**'])
.pipe(eslint({ configFile: './.eslintrc.js' }))
.pipe(eslint.format(friendlyFormatter))
// .pipe(eslint.failAfterError())
.pipe(eslint.results(results => {
// Called once for all ESLint results.
console.log(`- Total Results: ${results.length}`)
console.log(`- Total Warnings: ${results.warningCount}`)
console.log(`- Total Errors: ${results.errorCount}`)
}))
})
gulp.task('ESlint_nodemon', gulp.series('ESlint', function () {
var stream = nodemon({
script: 'build/dev-server.js',
execMap: {
js: jsScript
},
tasks: function (changedFiles) {
lintOne(changedFiles)
return []
},
verbose: true,
ignore: ['build/*.js', 'dist/*.js', 'nodemon.json', '.git', 'node_modules/**/node_modules', 'gulpfile.js'],
env: {
NODE_ENV: 'development'
},
ext: 'js json'
})
return stream
.on('restart', function () {
// console.log('Application has restarted!')
})
.on('crash', function () {
console.error('Application has crashed!\n')
// stream.emit('restart', 20) // restart the server in 20 seconds
})
}))
gulp.task('nodemon', function () {
return nodemon({
script: 'build/dev-server.js',
execMap: {
js: jsScript
},
verbose: true,
ignore: ['build/*.js', 'dist/*.js', 'nodemon.json', '.git', 'node_modules/**/node_modules', 'gulpfile.js'],
env: {
NODE_ENV: 'development'
},
ext: 'js json'
})
})
gulp.task('default', gulp.series('ESlint', 'ESlint_nodemon', function () {
// console.log('ESlin检查完成')
}))
+1
View File
@@ -0,0 +1 @@
ok
+12100
View File
File diff suppressed because it is too large Load Diff
+61
View File
@@ -0,0 +1,61 @@
{
"name": "koa2-api-scaffold",
"version": "1.0.4",
"description": "Koa2 RESTful API 服务器的脚手架",
"author": "yi-ge <a@wyr.me>",
"license": "WTFPL",
"scripts": {
"start": "gulp nodemon",
"dev": "gulp",
"build": "babel src -d dist",
"production": "node dist/app.js",
"test": "jest",
"link": "eslint .",
"link:fix": "eslint --fix ."
},
"dependencies": {
"jsonwebtoken": "^8.5.1",
"koa": "^2.11.0",
"koa-body": "^4.1.1",
"koa-compose": "^4.1.0",
"koa-cors": "0.0.16",
"koa-jwt": "^3.6.0",
"koa-router": "^8.0.8",
"koa-static2": "^0.1.8",
"nodemailer": "^6.4.6",
"pg": "^8.3.3",
"pg-hstore": "^2.3.3",
"promise-mysql": "^4.1.3",
"sequelize": "^5.22.3"
},
"devDependencies": {
"@babel/cli": "^7.8.4",
"@babel/core": "^7.9.0",
"@babel/plugin-external-helpers": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.9.0",
"@babel/preset-env": "^7.9.5",
"@babel/register": "^7.9.0",
"@babel/runtime": "^7.9.2",
"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^25.3.0",
"eslint": "^6.8.0",
"eslint-config-standard": "^14.1.1",
"eslint-friendly-formatter": "^4.0.1",
"eslint-plugin-html": "^6.0.1",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-jest": "^23.8.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"gulp": "^4.0.2",
"gulp-eslint": "^6.0.0",
"gulp-nodemon": "^2.5.0",
"jest": "^25.3.0",
"koa-logger": "^3.2.1"
},
"engines": {
"node": ">= 7.8.0",
"npm": ">= 4.2.0"
}
}
+17
View File
@@ -0,0 +1,17 @@
module.exports = {
apps: [{
name: 'RESRful API Server',
script: './dist/app.js',
watch: false, // 默认关闭watch 可替换为 ['src']
ignore_watch: ['node_modules', 'build', 'logs'],
out_file: '/logs/out.log', // 日志输出
error_file: '/logs/error.log', // 错误日志
max_memory_restart: '2G', // 超过多大内存自动重启,仅防止内存泄露有意义,需要根据自己的业务设置
env: {
NODE_ENV: 'production'
},
exec_mode: 'cluster', // 开启多线程模式,用于负载均衡
instances: 'max', // 启用多少个实例,可用于负载均衡
autorestart: true // 程序崩溃后自动重启
}]
}
+1
View File
@@ -0,0 +1 @@
wqdjkwl1e21FQlk1j2
+76
View File
@@ -0,0 +1,76 @@
import Koa2 from 'koa'
import KoaBody from 'koa-body'
// import cors from 'koa-cors'
import KoaStatic from 'koa-static2'
import {
System as SystemConfig
} from './config'
import path from 'path'
import MainRoutes from './routes/main-routes'
import ErrorRoutesCatch from './middleware/ErrorRoutesCatch'
import ErrorRoutes from './routes/error-routes'
import jwt from 'koa-jwt'
import fs from 'fs'
import db from './lib/sequelize'
const app = new Koa2()
const env = process.env.NODE_ENV || 'development' // Current mode
const publicKey = fs.readFileSync(path.join(__dirname, '../publicKey.pub'))
app
.use((ctx, next) => {
if (ctx.request.header.host.split(':')[0] === 'localhost' || ctx.request.header.host.split(':')[0] === '127.0.0.1') {
ctx.set('Access-Control-Allow-Origin', '*')
} else {
ctx.set('Access-Control-Allow-Origin', SystemConfig.HTTP_server_host)
}
ctx.set('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')
ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS')
ctx.set('Access-Control-Allow-Credentials', true) // 允许带上 cookie
return next()
})
// .use(cors)
.use(ErrorRoutesCatch())
.use(KoaStatic('assets', path.resolve(__dirname, '../assets'))) // Static resource
.use(jwt({ secret: publicKey }).unless({ path: [/^\/public|\/user\/login|\/assets/] }))
.use(KoaBody({
multipart: true,
parsedMethods: ['POST', 'PUT', 'PATCH', 'GET', 'HEAD', 'DELETE'], // parse GET, HEAD, DELETE requests
formidable: {
uploadDir: path.join(__dirname, '../assets/uploads/tmp')
},
jsonLimit: '10mb',
formLimit: '10mb',
textLimit: '10mb'
})) // Processing request
// .use(PluginLoader(SystemConfig.System_plugin_path))
.use(MainRoutes.routes())
.use(MainRoutes.allowedMethods())
.use(ErrorRoutes())
if (env === 'development') { // logger
app.use((ctx, next) => {
const start = new Date()
return next().then(() => {
const ms = new Date() - start
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})
})
}
app.listen(SystemConfig.API_server_port)
console.log('Now start API server on port ' + SystemConfig.API_server_port + '...')
db.sequelize.authenticate()
.then(() => {
console.log('資料庫連接成功')
})
.catch((error) => {
console.error(`資料庫連接錯誤:${error}`)
})
db.sequelize.sync()
export default app
+39
View File
@@ -0,0 +1,39 @@
import path from 'path'
// 系统配置
export const System = {
API_server_type: 'http://', // API服务器协议类型,包含"http://"或"https://"
API_server_host: 'localhost', // API服务器暴露的域名地址,请勿添加"http://"
API_server_port: '3000', // API服务器监听的端口号
HTTP_server_type: 'http://', // HTTP服务器协议类型,包含"http://"或"https://"
HTTP_server_host: 'www.XXX.com', // HTTP服务器地址,请勿添加"http://" (即前端调用使用的服务器地址,如果是APP请设置为 * )
HTTP_server_port: '65534', // HTTP服务器端口号
System_country: 'zh-cn', // 所在国家的国家代码
System_plugin_path: path.join(__dirname, './plugins'), // 插件路径
Session_Key: 'RESTfulAPI', // 生产环境务必随机设置一个值
db_type: 'mysql' // 数据库类型
}
export const DB = {
host: 'localhost', // 服务器地址
port: 3306, // 数据库端口号
username: 'admin', // 数据库用户名
password: 'admin888', // 数据库密码
database: 'development', // 数据库名称
prefix: 'api_' // 默认"api_"
}
export const pgDB = {
host: '127.0.0.1', // 服务器地址
port: 54321, // 数据库端口号
username: 'biopro', // 数据库用户名
password: 'BioProControlBox', // 数据库密码
database: 'postgres' // 数据库名称
}
export const SendEmail = {
service: 'smtp.abcd.com', // SMTP服务提供商域名
username: 'postmaster%40abcd.com', // 用户名/用户邮箱
password: 'password', // 邮箱密码
sender_address: '"XX平台 👥" <postmaster@abcd.com>'
}
+39
View File
@@ -0,0 +1,39 @@
export const Get = async (ctx, next) => {
ctx.body = {
result: 'get',
name: ctx.params.name,
para: ctx.query
}
next()
}
export const Post = async (ctx, next) => {
ctx.body = {
result: 'post',
name: ctx.params.name,
para: ctx.request.body
}
next()
}
export const Put = (ctx, next) => {
ctx.body = {
result: 'put',
name: ctx.params.name,
para: ctx.request.body
}
next()
}
export const Delete = (ctx, next) => {
ctx.body = {
result: 'delete',
name: ctx.params.name,
para: ctx.request.body
}
next()
}
+49
View File
@@ -0,0 +1,49 @@
import jwt from 'jsonwebtoken'
import fs from 'fs'
import path from 'path'
const publicKey = fs.readFileSync(path.join(__dirname, '../../publicKey.pub'))
// 用户登录的时候返回token
// let token = jwt.sign({
// userInfo: userInfo // 你要保存到token的数据
// }, publicKey, { expiresIn: '7d' })
/**
* 检查授权是否合法
*/
const CheckAuth = (ctx, next) => {
const token = ctx.request.header.authorization
try {
const decoded = jwt.verify(token.substr(7), publicKey)
if (decoded.userInfo) {
return {
status: 1,
result: decoded.userInfo
}
} else {
return {
status: 403,
result: {
errInfo: '没有授权'
}
}
}
} catch (err) {
return {
status: 503,
result: {
errInfo: '解密错误'
}
}
}
}
export const Post = (ctx, next) => {
switch (ctx.params.action) {
case 'check':
return CheckAuth(ctx).then(result => { ctx.body = result; next() })
default:
return CheckAuth(ctx).then(result => { ctx.body = result; next() })
}
}
+9
View File
@@ -0,0 +1,9 @@
import upload from './upload'
import * as api from './api'
import * as auth from './auth'
export default {
upload,
api,
auth
}
+68
View File
@@ -0,0 +1,68 @@
import fs from 'fs'
import path from 'path'
import { System as SystemConfig } from '../config'
export default (ctx) => {
// 设置允许跨域的域名称
ctx.set('Access-Control-Allow-Origin', '*')
ctx.set('Access-Control-Allow-Headers', 'X-Requested-With')
ctx.set('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS')
// ----- 情况1:跨域时,先发送一个options请求,此处要返回200 -----
if (ctx.method === 'OPTIONS') {
console.log('options 请求时,返回 200')
// 返回结果
ctx.status = 200
ctx.body = 'options OK'
return
}
// ----- 情况2:发送post请求,上传图片 -----
// 处理 request
console.log('parse ok')
// 文件将要上传到哪个文件夹下面
var uploadfolderpath = path.join(__dirname, '../../assets/uploads')
var files = ctx.request.body.files
// formidable 会将上传的文件存储为一个临时文件,现在获取这个文件的目录
var tempfilepath = files.file.path
// 获取文件类型
var type = files.file.type
// 获取文件名,并根据文件名获取扩展名
var filename = files.file.name
var extname = filename.lastIndexOf('.') >= 0 ? filename.slice(filename.lastIndexOf('.') - filename.length) : ''
// 文件名没有扩展名时候,则从文件类型中取扩展名
if (extname === '' && type.indexOf('/') >= 0) {
extname = '.' + type.split('/')[1]
}
// 将文件名重新赋值为一个随机数(避免文件重名)
filename = Math.random().toString().slice(2) + extname
// 构建将要存储的文件的路径
var filenewpath = path.join(uploadfolderpath, filename)
var result = ''
// 将临时文件保存为正式的文件
try {
fs.renameSync(tempfilepath, filenewpath)
} catch (err) {
if (err) {
// 发生错误
console.log('fs.rename err')
result = 'error|save error'
}
}
// 保存成功
console.log('fs.rename done')
// 拼接url地址
result = SystemConfig.API_server_type + SystemConfig.API_server_host + ':' + SystemConfig.API_server_port + '/assets/uploads' + filename
// 返回结果
ctx.body = result
}
+15
View File
@@ -0,0 +1,15 @@
module.exports = (sequelize, Sequelize) => {
const Tutorial = sequelize.define('tutorial', {
title: {
type: Sequelize.STRING
},
description: {
type: Sequelize.STRING
},
published: {
type: Sequelize.BOOLEAN
}
})
return Tutorial
}
+33
View File
@@ -0,0 +1,33 @@
import fs from 'fs'
import path from 'path'
import compose from 'koa-compose'
function getDirs (srcpath) {
return fs.readdirSync(srcpath).filter(file => {
return fs.statSync(path.join(srcpath, file)).isDirectory()
})
}
module.exports = (srcpath, filename = 'index.js') => {
const plugins = {}
const dirs = getDirs(srcpath)
const list = []
for (const name of dirs) {
let fn = require(path.join(srcpath, name, filename))
if (typeof fn !== 'function' && typeof fn.default === 'function') {
fn = fn.default
} else {
throw (new Error('plugin must be a function!'))
}
plugins[name] = fn
list.push(function (ctx, next) {
return fn(ctx, next) || next()
})
}
return compose(list)
}
+26
View File
@@ -0,0 +1,26 @@
import { pgDB as DBConfig } from '../config'
const Sequelize = require('sequelize')
const sequelize = new Sequelize(
DBConfig.database,
DBConfig.username,
DBConfig.password,
{
host: DBConfig.host,
port: DBConfig.port,
dialect: 'postgres',
pool: {
max: 50,
min: 0,
idle: 10000
}
})
const db = {}
db.Sequelize = Sequelize
db.sequelize = sequelize
db.tutorials = require('../db/schema/tutirial.model')(sequelize, Sequelize)
module.exports = db
+20
View File
@@ -0,0 +1,20 @@
module.exports = function () {
return function (ctx, next) {
return next().catch((err) => {
switch (err.status) {
case 401:
ctx.status = 200
ctx.body = {
status: 401,
result: {
err: 'Authentication Error',
errInfo: 'Protected resource, use Authorization header to get access.'
}
}
break
default:
throw err
}
})
}
}
+2
View File
@@ -0,0 +1,2 @@
const requireDirectory = require('require-directory')
module.exports = requireDirectory(module)
+43
View File
@@ -0,0 +1,43 @@
import nodemailer from 'nodemailer'
import { SendEmail } from '../../config'
export default () => {
// console.log('ok')
}
// 发送Email(目前使用的是阿里云SMTP发送邮件)
// receivers 目标邮箱,可以用英文逗号分隔多个。(我没试过)
// subject 邮件标题
// text 文本版本的邮件内容
// html HTML版本的邮件内容
// 返回
// result 200是成功,500是失败
// info 是返回的消息,可能是结果的文本,也可能是对象。(这个错误不要暴露给用户)
export const sendemail = (receivers, subject, text, html) => {
return new Promise(function (resolve) {
const transporter = nodemailer.createTransport('smtp://' + SendEmail.username + ':' + SendEmail.password + '@' + SendEmail.service)
// setup e-mail data with unicode symbols
const mailOptions = {
from: SendEmail.sender_address, // sender address
to: receivers,
subject: subject,
text: text || 'Hello world 🐴', // plaintext body
html: html || '<b>Hello world 🐴</b>' // html body
}
transporter.sendMail(mailOptions, function (error, info) {
if (error) {
resolve({
result: 500,
info: error
})
} else {
resolve({
result: 200,
info: info.response
})
}
})
})
}
+10
View File
@@ -0,0 +1,10 @@
module.exports = function () {
return function (ctx, next) {
switch (ctx.status) {
case 404:
ctx.body = '没有找到内容 - 404'
break
}
return next()
}
}
+15
View File
@@ -0,0 +1,15 @@
import KoaRouter from 'koa-router'
import controllers from '../controllers'
const router = new KoaRouter()
export default router
.get('/public/get', function (ctx, next) {
ctx.body = '禁止访问!'
}) // 以/public开头则不经过权限认证
.all('/upload', controllers.upload)
.get('/public/api/:name', controllers.api.Get)
.post('/api/:name', controllers.api.Post)
.put('/api/:name', controllers.api.Put)
.del('/api/:name', controllers.api.Delete)
.post('/auth/:action', controllers.auth.Post)
+1
View File
@@ -0,0 +1 @@
1
+63
View File
@@ -0,0 +1,63 @@
import {
SystemConfig
} from '../config'
// 截取字符串,多余的部分用...代替
export const setString = (str, len) => {
let StrLen = 0
let s = ''
for (let i = 0; i < str.length; i++) {
if (str.charCodeAt(i) > 128) {
StrLen += 2
} else {
StrLen++
}
s += str.charAt(i)
if (StrLen >= len) {
return s + '...'
}
}
return s
}
// 格式化设置
export const OptionFormat = (GetOptions) => {
let options = '{'
for (let n = 0; n < GetOptions.length; n++) {
options = options + '\'' + GetOptions[n].option_name + '\':\'' + GetOptions[n].option_value + '\''
if (n < GetOptions.length - 1) {
options = options + ','
}
}
return JSON.parse(options + '}')
}
// 替换SQL字符串中的前缀
export const SqlFormat = (str) => {
if (SystemConfig.mysql_prefix !== 'api_') {
str = str.replace(/api_/g, SystemConfig.mysql_prefix)
}
return str
}
// 数组去重
export const HovercUnique = (arr) => {
const n = {}
const r = []
for (var i = 0; i < arr.length; i++) {
if (!n[arr[i]]) {
n[arr[i]] = true
r.push(arr[i])
}
}
return r
}
// 获取json长度
export const getJsonLength = (jsonData) => {
var arr = []
for (var item in jsonData) {
arr.push(jsonData[item])
}
return arr.length
}
+7
View File
@@ -0,0 +1,7 @@
import app from '../src/app'
describe('Example', () => {
test('should be defined', () => {
expect(app).toBeDefined();
})
})
+7690
View File
File diff suppressed because it is too large Load Diff