<repo_name>Hoofoo-WHU/memo-server<file_sep>/src/server.ts
import AV = require('leanengine')

import app from '@/app'

AV.init({
  appId: process.env.LEANCLOUD_APP_ID!,
  appKey: process.env.LEANCLOUD_APP_KEY!,
  masterKey: process.env.LEANCLOUD_APP_MASTER_KEY!
});

(AV.Cloud as any).useMasterKey()

const PORT = parseInt(process.env.LEANCLOUD_APP_PORT || process.env.PORT || '3000')

app.listen(PORT, () => {
  console.log('Memo app is running on port:', PORT)

  // 注册全局未捕获异常处理器
  process.on('uncaughtException', (err) => {
    console.error('Caught exception:', err.stack)
  })

  process.on('unhandledRejection', (reason, p) => {
    console.error('Unhandled Rejection at: Promise ', p, ' reason: ', reason.stack)
  })
})
<file_sep>/docs/README.md
# 开发文档

## 项目目录
```
config
├── nodemon.config.json
├── webpack.config.js
src
├── app.ts
├── routes
│   ├── login.ts
│   └── memo.ts
├── server.ts
├── tslint.json
tsconfig.json
```

## 技术要点
### 使用nodemon自动重载
nodemon可以像node一样使用还可以监听项目内文件更新自动重载，但是使用typescript的就不那么方便了，需要将ts-node注册并监听*.ts文件
```shell
nodemon --inspect -r ts-node/register -r tsconfig-paths/register --config config/nodemon.config.json src/server.ts
```
由于tsc的编译bug会导致paths配置的相对路径不会被编译，从而使node找不到模块，注册tsconfig-path即可解决

### 打包
使用tsc构建会使node找不到模块，而且tsconfig-path在node无效，也没有提供命令行工具，又不想在服务端使用ts-node，所以产品打包改用webpack，其resolve路径的功能可以解决模块丢失问题。

### lint
使用ts-lint来检查代码风格，配合vscode的ts-lint插件可以维持代码的可维护性

### 自动化流程
使用precommit-hook在commit前进行lint和build，使用GitHub-Hook使master更新后自动部署到leanengine<file_sep>/src/routes/login.ts
import Axios from 'axios'
import Router = require('koa-router')
import jwt = require('jsonwebtoken')
const router = new Router({ prefix: '/login' })

const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET

router.post('/github/:code', async (ctx) => {
  if (ctx.state.user) {
    ctx.body = {
      avatar: ctx.state.user.avatar,
      name: ctx.state.user.name,
      token: jwt.sign(
        {
          avatar: ctx.state.user.avatar,
          name: ctx.state.user.name,
          id: ctx.state.user.id
        },
        process.env.LEANCLOUD_APP_KEY!
      )
    }
    return
  }
  if (ctx.params.code === 'undefined') {
    ctx.status = 401
    return
  }
  const res = await Axios({
    data: {
      client_id: GITHUB_CLIENT_ID,
      client_secret: GITHUB_CLIENT_SECRET,
      code: ctx.params.code
    },
    headers: {
      Accept: 'application/json'
    },
    method: 'post',
    url: 'https://github.com/login/oauth/access_token'
  })
  if (res.data.error) {
    ctx.status = 401
    ctx.body = {
      error: res.data.error,
      error_description: res.data.error_description
    }
    return
  } else {
    const res2 = await Axios.get('https://api.github.com/user', {
      headers: {
        Authorization: `token ${res.data.access_token}`
      }
    })
    ctx.body = {
      avatar: res2.data.avatar_url,
      name: res2.data.name,
      token: jwt.sign(
        {
          avatar: res2.data.avatar_url,
          name: res2.data.name,
          id: res2.data.id
        },
        process.env.LEANCLOUD_APP_KEY!,
        { expiresIn: '2h' }
      )
    }
  }
})

export default router
<file_sep>/src/routes/memo.ts
import Router = require('koa-router')
import AV = require('leanengine')
const router = new Router()

router.prefix('/memo')
const Memo = AV.Object.extend('Memo')

router.get('/', async (ctx) => {
  const query = new AV.Query('Memo')
  query.equalTo('id', ctx.state.user.id)
  const data = await query.find()
  ctx.body = data
})

router.get('/:id', async (ctx) => {
  try {
    const query = new AV.Query('Memo')
    const data = await query.get(ctx.params.id)
    if (data.get('id') === ctx.state.user.id) {
      ctx.body = data
    } else {
      ctx.status = 403
    }
  } catch (e) {
    ctx.status = 404
  }
})

router.post('/', async (ctx) => {
  const memo = new Memo()
  memo.set('id', ctx.state.user.id)
  const data = await memo.save()
  ctx.body = data
})

router.patch('/:id', async (ctx) => {
  try {
    const query = new AV.Query('Memo')
    const memo = await query.get(ctx.params.id)
    if (memo.get('id') !== ctx.state.user.id) {
      ctx.status = 403
      return
    }
    Reflect.ownKeys((ctx.request as any).body).forEach((val) => {
      memo.set(val as string, (ctx.request as any).body[val])
    })
    const data = await memo.save()
    ctx.body = data
  } catch (e) {
    ctx.status = 400
  }
})

router.delete('/:id', async (ctx) => {
  try {
    const query = new AV.Query('Memo')
    const memo = await query.get(ctx.params.id)
    if (memo.get('id') !== ctx.state.user.id) {
      ctx.status = 403
      return
    }
    const data = await memo.destroy()
    ctx.body = data
  } catch (e) {
    ctx.status = 400
  }
})

export default router
<file_sep>/README.md
# memo-server
一个基于node.js的在线便签服务端

## 客户端项目
[Memo](https://github.com/Hoofoo-WHU/memo)

## 本地运行

首先确认本机已经安装 Node.js 运行环境

安装依赖
```shell
yarn install
```

启动项目
```shell
yarn dev
```

构建项目
```shell
yarn build
```

代码检查
```shell
yarn lint
```

使用leancloud启动项目，确保环境变量设置正确
```shell
brew install lean-cli -g
lean login
lean switch
lean up
```

Debug
```shell
chrome://inspect
```

## 相关文档
[开发文档](docs)

[API文档](docs/api.md)<file_sep>/src/app.ts
import login from '@/routes/login'
import memo from '@/routes/memo'
import Koa = require('koa')
import bodyParser = require('koa-bodyparser')
import convert = require('koa-convert')
import logger = require('koa-logger')
import Router = require('koa-router')
import AV = require('leanengine')
import jwt = require('koa-jwt')

const app = new Koa()
app.keys = ['memo']
app.use(convert(AV.koa() as any))
app.use(logger())
app.use(bodyParser())

app.use(async (ctx, next) => {
  ctx.set('Access-Control-Allow-Origin', ctx.header.origin as any)
  ctx.set('Access-Control-Allow-Methods', 'PATCH,OPTIONS,POST,GET,DELETE')
  ctx.set('Access-Control-Allow-Headers', 'Content-Type,Authorization')
  ctx.set('Access-Control-Allow-Credentials', 'true')
  await next()
})

app.use((ctx, next) => {
  if (ctx.method && /options/i.test(ctx.method)) {
    ctx.body = ''
    return
  }
  return next()
})

app.use(jwt({ secret: process.env.LEANCLOUD_APP_KEY! }).unless({ path: [/\/api\/login/] }))

const api = new Router({ prefix: '/api' })
api.use(memo.routes())
api.use(login.routes())
app.use(api.routes())

export default app
<file_sep>/docs/api.md
# API文档
## 响应格式
所有的响应格式均为application/json
## 登录
### 请求
```
POST /api/login/github/:code
```
其中code为GitHub OAuth提供的code

### 响应
403：登录失败
```json
{
  "error": "错误",
  "error_description": "错误描述"
}
```

200：登陆成功
```json
{
  "avatar": "GitHub头像Url",
  "name": "GitHub用户名"
}
```
## 获取所有便签
### 请求
```
GET /api/memo/
```
### 响应
403: 未登录

200: OK
```json
[
  {
    "position": {
        "x": 429,
        "y": 51,
        "z": -2147483597
    },
    "color": "pink",
    "text": "这是第一个标签",
    "objectId": "5c52be4a17b54d0057295f66",
    "createdAt": "2019-01-31T09:22:18.633Z",
    "updatedAt": "2019-01-31T15:13:10.819Z"
  }
]
```
## 根据id获取便签
### 请求
```
GET /api/memo/:id
```
其中id为objectId
### 响应
403: 没有权限访问或未登录

200: OK
```json
{
  "position": {
      "x": 429,
      "y": 51,
      "z": -2147483597
  },
  "color": "pink",
  "text": "这是第一个标签",
  "objectId": "5c52be4a17b54d0057295f66",
  "createdAt": "2019-01-31T09:22:18.633Z",
  "updatedAt": "2019-01-31T15:13:10.819Z"
}
```
## 新建标签
### 请求
```
POST /api/memo
```
### 响应
403: 未登录，没有权限

200: OK
```json
{
  "objectId": "5c52be4a17b54d0057295f66",
  "createdAt": "2019-01-31T09:22:18.633Z",
  "updatedAt": "2019-01-31T15:13:10.819Z"
}
```

## 更新标签
### 请求
```
PATCH /api/memo/:id
```
```json
{
  "position": {
      "x": 429,
      "y": 51,
      "z": -2147483597
  },
  "color": "pink",
  "text": "这是第一个标签修改后"
}
```
id为ObjectId，请求体参数可以任意组合
### 响应
403：没有权限

400: 请求错误

200：更新成功
## 删除标签
### 请求
```
DELETE /api/memo/:id
```
id为ObjectId
### 响应
400: 请求错误

403: 未登录或没有权限

200: 删除成功<file_sep>/config/webpack.config.js
const path = require('path')
const webpackModeExternals = require('webpack-node-externals')
const CleanWebpackPlugin = require('clean-webpack-plugin')
module.exports = {
  entry: path.resolve('src/server.ts'),
  mode: 'production',
  target: 'node',
  output: {
    path: path.resolve('dist'),
    filename: 'server.js'
  },
  resolve: {
    alias: {
      '@': path.resolve('src')
    },
    extensions: ['.ts']
  },
  externals: [
    webpackModeExternals()
  ],
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: [
          'ts-loader'
        ]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(path.resolve('dist'), { root: path.resolve('') })
  ]
}