Skip to content

Commit 5c9e17f

Browse files
committed
新增 HTTP API 方式操作短链接和管理员
1 parent 4ef7428 commit 5c9e17f

File tree

10 files changed

+425
-6
lines changed

10 files changed

+425
-6
lines changed

README.md

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
1. 支持 Docker One Stop 部署、Makefile 编译打包
66
1. 支持短链接生产、查询、存储、302转向
77
1. 支持访问日志查询、访问量统计、独立IP数统计
8+
1. 支持 HTTP API 方式新建短链接、禁用/启用短链接、查看短链接统计信息、新建管理员、修改管理员密码
89

910

1011
![Screenshot](screenshot.jpg)
@@ -92,6 +93,161 @@ func PasswordBase58Hash(password string) (string, error) {
9293
9394
亦可参照 `storage/users_storage_test.go` 中的 `TestNewUser()` 方法
9495
96+
## HTTP API 支持
97+
98+
### `/api` 接口权限说明
99+
100+
所有 `/api/*` 接口需要通过 `Bearer Token` 方式验证权限,亦即:每个请求 Header 须携带
101+
```
102+
Authorization: Bearer {sha256_of_password}
103+
```
104+
105+
`sha256_of_password` 的加密规则,与 `storage/users_storage.go` 中的 `PasswordBase58Hash()` 保持同步
106+
107+
### 1. 新增短链接 `POST /api/url`
108+
109+
接受参数:
110+
1. `dest_url` 目标链接,必填
111+
2. `memo` 备注信息,选填
112+
113+
请求示例:
114+
```
115+
curl --request POST \
116+
--url http://localhost:9092/api/url \
117+
--header 'Authorization: Bearer EZ2zQjC3fqbkvtggy9p2YaJiLwx1kKPTJxvqVzowtx6t' \
118+
--header 'Content-Type: application/x-www-form-urlencoded' \
119+
--data dest_url=http://localhost:9092/admin/dashboard \
120+
--data memo=dashboard
121+
```
122+
123+
返回结果:
124+
```
125+
{
126+
"code": 200,
127+
"status": true,
128+
"message": "success",
129+
"result": {
130+
"short_url": "http://localhost:9091/BUUtpbGp"
131+
},
132+
"date": "2022-04-10T21:31:29.36559+08:00"
133+
}
134+
```
135+
136+
### 2. 禁用/启用 短链接 `PUT /api/url/:url/change_state`
137+
138+
接受参数:
139+
1. `url` path 参数,指定短链接,必填
140+
2. `enable` 禁用时,传入 false;启用时,传入 true
141+
142+
请求示例:
143+
```
144+
curl --request PUT \
145+
--url http://localhost:9092/api/url/33R5QUtD/change_state \
146+
--header 'Authorization: Bearer EZ2zQjC3fqbkvtggy9p2YaJiLwx1kKPTJxvqVzowtx6t' \
147+
--header 'Content-Type: application/x-www-form-urlencoded' \
148+
--data enable=false
149+
```
150+
151+
返回结果:
152+
```
153+
{
154+
"code": 200,
155+
"status": true,
156+
"message": "success",
157+
"result": true,
158+
"date": "2022-04-10T21:31:25.7744402+08:00"
159+
}
160+
```
161+
162+
### 3. 查询短链接统计数据 `GET /api/url/:url`
163+
164+
接受参数:
165+
1. `url` path 参数,指定短链接,必填
166+
167+
请求示例:
168+
```
169+
curl --request GET \
170+
--url http://localhost:9092/api/url/33R5QUtD \
171+
--header 'Authorization: Bearer EZ2zQjC3fqbkvtggy9p2YaJiLwx1kKPTJxvqVzowtx6t' \
172+
--header 'Content-Type: application/x-www-form-urlencoded'
173+
```
174+
175+
返回结果:
176+
```
177+
{
178+
"code": 200,
179+
"status": true,
180+
"message": "success",
181+
"result": {
182+
"short_url": "33R5QUtD",
183+
"today_count": 3,
184+
"yesterday_count": 0,
185+
"last_7_days_count": 0,
186+
"monthly_count": 3,
187+
"total_count": 3,
188+
"d_today_count": 1,
189+
"d_yesterday_count": 0,
190+
"d_last_7_days_count": 0,
191+
"d_monthly_count": 1,
192+
"d_total_count": 1
193+
},
194+
"date": "2022-04-10T21:31:22.059596+08:00"
195+
}
196+
```
197+
198+
### 4. 新建管理员 `POST /api/account`
199+
200+
接受参数:
201+
1. `account` 管理员帐号,必填
202+
2. `password` 管理员密码,必填,最小长度8
203+
204+
请求示例:
205+
```
206+
curl --request POST \
207+
--url http://localhost:9092/api/account \
208+
--header 'Authorization: Bearer EZ2zQjC3fqbkvtggy9p2YaJiLwx1kKPTJxvqVzowtx6t' \
209+
--header 'Content-Type: application/x-www-form-urlencoded' \
210+
--data account=hello1 \
211+
--data password=12345678
212+
```
213+
214+
返回结果:
215+
```
216+
{
217+
"code": 200,
218+
"status": true,
219+
"message": "success",
220+
"result": null,
221+
"date": "2022-04-10T21:31:39.7353132+08:00"
222+
}
223+
```
224+
225+
### 5. 修改管理员密码 `PUT /api/account/:account/update`
226+
227+
接受参数:
228+
1. `account` path 参数,管理员帐号,必填
229+
1. `password` 管理员密码,必填,最小长度8
230+
231+
请求示例:
232+
```
233+
curl --request PUT \
234+
--url http://localhost:9092/api/account/hello/update \
235+
--header 'Authorization: Bearer EZ2zQjC3fqbkvtggy9p2YaJiLwx1kKPTJxvqVzowtx6t' \
236+
--header 'Content-Type: application/x-www-form-urlencoded' \
237+
--data password=world123
238+
```
239+
240+
返回结果:
241+
```
242+
{
243+
"code": 200,
244+
"status": true,
245+
"message": "success",
246+
"result": null,
247+
"date": "2022-04-10T21:31:32.5880538+08:00"
248+
}
249+
```
250+
95251
## 短链接在应用启动时会存入 Redis 中
96252
97253
所有短链接再系统启动时会以 `Key(short_url) -> Value(original_url)` 的形式存储在 Redis 中。

controller/api_contraoller.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright (c) [2022] [巴拉迪维 BaratSemet]
2+
// [ohUrlShortener] is licensed under Mulan PSL v2.
3+
// You can use this software according to the terms and conditions of the Mulan PSL v2.
4+
// You may obtain a copy of Mulan PSL v2 at:
5+
// http://license.coscl.org.cn/MulanPSL2
6+
// 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.
7+
// See the Mulan PSL v2 for more details.
8+
9+
package controller
10+
11+
import (
12+
"fmt"
13+
"net/http"
14+
"ohurlshortener/core"
15+
"ohurlshortener/service"
16+
"ohurlshortener/utils"
17+
"strconv"
18+
"strings"
19+
20+
"github.com/gin-gonic/gin"
21+
)
22+
23+
// Add new admin user
24+
func APINewAdmin(ctx *gin.Context) {
25+
account := ctx.PostForm("account")
26+
password := ctx.PostForm("password")
27+
if utils.EemptyString(account) || utils.EemptyString(password) {
28+
ctx.JSON(http.StatusBadRequest, core.ResultJsonBadRequest("用户名或密码不能为空"))
29+
return
30+
}
31+
32+
if len(password) < 8 {
33+
ctx.JSON(http.StatusBadRequest, core.ResultJsonBadRequest("密码长度最少8位"))
34+
return
35+
}
36+
37+
err := service.NewUser(account, password)
38+
39+
if err != nil {
40+
ctx.JSON(http.StatusBadRequest, core.ResultJsonBadRequest(err.Error()))
41+
return
42+
}
43+
44+
ctx.JSON(http.StatusOK, core.ResultJsonSuccess())
45+
}
46+
47+
// Update password of given admin user
48+
func APIAdminUpdate(ctx *gin.Context) {
49+
account := ctx.Param("account")
50+
password := ctx.PostForm("password")
51+
52+
if utils.EemptyString(account) || utils.EemptyString(password) {
53+
ctx.JSON(http.StatusBadRequest, core.ResultJsonBadRequest("用户名或密码不能为空"))
54+
return
55+
}
56+
57+
if len(password) < 8 {
58+
ctx.JSON(http.StatusBadRequest, core.ResultJsonBadRequest("密码长度最少8位"))
59+
return
60+
}
61+
62+
err := service.UpdatePassword(strings.TrimSpace(account), password)
63+
if err != nil {
64+
ctx.JSON(http.StatusBadRequest, core.ResultJsonBadRequest("修改失败"))
65+
return
66+
}
67+
68+
ctx.JSON(http.StatusOK, core.ResultJsonSuccess())
69+
}
70+
71+
// Generate new short url
72+
func APIGenShortUrl(ctx *gin.Context) {
73+
url := ctx.PostForm("dest_url")
74+
memo := ctx.PostForm("memo")
75+
76+
if utils.EemptyString(strings.TrimSpace(url)) {
77+
ctx.JSON(http.StatusBadRequest, core.ResultJsonBadRequest("dest_url 不能为空"))
78+
return
79+
}
80+
81+
res, err := service.GenerateShortUrl(strings.TrimSpace(url), strings.TrimSpace(memo))
82+
if err != nil {
83+
ctx.JSON(http.StatusBadRequest, core.ResultJsonBadRequest(err.Error()))
84+
return
85+
}
86+
87+
json := map[string]string{
88+
"short_url": fmt.Sprintf("%s%s", utils.AppConfig.UrlPrefix, res),
89+
}
90+
ctx.JSON(http.StatusOK, core.ResultJsonSuccessWithData(json))
91+
}
92+
93+
// Get Short Url Stat Info.
94+
func APIUrlInfo(ctx *gin.Context) {
95+
url := ctx.Param("url")
96+
if utils.EemptyString(strings.TrimSpace(url)) {
97+
ctx.JSON(http.StatusBadRequest, core.ResultJsonBadRequest("url 不能为空"))
98+
return
99+
}
100+
101+
stat, err := service.GetShortUrlStats(strings.TrimSpace(url))
102+
if utils.EemptyString(strings.TrimSpace(url)) {
103+
ctx.JSON(http.StatusInternalServerError, core.ResultJsonError(err.Error()))
104+
return
105+
}
106+
107+
ctx.JSON(http.StatusOK, core.ResultJsonSuccessWithData(stat))
108+
}
109+
110+
// Enable or Disable Short Url
111+
func APIUpdateUrl(ctx *gin.Context) {
112+
url := ctx.Param("url")
113+
enableStr := ctx.PostForm("enable")
114+
if utils.EemptyString(strings.TrimSpace(url)) {
115+
ctx.JSON(http.StatusBadRequest, core.ResultJsonBadRequest("url 不能为空"))
116+
return
117+
}
118+
119+
enable, err := strconv.ParseBool(enableStr)
120+
if err != nil {
121+
ctx.JSON(http.StatusBadRequest, core.ResultJsonBadRequest("enable 参数值非法"))
122+
return
123+
}
124+
125+
res, err := service.ChangeState(url, enable)
126+
if err != nil {
127+
ctx.JSON(http.StatusBadRequest, core.ResultJsonBadRequest(err.Error()))
128+
return
129+
}
130+
131+
ctx.JSON(http.StatusOK, core.ResultJsonSuccessWithData(res))
132+
}

controller/handlers.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,55 @@ import (
1414
"net/http"
1515
"ohurlshortener/core"
1616
"ohurlshortener/service"
17+
"ohurlshortener/storage"
1718
"ohurlshortener/utils"
1819
"strconv"
1920
"strings"
2021

2122
"github.com/gin-gonic/gin"
2223
)
2324

25+
const (
26+
authoriationHeaderKey = "Authorization"
27+
authoriationTypeBearer = "Bearer"
28+
)
29+
30+
// Authorization for /api
31+
func APIAuthHandler() gin.HandlerFunc {
32+
return func(ctx *gin.Context) {
33+
authHeader := ctx.GetHeader(authoriationHeaderKey)
34+
if utils.EemptyString(authHeader) {
35+
ctx.AbortWithStatusJSON(http.StatusUnauthorized, core.ResultJsonUnauthorized("Authorization Header is empty"))
36+
return
37+
}
38+
39+
fields := strings.Fields(authHeader)
40+
if len(fields) < 2 {
41+
ctx.AbortWithStatusJSON(http.StatusUnauthorized, core.ResultJsonUnauthorized("Invalid Authorization Header"))
42+
return
43+
}
44+
45+
if fields[0] != authoriationTypeBearer {
46+
ctx.AbortWithStatusJSON(http.StatusUnauthorized, core.ResultJsonUnauthorized("Unsupported Authorization Type"))
47+
return
48+
}
49+
50+
token := fields[1]
51+
res, err := validateToken(token)
52+
if err != nil {
53+
ctx.AbortWithStatusJSON(http.StatusInternalServerError, core.ResultJsonError("Internal error"))
54+
return
55+
}
56+
57+
if !res {
58+
ctx.AbortWithStatusJSON(http.StatusUnauthorized, core.ResultJsonUnauthorized("Authorization failed"))
59+
return
60+
}
61+
62+
ctx.Next()
63+
}
64+
}
65+
2466
func AdminCookieValue(user core.User) (string, error) {
2567
var result string
2668
data, err := utils.Sha256Of(user.Account + "a=" + user.Password + "=e" + strconv.Itoa(user.ID))
@@ -94,3 +136,18 @@ func WebLogFormatHandler(server string) gin.HandlerFunc {
94136
return ""
95137
}) //end of formatter
96138
} //end of func
139+
140+
func validateToken(token string) (bool, error) {
141+
users, err := storage.FindAllUsers()
142+
if err != nil {
143+
return false, err
144+
}
145+
146+
for _, u := range users {
147+
if u.Password == token {
148+
return true, nil
149+
}
150+
}
151+
152+
return false, nil
153+
}

0 commit comments

Comments
 (0)