v0.0.1
This commit is contained in:
commit
878d9b63f6
4
.babelrc
Normal file
4
.babelrc
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"presets": ["es2015", "stage-2"],
|
||||
"plugins": ["transform-runtime"]
|
||||
}
|
9
.editorconfig
Normal file
9
.editorconfig
Normal 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
|
2
.eslintignore
Normal file
2
.eslintignore
Normal file
@ -0,0 +1,2 @@
|
||||
build/*.js
|
||||
config/*.js
|
25
.eslintrc.js
Normal file
25
.eslintrc.js
Normal file
@ -0,0 +1,25 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parserOptions: {
|
||||
ecmaVersion: 6, //指定ECMAScript支持的版本,6为ES6
|
||||
sourceType: 'module'
|
||||
},
|
||||
extends: 'standard',
|
||||
plugins: [
|
||||
'html',
|
||||
"promise"
|
||||
],
|
||||
env: {
|
||||
"node": true
|
||||
},
|
||||
rules: { // add your custom rules here
|
||||
// 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
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
.DS_Store
|
||||
node_modules/
|
||||
dist/
|
||||
npm-debug.log
|
||||
test/unit/coverage
|
||||
test/e2e/reports
|
||||
selenium-debug.log
|
||||
.idea/
|
396
README.md
Normal file
396
README.md
Normal file
@ -0,0 +1,396 @@
|
||||
# Koa2 RESTful API 服务器脚手架
|
||||
|
||||
这是一个基于Koa2的轻量级Resultful API Server脚手架,支持ES6。
|
||||
|
||||
约定使用JSON格式传输数据,POST、PUT、DELET方法支持的Content-Type为application/x-www-form-urlencoded和multipart/form-data、application/json,可配置支持跨域。非上传文件推荐application/x-www-form-urlencoded。通常情况下返回application/json格式的JSON数据。
|
||||
|
||||
可选用mongodb、redis非关系型数据库和PostgreSQL, MySQL, MariaDB, SQLite, MSSQL关系型数据库,考虑RESTful API Server的实际开发需要,这里通过sequelize.js作为ORM,同时提通过Promise执行SQL直接操作Mysql数据库的方法(不管什么方法,注意安全哦)。
|
||||
|
||||
此脚手架只安装了一些和Koa2不冲突的搭建RESTful API Server的必要插件,附带每一个插件的说明。采用ESlint进行语法检查。
|
||||
|
||||
因此脚手架主要提供RESTful API,故暂时不考虑前端静态资源处理。基本目录结构与vue-cli保持一致,可配合React、AngularJS、Vue.js等前端框架使用。在Cordova/PhoneGap中使用时需要开启跨域功能。
|
||||
|
||||
**免责声明:** 此脚手架仅为方便开发提供基础环境,任何人或组织均可随意克隆使用,使用引入的框架需遵循原作者规定的相关协议(框架列表及来源地址在下方)。采用此脚手架产生的任何后果请自行承担,本人不对此脚手架负任何法律责任,使用即代表同意此条。
|
||||
|
||||
目前暂未加入软件测试模块,下一个版本会加入该功能并提供集成方案。
|
||||
|
||||
China大陆用户请自行优化网络。
|
||||
|
||||
## 开发使用说明
|
||||
```
|
||||
$ git clone https://github.com/yi-ge/koa2-API-scaffold.git
|
||||
|
||||
$ cd mv koa2-API-scaffold
|
||||
$ npm install
|
||||
$ npm run dev #可执行npm start跳过ESlint检查。
|
||||
```
|
||||
访问: http://127.0.0.1:3000/
|
||||
|
||||
另外一种方式,如果你使用git仓库管理你的代码:
|
||||
|
||||
```
|
||||
$ git clone https://github.com/yi-ge/koa2-API-scaffold.git
|
||||
|
||||
$ mv koa2-API-scaffold `您的项目名称`
|
||||
$ cd `您的项目名称`
|
||||
$ rm -rf .git
|
||||
$ git init
|
||||
$ git remote add origin `您的git仓库地址`
|
||||
$ npm install
|
||||
$ npm run dev #可执行npm start跳过ESlint检查。
|
||||
```
|
||||
|
||||
## 调试说明
|
||||
|
||||
```
|
||||
$ npm start --debug
|
||||
|
||||
Or
|
||||
|
||||
$ npm start --debug
|
||||
```
|
||||
|
||||
支持Node.js原生调试功能:https://nodejs.org/api/debugger.html
|
||||
|
||||
## 开发环境部署
|
||||
|
||||
生成node直接可以执行的代码到dist目录:
|
||||
```
|
||||
$ npm run build
|
||||
```
|
||||
|
||||
```
|
||||
$ npm run production # 生产模式运行
|
||||
|
||||
Or
|
||||
|
||||
$ node dist/app.js
|
||||
```
|
||||
|
||||
### PM2部署说明
|
||||
提供了PM2部署RESTful API Server的示例配置,位于“pm2.js”文件中。
|
||||
```
|
||||
$ pm2 start pm2.js
|
||||
```
|
||||
|
||||
PM2配合Docker部署说明:
|
||||
http://pm2.keymetrics.io/docs/usage/docker-pm2-nodejs/
|
||||
|
||||
### Docker部署说明
|
||||
```
|
||||
$ docker pull node
|
||||
$ docker run -itd --name RESTfulAPI -v "$PWD":/usr/src/app -w /usr/src/app node node ./dist/app.js
|
||||
```
|
||||
|
||||
通过
|
||||
```
|
||||
$ docker ps
|
||||
```
|
||||
查看是否运行成功及运行状态
|
||||
|
||||
### Linux/Mac 直接后台运行生产环境代码
|
||||
有时候为了简单,我们也这样做:
|
||||
```
|
||||
$ nohup node ./dist/app.js > logs/out.log &
|
||||
```
|
||||
|
||||
查看运行状态(如果有"node app.js"出现则说明正在后台运行):
|
||||
```
|
||||
$ ps aux|grep app.js
|
||||
```
|
||||
|
||||
查看运行日志
|
||||
```
|
||||
$ cat logs/out.log
|
||||
```
|
||||
|
||||
监控运行状态
|
||||
```
|
||||
$ tail -f logs/out.log
|
||||
```
|
||||
|
||||
### 配合Vue-cli部署说明
|
||||
Vue-cli(Vue2)运行"npm run build"后会在"dist"目录中生成所有静态资源文件。推荐使用Nginx处理静态资源以达最佳利用效果,然后通过上述任意一种方法部署RESTful API服务器。前后端是完全分离的,请注意Koa2 RESTful API Server项目中config/main.json里面的跨域配置。
|
||||
|
||||
推荐的Nginx配置文件:
|
||||
```
|
||||
server
|
||||
{
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name abc.com www.abc.com; #绑定域名
|
||||
index index.html index.htm;
|
||||
root /www/app/dist; #Vue-cli编译后的dist目录
|
||||
|
||||
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
|
||||
{
|
||||
expires 30d;
|
||||
}
|
||||
|
||||
location ~ .*\.(js|css)?$
|
||||
{
|
||||
expires 12h;
|
||||
}
|
||||
|
||||
location ~ /\.
|
||||
{
|
||||
deny all;
|
||||
}
|
||||
|
||||
access_log off; #访问日志路径
|
||||
}
|
||||
```
|
||||
Docker中Nginx运行命令(将上述配置文件任意命名放置于nginx_config目录中即可):
|
||||
```
|
||||
$ docker run -itd -p 80:80 -p 443:443 -v `pwd`/nginx_config:/etc/nginx/conf.d nginx
|
||||
```
|
||||
|
||||
## 引入插件介绍
|
||||
|
||||
> 引入插件的版本将会持续更新
|
||||
|
||||
引入的插件:
|
||||
`` koa nodemon babel-cli babel-register babel-plugin-transform-runtime babel-preset-es2015 babel-preset-stage-2 gulp gulp-eslint eslint eslint-config-standard eslint-friendly-formatter eslint-plugin-html eslint-plugin-promise ``
|
||||
|
||||
"koa-bodyparser": "^3.2.0",
|
||||
"koa-compose": "^3.1.0",
|
||||
"koa-convert": "^1.2.0",
|
||||
"koa-favicon": "^2.0.0",
|
||||
"koa-json": "^1.1.3",
|
||||
"koa-logger": "^1.3.0",
|
||||
"koa-multer": "^1.0.0",
|
||||
"koa-router": "^7.0.1",
|
||||
"koa-session": "^3.3.1",
|
||||
"koa-static2": "^0.1.8",
|
||||
"koa-onerror": "^3.0.1",
|
||||
|
||||
**koa**: HTTP框架
|
||||
Synopsis: HTTP framework.
|
||||
From: https://github.com/koajs/koa
|
||||
|
||||
**babel-cli**: Babel编译ES6代码为ES5代码
|
||||
Synopsis: Babel is a JavaScript compiler, ES6 to ES5.
|
||||
From: https://github.com/babel/babel/tree/master/packages/babel-cli
|
||||
|
||||
**babel-register**: Babel开发环境实时编译ES6代码
|
||||
Synopsis: Babel hook.
|
||||
From: https://github.com/babel/babel/tree/master/packages/babel-cli
|
||||
|
||||
**babel-plugin-transform-runtime**: Babel配置ES6的依赖项
|
||||
**babel-preset-es2015**: 同上
|
||||
**babel-preset-stage-2**: 同上
|
||||
|
||||
**gulp**: 基于流的自动化构建工具
|
||||
Synopsis: Gulp is a toolkit for automating painful or time-consuming tasks.
|
||||
From: https://github.com/gulpjs/gulp
|
||||
|
||||
**gulp-eslint**: gulp的ESLint检查插件
|
||||
Synopsis: A gulp plugin for ESLint.
|
||||
From: https://github.com/adametry/gulp-eslint
|
||||
|
||||
**gulp-nodemon**: 修改JS代码后自动重启
|
||||
Synopsis: nodemon will watch the files in the directory in which nodemon was started, and if any files change, nodemon will automatically restart your node application.
|
||||
From: https://github.com/remy/nodemon
|
||||
|
||||
**eslint**: JavaScript语法检查工具
|
||||
Synopsis: A fully pluggable tool for identifying and reporting on patterns in JavaScript.
|
||||
From:
|
||||
|
||||
**eslint-config-standard**: 一个ESlint配置
|
||||
Synopsis: ESLint Shareable Config for JavaScript Standard Style.
|
||||
From: https://github.com/feross/eslint-config-standard
|
||||
|
||||
**eslint-friendly-formatter**: 使得ESlint提示在Sublime Text或iterm2中更友好,Atom也有对应的ESlint插件。
|
||||
Synopsis: A simple formatter/reporter for ESLint that's friendly with Sublime Text and iterm2 "click to open file" functionality
|
||||
From: https://github.com/royriojas/eslint-friendly-formatter
|
||||
|
||||
**eslint-plugin-html**: 检查HTML文件中的JS代码规范
|
||||
Synopsis: An ESLint plugin to extract and lint scripts from HTML files.
|
||||
From: https://github.com/BenoitZugmeyer/eslint-plugin-html
|
||||
|
||||
**eslint-plugin-promise**: 检查JavaScript promises
|
||||
Synopsis: Enforce best practices for JavaScript promises.
|
||||
From: https://github.com/xjamundx/eslint-plugin-promise
|
||||
|
||||
**eslint-plugin-promise**: ESlint依赖项
|
||||
Synopsis: ESlint Rules for the Standard Linter.
|
||||
From: https://github.com/xjamundx/eslint-plugin-standard
|
||||
|
||||
支持Koa2的中间件列表:https://github.com/koajs/koa/wiki
|
||||
|
||||
其它经常配合Koa2的插件:
|
||||
**koa-nunjucks-2**:
|
||||
一个好用的模版引擎,可用于前后端,nunjucks。https://github.com/mozilla/nunjucks
|
||||
|
||||
## 目录结构说明
|
||||
|
||||
```bash
|
||||
.
|
||||
├── README.md
|
||||
├── .babelrc # Babel 配置文件
|
||||
├── .editorconfig # 编辑器风格定义文件
|
||||
├── .eslintignore # ESlint 忽略文件列表
|
||||
├── .eslintrc.js # ESlint 配置文件
|
||||
├── .gitignore # Git 忽略文件列表
|
||||
├── gulpfile.js # Gulp配置文件
|
||||
├── package.json # 描述文件
|
||||
├── pm2.js # pm2 部署示例文件
|
||||
├── build # build 入口目录
|
||||
│ └── dev-server.js # 开发环境 Babel 实时编译入口
|
||||
├── config
|
||||
│ └── main.js # 主配置文件(*谨防泄密!)
|
||||
├── src # 源代码目录,编译后目标源代码位于 dist 目录
|
||||
│ ├── app.js # koa 配置
|
||||
│ ├── config # 配置目录
|
||||
│ ├── controllers # 控制器
|
||||
│ ├── index.js # 入口文件
|
||||
│ ├── models # 模型
|
||||
│ ├── routes # 路由
|
||||
│ └── services # service
|
||||
└── logs # 日志目录
|
||||
```
|
||||
|
||||
## 各类主流框架调用RESTful API的示例代码(仅供参考)
|
||||
|
||||
### AngularJS (Ionic同)
|
||||
```
|
||||
$http({
|
||||
method: "post",
|
||||
url: "http://localhost:3000/xxx",
|
||||
data: {para1:"para1",para2:"para2"},
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
}).success(function (data) {
|
||||
}).error(function (data) {
|
||||
});
|
||||
```
|
||||
|
||||
### jQuery
|
||||
```
|
||||
$.ajax({
|
||||
cache: false,
|
||||
type: 'POST',
|
||||
url: "http://localhost:3000/xxx",
|
||||
data: {
|
||||
para1: para1
|
||||
},
|
||||
async: false,
|
||||
dataType: "json",
|
||||
success: function (result) {
|
||||
},
|
||||
error: function (err) {
|
||||
console.log(err);
|
||||
}
|
||||
});
|
||||
|
||||
// 上传文件
|
||||
//创建FormData对象
|
||||
var data = new FormData();
|
||||
//为FormData对象添加数据
|
||||
//
|
||||
$.each($('#inputfile')[0].files, function (i, file) {
|
||||
data.append('upload_file', file);
|
||||
});
|
||||
$.ajax({
|
||||
url: 'http://127.0.0.1:3000/api/upload_oss_img_demo',
|
||||
type: 'POST',
|
||||
data: data,
|
||||
cache: false,
|
||||
contentType: false, //不可缺
|
||||
processData: false, //不可缺
|
||||
success: function (data) {
|
||||
console.log(data);
|
||||
if (data.result == "ok") {
|
||||
$("#zzzz").attr("src", data.img_url);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### MUI
|
||||
```
|
||||
mui.ajax({ url: "http://localhost:3000/xxx", dataType: "json",
|
||||
success: function(data){
|
||||
|
||||
},
|
||||
error: function(data){
|
||||
console.log("error!");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### JavaScript
|
||||
```
|
||||
var xhr = new XMLHttpRequest()
|
||||
xhr.open("POST", "http://localhost:3000/xxx", true) //POST或GET,true(异步)或 false(同步)
|
||||
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded")
|
||||
xhr.withCredentials = true
|
||||
xhr.onreadystatechange = function () {
|
||||
if (obj.readyState == 4 && obj.status == 200 || obj.status == 304) {
|
||||
var gotServices = JSON.parse(xhr.responseText)
|
||||
}else{
|
||||
console.log("ajax失败了")
|
||||
}
|
||||
}
|
||||
xhr.send({para1: para1})
|
||||
```
|
||||
|
||||
### vue-resource
|
||||
https://github.com/pagekit/vue-resource
|
||||
```
|
||||
// global Vue object
|
||||
Vue.http.post('/someUrl', [body], {
|
||||
headers: {"Content-type", "application/x-www-form-urlencoded"}
|
||||
}).then(successCallback, errorCallback);
|
||||
```
|
||||
|
||||
### fetch
|
||||
https://github.com/github/fetch
|
||||
```
|
||||
fetch('/users', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: 'Hubot',
|
||||
login: 'hubot',
|
||||
})
|
||||
}).then(function(response) {
|
||||
// response.text()
|
||||
}).then(function(body) {
|
||||
// body
|
||||
})
|
||||
|
||||
// 文件上传
|
||||
var input = document.querySelector('input[type="file"]')
|
||||
|
||||
var data = new FormData()
|
||||
data.append('file', input.files[0])
|
||||
data.append('user', 'hubot')
|
||||
|
||||
fetch('/avatars', {
|
||||
method: 'POST',
|
||||
body: data
|
||||
})
|
||||
```
|
||||
|
||||
### superagent
|
||||
https://github.com/visionmedia/superagent
|
||||
```
|
||||
request.post('/user')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send('{"name":"tj","pet":"tobi"}')
|
||||
.end(callback)
|
||||
```
|
||||
|
||||
在React中可以将上述任意方法其置于componentDidMount()中,Vue.js同理。
|
||||
|
||||
## 彻底移除ESlint方法
|
||||
删除package.json的devDependencies中所有eslint开头的插件,根目录下的“.eslintignore、.eslintrc.js”文件,并且修改package.json的dev为:
|
||||
```
|
||||
"dev": "gulp start"
|
||||
```
|
||||
删除gulpfile.js中的lint、eslint_start两个任务,并且把default改为“gulp.task('default', ['start']”。
|
2
build/dev-server.js
Normal file
2
build/dev-server.js
Normal file
@ -0,0 +1,2 @@
|
||||
require("babel-register")
|
||||
require("../src/app")
|
0
config/main.js
Normal file
0
config/main.js
Normal file
66
gulpfile.js
Normal file
66
gulpfile.js
Normal file
@ -0,0 +1,66 @@
|
||||
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.indexOf('debug') > 0) {
|
||||
jsScript = 'node debug'
|
||||
}
|
||||
|
||||
gulp.task('lint', () => {
|
||||
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_start', ['lint'], function () {
|
||||
var stream = nodemon({
|
||||
script: 'build/dev-server.js',
|
||||
execMap: {
|
||||
js: jsScript
|
||||
},
|
||||
tasks: ['lint'],
|
||||
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('start', 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', ['lint', 'eslint_start'], function () {
|
||||
// console.log('ESlin检查完成')
|
||||
})
|
1
logs/out.log
Normal file
1
logs/out.log
Normal file
@ -0,0 +1 @@
|
||||
ok
|
35
package.json
Normal file
35
package.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "koa2-API-scaffold",
|
||||
"version": "1.0.0",
|
||||
"description": "Koa2 RESTful API 服务器的脚手架",
|
||||
"author": "轶哥 <a@wyr.me>",
|
||||
"scripts": {
|
||||
"start": "gulp start",
|
||||
"dev": "gulp",
|
||||
"build": "babel src -d dist",
|
||||
"production": "node dist/app.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"koa": "^1.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.22.2",
|
||||
"babel-plugin-transform-runtime": "^6.22.0",
|
||||
"babel-preset-es2015": "^6.22.0",
|
||||
"babel-preset-stage-2": "^6.22.0",
|
||||
"babel-register": "^6.22.0",
|
||||
"eslint": "^3.14.0",
|
||||
"eslint-config-standard": "^6.2.1",
|
||||
"eslint-friendly-formatter": "^2.0.7",
|
||||
"eslint-plugin-html": "^1.7.0",
|
||||
"eslint-plugin-promise": "^3.4.0",
|
||||
"eslint-plugin-standard": "^2.0.1",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-eslint": "^3.0.1",
|
||||
"gulp-nodemon": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.9.4",
|
||||
"npm": ">= 3.10.10"
|
||||
}
|
||||
}
|
17
pm2.js
Normal file
17
pm2.js
Normal 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 // 程序崩溃后自动重启
|
||||
}]
|
||||
}
|
3
src/app.js
Normal file
3
src/app.js
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
console.log('ok')
|
||||
console.log('aaa')
|
Loading…
Reference in New Issue
Block a user