Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: API支持 #206

Merged
merged 10 commits into from
May 18, 2022
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions cmd/dashboard/controller/api_v1.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package controller

import (
"github.com/gin-gonic/gin"
"github.com/naiba/nezha/pkg/mygin"
"github.com/naiba/nezha/service/singleton"
"strconv"
"strings"
)

type apiV1 struct {
r gin.IRouter
}

func (v *apiV1) serve() {
r := v.r.Group("")
// API
r.Use(mygin.Authorize(mygin.AuthorizeOption{
Member: true,
IsPage: false,
AllowAPI: true,
Msg: "访问此接口需要认证",
Btn: "点此登录",
Redirect: "/login",
}))
r.GET("/server/list", v.serverList)
r.GET("/server/details", v.serverDetails)

}

// serverList 获取服务器列表 不传入Query参数则获取全部
// header: Authorization: Token
// query: tag (服务器分组)
func (v *apiV1) serverList(c *gin.Context) {
token, _ := c.Cookie("Authorization")
tag := c.Query("tag")
serverAPI := &singleton.ServerAPI{
Token: token,
AkkiaS7 marked this conversation as resolved.
Show resolved Hide resolved
Tag: tag,
}
if tag != "" {
c.JSON(200, serverAPI.GetListByTag())
AkkiaS7 marked this conversation as resolved.
Show resolved Hide resolved
return
}
c.JSON(200, serverAPI.GetAllList())
}

// serverDetails 获取服务器信息 不传入Query参数则获取全部
// header: Authorization: Token
// query: id (服务器ID,逗号分隔,优先级高于tag查询)
// query: tag (服务器分组)
func (v *apiV1) serverDetails(c *gin.Context) {
token, _ := c.Cookie("Authorization")
var idList []uint64
idListStr := strings.Split(c.Query("id"), ",")
if c.Query("id") != "" {
idList = make([]uint64, len(idListStr))
for i, v := range idListStr {
id, _ := strconv.ParseUint(v, 10, 64)
idList[i] = id
}
}
tag := c.Query("tag")
serverAPI := &singleton.ServerAPI{
Token: token,
IDList: idList,
Tag: tag,
}
if tag != "" {
c.JSON(200, serverAPI.GetStatusByTag())
return
}
if len(idList) != 0 {
c.JSON(200, serverAPI.GetStatusByIDList())
AkkiaS7 marked this conversation as resolved.
Show resolved Hide resolved
return
}
c.JSON(200, serverAPI.GetAllStatus())
}
132 changes: 132 additions & 0 deletions cmd/dashboard/controller/member_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,120 @@ func (ma *memberAPI) serve() {
mr.POST("/setting", ma.updateSetting)
mr.DELETE("/:model/:id", ma.delete)
mr.POST("/logout", ma.logout)
mr.GET("/token", ma.getToken)
mr.POST("/token", ma.issueNewToken)
mr.DELETE("/token/:token", ma.deleteToken)

// API
v1 := ma.r.Group("v1")
{
apiv1 := &apiV1{v1}
apiv1.serve()
}
}

type apiResult struct {
Token string `json:"token"`
Note string `json:"note"`
}

// getToken 获取 Token
func (ma *memberAPI) getToken(c *gin.Context) {
u := c.MustGet(model.CtxKeyAuthorizedUser).(*model.User)
singleton.ApiLock.RLock()
defer singleton.ApiLock.RUnlock()

tokenList := singleton.UserIDToApiTokenList[u.ID]
res := make([]*apiResult, len(tokenList))
for i, token := range tokenList {
res[i] = &apiResult{
Token: token,
Note: singleton.ApiTokenList[token].Note,
}
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"result": res,
})
}

type TokenForm struct {
Note string
}

// issueNewToken 生成新的 token
func (ma *memberAPI) issueNewToken(c *gin.Context) {
u := c.MustGet(model.CtxKeyAuthorizedUser).(*model.User)
tf := &TokenForm{}
err := c.ShouldBindJSON(tf)
if err != nil {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("请求错误:%s", err),
})
return
}
token := &model.ApiToken{
UserID: u.ID,
Token: utils.MD5(fmt.Sprintf("%d%d%s", time.Now().UnixNano(), u.ID, u.Login)),
Note: tf.Note,
}
singleton.DB.Create(token)

singleton.ApiLock.Lock()
singleton.ApiTokenList[token.Token] = token
singleton.UserIDToApiTokenList[u.ID] = append(singleton.UserIDToApiTokenList[u.ID], token.Token)
singleton.ApiLock.Unlock()

c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK,
Message: "success",
Result: map[string]string{
"token": token.Token,
"note": token.Note,
},
})
}

// deleteToken 删除 token
func (ma *memberAPI) deleteToken(c *gin.Context) {
token := c.Param("token")
if token == "" {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: "token 不能为空",
})
return
}
singleton.ApiLock.Lock()
defer singleton.ApiLock.Unlock()
if _, ok := singleton.ApiTokenList[token]; !ok {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: "token 不存在",
})
return
}
// 在数据库中删除该Token
singleton.DB.Unscoped().Delete(&model.ApiToken{}, "token = ?", token)

// 在UserIDToApiTokenList中删除该Token
for i, t := range singleton.UserIDToApiTokenList[singleton.ApiTokenList[token].UserID] {
if t == token {
singleton.UserIDToApiTokenList[singleton.ApiTokenList[token].UserID] = append(singleton.UserIDToApiTokenList[singleton.ApiTokenList[token].UserID][:i], singleton.UserIDToApiTokenList[singleton.ApiTokenList[token].UserID][i+1:]...)
break
}
}
if len(singleton.UserIDToApiTokenList[singleton.ApiTokenList[token].UserID]) == 0 {
delete(singleton.UserIDToApiTokenList, singleton.ApiTokenList[token].UserID)
}
// 在ApiTokenList中删除该Token
delete(singleton.ApiTokenList, token)
c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK,
Message: "success",
})
}

func (ma *memberAPI) delete(c *gin.Context) {
Expand Down Expand Up @@ -194,6 +308,23 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) {
// 设置新的 Secret-ID 绑定关系
delete(singleton.SecretToID, singleton.ServerList[s.ID].Secret)
}
// 如果修改了Tag
if s.Tag != singleton.ServerList[s.ID].Tag {
index := 0
for index < len(singleton.ServerTagToIDList[s.Tag]) {
if singleton.ServerTagToIDList[s.Tag][index] == s.ID {
break
}
index++
}
// 删除旧 Tag-ID 绑定关系
singleton.ServerTagToIDList[singleton.ServerList[s.ID].Tag] = append(singleton.ServerTagToIDList[singleton.ServerList[s.ID].Tag][:index], singleton.ServerTagToIDList[singleton.ServerList[s.ID].Tag][index+1:]...)
// 设置新的 Tag-ID 绑定关系
singleton.ServerTagToIDList[s.Tag] = append(singleton.ServerTagToIDList[s.Tag], s.ID)
if len(singleton.ServerTagToIDList[s.Tag]) == 0 {
delete(singleton.ServerTagToIDList, s.Tag)
}
}
singleton.ServerList[s.ID] = &s
singleton.ServerLock.Unlock()
} else {
Expand All @@ -202,6 +333,7 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) {
singleton.ServerLock.Lock()
singleton.SecretToID[s.Secret] = s.ID
singleton.ServerList[s.ID] = &s
singleton.ServerTagToIDList[s.Tag] = append(singleton.ServerTagToIDList[s.Tag], s.ID)
singleton.ServerLock.Unlock()
}
singleton.ReSortServer()
Expand Down
10 changes: 10 additions & 0 deletions cmd/dashboard/controller/member_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ func (mp *memberPage) serve() {
mr.GET("/cron", mp.cron)
mr.GET("/notification", mp.notification)
mr.GET("/setting", mp.setting)
mr.GET("/api", mp.api)
}

func (mp *memberPage) api(c *gin.Context) {
singleton.ApiLock.RLock()
defer singleton.ApiLock.RUnlock()
c.HTML(http.StatusOK, "dashboard/api", mygin.CommonEnvironment(c, gin.H{
"title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ApiManagement"}),
"Tokens": singleton.ApiTokenList,
}))
}

func (mp *memberPage) server(c *gin.Context) {
Expand Down
8 changes: 8 additions & 0 deletions model/api_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package model

type ApiToken struct {
Common
UserID uint64 `json:"user_id"`
Token string `json:"token"`
Note string `json:"note"`
}
19 changes: 18 additions & 1 deletion pkg/mygin/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type AuthorizeOption struct {
Guest bool
Member bool
IsPage bool
AllowAPI bool
Msg string
Redirect string
Btn string
Expand All @@ -34,7 +35,6 @@ func Authorize(opt AuthorizeOption) func(*gin.Context) {
Link: opt.Redirect,
Btn: opt.Btn,
}

var isLogin bool

// 用户鉴权
Expand All @@ -50,6 +50,23 @@ func Authorize(opt AuthorizeOption) func(*gin.Context) {
}
}

// API鉴权
if opt.AllowAPI {
apiToken := c.GetHeader("Authorization")
if apiToken != "" {
var u model.User
singleton.ApiLock.RLock()
if _, ok := singleton.ApiTokenList[apiToken]; ok {
err := singleton.DB.First(&u).Where("id = ?", singleton.ApiTokenList[apiToken].UserID).Error
isLogin = err == nil
}
singleton.ApiLock.RUnlock()
if isLogin {
c.Set(model.CtxKeyAuthorizedUser, &u)
c.Set("isAPI", true)
}
}
}
// 已登录且只能游客访问
if isLogin && opt.Guest {
ShowErrorPage(c, commonErr, opt.IsPage)
Expand Down
1 change: 1 addition & 0 deletions pkg/mygin/mygin.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ var adminPage = map[string]bool{
"/setting": true,
"/notification": true,
"/cron": true,
"/api": true,
}

func CommonEnvironment(c *gin.Context, data map[string]interface{}) gin.H {
Expand Down
25 changes: 25 additions & 0 deletions pkg/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"math/rand"
"os"
"regexp"
"strings"
"time"
"unsafe"

Expand Down Expand Up @@ -68,3 +69,27 @@ func IPDesensitize(ipAddr string) string {
ipAddr = ipv6Desensitize(ipAddr)
return ipAddr
}

// SplitIPAddr 传入/分割的v4v6混合地址,返回v4和v6地址与有效地址
func SplitIPAddr(v4v6Bundle string) (string, string, string) {
ipList := strings.Split(v4v6Bundle, "/")
ipv4 := ""
ipv6 := ""
validIP := ""
if len(ipList) > 1 {
// 双栈
ipv4 = ipList[0]
ipv6 = ipList[1]
validIP = ipv4
} else if len(ipList) == 1 {
// 仅ipv4|ipv6
if strings.Contains(ipList[0], ":") {
ipv6 = ipList[0]
validIP = ipv6
} else {
ipv4 = ipList[0]
validIP = ipv4
}
}
return ipv4, ipv6, validIP
}
15 changes: 15 additions & 0 deletions resource/l10n/en-US.toml
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,21 @@ other = "Services"
[ScheduledTasks]
other = "Scheduled Tasks"

[ApiManagement]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

en 和 es 需要恢复,在仓库直接改会让 crowdin 混乱

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

最新的commit是在en和es里直接删掉了这几个设置
不太确定是不是要这样做
现在conf设置成en-US会panic

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

没事,crowdin 会自己补缺失的 key,先不发版,提交了翻译完再发

other="API"

[IssueNewApiToken]
other="Create Token"

[Token]
other="Token"

[DeleteToken]
other="Delete Token"

[ConfirmToDeleteThisToken]
other="Confirm Delete?"

[YouAreNotAuthorized]
other = "You are not authorized"

Expand Down
15 changes: 15 additions & 0 deletions resource/l10n/es-ES.toml
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,21 @@ other = "Monitorización del servicio"
[ScheduledTasks]
other = "Tareas programadas"

[ApiManagement]
other="API"

[IssueNewApiToken]
other="Create Token"

[Token]
other="Token"

[DeleteToken]
other="Delete Token"

[ConfirmToDeleteThisToken]
other="Confirm Delete?"

[YouAreNotAuthorized]
other = "Esta página requiere un acceso"

Expand Down
Loading