diff --git a/api/streams/advance.go b/api/streams/advance.go index cf7c7b0e..62a2ad65 100644 --- a/api/streams/advance.go +++ b/api/streams/advance.go @@ -1,12 +1,13 @@ package streams import ( + "net/http" + "github.com/0xJacky/Nginx-UI/api" "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" ) func AdvancedEdit(c *gin.Context) { @@ -21,7 +22,7 @@ func AdvancedEdit(c *gin.Context) { name := c.Param("name") path := nginx.GetConfPath("streams-available", name) - s := query.Site + s := query.Stream _, err := s.Where(s.Path.Eq(path)).FirstOrCreate() if err != nil { @@ -39,5 +40,4 @@ func AdvancedEdit(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "ok", }) - } diff --git a/api/streams/router.go b/api/streams/router.go index 4d0ce2dd..67fa4334 100644 --- a/api/streams/router.go +++ b/api/streams/router.go @@ -6,6 +6,7 @@ func InitRouter(r *gin.RouterGroup) { r.GET("streams", GetStreams) r.GET("streams/:name", GetStream) r.POST("streams/:name", SaveStream) + r.POST("streams/:name/rename", RenameStream) r.POST("streams/:name/enable", EnableStream) r.POST("streams/:name/disable", DisableStream) r.POST("streams/:name/advance", AdvancedEdit) diff --git a/api/streams/streams.go b/api/streams/streams.go index 7a5056c0..9394d336 100644 --- a/api/streams/streams.go +++ b/api/streams/streams.go @@ -1,18 +1,19 @@ package streams import ( - "github.com/0xJacky/Nginx-UI/api" - "github.com/0xJacky/Nginx-UI/internal/config" - "github.com/0xJacky/Nginx-UI/internal/helper" - "github.com/0xJacky/Nginx-UI/internal/nginx" - "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" "time" + + "github.com/0xJacky/Nginx-UI/api" + "github.com/0xJacky/Nginx-UI/internal/config" + "github.com/0xJacky/Nginx-UI/internal/nginx" + "github.com/0xJacky/Nginx-UI/internal/stream" + "github.com/0xJacky/Nginx-UI/query" + "github.com/gin-gonic/gin" + "github.com/sashabaranov/go-openai" + "github.com/uozi-tech/cosy" ) type Stream struct { @@ -24,6 +25,7 @@ type Stream struct { ChatGPTMessages []openai.ChatCompletionMessage `json:"chatgpt_messages,omitempty"` Tokenized *nginx.NgxConfig `json:"tokenized,omitempty"` Filepath string `json:"filepath"` + SyncNodeIDs []uint64 `json:"sync_node_ids" gorm:"serializer:json"` } func GetStreams(c *gin.Context) { @@ -32,14 +34,12 @@ func GetStreams(c *gin.Context) { sort := c.DefaultQuery("sort", "desc") configFiles, err := os.ReadDir(nginx.GetConfPath("streams-available")) - if err != nil { api.ErrHandler(c, err) return } enabledConfig, err := os.ReadDir(nginx.GetConfPath("streams-enabled")) - if err != nil { api.ErrHandler(c, err) return @@ -77,15 +77,8 @@ func GetStreams(c *gin.Context) { } func GetStream(c *gin.Context) { - rewriteName, ok := c.Get("rewriteConfigFileName") - name := c.Param("name") - // for modify filename - if ok { - name = rewriteName.(string) - } - path := nginx.GetConfPath("streams-available", name) file, err := os.Stat(path) if os.IsNotExist(err) { @@ -114,14 +107,13 @@ func GetStream(c *gin.Context) { } s := query.Stream - stream, err := s.Where(s.Path.Eq(path)).FirstOrInit() - + streamModel, err := s.Where(s.Path.Eq(path)).FirstOrCreate() if err != nil { api.ErrHandler(c, err) return } - if stream.Advanced { + if streamModel.Advanced { origContent, err := os.ReadFile(path) if err != nil { api.ErrHandler(c, err) @@ -130,12 +122,13 @@ func GetStream(c *gin.Context) { c.JSON(http.StatusOK, Stream{ ModifiedAt: file.ModTime(), - Advanced: stream.Advanced, + Advanced: streamModel.Advanced, Enabled: enabled, Name: name, Config: string(origContent), ChatGPTMessages: chatgpt.Content, Filepath: path, + SyncNodeIDs: streamModel.SyncNodeIDs, }) return } @@ -149,207 +142,86 @@ func GetStream(c *gin.Context) { c.JSON(http.StatusOK, Stream{ ModifiedAt: file.ModTime(), - Advanced: stream.Advanced, + Advanced: streamModel.Advanced, Enabled: enabled, Name: name, Config: nginxConfig.FmtCode(), Tokenized: nginxConfig, ChatGPTMessages: chatgpt.Content, Filepath: path, + SyncNodeIDs: streamModel.SyncNodeIDs, }) } func SaveStream(c *gin.Context) { name := c.Param("name") - if name == "" { - c.JSON(http.StatusNotAcceptable, gin.H{ - "message": "param name is empty", - }) - return - } - var json struct { - Name string `json:"name" binding:"required"` - Content string `json:"content" binding:"required"` - Overwrite bool `json:"overwrite"` + Content string `json:"content" binding:"required"` + SyncNodeIDs []uint64 `json:"sync_node_ids"` + Overwrite bool `json:"overwrite"` } if !cosy.BindAndValid(c, &json) { return } - path := nginx.GetConfPath("streams-available", name) - - if !json.Overwrite && helper.FileExists(path) { - c.JSON(http.StatusNotAcceptable, gin.H{ - "message": "File exists", - }) - return - } - - err := os.WriteFile(path, []byte(json.Content), 0644) + err := stream.Save(name, json.Content, json.Overwrite, json.SyncNodeIDs) if err != nil { api.ErrHandler(c, err) return } - enabledConfigFilePath := nginx.GetConfPath("streams-enabled", name) - // rename the config file if needed - if name != json.Name { - newPath := nginx.GetConfPath("streams-available", json.Name) - s := query.Stream - _, err = s.Where(s.Path.Eq(path)).Update(s.Path, newPath) - - // check if dst file exists, do not rename - if helper.FileExists(newPath) { - c.JSON(http.StatusNotAcceptable, gin.H{ - "message": "File exists", - }) - return - } - // recreate a soft link - if helper.FileExists(enabledConfigFilePath) { - _ = os.Remove(enabledConfigFilePath) - enabledConfigFilePath = nginx.GetConfPath("streams-enabled", json.Name) - err = os.Symlink(newPath, enabledConfigFilePath) - - if err != nil { - api.ErrHandler(c, err) - return - } - } - - err = os.Rename(path, newPath) - if err != nil { - api.ErrHandler(c, err) - return - } - - name = json.Name - c.Set("rewriteConfigFileName", name) - } - - enabledConfigFilePath = nginx.GetConfPath("streams-enabled", name) - if helper.FileExists(enabledConfigFilePath) { - // Test nginx configuration - output := nginx.TestConf() - - if nginx.GetLogLevel(output) > nginx.Warn { - c.JSON(http.StatusInternalServerError, gin.H{ - "message": output, - }) - return - } - - output = nginx.Reload() - - if nginx.GetLogLevel(output) > nginx.Warn { - c.JSON(http.StatusInternalServerError, gin.H{ - "message": output, - }) - return - } - } GetStream(c) } func EnableStream(c *gin.Context) { - configFilePath := nginx.GetConfPath("streams-available", c.Param("name")) - enabledConfigFilePath := nginx.GetConfPath("streams-enabled", c.Param("name")) - - _, err := os.Stat(configFilePath) - + err := stream.Enable(c.Param("name")) if err != nil { api.ErrHandler(c, err) return } - if _, err = os.Stat(enabledConfigFilePath); os.IsNotExist(err) { - err = os.Symlink(configFilePath, enabledConfigFilePath) - - if err != nil { - api.ErrHandler(c, err) - return - } - } - - // Test nginx config, if not pass, then disable the stream. - output := nginx.TestConf() - - if nginx.GetLogLevel(output) > nginx.Warn { - _ = os.Remove(enabledConfigFilePath) - c.JSON(http.StatusInternalServerError, gin.H{ - "message": output, - }) - return - } - - output = nginx.Reload() - - if nginx.GetLogLevel(output) > nginx.Warn { - c.JSON(http.StatusInternalServerError, gin.H{ - "message": output, - }) - return - } - c.JSON(http.StatusOK, gin.H{ "message": "ok", }) } func DisableStream(c *gin.Context) { - enabledConfigFilePath := nginx.GetConfPath("streams-enabled", c.Param("name")) - - _, err := os.Stat(enabledConfigFilePath) - + err := stream.Disable(c.Param("name")) if err != nil { api.ErrHandler(c, err) return } - err = os.Remove(enabledConfigFilePath) - - if err != nil { - api.ErrHandler(c, err) - return - } - output := nginx.Reload() - - if nginx.GetLogLevel(output) > nginx.Warn { - c.JSON(http.StatusInternalServerError, gin.H{ - "message": output, - }) - return - } - c.JSON(http.StatusOK, gin.H{ "message": "ok", }) } func DeleteStream(c *gin.Context) { - var err error - name := c.Param("name") - availablePath := nginx.GetConfPath("streams-available", name) - enabledPath := nginx.GetConfPath("streams-enabled", name) - - if _, err = os.Stat(availablePath); os.IsNotExist(err) { - c.JSON(http.StatusNotFound, gin.H{ - "message": "stream not found", - }) - return - } - - if _, err = os.Stat(enabledPath); err == nil { - c.JSON(http.StatusNotAcceptable, gin.H{ - "message": "stream is enabled", - }) - return - } - - if err = os.Remove(availablePath); err != nil { + err := stream.Delete(c.Param("name")) + if err != nil { + api.ErrHandler(c, err) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "ok", + }) +} + +func RenameStream(c *gin.Context) { + oldName := c.Param("name") + var json struct { + NewName string `json:"new_name"` + } + if !cosy.BindAndValid(c, &json) { + return + } + + err := stream.Rename(oldName, json.NewName) + if err != nil { api.ErrHandler(c, err) return } diff --git a/app/src/api/stream.ts b/app/src/api/stream.ts index 6516d0f6..dbe87f38 100644 --- a/app/src/api/stream.ts +++ b/app/src/api/stream.ts @@ -12,6 +12,7 @@ export interface Stream { config: string chatgpt_messages: ChatComplicationMessage[] tokenized?: NgxConfig + sync_node_ids: number[] } class StreamCurd extends Curd { @@ -31,6 +32,10 @@ class StreamCurd extends Curd { advance_mode(name: string, data: { advanced: boolean }) { return http.post(`${this.baseUrl}/${name}/advance`, data) } + + rename(name: string, newName: string) { + return http.post(`${this.baseUrl}/${name}/rename`, { new_name: newName }) + } } const stream = new StreamCurd('/streams') diff --git a/app/src/components/NodeSelector/NodeSelector.vue b/app/src/components/NodeSelector/NodeSelector.vue index 65387018..c3c00d6d 100644 --- a/app/src/components/NodeSelector/NodeSelector.vue +++ b/app/src/components/NodeSelector/NodeSelector.vue @@ -32,6 +32,12 @@ function newSSE() { s.onmessage = (e: SSEvent) => { data.value = JSON.parse(e.data) + nextTick(() => { + data_map.value = data.value.reduce((acc, node) => { + acc[node.id] = node + return acc + }, {} as Record) + }) } // reconnect diff --git a/app/src/constants/errors/stream.ts b/app/src/constants/errors/stream.ts new file mode 100644 index 00000000..8c45d3e9 --- /dev/null +++ b/app/src/constants/errors/stream.ts @@ -0,0 +1,5 @@ +export default { + 40401: () => $gettext('Stream not found'), + 50001: () => $gettext('Destination file already exists'), + 50002: () => $gettext('Stream is enabled'), +} diff --git a/app/src/constants/errors/user.ts b/app/src/constants/errors/user.ts index acf5f2df..9540257f 100644 --- a/app/src/constants/errors/user.ts +++ b/app/src/constants/errors/user.ts @@ -3,6 +3,7 @@ export default { 40303: () => $gettext('User banned'), 40304: () => $gettext('Invalid otp code'), 40305: () => $gettext('Invalid recovery code'), + 40306: () => $gettext('Legacy recovery code not allowed since totp is not enabled'), 50000: () => $gettext('WebAuthn settings are not configured'), 50001: () => $gettext('User not enabled otp as 2fa'), 50002: () => $gettext('Otp or recovery code empty'), diff --git a/app/src/routes/index.ts b/app/src/routes/index.ts index bfabc104..8d17bc4c 100644 --- a/app/src/routes/index.ts +++ b/app/src/routes/index.ts @@ -91,7 +91,7 @@ export const routes: RouteRecordRaw[] = [ }, }, { - path: 'stream/:name', + path: 'streams/:name', name: 'Edit Stream', component: () => import('@/views/stream/StreamEdit.vue'), meta: { diff --git a/app/src/views/stream/StreamEdit.vue b/app/src/views/stream/StreamEdit.vue index bb94b391..06acf4b4 100644 --- a/app/src/views/stream/StreamEdit.vue +++ b/app/src/views/stream/StreamEdit.vue @@ -23,7 +23,7 @@ watch(route, () => { name.value = route.params?.name?.toString() ?? '' }) -const ngx_config: NgxConfig = reactive({ +const ngxConfig: NgxConfig = reactive({ name: '', upstreams: [], servers: [], @@ -31,81 +31,81 @@ const ngx_config: NgxConfig = reactive({ const enabled = ref(false) const configText = ref('') -const advance_mode_ref = ref(false) +const advanceModeRef = ref(false) const saving = ref(false) const filename = ref('') const filepath = ref('') -const parse_error_status = ref(false) -const parse_error_message = ref('') -const data = ref({}) +const parseErrorStatus = ref(false) +const parseErrorMessage = ref('') +const data = ref({} as Stream) init() -const advance_mode = computed({ +const advanceMode = computed({ get() { - return advance_mode_ref.value || parse_error_status.value + return advanceModeRef.value || parseErrorStatus.value }, set(v: boolean) { - advance_mode_ref.value = v + advanceModeRef.value = v }, }) -const history_chatgpt_record = ref([]) as Ref +const historyChatgptRecord = ref([]) as Ref -function handle_response(r: Stream) { +function handleResponse(r: Stream) { if (r.advanced) - advance_mode.value = true + advanceMode.value = true if (r.advanced) - advance_mode.value = true + advanceMode.value = true - parse_error_status.value = false - parse_error_message.value = '' + parseErrorStatus.value = false + parseErrorMessage.value = '' filename.value = r.name filepath.value = r.filepath configText.value = r.config enabled.value = r.enabled - history_chatgpt_record.value = r.chatgpt_messages + historyChatgptRecord.value = r.chatgpt_messages data.value = r - Object.assign(ngx_config, r.tokenized) + Object.assign(ngxConfig, r.tokenized) } function init() { if (name.value) { stream.get(name.value).then(r => { - handle_response(r) - }).catch(handle_parse_error) + handleResponse(r) + }).catch(handleParseError) } else { - history_chatgpt_record.value = [] + historyChatgptRecord.value = [] } } -function handle_parse_error(e: { error?: string, message: string }) { +function handleParseError(e: { error?: string, message: string }) { console.error(e) - parse_error_status.value = true - parse_error_message.value = e.message + parseErrorStatus.value = true + parseErrorMessage.value = e.message config.get(`streams-available/${name.value}`).then(r => { configText.value = r.content }) } -function on_mode_change(advanced: CheckedType) { +function onModeChange(advanced: CheckedType) { stream.advance_mode(name.value, { advanced: advanced as boolean }).then(() => { - advance_mode.value = advanced as boolean + advanceMode.value = advanced as boolean if (advanced) { - build_config() + buildConfig() } else { return ngx.tokenize_config(configText.value).then(r => { - Object.assign(ngx_config, r) - }).catch(handle_parse_error) + Object.assign(ngxConfig, r) + }).catch(handleParseError) } }) } -async function build_config() { - return ngx.build_config(ngx_config).then(r => { +async function buildConfig() { + return ngx.build_config(ngxConfig).then(r => { configText.value = r.content }) } @@ -113,9 +113,9 @@ async function build_config() { async function save() { saving.value = true - if (!advance_mode.value) { + if (!advanceMode.value) { try { - await build_config() + await buildConfig() } catch { saving.value = false @@ -129,22 +129,23 @@ async function save() { name: filename.value || name.value, content: configText.value, overwrite: true, + sync_node_ids: data.value?.sync_node_ids, }).then(r => { - handle_response(r) + handleResponse(r) router.push({ - path: `/stream/${filename.value}`, + path: `/streams/${filename.value}`, query: route.query, }) message.success($gettext('Saved successfully')) - }).catch(handle_parse_error).finally(() => { + }).catch(handleParseError).finally(() => { saving.value = false }) } provide('save_config', save) provide('configText', configText) -provide('ngx_config', ngx_config) -provide('history_chatgpt_record', history_chatgpt_record) +provide('ngx_config', ngxConfig) +provide('history_chatgpt_record', historyChatgptRecord) provide('enabled', enabled) provide('name', name) provide('filename', filename) @@ -180,12 +181,12 @@ provide('data', data)
-