Skip to content

Commit

Permalink
新增 HTTP API 方式操作短链接和管理员
Browse files Browse the repository at this point in the history
  • Loading branch information
barats committed Apr 10, 2022
1 parent 4ef7428 commit 5c9e17f
Show file tree
Hide file tree
Showing 10 changed files with 425 additions and 6 deletions.
156 changes: 156 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
1. 支持 Docker One Stop 部署、Makefile 编译打包
1. 支持短链接生产、查询、存储、302转向
1. 支持访问日志查询、访问量统计、独立IP数统计
1. 支持 HTTP API 方式新建短链接、禁用/启用短链接、查看短链接统计信息、新建管理员、修改管理员密码


![Screenshot](screenshot.jpg)
Expand Down Expand Up @@ -92,6 +93,161 @@ func PasswordBase58Hash(password string) (string, error) {

亦可参照 `storage/users_storage_test.go` 中的 `TestNewUser()` 方法

## HTTP API 支持

### `/api` 接口权限说明

所有 `/api/*` 接口需要通过 `Bearer Token` 方式验证权限,亦即:每个请求 Header 须携带
```
Authorization: Bearer {sha256_of_password}
```

`sha256_of_password` 的加密规则,与 `storage/users_storage.go` 中的 `PasswordBase58Hash()` 保持同步

### 1. 新增短链接 `POST /api/url`

接受参数:
1. `dest_url` 目标链接,必填
2. `memo` 备注信息,选填

请求示例:
```
curl --request POST \
--url http://localhost:9092/api/url \
--header 'Authorization: Bearer EZ2zQjC3fqbkvtggy9p2YaJiLwx1kKPTJxvqVzowtx6t' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data dest_url=http://localhost:9092/admin/dashboard \
--data memo=dashboard
```

返回结果:
```
{
"code": 200,
"status": true,
"message": "success",
"result": {
"short_url": "http://localhost:9091/BUUtpbGp"
},
"date": "2022-04-10T21:31:29.36559+08:00"
}
```

### 2. 禁用/启用 短链接 `PUT /api/url/:url/change_state`

接受参数:
1. `url` path 参数,指定短链接,必填
2. `enable` 禁用时,传入 false;启用时,传入 true

请求示例:
```
curl --request PUT \
--url http://localhost:9092/api/url/33R5QUtD/change_state \
--header 'Authorization: Bearer EZ2zQjC3fqbkvtggy9p2YaJiLwx1kKPTJxvqVzowtx6t' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data enable=false
```

返回结果:
```
{
"code": 200,
"status": true,
"message": "success",
"result": true,
"date": "2022-04-10T21:31:25.7744402+08:00"
}
```

### 3. 查询短链接统计数据 `GET /api/url/:url`

接受参数:
1. `url` path 参数,指定短链接,必填

请求示例:
```
curl --request GET \
--url http://localhost:9092/api/url/33R5QUtD \
--header 'Authorization: Bearer EZ2zQjC3fqbkvtggy9p2YaJiLwx1kKPTJxvqVzowtx6t' \
--header 'Content-Type: application/x-www-form-urlencoded'
```

返回结果:
```
{
"code": 200,
"status": true,
"message": "success",
"result": {
"short_url": "33R5QUtD",
"today_count": 3,
"yesterday_count": 0,
"last_7_days_count": 0,
"monthly_count": 3,
"total_count": 3,
"d_today_count": 1,
"d_yesterday_count": 0,
"d_last_7_days_count": 0,
"d_monthly_count": 1,
"d_total_count": 1
},
"date": "2022-04-10T21:31:22.059596+08:00"
}
```

### 4. 新建管理员 `POST /api/account`

接受参数:
1. `account` 管理员帐号,必填
2. `password` 管理员密码,必填,最小长度8

请求示例:
```
curl --request POST \
--url http://localhost:9092/api/account \
--header 'Authorization: Bearer EZ2zQjC3fqbkvtggy9p2YaJiLwx1kKPTJxvqVzowtx6t' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data account=hello1 \
--data password=12345678
```

返回结果:
```
{
"code": 200,
"status": true,
"message": "success",
"result": null,
"date": "2022-04-10T21:31:39.7353132+08:00"
}
```

### 5. 修改管理员密码 `PUT /api/account/:account/update`

接受参数:
1. `account` path 参数,管理员帐号,必填
1. `password` 管理员密码,必填,最小长度8

请求示例:
```
curl --request PUT \
--url http://localhost:9092/api/account/hello/update \
--header 'Authorization: Bearer EZ2zQjC3fqbkvtggy9p2YaJiLwx1kKPTJxvqVzowtx6t' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data password=world123
```

返回结果:
```
{
"code": 200,
"status": true,
"message": "success",
"result": null,
"date": "2022-04-10T21:31:32.5880538+08:00"
}
```

## 短链接在应用启动时会存入 Redis 中

所有短链接再系统启动时会以 `Key(short_url) -> Value(original_url)` 的形式存储在 Redis 中。
Expand Down
132 changes: 132 additions & 0 deletions controller/api_contraoller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright (c) [2022] [巴拉迪维 BaratSemet]
// [ohUrlShortener] is licensed under Mulan PSL v2.
// You can use this software according to the terms and conditions of the Mulan PSL v2.
// You may obtain a copy of Mulan PSL v2 at:
// http://license.coscl.org.cn/MulanPSL2
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
// See the Mulan PSL v2 for more details.

package controller

import (
"fmt"
"net/http"
"ohurlshortener/core"
"ohurlshortener/service"
"ohurlshortener/utils"
"strconv"
"strings"

"github.com/gin-gonic/gin"
)

// Add new admin user
func APINewAdmin(ctx *gin.Context) {
account := ctx.PostForm("account")
password := ctx.PostForm("password")
if utils.EemptyString(account) || utils.EemptyString(password) {
ctx.JSON(http.StatusBadRequest, core.ResultJsonBadRequest("用户名或密码不能为空"))
return
}

if len(password) < 8 {
ctx.JSON(http.StatusBadRequest, core.ResultJsonBadRequest("密码长度最少8位"))
return
}

err := service.NewUser(account, password)

if err != nil {
ctx.JSON(http.StatusBadRequest, core.ResultJsonBadRequest(err.Error()))
return
}

ctx.JSON(http.StatusOK, core.ResultJsonSuccess())
}

// Update password of given admin user
func APIAdminUpdate(ctx *gin.Context) {
account := ctx.Param("account")
password := ctx.PostForm("password")

if utils.EemptyString(account) || utils.EemptyString(password) {
ctx.JSON(http.StatusBadRequest, core.ResultJsonBadRequest("用户名或密码不能为空"))
return
}

if len(password) < 8 {
ctx.JSON(http.StatusBadRequest, core.ResultJsonBadRequest("密码长度最少8位"))
return
}

err := service.UpdatePassword(strings.TrimSpace(account), password)
if err != nil {
ctx.JSON(http.StatusBadRequest, core.ResultJsonBadRequest("修改失败"))
return
}

ctx.JSON(http.StatusOK, core.ResultJsonSuccess())
}

// Generate new short url
func APIGenShortUrl(ctx *gin.Context) {
url := ctx.PostForm("dest_url")
memo := ctx.PostForm("memo")

if utils.EemptyString(strings.TrimSpace(url)) {
ctx.JSON(http.StatusBadRequest, core.ResultJsonBadRequest("dest_url 不能为空"))
return
}

res, err := service.GenerateShortUrl(strings.TrimSpace(url), strings.TrimSpace(memo))
if err != nil {
ctx.JSON(http.StatusBadRequest, core.ResultJsonBadRequest(err.Error()))
return
}

json := map[string]string{
"short_url": fmt.Sprintf("%s%s", utils.AppConfig.UrlPrefix, res),
}
ctx.JSON(http.StatusOK, core.ResultJsonSuccessWithData(json))
}

// Get Short Url Stat Info.
func APIUrlInfo(ctx *gin.Context) {
url := ctx.Param("url")
if utils.EemptyString(strings.TrimSpace(url)) {
ctx.JSON(http.StatusBadRequest, core.ResultJsonBadRequest("url 不能为空"))
return
}

stat, err := service.GetShortUrlStats(strings.TrimSpace(url))
if utils.EemptyString(strings.TrimSpace(url)) {
ctx.JSON(http.StatusInternalServerError, core.ResultJsonError(err.Error()))
return
}

ctx.JSON(http.StatusOK, core.ResultJsonSuccessWithData(stat))
}

// Enable or Disable Short Url
func APIUpdateUrl(ctx *gin.Context) {
url := ctx.Param("url")
enableStr := ctx.PostForm("enable")
if utils.EemptyString(strings.TrimSpace(url)) {
ctx.JSON(http.StatusBadRequest, core.ResultJsonBadRequest("url 不能为空"))
return
}

enable, err := strconv.ParseBool(enableStr)
if err != nil {
ctx.JSON(http.StatusBadRequest, core.ResultJsonBadRequest("enable 参数值非法"))
return
}

res, err := service.ChangeState(url, enable)
if err != nil {
ctx.JSON(http.StatusBadRequest, core.ResultJsonBadRequest(err.Error()))
return
}

ctx.JSON(http.StatusOK, core.ResultJsonSuccessWithData(res))
}
57 changes: 57 additions & 0 deletions controller/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,55 @@ import (
"net/http"
"ohurlshortener/core"
"ohurlshortener/service"
"ohurlshortener/storage"
"ohurlshortener/utils"
"strconv"
"strings"

"github.com/gin-gonic/gin"
)

const (
authoriationHeaderKey = "Authorization"
authoriationTypeBearer = "Bearer"
)

// Authorization for /api
func APIAuthHandler() gin.HandlerFunc {
return func(ctx *gin.Context) {
authHeader := ctx.GetHeader(authoriationHeaderKey)
if utils.EemptyString(authHeader) {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, core.ResultJsonUnauthorized("Authorization Header is empty"))
return
}

fields := strings.Fields(authHeader)
if len(fields) < 2 {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, core.ResultJsonUnauthorized("Invalid Authorization Header"))
return
}

if fields[0] != authoriationTypeBearer {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, core.ResultJsonUnauthorized("Unsupported Authorization Type"))
return
}

token := fields[1]
res, err := validateToken(token)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, core.ResultJsonError("Internal error"))
return
}

if !res {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, core.ResultJsonUnauthorized("Authorization failed"))
return
}

ctx.Next()
}
}

func AdminCookieValue(user core.User) (string, error) {
var result string
data, err := utils.Sha256Of(user.Account + "a=" + user.Password + "=e" + strconv.Itoa(user.ID))
Expand Down Expand Up @@ -94,3 +136,18 @@ func WebLogFormatHandler(server string) gin.HandlerFunc {
return ""
}) //end of formatter
} //end of func

func validateToken(token string) (bool, error) {
users, err := storage.FindAllUsers()
if err != nil {
return false, err
}

for _, u := range users {
if u.Password == token {
return true, nil
}
}

return false, nil
}

0 comments on commit 5c9e17f

Please sign in to comment.