feat(wip): node selector supports sse

This commit is contained in:
Jacky 2024-11-14 20:14:02 +08:00
parent ed0dca6820
commit bc70567dc1
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
31 changed files with 176 additions and 166 deletions

View file

@ -1,15 +1,10 @@
package api
import (
"errors"
"github.com/0xJacky/Nginx-UI/model"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/uozi-tech/cosy/logger"
"net/http"
"reflect"
"regexp"
"strings"
)
func CurrentUser(c *gin.Context) *model.User {
@ -23,105 +18,10 @@ func ErrHandler(c *gin.Context, err error) {
})
}
type ValidError struct {
Key string
Message string
}
func BindAndValid(c *gin.Context, target interface{}) bool {
err := c.ShouldBindJSON(target)
if err != nil {
logger.Error("bind err", err)
var verrs validator.ValidationErrors
ok := errors.As(err, &verrs)
if !ok {
c.JSON(http.StatusNotAcceptable, gin.H{
"message": "Requested with wrong parameters",
"code": http.StatusNotAcceptable,
})
return false
}
t := reflect.TypeOf(target).Elem()
errorsMap := make(map[string]interface{})
for _, value := range verrs {
var path []string
namespace := strings.Split(value.StructNamespace(), ".")
// logger.Debug(t.Name(), namespace)
if t.Name() != "" && len(namespace) > 1 {
namespace = namespace[1:]
}
getJsonPath(t, namespace, &path)
insertError(errorsMap, path, value.Tag())
}
c.JSON(http.StatusNotAcceptable, gin.H{
"errors": errorsMap,
"message": "Requested with wrong parameters",
"code": http.StatusNotAcceptable,
})
return false
}
return true
}
// findField recursively finds the field in a nested struct
func getJsonPath(t reflect.Type, fields []string, path *[]string) {
field := fields[0]
// used in case of array
var index string
if field[len(field)-1] == ']' {
re := regexp.MustCompile(`(\w+)\[(\d+)\]`)
matches := re.FindStringSubmatch(field)
if len(matches) > 2 {
field = matches[1]
index = matches[2]
}
}
f, ok := t.FieldByName(field)
if !ok {
return
}
*path = append(*path, f.Tag.Get("json"))
if index != "" {
*path = append(*path, index)
}
if len(fields) > 1 {
subFields := fields[1:]
getJsonPath(f.Type, subFields, path)
}
}
// insertError inserts an error into the errors map
func insertError(errorsMap map[string]interface{}, path []string, errorTag string) {
if len(path) == 0 {
return
}
jsonTag := path[0]
if len(path) == 1 {
// Last element in the path, set the error
errorsMap[jsonTag] = errorTag
return
}
// Create a new map if necessary
if _, ok := errorsMap[jsonTag]; !ok {
errorsMap[jsonTag] = make(map[string]interface{})
}
// Recursively insert into the nested map
subMap, _ := errorsMap[jsonTag].(map[string]interface{})
insertError(subMap, path[1:], errorTag)
func SetSSEHeaders(c *gin.Context) {
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
// https://stackoverflow.com/questions/27898622/server-sent-events-stopped-work-after-enabling-ssl-on-proxy/27960243#27960243
c.Header("X-Accel-Buffering", "no")
}

View file

@ -97,7 +97,7 @@ type certJson struct {
func AddCert(c *gin.Context) {
var json certJson
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}
@ -145,7 +145,7 @@ func ModifyCert(c *gin.Context) {
var json certJson
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}
@ -202,7 +202,7 @@ func RemoveCert(c *gin.Context) {
func SyncCertificate(c *gin.Context) {
var json cert.SyncCertificatePayload
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}

View file

@ -47,7 +47,7 @@ type DnsCredentialManageJson struct {
func AddDnsCredential(c *gin.Context) {
var json DnsCredentialManageJson
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}
@ -73,7 +73,7 @@ func EditDnsCredential(c *gin.Context) {
id := cast.ToUint64(c.Param("id"))
var json DnsCredentialManageJson
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}

View file

@ -1,6 +1,9 @@
package cluster
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"github.com/0xJacky/Nginx-UI/api"
"github.com/0xJacky/Nginx-UI/internal/analytic"
"github.com/0xJacky/Nginx-UI/internal/cluster"
@ -11,7 +14,9 @@ import (
"github.com/spf13/cast"
"github.com/uozi-tech/cosy"
"gorm.io/gorm"
"io"
"net/http"
"time"
)
func GetEnvironment(c *gin.Context) {
@ -44,6 +49,85 @@ func GetEnvironmentList(c *gin.Context) {
}).PagingList()
}
func GetAllEnabledEnvironment(c *gin.Context) {
api.SetSSEHeaders(c)
notify := c.Writer.CloseNotify()
interval := 10
type respEnvironment struct {
*model.Environment
Status bool `json:"status"`
}
f := func() (any, bool) {
return cosy.Core[model.Environment](c).
SetFussy("name").
SetTransformer(func(m *model.Environment) any {
resp := respEnvironment{
Environment: m,
Status: analytic.GetNode(m).Status,
}
return resp
}).ListAllData()
}
getHash := func(data any) string {
bytes, _ := json.Marshal(data)
hash := sha256.New()
hash.Write(bytes)
hashSum := hash.Sum(nil)
return hex.EncodeToString(hashSum)
}
dataHash := ""
{
data, ok := f()
if !ok {
return
}
c.Stream(func(w io.Writer) bool {
c.SSEvent("message", data)
dataHash = getHash(data)
return false
})
}
for {
select {
case <-time.After(time.Duration(interval) * time.Second):
data, ok := f()
if !ok {
return
}
// if data is not changed, send heartbeat
if dataHash == getHash(data) {
c.Stream(func(w io.Writer) bool {
c.SSEvent("heartbeat", "")
return false
})
return
}
dataHash = getHash(data)
c.Stream(func(w io.Writer) bool {
c.SSEvent("message", data)
return false
})
case <-time.After(30 * time.Second):
c.Stream(func(w io.Writer) bool {
c.SSEvent("heartbeat", "")
return false
})
case <-notify:
return
}
}
}
func AddEnvironment(c *gin.Context) {
cosy.Core[model.Environment](c).SetValidRules(gin.H{
"name": "required",

View file

@ -5,6 +5,7 @@ import "github.com/gin-gonic/gin"
func InitRouter(r *gin.RouterGroup) {
// Environment
r.GET("environments", GetEnvironmentList)
r.GET("environments/enabled", GetAllEnabledEnvironment)
r.POST("environments/load_from_settings", LoadEnvironmentFromSettings)
envGroup := r.Group("environments")
{

View file

@ -9,6 +9,7 @@ import (
"github.com/0xJacky/Nginx-UI/query"
"github.com/gin-gonic/gin"
"github.com/sashabaranov/go-openai"
"github.com/uozi-tech/cosy"
"net/http"
"os"
"path/filepath"
@ -24,7 +25,7 @@ func AddConfig(c *gin.Context) {
SyncNodeIds []uint64 `json:"sync_node_ids"`
}
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}

View file

@ -5,6 +5,7 @@ import (
"github.com/0xJacky/Nginx-UI/internal/helper"
"github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/gin-gonic/gin"
"github.com/uozi-tech/cosy"
"net/http"
"os"
)
@ -14,7 +15,7 @@ func Mkdir(c *gin.Context) {
BasePath string `json:"base_path"`
FolderName string `json:"folder_name"`
}
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}
fullPath := nginx.GetConfPath(json.BasePath, json.FolderName)

View file

@ -9,6 +9,7 @@ import (
"github.com/0xJacky/Nginx-UI/query"
"github.com/gin-gonic/gin"
"github.com/sashabaranov/go-openai"
"github.com/uozi-tech/cosy"
"net/http"
"os"
"path/filepath"
@ -26,7 +27,7 @@ func EditConfig(c *gin.Context) {
SyncOverwrite bool `json:"sync_overwrite"`
SyncNodeIds []uint64 `json:"sync_node_ids"`
}
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}

View file

@ -8,6 +8,8 @@ import (
"github.com/0xJacky/Nginx-UI/model"
"github.com/0xJacky/Nginx-UI/query"
"github.com/gin-gonic/gin"
"github.com/uozi-tech/cosy"
"github.com/uozi-tech/cosy/logger"
"net/http"
"os"
"path/filepath"
@ -21,8 +23,7 @@ func Rename(c *gin.Context) {
NewName string `json:"new_name"`
SyncNodeIds []uint64 `json:"sync_node_ids" gorm:"serializer:json"`
}
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}

View file

@ -4,12 +4,13 @@ import (
"github.com/0xJacky/Nginx-UI/api"
"github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/gin-gonic/gin"
"github.com/uozi-tech/cosy"
"net/http"
)
func BuildNginxConfig(c *gin.Context) {
var ngxConf nginx.NgxConfig
if !api.BindAndValid(c, &ngxConf) {
if !cosy.BindAndValid(c, &ngxConf) {
return
}
content, err := ngxConf.BuildConfig()
@ -27,7 +28,7 @@ func TokenizeNginxConfig(c *gin.Context) {
Content string `json:"content" binding:"required"`
}
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}
@ -45,7 +46,7 @@ func FormatNginxConfig(c *gin.Context) {
Content string `json:"content" binding:"required"`
}
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}
content, err := nginx.FmtCode(json.Content)

View file

@ -3,7 +3,6 @@ package nginx
import (
"encoding/json"
"fmt"
"github.com/0xJacky/Nginx-UI/api"
"github.com/0xJacky/Nginx-UI/internal/cache"
"github.com/0xJacky/Nginx-UI/internal/helper"
"github.com/0xJacky/Nginx-UI/internal/nginx"
@ -13,6 +12,7 @@ import (
"github.com/hpcloud/tail"
"github.com/pkg/errors"
"github.com/spf13/cast"
"github.com/uozi-tech/cosy"
"github.com/uozi-tech/cosy/logger"
"io"
"net/http"
@ -44,7 +44,7 @@ func GetNginxLogPage(c *gin.Context) {
}
var control controlStruct
if !api.BindAndValid(c, &control) {
if !cosy.BindAndValid(c, &control) {
return
}

View file

@ -1,6 +1,7 @@
package notification
import (
"github.com/0xJacky/Nginx-UI/api"
"github.com/0xJacky/Nginx-UI/internal/notification"
"github.com/0xJacky/Nginx-UI/model"
"github.com/gin-gonic/gin"
@ -9,11 +10,7 @@ import (
)
func Live(c *gin.Context) {
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
// https://stackoverflow.com/questions/27898622/server-sent-events-stopped-work-after-enabling-ssl-on-proxy/27960243#27960243
c.Header("X-Accel-Buffering", "no")
api.SetSSEHeaders(c)
evtChan := make(chan *model.Notification)

View file

@ -3,13 +3,13 @@ package openai
import (
"context"
"fmt"
"github.com/0xJacky/Nginx-UI/api"
"github.com/0xJacky/Nginx-UI/internal/chatbot"
"github.com/0xJacky/Nginx-UI/internal/transport"
"github.com/0xJacky/Nginx-UI/settings"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"github.com/sashabaranov/go-openai"
"github.com/uozi-tech/cosy"
"io"
"net/http"
)
@ -26,7 +26,7 @@ func MakeChatCompletionRequest(c *gin.Context) {
Messages []openai.ChatCompletionMessage `json:"messages"`
}
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}

View file

@ -6,6 +6,7 @@ import (
"github.com/0xJacky/Nginx-UI/query"
"github.com/gin-gonic/gin"
"github.com/sashabaranov/go-openai"
"github.com/uozi-tech/cosy"
"net/http"
)
@ -15,7 +16,7 @@ func StoreChatGPTRecord(c *gin.Context) {
Messages []openai.ChatCompletionMessage `json:"messages"`
}
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}

View file

@ -1,10 +1,10 @@
package settings
import (
"github.com/0xJacky/Nginx-UI/api"
"github.com/0xJacky/Nginx-UI/query"
"github.com/0xJacky/Nginx-UI/settings"
"github.com/gin-gonic/gin"
"github.com/uozi-tech/cosy"
"net/http"
"time"
)
@ -19,7 +19,7 @@ func GetBanLoginIP(c *gin.Context) {
b.ExpiredAt.Gte(time.Now().Unix()),
b.Attempts.Gte(settings.AuthSettings.MaxAttempts)).Find()
if err != nil {
api.ErrHandler(c, err)
cosy.ErrHandler(c, err)
return
}
c.JSON(http.StatusOK, banIps)
@ -29,7 +29,7 @@ func RemoveBannedIP(c *gin.Context) {
var json struct {
IP string `json:"ip"`
}
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}
@ -37,7 +37,7 @@ func RemoveBannedIP(c *gin.Context) {
_, err := b.Where(b.IP.Eq(json.IP)).Delete()
if err != nil {
api.ErrHandler(c, err)
cosy.ErrHandler(c, err)
return
}

View file

@ -7,6 +7,7 @@ import (
"github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/0xJacky/Nginx-UI/settings"
"github.com/gin-gonic/gin"
"github.com/uozi-tech/cosy"
cSettings "github.com/uozi-tech/cosy/settings"
"net/http"
)
@ -68,7 +69,7 @@ func SaveSettings(c *gin.Context) {
Logrotate settings.Logrotate `json:"logrotate"`
}
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}

View file

@ -5,6 +5,7 @@ import (
"github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/0xJacky/Nginx-UI/query"
"github.com/gin-gonic/gin"
"github.com/uozi-tech/cosy"
"net/http"
)
@ -13,7 +14,7 @@ func DomainEditByAdvancedMode(c *gin.Context) {
Advanced bool `json:"advanced"`
}
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}

View file

@ -6,6 +6,7 @@ import (
"github.com/0xJacky/Nginx-UI/model"
"github.com/gin-gonic/gin"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/uozi-tech/cosy"
"net/http"
)
@ -19,7 +20,7 @@ func AddDomainToAutoCert(c *gin.Context) {
KeyType certcrypto.KeyType `json:"key_type"`
}
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}

View file

@ -4,6 +4,7 @@ import (
"github.com/0xJacky/Nginx-UI/api"
"github.com/0xJacky/Nginx-UI/internal/site"
"github.com/gin-gonic/gin"
"github.com/uozi-tech/cosy"
"net/http"
)
@ -16,7 +17,7 @@ func DuplicateSite(c *gin.Context) {
Name string `json:"name" binding:"required"`
}
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}

View file

@ -120,7 +120,7 @@ func SaveSite(c *gin.Context) {
Overwrite bool `json:"overwrite"`
}
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}

View file

@ -5,6 +5,7 @@ import (
"github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/0xJacky/Nginx-UI/query"
"github.com/gin-gonic/gin"
"github.com/uozi-tech/cosy"
"net/http"
)
@ -13,7 +14,7 @@ func AdvancedEdit(c *gin.Context) {
Advanced bool `json:"advanced"`
}
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}

View file

@ -5,6 +5,7 @@ import (
"github.com/0xJacky/Nginx-UI/internal/helper"
"github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/gin-gonic/gin"
"github.com/uozi-tech/cosy"
"net/http"
)
@ -17,7 +18,7 @@ func Duplicate(c *gin.Context) {
Name string `json:"name" binding:"required"`
}
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}

View file

@ -8,6 +8,7 @@ import (
"github.com/0xJacky/Nginx-UI/query"
"github.com/gin-gonic/gin"
"github.com/sashabaranov/go-openai"
"github.com/uozi-tech/cosy"
"net/http"
"os"
"strings"
@ -174,7 +175,7 @@ func SaveStream(c *gin.Context) {
Overwrite bool `json:"overwrite"`
}
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}

View file

@ -8,6 +8,7 @@ import (
"github.com/0xJacky/Nginx-UI/settings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/uozi-tech/cosy"
cSettings "github.com/uozi-tech/cosy/settings"
"golang.org/x/crypto/bcrypt"
"net/http"
@ -39,7 +40,7 @@ func InstallNginxUI(c *gin.Context) {
return
}
var json InstallJson
ok := api.BindAndValid(c, &json)
ok := cosy.BindAndValid(c, &json)
if !ok {
return
}

View file

@ -12,6 +12,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/google/uuid"
"github.com/uozi-tech/cosy"
"net/http"
"strings"
"time"
@ -71,7 +72,7 @@ func Start2FASecureSessionByOTP(c *gin.Context) {
OTP string `json:"otp"`
RecoveryCode string `json:"recovery_code"`
}
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}
u := api.CurrentUser(c)

View file

@ -7,6 +7,7 @@ import (
"github.com/0xJacky/Nginx-UI/settings"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"github.com/uozi-tech/cosy"
"github.com/uozi-tech/cosy/logger"
"math/rand/v2"
"net/http"
@ -61,7 +62,7 @@ func Login(c *gin.Context) {
}
var json LoginUser
ok := api.BindAndValid(c, &json)
ok := cosy.BindAndValid(c, &json)
if !ok {
return
}

View file

@ -8,6 +8,7 @@ import (
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"github.com/uozi-tech/cosy"
"gorm.io/gorm"
"net/http"
"net/url"
@ -22,7 +23,7 @@ type CasdoorLoginUser struct {
func CasdoorCallback(c *gin.Context) {
var loginUser CasdoorLoginUser
ok := api.BindAndValid(c, &loginUser)
ok := cosy.BindAndValid(c, &loginUser)
if !ok {
return
}

View file

@ -13,6 +13,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/uozi-tech/cosy"
"image/jpeg"
"net/http"
"strings"
@ -81,7 +82,7 @@ func EnrollTOTP(c *gin.Context) {
Secret string `json:"secret" binding:"required"`
Passcode string `json:"passcode" binding:"required"`
}
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}
@ -117,7 +118,7 @@ func ResetOTP(c *gin.Context) {
var json struct {
RecoveryCode string `json:"recovery_code"`
}
if !api.BindAndValid(c, &json) {
if !cosy.BindAndValid(c, &json) {
return
}
recoverCode, err := hex.DecodeString(json.RecoveryCode)

View file

@ -38,7 +38,7 @@ type UserJson struct {
func AddUser(c *gin.Context) {
var json UserJson
ok := api.BindAndValid(c, &json)
ok := cosy.BindAndValid(c, &json)
if !ok {
return
}
@ -79,7 +79,7 @@ func EditUser(c *gin.Context) {
}
var json UserJson
ok := api.BindAndValid(c, &json)
ok := cosy.BindAndValid(c, &json)
if !ok {
return
}

View file

@ -1,7 +1,8 @@
<script setup lang="ts">
import type { Environment } from '@/api/environment'
import type { Ref } from 'vue'
import environment from '@/api/environment'
import { useUserStore } from '@/pinia'
import { SSE, type SSEvent } from 'sse.js'
const props = defineProps<{
hiddenLocal?: boolean
@ -9,26 +10,35 @@ const props = defineProps<{
const target = defineModel<number[]>('target')
const map = defineModel<Record<number, string>>('map')
const { token } = storeToRefs(useUserStore())
const data = ref([]) as Ref<Environment[]>
const data_map = ref({}) as Ref<Record<number, Environment>>
onMounted(async () => {
let hasMore = true
let page = 1
while (hasMore) {
await environment.get_list({ page, enabled: true }).then(r => {
data.value.push(...r.data)
r.data?.forEach(node => {
data_map.value[node.id] = node
})
hasMore = r.data.length === r.pagination?.per_page
page++
}).catch(() => {
hasMore = false
})
const sse = shallowRef(newSSE())
function reconnect() {
setTimeout(() => {
sse.value = newSSE()
}, 5000)
}
function newSSE() {
const s = new SSE('/api/environments/enabled', {
headers: {
Authorization: token.value,
},
})
s.onmessage = (e: SSEvent) => {
data.value = JSON.parse(e.data)
}
})
// reconnect
s.onerror = reconnect
return s
}
const value = computed({
get() {

View file

@ -35,7 +35,7 @@ function batchUpgrade() {
:api="environment"
:columns="envColumns"
>
<template #extra>
<template #beforeAdd>
<a @click="loadFromSettings">{{ $gettext('Load from settings') }}</a>
</template>
</StdCurd>