فهرست منبع

feat: rename folder or file in configurations list

Jacky 9 ماه پیش
والد
کامیت
ace8d7a0fe

+ 36 - 0
api/config/folder.go

@@ -0,0 +1,36 @@
+package config
+
+import (
+	"github.com/0xJacky/Nginx-UI/api"
+	"github.com/0xJacky/Nginx-UI/internal/helper"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/gin-gonic/gin"
+	"net/http"
+	"os"
+)
+
+func Mkdir(c *gin.Context) {
+	var json struct {
+		BasePath   string `json:"base_path"`
+		FolderName string `json:"folder_name"`
+	}
+	if !api.BindAndValid(c, &json) {
+		return
+	}
+	fullPath := nginx.GetConfPath(json.BasePath, json.FolderName)
+	if !helper.IsUnderDirectory(fullPath, nginx.GetConfPath()) {
+		c.JSON(http.StatusForbidden, gin.H{
+			"message": "You are not allowed to create a folder " +
+				"outside of the nginx configuration directory",
+		})
+		return
+	}
+	err := os.Mkdir(fullPath, 0755)
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+	c.JSON(http.StatusOK, gin.H{
+		"message": "ok",
+	})
+}

+ 68 - 0
api/config/rename.go

@@ -0,0 +1,68 @@
+package config
+
+import (
+	"github.com/0xJacky/Nginx-UI/api"
+	"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"
+	"net/http"
+	"os"
+)
+
+func Rename(c *gin.Context) {
+	var json struct {
+		BasePath string `json:"base_path"`
+		OrigName string `json:"orig_name"`
+		NewName  string `json:"new_name"`
+	}
+	if !api.BindAndValid(c, &json) {
+		return
+	}
+	if json.OrigName == json.OrigName {
+		c.JSON(http.StatusOK, gin.H{
+			"message": "ok",
+		})
+		return
+	}
+	origFullPath := nginx.GetConfPath(json.BasePath, json.OrigName)
+	newFullPath := nginx.GetConfPath(json.BasePath, json.NewName)
+	if !helper.IsUnderDirectory(origFullPath, nginx.GetConfPath()) ||
+		!helper.IsUnderDirectory(newFullPath, nginx.GetConfPath()) {
+		c.JSON(http.StatusForbidden, gin.H{
+			"message": "you are not allowed to rename a file " +
+				"outside of the nginx config path",
+		})
+		return
+	}
+
+	stat, err := os.Stat(origFullPath)
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	if helper.FileExists(newFullPath) {
+		c.JSON(http.StatusNotAcceptable, gin.H{
+			"message": "target file already exists",
+		})
+		return
+	}
+
+	err = os.Rename(origFullPath, newFullPath)
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	if !stat.IsDir() {
+		// update ChatGPT records
+		g := query.ChatGPTLog
+		_, _ = g.Where(g.Name.Eq(newFullPath)).Delete()
+		_, _ = g.Where(g.Name.Eq(origFullPath)).Update(g.Name, newFullPath)
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"message": "ok",
+	})
+}

+ 4 - 1
api/config/router.go

@@ -3,9 +3,12 @@ package config
 import "github.com/gin-gonic/gin"
 
 func InitRouter(r *gin.RouterGroup) {
+	r.GET("config_base_path", GetBasePath)
+
 	r.GET("configs", GetConfigs)
 	r.GET("config/*name", GetConfig)
 	r.POST("config", AddConfig)
 	r.POST("config/*name", EditConfig)
-	r.GET("config_base_path", GetBasePath)
+	r.POST("config_mkdir", Mkdir)
+	r.POST("config_rename", Rename)
 }

+ 8 - 0
app/src/api/config.ts

@@ -18,6 +18,14 @@ class ConfigCurd extends Curd<Config> {
   get_base_path() {
     return http.get('/config_base_path')
   }
+
+  mkdir(basePath: string, name: string) {
+    return http.post('/config_mkdir', { base_path: basePath, folder_name: name })
+  }
+
+  rename(basePath: string, origName: string, newName: string) {
+    return http.post('/config_rename', { base_path: basePath, orig_name: origName, new_name: newName })
+  }
 }
 
 const config: ConfigCurd = new ConfigCurd()

+ 4 - 0
app/src/components/OTP/useOTPModal.ts

@@ -3,6 +3,7 @@ import { Modal, message } from 'ant-design-vue'
 import { useCookies } from '@vueuse/integrations/useCookies'
 import OTPAuthorization from '@/components/OTP/OTPAuthorization.vue'
 import otp from '@/api/otp'
+import { useUserStore } from '@/pinia'
 
 export interface OTPModalProps {
   onOk?: (secureSessionId: string) => void
@@ -12,6 +13,7 @@ export interface OTPModalProps {
 const useOTPModal = () => {
   const refOTPAuthorization = ref<typeof OTPAuthorization>()
   const randomId = Math.random().toString(36).substring(2, 8)
+  const { secureSessionId } = storeToRefs(useUserStore())
 
   const injectStyles = () => {
     const style = document.createElement('style')
@@ -36,6 +38,7 @@ const useOTPModal = () => {
     const ssid = cookies.get('secure_session_id')
     if (ssid) {
       onOk?.(ssid)
+      secureSessionId.value = ssid
 
       return
     }
@@ -55,6 +58,7 @@ const useOTPModal = () => {
         cookies.set('secure_session_id', r.session_id, { maxAge: 60 * 3 })
         onOk?.(r.session_id)
         close()
+        secureSessionId.value = r.session_id
       }).catch(async () => {
         refOTPAuthorization.value?.clearInput()
         await message.error($gettext('Invalid passcode or recovery code'))

+ 7 - 2
app/src/lib/http/index.ts

@@ -9,7 +9,7 @@ import router from '@/routes'
 
 const user = useUserStore()
 const settings = useSettingsStore()
-const { token } = storeToRefs(user)
+const { token, secureSessionId } = storeToRefs(user)
 
 const instance = axios.create({
   baseURL: import.meta.env.VITE_API_ROOT,
@@ -28,7 +28,7 @@ const instance = axios.create({
 instance.interceptors.request.use(
   config => {
     NProgress.start()
-    if (token) {
+    if (token.value) {
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       (config.headers as any).Authorization = token.value
     }
@@ -38,6 +38,11 @@ instance.interceptors.request.use(
       (config.headers as any)['X-Node-ID'] = settings.environment.id
     }
 
+    if (secureSessionId.value) {
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      (config.headers as any)['X-Secure-Session-ID'] = secureSessionId.value
+    }
+
     return config
   },
   err => {

+ 1 - 0
app/src/pinia/moudule/user.ts

@@ -4,6 +4,7 @@ export const useUserStore = defineStore('user', {
   state: () => ({
     token: '',
     unreadCount: 0,
+    secureSessionId: '',
   }),
   getters: {
     is_login(state): boolean {

+ 22 - 1
app/src/views/config/Config.vue

@@ -1,10 +1,13 @@
 <script setup lang="ts">
+import { $gettext } from '../../gettext'
 import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
 import config from '@/api/config'
 import configColumns from '@/views/config/configColumns'
 import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
 import InspectConfig from '@/views/config/InspectConfig.vue'
 import { useBreadcrumbs } from '@/composables/useBreadcrumbs'
+import Mkdir from '@/views/config/components/Mkdir.vue'
+import Rename from '@/views/config/components/Rename.vue'
 
 const table = ref()
 const route = useRoute()
@@ -79,17 +82,22 @@ function goBack() {
     },
   })
 }
+
+const refMkdir = ref()
+const refRename = ref()
 </script>
 
 <template>
   <ACard :title="$gettext('Configurations')">
     <template #extra>
       <a
+        class="mr-4"
         @click="router.push({
           path: '/config/add',
           query: { basePath: basePath || undefined },
         })"
-      >{{ $gettext('Add') }}</a>
+      >{{ $gettext('Create File') }}</a>
+      <a @click="() => refMkdir.open(basePath)">{{ $gettext('Create Folder') }}</a>
     </template>
     <InspectConfig ref="refInspectConfig" />
     <StdTable
@@ -116,6 +124,19 @@ function goBack() {
           })
         }
       }"
+    >
+      <template #actions="{ record }">
+        <ADivider type="vertical" />
+        <a @click="() => refRename.open(basePath, record.name)">{{ $gettext('Rename ') }}</a>
+      </template>
+    </StdTable>
+    <Mkdir
+      ref="refMkdir"
+      @created="() => table.get_list()"
+    />
+    <Rename
+      ref="refRename"
+      @renamed="() => table.get_list()"
     />
     <FooterToolBar v-if="basePath">
       <AButton @click="goBack">

+ 76 - 0
app/src/views/config/components/Mkdir.vue

@@ -0,0 +1,76 @@
+<script setup lang="ts">
+
+import { message } from 'ant-design-vue'
+import config from '@/api/config'
+import useOTPModal from '@/components/OTP/useOTPModal'
+
+const emit = defineEmits(['created'])
+const visible = ref(false)
+
+const data = ref({
+  basePath: '',
+  name: '',
+})
+
+const refForm = ref()
+function open(basePath: string) {
+  visible.value = true
+  data.value.name = ''
+  data.value.basePath = basePath
+}
+
+defineExpose({
+  open,
+})
+
+function ok() {
+  refForm.value.validate().then(() => {
+    const otpModal = useOTPModal()
+
+    otpModal.open({
+      onOk() {
+        config.mkdir(data.value.basePath, data.value.name).then(() => {
+          visible.value = false
+
+          message.success($gettext('Created successfully'))
+          emit('created')
+        }).catch(e => {
+          message.error(`${$gettext('Server error')} ${e?.message}`)
+        })
+      },
+    })
+  })
+}
+</script>
+
+<template>
+  <AModal
+    v-model:open="visible"
+    :mask="false"
+    :title="$gettext('Create Folder')"
+    @ok="ok"
+  >
+    <AForm
+      ref="refForm"
+      layout="vertical"
+      :model="data"
+      :rules="{
+        name: [
+          { required: true, message: $gettext('Please input a folder name') },
+          { pattern: /^[^\\/]+$/, message: $gettext('Invalid folder name') },
+        ],
+      }"
+    >
+      <AFormItem name="name">
+        <AInput
+          v-model:value="data.name"
+          :placeholder="$gettext('Name')"
+        />
+      </AFormItem>
+    </AForm>
+  </AModal>
+</template>
+
+<style scoped lang="less">
+
+</style>

+ 81 - 0
app/src/views/config/components/Rename.vue

@@ -0,0 +1,81 @@
+<script setup lang="ts">
+import { message } from 'ant-design-vue'
+import config from '@/api/config'
+import useOTPModal from '@/components/OTP/useOTPModal'
+
+const emit = defineEmits(['renamed'])
+const visible = ref(false)
+
+const data = ref({
+  basePath: '',
+  orig_name: '',
+  new_name: '',
+})
+
+const refForm = ref()
+function open(basePath: string, origName: string) {
+  visible.value = true
+  data.value.orig_name = origName
+  data.value.new_name = origName
+  data.value.basePath = basePath
+}
+
+defineExpose({
+  open,
+})
+
+function ok() {
+  refForm.value.validate().then(() => {
+    const { basePath, orig_name, new_name } = data.value
+
+    const otpModal = useOTPModal()
+
+    otpModal.open({
+      onOk() {
+        config.rename(basePath, orig_name, new_name).then(() => {
+          visible.value = false
+          message.success($gettext('Rename successfully'))
+          emit('renamed')
+        }).catch(e => {
+          message.error(`${$gettext('Server error')} ${e?.message}`)
+        })
+      },
+    })
+  })
+}
+</script>
+
+<template>
+  <AModal
+    v-model:open="visible"
+    :mask="false"
+    :title="$gettext('Rename')"
+    @ok="ok"
+  >
+    <AForm
+      ref="refForm"
+      layout="vertical"
+      :model="data"
+      :rules="{
+        new_name: [
+          { required: true, message: $gettext('Please input a filename') },
+          { pattern: /^[^\\/]+$/, message: $gettext('Invalid filename') },
+        ],
+      }"
+    >
+      <AFormItem :label="$gettext('Original name')">
+        <p>{{ data.orig_name }}</p>
+      </AFormItem>
+      <AFormItem
+        :label="$gettext('New name')"
+        name="new_name"
+      >
+        <AInput v-model:value="data.new_name" />
+      </AFormItem>
+    </AForm>
+  </AModal>
+</template>
+
+<style scoped lang="less">
+
+</style>

+ 1 - 1
go.mod

@@ -1,6 +1,6 @@
 module github.com/0xJacky/Nginx-UI
 
-go 1.22.0
+go 1.22.5
 
 require (
 	github.com/0xJacky/pofile v0.2.1

+ 1 - 0
internal/helper/directory_test.go

@@ -9,4 +9,5 @@ func TestIsUnderDirectory(t *testing.T) {
 	assert.Equal(t, true, IsUnderDirectory("/etc/nginx/nginx.conf", "/etc/nginx"))
 	assert.Equal(t, false, IsUnderDirectory("../../root/nginx.conf", "/etc/nginx"))
 	assert.Equal(t, false, IsUnderDirectory("/etc/nginx/../../root/nginx.conf", "/etc/nginx"))
+	assert.Equal(t, false, IsUnderDirectory("/etc/nginx/../../etc/nginx/../../root/nginx.conf", "/etc/nginx"))
 }