mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-11 02:15:48 +02:00
feat: rename folder or file in configurations list
This commit is contained in:
parent
5dd2c8f931
commit
ace8d7a0fe
12 changed files with 309 additions and 5 deletions
36
api/config/folder.go
Normal file
36
api/config/folder.go
Normal file
|
@ -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
api/config/rename.go
Normal file
68
api/config/rename.go
Normal file
|
@ -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",
|
||||||
|
})
|
||||||
|
}
|
|
@ -3,9 +3,12 @@ package config
|
||||||
import "github.com/gin-gonic/gin"
|
import "github.com/gin-gonic/gin"
|
||||||
|
|
||||||
func InitRouter(r *gin.RouterGroup) {
|
func InitRouter(r *gin.RouterGroup) {
|
||||||
|
r.GET("config_base_path", GetBasePath)
|
||||||
|
|
||||||
r.GET("configs", GetConfigs)
|
r.GET("configs", GetConfigs)
|
||||||
r.GET("config/*name", GetConfig)
|
r.GET("config/*name", GetConfig)
|
||||||
r.POST("config", AddConfig)
|
r.POST("config", AddConfig)
|
||||||
r.POST("config/*name", EditConfig)
|
r.POST("config/*name", EditConfig)
|
||||||
r.GET("config_base_path", GetBasePath)
|
r.POST("config_mkdir", Mkdir)
|
||||||
|
r.POST("config_rename", Rename)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,14 @@ class ConfigCurd extends Curd<Config> {
|
||||||
get_base_path() {
|
get_base_path() {
|
||||||
return http.get('/config_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()
|
const config: ConfigCurd = new ConfigCurd()
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { Modal, message } from 'ant-design-vue'
|
||||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||||
import OTPAuthorization from '@/components/OTP/OTPAuthorization.vue'
|
import OTPAuthorization from '@/components/OTP/OTPAuthorization.vue'
|
||||||
import otp from '@/api/otp'
|
import otp from '@/api/otp'
|
||||||
|
import { useUserStore } from '@/pinia'
|
||||||
|
|
||||||
export interface OTPModalProps {
|
export interface OTPModalProps {
|
||||||
onOk?: (secureSessionId: string) => void
|
onOk?: (secureSessionId: string) => void
|
||||||
|
@ -12,6 +13,7 @@ export interface OTPModalProps {
|
||||||
const useOTPModal = () => {
|
const useOTPModal = () => {
|
||||||
const refOTPAuthorization = ref<typeof OTPAuthorization>()
|
const refOTPAuthorization = ref<typeof OTPAuthorization>()
|
||||||
const randomId = Math.random().toString(36).substring(2, 8)
|
const randomId = Math.random().toString(36).substring(2, 8)
|
||||||
|
const { secureSessionId } = storeToRefs(useUserStore())
|
||||||
|
|
||||||
const injectStyles = () => {
|
const injectStyles = () => {
|
||||||
const style = document.createElement('style')
|
const style = document.createElement('style')
|
||||||
|
@ -36,6 +38,7 @@ const useOTPModal = () => {
|
||||||
const ssid = cookies.get('secure_session_id')
|
const ssid = cookies.get('secure_session_id')
|
||||||
if (ssid) {
|
if (ssid) {
|
||||||
onOk?.(ssid)
|
onOk?.(ssid)
|
||||||
|
secureSessionId.value = ssid
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -55,6 +58,7 @@ const useOTPModal = () => {
|
||||||
cookies.set('secure_session_id', r.session_id, { maxAge: 60 * 3 })
|
cookies.set('secure_session_id', r.session_id, { maxAge: 60 * 3 })
|
||||||
onOk?.(r.session_id)
|
onOk?.(r.session_id)
|
||||||
close()
|
close()
|
||||||
|
secureSessionId.value = r.session_id
|
||||||
}).catch(async () => {
|
}).catch(async () => {
|
||||||
refOTPAuthorization.value?.clearInput()
|
refOTPAuthorization.value?.clearInput()
|
||||||
await message.error($gettext('Invalid passcode or recovery code'))
|
await message.error($gettext('Invalid passcode or recovery code'))
|
||||||
|
|
|
@ -9,7 +9,7 @@ import router from '@/routes'
|
||||||
|
|
||||||
const user = useUserStore()
|
const user = useUserStore()
|
||||||
const settings = useSettingsStore()
|
const settings = useSettingsStore()
|
||||||
const { token } = storeToRefs(user)
|
const { token, secureSessionId } = storeToRefs(user)
|
||||||
|
|
||||||
const instance = axios.create({
|
const instance = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_ROOT,
|
baseURL: import.meta.env.VITE_API_ROOT,
|
||||||
|
@ -28,7 +28,7 @@ const instance = axios.create({
|
||||||
instance.interceptors.request.use(
|
instance.interceptors.request.use(
|
||||||
config => {
|
config => {
|
||||||
NProgress.start()
|
NProgress.start()
|
||||||
if (token) {
|
if (token.value) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
(config.headers as any).Authorization = token.value
|
(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
|
(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
|
return config
|
||||||
},
|
},
|
||||||
err => {
|
err => {
|
||||||
|
|
|
@ -4,6 +4,7 @@ export const useUserStore = defineStore('user', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
token: '',
|
token: '',
|
||||||
unreadCount: 0,
|
unreadCount: 0,
|
||||||
|
secureSessionId: '',
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
is_login(state): boolean {
|
is_login(state): boolean {
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { $gettext } from '../../gettext'
|
||||||
import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
|
import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
|
||||||
import config from '@/api/config'
|
import config from '@/api/config'
|
||||||
import configColumns from '@/views/config/configColumns'
|
import configColumns from '@/views/config/configColumns'
|
||||||
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
|
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
|
||||||
import InspectConfig from '@/views/config/InspectConfig.vue'
|
import InspectConfig from '@/views/config/InspectConfig.vue'
|
||||||
import { useBreadcrumbs } from '@/composables/useBreadcrumbs'
|
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 table = ref()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
@ -79,17 +82,22 @@ function goBack() {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refMkdir = ref()
|
||||||
|
const refRename = ref()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ACard :title="$gettext('Configurations')">
|
<ACard :title="$gettext('Configurations')">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<a
|
<a
|
||||||
|
class="mr-4"
|
||||||
@click="router.push({
|
@click="router.push({
|
||||||
path: '/config/add',
|
path: '/config/add',
|
||||||
query: { basePath: basePath || undefined },
|
query: { basePath: basePath || undefined },
|
||||||
})"
|
})"
|
||||||
>{{ $gettext('Add') }}</a>
|
>{{ $gettext('Create File') }}</a>
|
||||||
|
<a @click="() => refMkdir.open(basePath)">{{ $gettext('Create Folder') }}</a>
|
||||||
</template>
|
</template>
|
||||||
<InspectConfig ref="refInspectConfig" />
|
<InspectConfig ref="refInspectConfig" />
|
||||||
<StdTable
|
<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">
|
<FooterToolBar v-if="basePath">
|
||||||
<AButton @click="goBack">
|
<AButton @click="goBack">
|
||||||
|
|
76
app/src/views/config/components/Mkdir.vue
Normal file
76
app/src/views/config/components/Mkdir.vue
Normal file
|
@ -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
app/src/views/config/components/Rename.vue
Normal file
81
app/src/views/config/components/Rename.vue
Normal file
|
@ -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>
|
2
go.mod
2
go.mod
|
@ -1,6 +1,6 @@
|
||||||
module github.com/0xJacky/Nginx-UI
|
module github.com/0xJacky/Nginx-UI
|
||||||
|
|
||||||
go 1.22.0
|
go 1.22.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/0xJacky/pofile v0.2.1
|
github.com/0xJacky/pofile v0.2.1
|
||||||
|
|
|
@ -9,4 +9,5 @@ func TestIsUnderDirectory(t *testing.T) {
|
||||||
assert.Equal(t, true, IsUnderDirectory("/etc/nginx/nginx.conf", "/etc/nginx"))
|
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("../../root/nginx.conf", "/etc/nginx"))
|
||||||
assert.Equal(t, false, IsUnderDirectory("/etc/nginx/../../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"))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue