feat: rename folder or file in configurations list

This commit is contained in:
Jacky 2024-07-25 21:30:11 +08:00
parent 5dd2c8f931
commit ace8d7a0fe
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
12 changed files with 309 additions and 5 deletions

36
api/config/folder.go Normal file
View 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
View 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",
})
}

View file

@ -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)
}

View file

@ -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()

View file

@ -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'))

View file

@ -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 => {

View file

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

View file

@ -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">

View 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>

View 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
View file

@ -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

View file

@ -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"))
}