diff --git a/config/config.go b/config/config.go index aaca350..b59b0f9 100644 --- a/config/config.go +++ b/config/config.go @@ -10,6 +10,7 @@ type ServerConfig struct { MySQL MySQLConfig `yaml:"mysql"` Redis RedisConfig `yaml:"redis"` Jwt JwtConfig `yaml:"jwt"` + Email EmailConfig `yaml:"email"` } type MySQLConfig struct { @@ -31,3 +32,10 @@ type JwtConfig struct { RenewExpireDays uint `yaml:"renewExpireDays"` RenewAheadDays uint `yaml:"renewAheadDays"` } + +type EmailConfig struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + Account string `yaml:"account"` + Password string `yaml:"password"` +} diff --git a/docs/docs.go b/docs/docs.go index cbb2cfc..6fe0310 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -16,6 +16,228 @@ const docTemplate_swagger = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/article": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Article" + ], + "summary": "save article", + "parameters": [ + { + "description": "article", + "name": "Article", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.BackArticle" + } + }, + { + "type": "string", + "description": "token", + "name": "Token", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.GinResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/models.BackArticle" + } + } + } + ] + } + } + } + } + }, + "/article/list": { + "get": { + "description": "Admin can get not published article", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Article" + ], + "summary": "get all articles", + "parameters": [ + { + "type": "string", + "description": "token", + "name": "Token", + "in": "header" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.GinResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.BackArticle" + } + } + } + } + ] + } + } + } + } + }, + "/article/{id}": { + "get": { + "description": "Admin can get not published article", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Article" + ], + "summary": "get all articles", + "parameters": [ + { + "type": "string", + "description": "token", + "name": "Token", + "in": "header" + }, + { + "type": "string", + "description": "id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.GinResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/models.BackArticle" + } + } + } + ] + } + } + } + }, + "delete": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Article" + ], + "summary": "delete an article", + "parameters": [ + { + "type": "string", + "description": "token", + "name": "Token", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/utils.GinResponse" + } + } + } + } + }, + "/article/{id}/publish": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Article" + ], + "summary": "get all articles", + "parameters": [ + { + "type": "string", + "description": "token", + "name": "Token", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/utils.GinResponse" + } + } + } + } + }, "/statistics/china": { "get": { "produces": [ @@ -31,7 +253,7 @@ const docTemplate_swagger = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/models.GinResponse" + "$ref": "#/definitions/utils.GinResponse" }, { "type": "object", @@ -76,7 +298,7 @@ const docTemplate_swagger = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/models.GinResponse" + "$ref": "#/definitions/utils.GinResponse" }, { "type": "object", @@ -110,7 +332,7 @@ const docTemplate_swagger = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/models.GinResponse" + "$ref": "#/definitions/utils.GinResponse" }, { "type": "object", @@ -144,7 +366,7 @@ const docTemplate_swagger = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/models.GinResponse" + "$ref": "#/definitions/utils.GinResponse" }, { "type": "object", @@ -192,7 +414,7 @@ const docTemplate_swagger = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/models.GinResponse" + "$ref": "#/definitions/utils.GinResponse" }, { "type": "object", @@ -210,6 +432,236 @@ const docTemplate_swagger = `{ } } } + }, + "/user/approve": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "admin approve account, user can use account after approved", + "parameters": [ + { + "type": "string", + "description": "token", + "name": "Token", + "in": "header", + "required": true + }, + { + "description": "json", + "name": "json", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UserApprove" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/utils.GinResponse" + } + } + } + } + }, + "/user/chpwd": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "change user's password", + "parameters": [ + { + "description": "json", + "name": "json", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UserChangePwd" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/utils.GinResponse" + } + } + } + } + }, + "/user/code/{email}/{code}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "send verify code", + "parameters": [ + { + "type": "string", + "description": "email", + "name": "email", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "code", + "name": "code", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/utils.GinResponse" + } + } + } + } + }, + "/user/login": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "user login", + "parameters": [ + { + "description": "json", + "name": "json", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UserLogin" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/utils.GinResponse" + } + } + } + } + }, + "/user/register": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "user register account", + "parameters": [ + { + "description": "json", + "name": "json", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UserRegister" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/utils.GinResponse" + } + } + } + } + }, + "/user/registers": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "list register infos, which is to be approved", + "parameters": [ + { + "type": "string", + "description": "token", + "name": "Token", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/utils.GinResponse" + } + } + } + } + }, + "/user/{code}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "send verify code", + "parameters": [ + { + "type": "string", + "description": "email", + "name": "email", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/utils.GinResponse" + } + } + } + } } }, "definitions": { @@ -273,6 +725,55 @@ const docTemplate_swagger = `{ } } }, + "models.BackArticle": { + "type": "object", + "properties": { + "content": { + "description": "文章内容(如有需要可迁移至对象存储)", + "type": "string" + }, + "cover": { + "description": "文章封面", + "type": "string" + }, + "createTime": { + "description": "文章新建时间", + "type": "string" + }, + "createUser": { + "description": "文章创建者id", + "type": "string" + }, + "isDelete": { + "description": "删除标志", + "type": "integer" + }, + "isPublish": { + "description": "发布状态(0:未发布, 1: 发布)", + "type": "integer" + }, + "modifyTime": { + "description": "文章最后更新时间", + "type": "string" + }, + "modifyUser": { + "description": "文章最后更新者id", + "type": "string" + }, + "resume": { + "description": "文章简述", + "type": "string" + }, + "tags": { + "description": "文章Tag", + "type": "string" + }, + "title": { + "description": "文章标题", + "type": "string" + } + } + }, "models.ChinaAdd": { "type": "object", "properties": { @@ -369,7 +870,63 @@ const docTemplate_swagger = `{ } } }, - "models.GinResponse": { + "models.UserApprove": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "pass": { + "type": "boolean" + } + } + }, + "models.UserChangePwd": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "email": { + "type": "string" + }, + "newPassword": { + "type": "string" + } + } + }, + "models.UserLogin": { + "type": "object", + "properties": { + "account": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "models.UserRegister": { + "type": "object", + "properties": { + "aptitude": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "utils.GinResponse": { "type": "object", "properties": { "code": { diff --git a/docs/swagger.json b/docs/swagger.json index 49eb984..620d5a3 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -6,6 +6,228 @@ "version": "1.0" }, "paths": { + "/article": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Article" + ], + "summary": "save article", + "parameters": [ + { + "description": "article", + "name": "Article", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.BackArticle" + } + }, + { + "type": "string", + "description": "token", + "name": "Token", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.GinResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/models.BackArticle" + } + } + } + ] + } + } + } + } + }, + "/article/list": { + "get": { + "description": "Admin can get not published article", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Article" + ], + "summary": "get all articles", + "parameters": [ + { + "type": "string", + "description": "token", + "name": "Token", + "in": "header" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.GinResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.BackArticle" + } + } + } + } + ] + } + } + } + } + }, + "/article/{id}": { + "get": { + "description": "Admin can get not published article", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Article" + ], + "summary": "get all articles", + "parameters": [ + { + "type": "string", + "description": "token", + "name": "Token", + "in": "header" + }, + { + "type": "string", + "description": "id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/utils.GinResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/models.BackArticle" + } + } + } + ] + } + } + } + }, + "delete": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Article" + ], + "summary": "delete an article", + "parameters": [ + { + "type": "string", + "description": "token", + "name": "Token", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/utils.GinResponse" + } + } + } + } + }, + "/article/{id}/publish": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Article" + ], + "summary": "get all articles", + "parameters": [ + { + "type": "string", + "description": "token", + "name": "Token", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/utils.GinResponse" + } + } + } + } + }, "/statistics/china": { "get": { "produces": [ @@ -21,7 +243,7 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/models.GinResponse" + "$ref": "#/definitions/utils.GinResponse" }, { "type": "object", @@ -66,7 +288,7 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/models.GinResponse" + "$ref": "#/definitions/utils.GinResponse" }, { "type": "object", @@ -100,7 +322,7 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/models.GinResponse" + "$ref": "#/definitions/utils.GinResponse" }, { "type": "object", @@ -134,7 +356,7 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/models.GinResponse" + "$ref": "#/definitions/utils.GinResponse" }, { "type": "object", @@ -182,7 +404,7 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/models.GinResponse" + "$ref": "#/definitions/utils.GinResponse" }, { "type": "object", @@ -200,6 +422,236 @@ } } } + }, + "/user/approve": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "admin approve account, user can use account after approved", + "parameters": [ + { + "type": "string", + "description": "token", + "name": "Token", + "in": "header", + "required": true + }, + { + "description": "json", + "name": "json", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UserApprove" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/utils.GinResponse" + } + } + } + } + }, + "/user/chpwd": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "change user's password", + "parameters": [ + { + "description": "json", + "name": "json", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UserChangePwd" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/utils.GinResponse" + } + } + } + } + }, + "/user/code/{email}/{code}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "send verify code", + "parameters": [ + { + "type": "string", + "description": "email", + "name": "email", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "code", + "name": "code", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/utils.GinResponse" + } + } + } + } + }, + "/user/login": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "user login", + "parameters": [ + { + "description": "json", + "name": "json", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UserLogin" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/utils.GinResponse" + } + } + } + } + }, + "/user/register": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "user register account", + "parameters": [ + { + "description": "json", + "name": "json", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UserRegister" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/utils.GinResponse" + } + } + } + } + }, + "/user/registers": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "list register infos, which is to be approved", + "parameters": [ + { + "type": "string", + "description": "token", + "name": "Token", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/utils.GinResponse" + } + } + } + } + }, + "/user/{code}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "send verify code", + "parameters": [ + { + "type": "string", + "description": "email", + "name": "email", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/utils.GinResponse" + } + } + } + } } }, "definitions": { @@ -263,6 +715,55 @@ } } }, + "models.BackArticle": { + "type": "object", + "properties": { + "content": { + "description": "文章内容(如有需要可迁移至对象存储)", + "type": "string" + }, + "cover": { + "description": "文章封面", + "type": "string" + }, + "createTime": { + "description": "文章新建时间", + "type": "string" + }, + "createUser": { + "description": "文章创建者id", + "type": "string" + }, + "isDelete": { + "description": "删除标志", + "type": "integer" + }, + "isPublish": { + "description": "发布状态(0:未发布, 1: 发布)", + "type": "integer" + }, + "modifyTime": { + "description": "文章最后更新时间", + "type": "string" + }, + "modifyUser": { + "description": "文章最后更新者id", + "type": "string" + }, + "resume": { + "description": "文章简述", + "type": "string" + }, + "tags": { + "description": "文章Tag", + "type": "string" + }, + "title": { + "description": "文章标题", + "type": "string" + } + } + }, "models.ChinaAdd": { "type": "object", "properties": { @@ -359,7 +860,63 @@ } } }, - "models.GinResponse": { + "models.UserApprove": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "pass": { + "type": "boolean" + } + } + }, + "models.UserChangePwd": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "email": { + "type": "string" + }, + "newPassword": { + "type": "string" + } + } + }, + "models.UserLogin": { + "type": "object", + "properties": { + "account": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "models.UserRegister": { + "type": "object", + "properties": { + "aptitude": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "utils.GinResponse": { "type": "object", "properties": { "code": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index b089f6f..fd7c92e 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -38,6 +38,42 @@ definitions: wzz: type: integer type: object + models.BackArticle: + properties: + content: + description: 文章内容(如有需要可迁移至对象存储) + type: string + cover: + description: 文章封面 + type: string + createTime: + description: 文章新建时间 + type: string + createUser: + description: 文章创建者id + type: string + isDelete: + description: 删除标志 + type: integer + isPublish: + description: '发布状态(0:未发布, 1: 发布)' + type: integer + modifyTime: + description: 文章最后更新时间 + type: string + modifyUser: + description: 文章最后更新者id + type: string + resume: + description: 文章简述 + type: string + tags: + description: 文章Tag + type: string + title: + description: 文章标题 + type: string + type: object models.ChinaAdd: properties: confirm: @@ -101,7 +137,43 @@ definitions: suspect: type: integer type: object - models.GinResponse: + models.UserApprove: + properties: + email: + type: string + pass: + type: boolean + type: object + models.UserChangePwd: + properties: + code: + type: string + email: + type: string + newPassword: + type: string + type: object + models.UserLogin: + properties: + account: + type: string + password: + type: string + type: object + models.UserRegister: + properties: + aptitude: + type: string + email: + type: string + password: + type: string + phone: + type: string + username: + type: string + type: object + utils.GinResponse: properties: code: type: integer @@ -113,6 +185,143 @@ info: title: nCov Tracker version: "1.0" paths: + /article: + post: + consumes: + - application/json + parameters: + - description: article + in: body + name: Article + required: true + schema: + $ref: '#/definitions/models.BackArticle' + - description: token + in: header + name: Token + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/utils.GinResponse' + - properties: + data: + $ref: '#/definitions/models.BackArticle' + type: object + summary: save article + tags: + - Article + /article/{id}: + delete: + consumes: + - application/json + parameters: + - description: token + in: header + name: Token + required: true + type: string + - description: id + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/utils.GinResponse' + summary: delete an article + tags: + - Article + get: + consumes: + - application/json + description: Admin can get not published article + parameters: + - description: token + in: header + name: Token + type: string + - description: id + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/utils.GinResponse' + - properties: + data: + $ref: '#/definitions/models.BackArticle' + type: object + summary: get all articles + tags: + - Article + /article/{id}/publish: + post: + consumes: + - application/json + parameters: + - description: token + in: header + name: Token + required: true + type: string + - description: id + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/utils.GinResponse' + summary: get all articles + tags: + - Article + /article/list: + get: + consumes: + - application/json + description: Admin can get not published article + parameters: + - description: token + in: header + name: Token + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/utils.GinResponse' + - properties: + data: + items: + $ref: '#/definitions/models.BackArticle' + type: array + type: object + summary: get all articles + tags: + - Article /statistics/china: get: produces: @@ -122,7 +331,7 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/models.GinResponse' + - $ref: '#/definitions/utils.GinResponse' - properties: data: $ref: '#/definitions/models.ChinaData' @@ -149,7 +358,7 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/models.GinResponse' + - $ref: '#/definitions/utils.GinResponse' - properties: data: items: @@ -168,7 +377,7 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/models.GinResponse' + - $ref: '#/definitions/utils.GinResponse' - properties: data: items: @@ -187,7 +396,7 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/models.GinResponse' + - $ref: '#/definitions/utils.GinResponse' - properties: data: items: @@ -216,7 +425,7 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/models.GinResponse' + - $ref: '#/definitions/utils.GinResponse' - properties: data: items: @@ -226,4 +435,152 @@ paths: summary: province statistics tags: - Statistics + /user/{code}: + get: + parameters: + - description: email + in: path + name: email + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/utils.GinResponse' + summary: send verify code + tags: + - User + /user/approve: + post: + consumes: + - application/json + parameters: + - description: token + in: header + name: Token + required: true + type: string + - description: json + in: body + name: json + required: true + schema: + $ref: '#/definitions/models.UserApprove' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/utils.GinResponse' + summary: admin approve account, user can use account after approved + tags: + - User + /user/chpwd: + post: + consumes: + - application/json + parameters: + - description: json + in: body + name: json + required: true + schema: + $ref: '#/definitions/models.UserChangePwd' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/utils.GinResponse' + summary: change user's password + tags: + - User + /user/code/{email}/{code}: + get: + parameters: + - description: email + in: path + name: email + required: true + type: string + - description: code + in: path + name: code + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/utils.GinResponse' + summary: send verify code + tags: + - User + /user/login: + post: + consumes: + - application/json + parameters: + - description: json + in: body + name: json + required: true + schema: + $ref: '#/definitions/models.UserLogin' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/utils.GinResponse' + summary: user login + tags: + - User + /user/register: + post: + consumes: + - application/json + parameters: + - description: json + in: body + name: json + required: true + schema: + $ref: '#/definitions/models.UserRegister' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/utils.GinResponse' + summary: user register account + tags: + - User + /user/registers: + get: + parameters: + - description: token + in: header + name: Token + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/utils.GinResponse' + summary: list register infos, which is to be approved + tags: + - User swagger: "2.0" diff --git a/global/global.go b/global/global.go index f57a2b4..5966c87 100644 --- a/global/global.go +++ b/global/global.go @@ -36,4 +36,8 @@ func GetHttpClient(key string) (*http.Client, error) { const ( CHINA_NCOV_STATISTIC_URL = "https://view.inews.qq.com/g2/getOnsInfo?name=disease_h5" ENV_NOLOG = "nolog" + TOKEN_EXPIRE_DAYS = 15 + + REGISTER_REDIS_KEY = "register_key" + CHANGEPWD_REDIS_KEY = "changepwd_key" ) diff --git a/go.mod b/go.mod index 859f958..c156a7d 100644 --- a/go.mod +++ b/go.mod @@ -30,9 +30,11 @@ require ( github.com/go-sql-driver/mysql v1.6.0 // indirect github.com/golang-jwt/jwt/v4 v4.2.0 // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/google/uuid v1.3.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.4 // indirect + github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/leodido/go-urn v1.2.1 // indirect diff --git a/go.sum b/go.sum index 11559a0..545fae4 100644 --- a/go.sum +++ b/go.sum @@ -172,6 +172,8 @@ github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= @@ -188,6 +190,8 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr github.com/jinzhu/now v1.1.3/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas= github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= diff --git a/handler/article.go b/handler/article.go index c7a67a2..d12e6c2 100644 --- a/handler/article.go +++ b/handler/article.go @@ -8,53 +8,108 @@ import ( "strconv" ) +// SaveArticleHandler save an article +// @Tags Article +// @Accept json +// @Produce json +// @Summary save article +// @Success 200 {object} utils.GinResponse{data=models.BackArticle} +// @Router /article [post] +// @Param Article body models.BackArticle true "article" +// @Param Token header string true "token" func SaveArticleHandler(c *gin.Context) { - var articleSave models.BackArticle - err := c.ShouldBindJSON(&articleSave) - if err != nil { - var requestBody []byte - _, err := c.Request.Body.Read(requestBody) - if err != nil { - return - } - utils.RequestErr(c, requestBody) + jsonMap := bindJson(c) + if jsonMap == nil { return } - if ok := article.SaveArticle(&articleSave); !ok { - utils.ServerErr(c, "Save Failed") + colMap := models.MapJ2c[models.BackArticle](jsonMap, true) + if ok := article.SaveArticle(colMap); !ok { + ServerErr(c, "Save Failed") return } - utils.Succ(c, articleSave) + utils.Succ(c, jsonMap) } +// GetAllArticlesHandler get all article +// @Tags Article +// @Accept json +// @Produce json +// @Summary get all articles +// @Description Admin can get not published article +// @Success 200 {object} utils.GinResponse{data=[]models.BackArticle} +// @Router /article/list [get] +// @Param Token header string false "token" func GetAllArticlesHandler(c *gin.Context) { - articles := article.GetArticleList() + // TODO: admin need to show more articles + articles := article.ListAllArticles() utils.Succ(c, articles) } +// DeleteArticleHandler delete article +// @Tags Article +// @Accept json +// @Produce json +// @Summary delete an article +// @Success 200 {object} utils.GinResponse{} +// @Router /article/{id} [delete] +// @Param Token header string true "token" +// @Param id path string true "id" func DeleteArticleHandler(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { - utils.RequestErr(c, map[string]interface{}{"URI": c.Request.RequestURI}) + RequestErr(c, map[string]interface{}{"URI": c.Request.RequestURI}) return } if ok := article.DeleteArticle(id); !ok { - utils.DataNotFound(c, "The article not found id = "+strconv.Itoa(id)) + ServerErr(c, "Can't delete the article") return } utils.Succ(c, nil) } +// GetArticleHandler get an article +// @Tags Article +// @Accept json +// @Produce json +// @Summary get all articles +// @Description Admin can get not published article +// @Success 200 {object} utils.GinResponse{data=models.BackArticle} +// @Router /article/{id} [get] +// @Param Token header string false "token" +// @Param id path string true "id" func GetArticleHandler(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { - utils.RequestErr(c, map[string]interface{}{"URI": c.Request.RequestURI}) + RequestErr(c, map[string]interface{}{"URI": c.Request.RequestURI}) return } res := article.GetArticleById(id) + //TODO: if not admin, will not show not published article if res == nil { - utils.DataNotFound(c, nil) + DataNotFound(c, nil) return } utils.Succ(c, res) } + +// PublishArticleHandler publish an article +// @Tags Article +// @Accept json +// @Produce json +// @Summary get all articles +// @Success 200 {object} utils.GinResponse{} +// @Router /article/{id}/publish [post] +// @Param Token header string true "token" +// @Param id path string true "id" +func PublishArticleHandler(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + RequestErr(c, map[string]interface{}{"URI": c.Request.RequestURI}) + return + } + if ok := article.PublishArticle(id); !ok { + ServerErr(c, "Can't publish the article") + return + } + utils.Succ(c, nil) +} diff --git a/handler/errors.go b/handler/errors.go new file mode 100644 index 0000000..241bd7a --- /dev/null +++ b/handler/errors.go @@ -0,0 +1,33 @@ +package handler + +import ( + "github.com/gin-gonic/gin" + "nCovTrack-Backend/utils" + "net/http" +) + +// This file is define some business error + +const ( + BAD_REQUEST = "Bad Request" + DATA_NOT_FOUND = "Data not Found" + STATUS_DATA_NOT_FOUND = 210 +) + +func RequestError(c *gin.Context, code int, data interface{}) { + utils.Error(c, http.StatusBadRequest, code, BAD_REQUEST, data) +} + +func RequestErr(c *gin.Context, data interface{}) { + RequestError(c, http.StatusBadRequest, data) +} +func ServerError(c *gin.Context, code int, msg interface{}) { + utils.Err(c, http.StatusInternalServerError, code, msg) +} + +func ServerErr(c *gin.Context, msg interface{}) { + ServerError(c, http.StatusInternalServerError, msg) +} +func DataNotFound(c *gin.Context, data interface{}) { + utils.Success(c, http.StatusOK, STATUS_DATA_NOT_FOUND, DATA_NOT_FOUND, data) +} diff --git a/handler/statistics.go b/handler/statistics.go index a778920..9807b0b 100644 --- a/handler/statistics.go +++ b/handler/statistics.go @@ -12,7 +12,7 @@ import ( // @Tags Statistics // @Produce json // @Summary province statistics -// @Success 200 {object} models.GinResponse{data=[]models.AreaInfo} +// @Success 200 {object} utils.GinResponse{data=[]models.AreaInfo} // @Router /statistics/province/{sort} [get] // @Param sort path string false "data sorted by" Enums(today, total, now, default) func ProvinceDataHandler(c *gin.Context) { @@ -25,7 +25,7 @@ func ProvinceDataHandler(c *gin.Context) { // @Tags Statistics // @Produce json // @Summary city statistics -// @Success 200 {object} models.GinResponse{data=[]models.AreaInfo} +// @Success 200 {object} utils.GinResponse{data=[]models.AreaInfo} // @Router /statistics/city/{sort} [get] // @Param sort path string false "data sorted by" Enums(today, total, now, default) func CityDataHandler(c *gin.Context) { @@ -38,7 +38,7 @@ func CityDataHandler(c *gin.Context) { // @Tags Statistics // @Produce json // @Summary country statistics -// @Success 200 {object} models.GinResponse{data=[]models.AreaInfo} +// @Success 200 {object} utils.GinResponse{data=[]models.AreaInfo} // @Router /statistics/country/child [get] // @Router /statistics/country [get] func CountryDataHandler(c *gin.Context) { @@ -51,7 +51,7 @@ func CountryDataHandler(c *gin.Context) { // @Tags Statistics // @Produce json // @Summary china data -// @Success 200 {object} models.GinResponse{data=models.ChinaData} +// @Success 200 {object} utils.GinResponse{data=models.ChinaData} // @Router /statistics/china [get] func ChinaDataHandler(c *gin.Context) { data := service.GetChinaNCovStatistic() diff --git a/handler/user.go b/handler/user.go new file mode 100644 index 0000000..5c028ea --- /dev/null +++ b/handler/user.go @@ -0,0 +1,144 @@ +package handler + +import ( + "github.com/gin-gonic/gin" + "nCovTrack-Backend/models" + "nCovTrack-Backend/service/user" + "nCovTrack-Backend/utils" + "regexp" +) + +//UserRegisterHandler user register +// @Tags User +// @Accept json +// @Produce json +// @Summary user register account +// @Success 200 {object} utils.GinResponse{} +// @Router /user/register [post] +// @Param json body models.UserRegister true "json" +func UserRegisterHandler(c *gin.Context) { + jsonMap := bindJsonStruct[models.UserRegister](c) + if jsonMap == nil { + return + } + registered := user.NoDuplicatePhoneOrEmail(jsonMap["phone"].(string), jsonMap["email"].(string)) + if registered { + utils.Success(c, 200, 200, "Registered", nil) + } + colMap := models.MapJ2c[models.BackUser](jsonMap, true) + user.Register(colMap) +} + +//UserApproveHandler admin approve account +// @Tags User +// @Accept json +// @Produce json +// @Summary admin approve account, user can use account after approved +// @Success 200 {object} utils.GinResponse{} +// @Router /user/approve [post] +// @Param Token header string true "token" +// @Param json body models.UserApprove true "json" +func UserApproveHandler(c *gin.Context) { + //TODO: auth user is admin or not + jsonMap := bindJsonStruct[models.UserApprove](c) + if jsonMap == nil { + return + } + if !user.ApproveRegister(jsonMap["email"].(string), jsonMap["pass"].(bool)) { + RequestErr(c, "approve failed") + return + } + utils.Succ(c, nil) +} + +//UserLoginHandler admin approve account +// @Tags User +// @Accept json +// @Produce json +// @Summary user login +// @Success 200 {object} utils.GinResponse{} +// @Router /user/login [post] +// @Param json body models.UserLogin true "json" +func UserLoginHandler(c *gin.Context) { + jsonMap := bindJsonStruct[models.UserLogin](c) + if jsonMap == nil { + return + } + token := user.Login(jsonMap) + if token == "" { + // TODO: change to request error + utils.Succ(c, map[string]interface{}{"msg": "failed"}) + return + } + c.Writer.Header().Set("X-Token", token) + utils.Succ(c, nil) +} + +//ListRegisterUserHandler list register infos +// @Tags User +// @Produce json +// @Summary list register infos, which is to be approved +// @Success 200 {object} utils.GinResponse{} +// @Router /user/registers [get] +// @Param Token header string true "token" +func ListRegisterUserHandler(c *gin.Context) { + registers := user.ListRegister() + utils.Succ(c, registers) +} + +//SendEmailCodeHandler send verify code +// @Tags User +// @Produce json +// @Summary send verify code +// @Success 200 {object} utils.GinResponse{} +// @Router /user/{code} [get] +// @Param email path string true "email" +func SendEmailCodeHandler(c *gin.Context) { + email := c.Param("email") + match, _ := regexp.Match("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$", []byte(email)) + if !match { + RequestErr(c, map[string]interface{}{"email": email}) + return + } + if ok := user.SendEmailCode(email); !ok { + ServerErr(c, "Send Email Failed") + return + } + utils.Succ(c, nil) +} + +//VerifyEmailCodeHandler verify code +// @Tags User +// @Produce json +// @Summary send verify code +// @Success 200 {object} utils.GinResponse{} +// @Router /user/code/{email}/{code} [get] +// @Param email path string true "email" +// @Param code path string true "code" +func VerifyEmailCodeHandler(c *gin.Context) { + email := c.Param("email") + code := c.Param("code") + emailMatch, _ := regexp.Match("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$", []byte(email)) + codeMatch, _ := regexp.Match("^[\\w]{6}$", []byte(code)) + if !codeMatch || !emailMatch { + RequestErr(c, map[string]interface{}{"email": email, "code": code}) + return + } + utils.Succ(c, user.VerifyEmailCode(email, code)) +} + +//ChangePasswordHandler change user's password +// @Tags User +// @Accept json +// @Produce json +// @Summary change user's password +// @Success 200 {object} utils.GinResponse{} +// @Router /user/chpwd [post] +// @Param json body models.UserChangePwd true "json" +func ChangePasswordHandler(c *gin.Context) { + jsonMap := bindJsonStruct[models.UserChangePwd](c) + if jsonMap == nil { + return + } + utils.Succ(c, map[string]interface{}{"success": user.ChangePassword(jsonMap)}) +} diff --git a/handler/utils.go b/handler/utils.go new file mode 100644 index 0000000..e7eb807 --- /dev/null +++ b/handler/utils.go @@ -0,0 +1,48 @@ +package handler + +import ( + "encoding/json" + "github.com/gin-gonic/gin" +) + +// bindJson bind body as a map +func bindJson(c *gin.Context) map[string]interface{} { + var jsonMap map[string]interface{} + err := c.ShouldBindJSON(&jsonMap) + if err != nil { + var requestBody []byte + _, err := c.Request.Body.Read(requestBody) + if err != nil { + panic(err) + } + RequestErr(c, requestBody) + return nil + } + if jsonMap == nil || len(jsonMap) == 0 { + RequestErr(c, map[string]interface{}{"Body": nil}) + return nil + } + return jsonMap +} + +// bindJsonStruct bind json as a struct, and convert to map +func bindJsonStruct[T any](c *gin.Context) map[string]interface{} { + var bindObj T + err := c.ShouldBind(&bindObj) + if err != nil { + var requestBody []byte + _, err := c.Request.Body.Read(requestBody) + if err != nil { + panic(err) + } + RequestErr(c, requestBody) + return nil + } + jsonStr, _ := json.Marshal(bindObj) + var jsonMap map[string]interface{} + _ = json.Unmarshal(jsonStr, &jsonMap) + if jsonMap == nil || len(jsonMap) == 0 { + return nil + } + return jsonMap +} diff --git a/initialize/logger.go b/initialize/logger.go index 2f9791d..fe57abe 100644 --- a/initialize/logger.go +++ b/initialize/logger.go @@ -12,7 +12,7 @@ import ( func initLogger() { zapConfig := zap.NewProductionConfig() zapConfig.OutputPaths = []string{ - fmt.Sprintf("%slog_%s.log", global.ServerSettings.LogPath, utils.FormateDate(time.Now())), + fmt.Sprintf("%slog_%s.log", global.ServerSettings.LogPath, utils.FormatDate(time.Now())), "stdout", } logger, err := zapConfig.Build() diff --git a/middleware/auth.go b/middleware/auth.go index 61ddab4..e23d4ea 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -1,9 +1,14 @@ package middleware import ( + "fmt" "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v4" + "nCovTrack-Backend/global" "nCovTrack-Backend/utils" "net/http" + "strconv" + "time" ) const UNAUTH_MSG = "unauthorized" @@ -17,8 +22,15 @@ func Auth() gin.HandlerFunc { c.Abort() return } + // Write the field of token to request header + claims := utils.ParseClaims(oldToken[0]) + c.Request.Header.Set("role", fmt.Sprint(claims["role"])) + c.Request.Header.Set("email", claims["email"].(string)) + c.Request.Header.Set("id", fmt.Sprint(claims["id"])) + + // renew token, and judge the token's iat is expired or not renewToken := utils.RenewToken(oldToken[0]) - if renewToken == "" { + if renewToken == "" || !validAccountIssue(claims) { utils.Err(c, http.StatusUnauthorized, http.StatusUnauthorized, UNAUTH_MSG) c.Abort() return @@ -27,3 +39,23 @@ func Auth() gin.HandlerFunc { c.Next() } } + +// validAccountIssue validate token is valid or not +// If user change password, or logoff on all password, we need to judge use's token is valid? +// Due to the token is no status, so we need to record something on the server-end. +// We use the "IssueAt" field of token, to judge token expired or not. +// TODO: Move this to jwt utils +func validAccountIssue(claims jwt.MapClaims) bool { + iafStr := global.Redis.HGet(global.CHANGEPWD_REDIS_KEY, claims["email"].(string)).Val() + if iafStr == "" { + return true + } + iaf, _ := strconv.Atoi(iafStr) + // Due to we allow token renew, although it was expired, so the token validity period will more than token's validity period + tokenMaxValidSeconds := (global.TOKEN_EXPIRE_DAYS + global.ServerSettings.Jwt.RenewExpireDays) * 24 * 60 * 60 + if time.Now().Unix()-int64(iaf) > int64(tokenMaxValidSeconds) { + global.Redis.HDel(global.CHANGEPWD_REDIS_KEY, claims["email"].(string)) + return true + } + return int(claims["iat"].(float64)) > iaf +} diff --git a/models/article.go b/models/article.go index 31e36cf..01c3a6f 100644 --- a/models/article.go +++ b/models/article.go @@ -1,9 +1,12 @@ package models -import "time" +import ( + "time" +) +// BackArticle article struct type BackArticle struct { - ID int `gorm:"primaryKey;column:id" json:"id"` // 文章id + ID int `gorm:"primaryKey;column:id" json:"-"` // 文章id CreateTime time.Time `gorm:"column:create_time" json:"createTime"` // 文章新建时间 CreateUser string `gorm:"column:create_user" json:"createUser"` // 文章创建者id ModifyTime time.Time `gorm:"column:modify_time" json:"modifyTime"` // 文章最后更新时间 @@ -13,4 +16,20 @@ type BackArticle struct { Resume string `gorm:"column:resume" json:"resume"` // 文章简述 Cover string `gorm:"column:cover" json:"cover"` // 文章封面 Content string `gorm:"column:content" json:"content"` // 文章内容(如有需要可迁移至对象存储) + IsPublish int8 `gorm:"column:is_publish" json:"isPublish"` // 发布状态(0:未发布, 1: 发布) + IsDelete int8 `gorm:"column:is_delete" json:"isDelete"` // 删除标志 } + +func init() { + initJcMap[BackArticle]() +} + +//func ArticleMapJ2c(jsonMap map[string]interface{}, ignoreNil bool) map[string]interface{} { +// colMap := make(map[string]interface{}) +// for k, v := range jsonMap { +// if colKey := colKey != "" && (!ignoreNil && v == nil) { +// colMap[colKey] = v +// } +// } +// return colMap +//} diff --git a/models/user.go b/models/user.go index 06720d1..ef34291 100644 --- a/models/user.go +++ b/models/user.go @@ -3,13 +3,43 @@ package models import "time" type BackUser struct { - ID int `gorm:"primaryKey;column:id" json:"-"` // 用户ID - Username string `gorm:"column:username" json:"username"` // 用户真实姓名 - Password string `gorm:"column:password" json:"password"` // 用户密码 - Role int `gorm:"column:role" json:"role"` // 用户角色 - Email string `gorm:"unique;column:email" json:"email"` // 用户邮箱 - Phone string `gorm:"unique;column:phone" json:"phone"` // 用户手机号码 - Aptitude string `gorm:"column:aptitude" json:"aptitude"` // 用户资质证明(图片URL) - RegisterTime time.Time `gorm:"column:register_time" json:"registerTime"` // 用户注册时间 - Approver int `gorm:"column:approver" json:"approver"` // 注册审核人ID + ID int `gorm:"primaryKey;column:id" json:"-"` // 用户ID + Username string `gorm:"column:username" json:"username"` // 用户真实姓名 + Password string `gorm:"column:password" json:"password"` // 用户密码 + Role int `gorm:"column:role" json:"role"` // 用户角色 + Email string `gorm:"column:email" json:"email"` // 用户邮箱 + Phone string `gorm:"column:phone" json:"phone"` // 用户手机号码 + Aptitude string `gorm:"column:aptitude" json:"aptitude"` // 用户资质证明(图片URL) + CreateTime time.Time `gorm:"column:create_time" json:"createTime"` // 用户注册时间 + Approver int `gorm:"column:approver" json:"approver"` // 注册审核人ID + ModifyTime time.Time `gorm:"column:modify_time" json:"modifyTime"` + IsDelete int8 `gorm:"column:is_delete" json:"isDelete"` // 删除标志 +} + +type UserLogin struct { + Account string `json:"account"` + Password string `json:"password"` +} + +type UserRegister struct { + Username string `json:"username"` + Password string `json:"password"` + Email string `json:"email"` + Phone string `json:"phone"` + Aptitude string `json:"aptitude"` +} + +type UserChangePwd struct { + Email string `json:"email"` + Code string `json:"code"` + NewPassword string `json:"newPassword"` +} + +type UserApprove struct { + Email string `json:"email"` + Pass bool `json:"pass"` +} + +func init() { + initJcMap[BackUser]() } diff --git a/models/utils.go b/models/utils.go index e837bf1..bd54826 100644 --- a/models/utils.go +++ b/models/utils.go @@ -1,14 +1,181 @@ package models -type UtilRequestInfo struct { - Url string - Header string - Body string - Timeout int +import ( + "fmt" + "gorm.io/gorm" + "nCovTrack-Backend/global" + "reflect" + "regexp" + "time" +) + +var colNameReg, _ = regexp.Compile(".*column:(.*);?") +var j2cMap = make(map[string]map[string]string) +var c2jMap = make(map[string]map[string]string) + +const IS_DELETE = "is_delete" + +// initJcMap the gorm models need to call this function in init function +func initJcMap[T any]() { + t := reflect.TypeOf(new(T)).Elem() + tJ2cMap, tC2jMap := make(map[string]string), make(map[string]string) + for i := 0; i < t.NumField(); i++ { + colName := columnName(t.Field(i).Tag.Get("gorm")) + // TODO: Deal with (-) + jsonName := t.Field(i).Tag.Get("json") + if colName == "" || jsonName == "" { + continue + } + tJ2cMap[jsonName] = colName + tC2jMap[colName] = jsonName + } + j2cMap[t.Name()] = tJ2cMap + c2jMap[t.Name()] = tC2jMap } -type GinResponse struct { - Code int `json:"code"` - Msg interface{} `json:"msg"` - Data interface{} `json:"data"` +// columnName get the mysql column name of the tag +func columnName(gormTag string) string { + colNames := colNameReg.FindSubmatch([]byte(gormTag)) + if len(colNames) != 2 { + panic("Model tag regex error") + } + return string(colNames[1]) +} + +// MapJ2c convert jsonMap to colMap, which will used by gorm +func MapJ2c[T any](jsonMap map[string]interface{}, ignoreNil bool) (colMap map[string]interface{}) { + tName := reflect.TypeOf(new(T)).Elem().Name() + tJ2cMap := j2cMap[tName] + if tJ2cMap == nil { + panic(tName + " is not init registered int j2cMap") + } + + colMap = make(map[string]interface{}) + for k, v := range jsonMap { + //TODO 无法转换 + if colName := tJ2cMap[k]; colName != "" && (!ignoreNil || v != nil) { + colMap[colName] = v + } + } + return colMap +} + +// BeforeSave need to set some field while insert or update +func BeforeSave(colMap map[string]interface{}, user int) { + if colMap["id"] == nil { + colMap["create_time"] = time.Now() + if user != -1 { + colMap["create_user"] = user + } + } + colMap["modify_time"] = time.Now() + if user != -1 { + colMap["modify_user"] = user + } +} + +/*----------------------------------------------------------------------------------------------------------*/ +// Due to gorm can't deal with the zero value, so we use gorm with map. +// The generic will make the function is generally used to gorm models +// TODO: add uniqueKey map, which can be used when Upsert + +func Upsert[T any](colMap map[string]interface{}) (ok bool, rowsAffected int64) { + var tx *gorm.DB + if colMap["id"] == nil { + tx = global.Db.Model(new(T)).Create(colMap) + } else { + tx = global.Db.Model(new(T)).Where("id = ?", colMap["id"]).Updates(colMap) + } + if tx.Error != nil { + fmt.Println(tx.Error) + return false, 0 + } + return true, tx.RowsAffected +} + +// DeleteById will delete by id, not delete the record from database, only set the field "is_delete" as 1 +func DeleteById[T any](id int) (ok bool, rowsAffected int64) { + tx := global.Db.Model(new(T)).Where("id = ?", id).Update("is_delete", 1) + if tx.Error != nil { + return false, 0 + } + return true, rowsAffected +} + +func List[T any](queryMap []map[string]interface{}) *[]map[string]interface{} { + tx := global.Db.Model(new(T)) + for _, e := range queryMap { + e[IS_DELETE] = 0 + tx = tx.Or(e) + } + return ListByOrm(tx) +} + +func ListField[T any](queryMap []map[string]interface{}, isOmit bool, queryField ...string) *[]map[string]interface{} { + tx := global.Db.Model(new(T)) + for _, e := range queryMap { + e[IS_DELETE] = 0 + tx = tx.Or(e) + } + if len(queryMap) == 0 { + return ListByOrm(tx) + } + if isOmit { + tx = tx.Omit(queryField...) + } else { + tx = tx.Select(queryField[0], queryField[1:]) + } + return ListByOrm(tx) +} + +func ListByOrm(tx *gorm.DB) *[]map[string]interface{} { + var res []map[string]interface{} + tx.Find(&res) + return &res +} +func Get[T any](queryMap []map[string]interface{}) map[string]interface{} { + tx := global.Db.Model(new(T)) + for _, e := range queryMap { + e[IS_DELETE] = 0 + tx = tx.Or(e) + } + return GetByOrm(tx) +} + +func GetField[T any](queryMap []map[string]interface{}, isOmit bool, queryField ...string) map[string]interface{} { + tx := global.Db.Model(new(T)) + for _, e := range queryMap { + e[IS_DELETE] = 0 + tx = tx.Or(e) + } + if len(queryMap) == 0 { + return GetByOrm(tx) + } + if isOmit { + tx = tx.Omit(queryField...) + } else { + tx = tx.Select(queryField[0], queryField[1:]) + } + return GetByOrm(tx) +} + +func GetByOrm(tx *gorm.DB) map[string]interface{} { + var res map[string]interface{} + tx.Limit(1).Find(&res) + return res +} + +func Count[T any](queryMap []map[string]interface{}) int64 { + tx := global.Db.Model(new(T)) + for _, e := range queryMap { + e[IS_DELETE] = 0 + tx = tx.Or(e) + } + return CountByOrm(tx) +} + +func CountByOrm(tx *gorm.DB) int64 { + var count int64 + tx.Count(&count) + return count } diff --git a/router/article.go b/router/article.go index 2ab0847..cb62379 100644 --- a/router/article.go +++ b/router/article.go @@ -8,15 +8,16 @@ import ( func articlePrivateRouter(router *gin.RouterGroup) { articleRouter := router.Group("/article") { - articleRouter.POST("/:id", handler.SaveArticleHandler) + articleRouter.DELETE("/:id", handler.DeleteArticleHandler) + articleRouter.POST("/:id/publish", handler.PublishArticleHandler) } } func articlePublicRouter(router *gin.RouterGroup) { articleRouter := router.Group("/article") { + articleRouter.POST("", handler.SaveArticleHandler) articleRouter.GET("/list", handler.GetAllArticlesHandler) - articleRouter.DELETE("/:id", handler.DeleteArticleHandler) articleRouter.GET("/:id", handler.GetArticleHandler) } } diff --git a/router/router.go b/router/router.go index 02f6e2d..170ad25 100644 --- a/router/router.go +++ b/router/router.go @@ -14,10 +14,12 @@ func BusiRouter() { { statisticRouter(publicRouter) articlePublicRouter(publicRouter) + userPublicRouter(publicRouter) } // Private { articlePrivateRouter(privateRouter) + userPrivateRouter(privateRouter) } } diff --git a/router/user.go b/router/user.go new file mode 100644 index 0000000..63c03d7 --- /dev/null +++ b/router/user.go @@ -0,0 +1,24 @@ +package router + +import ( + "github.com/gin-gonic/gin" + "nCovTrack-Backend/handler" +) + +func userPublicRouter(router *gin.RouterGroup) { + userRouter := router.Group("/user") + { + userRouter.POST("/register", handler.UserRegisterHandler) + userRouter.POST("/login", handler.UserLoginHandler) + userRouter.GET("/code/:email", handler.SendEmailCodeHandler) + userRouter.GET("/code/:email/:code", handler.VerifyEmailCodeHandler) + userRouter.POST("/chpwd", handler.ChangePasswordHandler) + } +} +func userPrivateRouter(router *gin.RouterGroup) { + userRouter := router.Group("/user") + { + userRouter.POST("/approve", handler.UserApproveHandler) + userRouter.GET("/registers", handler.ListRegisterUserHandler) + } +} diff --git a/service/article/article.go b/service/article/article.go index 33674d7..cda04bc 100644 --- a/service/article/article.go +++ b/service/article/article.go @@ -1,37 +1,49 @@ package article import ( - "nCovTrack-Backend/global" "nCovTrack-Backend/models" - "nCovTrack-Backend/utils" ) -func GetArticleList() *[]models.BackArticle { - var articles []models.BackArticle - global.Db.Omit("content").Find(&articles) - return &articles +//ListPublishedArticles list the articles published, use to show the articles to all people +func ListPublishedArticles() *[]map[string]interface{} { + article := models.ListField[models.BackArticle]([]map[string]interface{}{{"is_publish": 0}}, true, "content") + if *article == nil { + article = &[]map[string]interface{}{} + } + return article } -func SaveArticle(article *models.BackArticle) (ok bool) { - return utils.Upsert(article) +//ListAllArticles list all articles, will show the articles not published of the user +// TODO: need only show the user's not published article +func ListAllArticles() *[]map[string]interface{} { + article := models.ListField[models.BackArticle]([]map[string]interface{}{{}}, true, "content") + if *article == nil { + article = &[]map[string]interface{}{} + } + return article } +//SaveArticle save the articles +func SaveArticle(article map[string]interface{}) (ok bool) { + models.BeforeSave(article, -1) + ok, rows := models.Upsert[models.BackArticle](article) + return ok && rows != 0 +} + +//DeleteArticle delete article by id func DeleteArticle(id int) (ok bool) { - tx := global.Db.Delete(&models.BackArticle{}, id) - if tx.Error != nil { - panic(tx.Error) - } - if tx.RowsAffected == 0 { - return false - } - return false + ok, rowsAffected := models.DeleteById[models.BackArticle](id) + return ok && rowsAffected != 0 } -func GetArticleById(id int) *models.BackArticle { - var article models.BackArticle - tx := global.Db.Limit(1).Find(&article, id) - if tx.RowsAffected == 0 { - return nil - } - return &article +//GetArticleById get an article +func GetArticleById(id int) map[string]interface{} { + return models.Get[models.BackArticle]([]map[string]interface{}{{"id": id}}) +} + +//PublishArticle publish an article +func PublishArticle(id int) (ok bool) { + colMap := map[string]interface{}{"id": id, "is_publish": 1} + ok, rowsAffected := models.Upsert[models.BackArticle](colMap) + return ok && rowsAffected != 0 } diff --git a/service/user/user.go b/service/user/user.go new file mode 100644 index 0000000..a091a0b --- /dev/null +++ b/service/user/user.go @@ -0,0 +1,140 @@ +package user + +import ( + "encoding/json" + "fmt" + "github.com/golang-jwt/jwt/v4" + "github.com/google/uuid" + "nCovTrack-Backend/global" + "nCovTrack-Backend/models" + "nCovTrack-Backend/utils" + "strings" + "time" +) + +const ( + EMAIL_CODE_REDIS_KEY = "verify_code_" +) + +// Login if login success, will return token +func Login(user map[string]interface{}) (token string) { + account := user["account"].(string) + var queryMap []map[string]interface{} + if strings.Contains(account, "@") { + queryMap = append(queryMap, map[string]interface{}{"email": account}) + } else { + queryMap = append(queryMap, map[string]interface{}{"phone": account}) + } + userInfo := models.Get[models.BackUser](queryMap) + if userInfo == nil { + return "" + } + if !utils.PasswordCompare(user["password"].(string), userInfo["password"].(string)) { + return "" + } + claims := jwt.MapClaims{ + "id": userInfo["id"], + "username": userInfo["username"], + "role": userInfo["role"], + "email": userInfo["email"], + } + return utils.GenerateToken(claims) +} + +// Register user register, user can use account after approved +func Register(user map[string]interface{}) { + user["password"] = utils.PasswordEncrypt(user["password"].(string)) + userStr, _ := json.Marshal(user) + // insert into redis, wait for approve + cmd := global.Redis.HMSet(global.REGISTER_REDIS_KEY, map[string]interface{}{user["email"].(string): userStr}) + if cmd.Err() != nil { + panic(cmd.Err()) + } +} + +// ListRegister list the registers in the redis to be approved +func ListRegister() *[]map[string]interface{} { + applyStrMap := global.Redis.HGetAll(global.REGISTER_REDIS_KEY).Val() + var applies []map[string]interface{} + for _, v := range applyStrMap { + var apply map[string]interface{} + _ = json.Unmarshal([]byte(v), &apply) + applies = append(applies, apply) + } + if applies == nil { + applies = []map[string]interface{}{} + } + return &applies +} + +// ApproveRegister approve a register +func ApproveRegister(email string, pass bool) bool { + if !pass { + rowsAffected := global.Redis.HDel(global.REGISTER_REDIS_KEY, email).Val() + return rowsAffected != 0 + } + // if pass, will get the register info from redis, and the insert into mysql, this mean user is register success + applyStr := global.Redis.HGet(global.REGISTER_REDIS_KEY, email).Val() + rowsAffected := global.Redis.HDel(global.REGISTER_REDIS_KEY, email).Val() + if rowsAffected == 0 { + return false + } + var apply map[string]interface{} + _ = json.Unmarshal([]byte(applyStr), &apply) + if !NoDuplicatePhoneOrEmail(apply["phone"].(string), apply["email"].(string)) { + return false + } + colMap := models.MapJ2c[models.BackUser](apply, true) + ok, rowsAffected := models.Upsert[models.BackUser](colMap) + return ok && rowsAffected != 0 +} + +// ChangePassword user change password, or user forgot password +func ChangePassword(changePwd map[string]interface{}) bool { + match := VerifyEmailCode(changePwd["email"].(string), changePwd["code"].(string))["match"].(bool) + if !match { + return false + } + newPassword := utils.PasswordEncrypt(changePwd["newPassword"].(string)) + colMap := map[string]interface{}{ + "id": 1, + "password": newPassword, + } + models.BeforeSave(colMap, -1) + delete(colMap, "id") + rowAffected := global.Db.Model(models.BackUser{}).Where("email = ?", changePwd["email"]).Updates(colMap).RowsAffected + if rowAffected == 0 { + return false + } + now := time.Now().Unix() + global.Redis.HSet(global.CHANGEPWD_REDIS_KEY, changePwd["email"].(string), now) + return true +} + +// NoDuplicatePhoneOrEmail detect the phone or email is registered or not +func NoDuplicatePhoneOrEmail(phone string, email string) bool { + var queryMap []map[string]interface{} + if phone != "" { + queryMap = append(queryMap, map[string]interface{}{"phone": phone}) + } + if email != "" { + queryMap = append(queryMap, map[string]interface{}{"email": email}) + } + return len(queryMap) != 0 && models.Count[models.BackUser](queryMap) == 0 +} + +// SendEmailCode used to send email verify code +func SendEmailCode(email string) bool { + code := uuid.New().String()[0:6] + text := fmt.Sprintf("Your Verify Code is :%s, Will Expire After 10 Minutes", code) + subject := "nCovTrack Verify" + // only set expired, not limit the frequency of use + global.Redis.Set(EMAIL_CODE_REDIS_KEY+email, code, 10*time.Minute) + return utils.SendEmail(subject, text, email) +} + +// VerifyEmailCode use to verify user's verify code is correct or not +func VerifyEmailCode(email string, code string) map[string]interface{} { + verifyCode := global.Redis.Get(EMAIL_CODE_REDIS_KEY + email).Val() + return map[string]interface{}{"match": verifyCode == code} +} diff --git a/settings-dev.yml b/settings-dev.yml index ecedebf..b16d6a0 100644 --- a/settings-dev.yml +++ b/settings-dev.yml @@ -21,3 +21,9 @@ jwt: secret: bWF5YmVJYWxzb3NhbWV0b2JlZm9yZe renewExpireDays: 7 renewAheadDays: 3 + +email: + host: smtp.qq.com + port: 587 + account: fallen-angle@foxmail.com + password: hxrisxltxsjvieec \ No newline at end of file diff --git a/utils/email.go b/utils/email.go new file mode 100644 index 0000000..8638213 --- /dev/null +++ b/utils/email.go @@ -0,0 +1,26 @@ +package utils + +import ( + "fmt" + "github.com/jordan-wright/email" + "nCovTrack-Backend/global" + "net/smtp" +) + +func SendEmail(subject string, text string, to ...string) bool { + //TODO: add logs + e := email.Email{ + From: "nCovTrack Server<1853633282@qq.com>", + To: to, + Subject: subject, + Text: []byte(text), + } + err := e.Send( + fmt.Sprintf("%s:%d", global.ServerSettings.Email.Host, global.ServerSettings.Email.Port), + smtp.PlainAuth("", global.ServerSettings.Email.Account, global.ServerSettings.Email.Password, global.ServerSettings.Email.Host), + ) + if err != nil { + return false + } + return true +} diff --git a/utils/encrypt.go b/utils/encrypt.go new file mode 100644 index 0000000..15e21c6 --- /dev/null +++ b/utils/encrypt.go @@ -0,0 +1,23 @@ +package utils + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "github.com/google/uuid" +) + +func PasswordEncrypt(password string) string { + salt := uuid.New().String()[24:] + password = password + salt + shaRes := hex.EncodeToString(sha256.New().Sum([]byte(password))) + encryptPwd := fmt.Sprintf("ncov$%s$%s", shaRes[0:24], salt) + return encryptPwd +} + +func PasswordCompare(plaintext string, ciphertext string) (ok bool) { + salt := ciphertext[30:] + password := plaintext + salt + shaRes := hex.EncodeToString(sha256.New().Sum([]byte(password))) + return shaRes[0:24] == ciphertext[5:29] +} diff --git a/utils/jwt.go b/utils/jwt.go index f68bc4f..2ba1971 100644 --- a/utils/jwt.go +++ b/utils/jwt.go @@ -58,7 +58,7 @@ func RenewToken(tokenStr string) string { } fmt.Println(expireDuration) - claims["exp"] = time.Now().Add(15 * 24 * time.Hour).Unix() + claims["exp"] = time.Now().Add(global.TOKEN_EXPIRE_DAYS * 24 * time.Hour).Unix() token = jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenStr, err = token.SignedString(JWT_KEY) if err != nil { @@ -66,3 +66,18 @@ func RenewToken(tokenStr string) string { } return tokenStr } + +func ParseClaims(tokenStr string) jwt.MapClaims { + token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) { + return JWT_KEY, nil + }) + if err != nil { + switch err.(*jwt.ValidationError).Errors { + case jwt.ValidationErrorSignatureInvalid: + return nil + case jwt.ValidationErrorIssuedAt: + return nil + } + } + return token.Claims.(jwt.MapClaims) +} diff --git a/utils/models.go b/utils/models.go new file mode 100644 index 0000000..f45c67e --- /dev/null +++ b/utils/models.go @@ -0,0 +1,14 @@ +package utils + +type UtilRequestInfo struct { + Url string + Header string + Body string + Timeout int +} + +type GinResponse struct { + Code int `json:"code"` + Msg interface{} `json:"msg"` + Data interface{} `json:"data"` +} diff --git a/utils/orm.go b/utils/orm.go deleted file mode 100644 index 578b66a..0000000 --- a/utils/orm.go +++ /dev/null @@ -1,67 +0,0 @@ -package utils - -import ( - "gorm.io/gorm/clause" - "nCovTrack-Backend/global" - "reflect" - "regexp" - "time" -) - -var colNameReg, _ = regexp.Compile(".*column:(.*);?") -var uniqueKeyReg, _ = regexp.Compile(".*(primaryKey|unique).*") - -func getNotZeroFields[T any](model T) []string { - t := reflect.TypeOf(model) - v := reflect.ValueOf(model) - var notZeroFields []string - for i := 0; i < t.NumField(); i++ { - if !v.Field(i).IsZero() { - colName := colNameReg.FindSubmatch([]byte(t.Field(i).Tag.Get("gorm"))) - if len(colName) != 2 { - panic("Model Tag regex error") - } - notZeroFields = append(notZeroFields, string(colName[1])) - } - } - return notZeroFields -} - -func Upsert[T any](model *T, forceUpdateFiled ...string) (ok bool) { - t := reflect.TypeOf(model).Elem() - v := reflect.ValueOf(model).Elem() - var uniqueKeyField []clause.Column - notZeroField := NewSet(forceUpdateFiled...).Add("modify_time") - for i := 0; i < t.NumField(); i++ { - gormTag := t.Field(i).Tag.Get("gorm") - if uniqueKey(gormTag) { - uniqueKeyField = append(uniqueKeyField, clause.Column{Name: columnName(gormTag)}) - continue - } - if !v.Field(i).IsZero() { - notZeroField.Add(columnName(gormTag)) - } else if t.Field(i).Type.Name() == "Time" { - v.Field(i).Set(reflect.ValueOf(time.Now())) - } - } - tx := global.Db.Clauses(clause.OnConflict{ - Columns: uniqueKeyField, - DoUpdates: clause.AssignmentColumns(notZeroField.ToSlice()), - }, clause.Returning{}).Create(model) - if tx.Error != nil { - return false - } - return true -} - -func columnName(gormTag string) string { - colNames := colNameReg.FindSubmatch([]byte(gormTag)) - if len(colNames) != 2 { - panic("Model tag regex error") - } - return string(colNames[1]) -} - -func uniqueKey(gormTag string) bool { - return uniqueKeyReg.Match([]byte(gormTag)) -} diff --git a/utils/reflect.go b/utils/reflect.go deleted file mode 100644 index d4b585b..0000000 --- a/utils/reflect.go +++ /dev/null @@ -1 +0,0 @@ -package utils diff --git a/utils/response.go b/utils/response.go index fe70572..0bf8a0e 100644 --- a/utils/response.go +++ b/utils/response.go @@ -1,51 +1,26 @@ package utils import ( - "nCovTrack-Backend/models" "net/http" "github.com/gin-gonic/gin" ) const ( - SUCCESS = "Success" - BAD_REQUEST = "Bad Request" - DATA_NOT_FOUND = "Data not Found" - STATUS_DATA_NOT_FOUND = 210 + SUCCESS = "Success" ) func Success(c *gin.Context, status int, code int, msg interface{}, data interface{}) { - c.JSON(http.StatusOK, models.GinResponse{Code: code, Msg: msg, Data: data}) + c.JSON(http.StatusOK, GinResponse{Code: code, Msg: msg, Data: data}) } func Error(c *gin.Context, status int, code int, msg interface{}, data interface{}) { - c.JSON(status, models.GinResponse{Code: code, Msg: msg, Data: data}) + c.JSON(status, GinResponse{Code: code, Msg: msg, Data: data}) } func Succ(c *gin.Context, data interface{}) { Success(c, http.StatusOK, http.StatusOK, SUCCESS, data) } - -func DataNotFound(c *gin.Context, data interface{}) { - Success(c, http.StatusOK, STATUS_DATA_NOT_FOUND, DATA_NOT_FOUND, data) -} - func Err(c *gin.Context, status int, code int, msg interface{}) { Error(c, status, code, msg, nil) } - -func ServerError(c *gin.Context, code int, msg interface{}) { - Err(c, http.StatusInternalServerError, code, msg) -} - -func ServerErr(c *gin.Context, msg interface{}) { - ServerError(c, http.StatusInternalServerError, msg) -} - -func RequestError(c *gin.Context, code int, data interface{}) { - Error(c, http.StatusBadRequest, code, BAD_REQUEST, data) -} - -func RequestErr(c *gin.Context, data interface{}) { - RequestError(c, http.StatusBadRequest, data) -} diff --git a/utils/set.go b/utils/set.go index e21b95a..3772f26 100644 --- a/utils/set.go +++ b/utils/set.go @@ -2,6 +2,8 @@ package utils type void struct{} +// This is set with generic + type Set[T comparable] struct { setMap map[T]void } diff --git a/utils/string.go b/utils/string.go deleted file mode 100644 index d4b585b..0000000 --- a/utils/string.go +++ /dev/null @@ -1 +0,0 @@ -package utils diff --git a/utils/time.go b/utils/time.go index 9434fb2..3e8a5f5 100644 --- a/utils/time.go +++ b/utils/time.go @@ -4,6 +4,6 @@ import ( "time" ) -func FormateDate(date time.Time) string { +func FormatDate(date time.Time) string { return date.Format("06-01-02") }