Browse Source

wip: dns credentials manager

0xJacky 2 years ago
parent
commit
418a53f4ad
39 changed files with 758 additions and 151 deletions
  1. 5 0
      frontend/src/api/dns_credential.ts
  2. 5 5
      frontend/src/components/Chart/AreaChart.vue
  3. 1 2
      frontend/src/components/ChatGPT/ChatGPT.vue
  4. 2 2
      frontend/src/components/NginxControl/NginxControl.vue
  5. 17 18
      frontend/src/components/StdDataDisplay/StdBatchEdit.vue
  6. 22 21
      frontend/src/components/StdDataDisplay/StdCurd.vue
  7. 5 5
      frontend/src/components/StdDataDisplay/StdPagination.vue
  8. 4 4
      frontend/src/components/StdDataEntry/components/StdPassword.vue
  9. 1 1
      frontend/src/components/StdDataEntry/index.tsx
  10. 18 6
      frontend/src/routes/index.ts
  11. 62 0
      frontend/src/views/cert/DNSCredential.vue
  12. 5 7
      frontend/src/views/config/Config.vue
  13. 1 2
      frontend/src/views/config/config.ts
  14. 0 1
      frontend/src/views/domain/DomainAdd.vue
  15. 2 3
      frontend/src/views/domain/DomainList.vue
  16. 1 2
      frontend/src/views/domain/cert/Cert.vue
  17. 1 1
      frontend/src/views/domain/cert/CertInfo.vue
  18. 1 1
      frontend/src/views/domain/cert/ChangeCert.vue
  19. 1 1
      frontend/src/views/domain/cert/IssueCert.vue
  20. 1 1
      frontend/src/views/domain/cert/components/AutoCertStepOne.vue
  21. 19 1
      frontend/src/views/domain/cert/components/DNSChallenge.vue
  22. 1 1
      frontend/src/views/domain/cert/components/ObtainCert.vue
  23. 0 1
      frontend/src/views/domain/ngx_conf/LocationEditor.vue
  24. 1 1
      frontend/src/views/domain/ngx_conf/LogEntry.vue
  25. 1 2
      frontend/src/views/domain/ngx_conf/NgxConfigEditor.vue
  26. 1 3
      frontend/src/views/domain/ngx_conf/config_template/ConfigTemplate.vue
  27. 0 1
      frontend/src/views/domain/ngx_conf/directive/DirectiveAdd.vue
  28. 0 1
      frontend/src/views/domain/ngx_conf/directive/DirectiveEditor.vue
  29. 0 1
      frontend/src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue
  30. 6 6
      frontend/src/views/other/Install.vue
  31. 2 3
      frontend/src/views/other/Login.vue
  32. 1 1
      frontend/src/views/preference/Preference.vue
  33. 122 0
      server/api/dns_credential.go
  34. 0 0
      server/model/config_backup.go
  35. 12 0
      server/model/dns_credential.go
  36. 1 0
      server/model/model.go
  37. 374 0
      server/query/dns_credentials.gen.go
  38. 54 46
      server/query/gen.go
  39. 8 0
      server/router/routers.go

+ 5 - 0
frontend/src/api/dns_credential.ts

@@ -0,0 +1,5 @@
+import Curd from '@/api/curd'
+
+const dns_credential = new Curd('/dns_credential')
+
+export default dns_credential

+ 5 - 5
frontend/src/components/Chart/AreaChart.vue

@@ -22,11 +22,11 @@ let chartOptions = {
             enabled: false
         },
         animations: {
-            enabled: false,
+            enabled: false
         },
         toolbar: {
             show: false
-        },
+        }
     },
     colors: ['#ff6385', '#36a3eb'],
     fill: {
@@ -41,7 +41,7 @@ let chartOptions = {
     },
     stroke: {
         curve: 'smooth',
-        width: 0,
+        width: 0
     },
     xaxis: {
         type: 'datetime',
@@ -75,7 +75,7 @@ let chartOptions = {
         },
         onItemHover: {
             highlightDataSeries: false
-        },
+        }
     }
 }
 
@@ -114,7 +114,7 @@ const callback = () => {
                 },
                 onItemHover: {
                     highlightDataSeries: false
-                },
+                }
             }
         }
     }

+ 1 - 2
frontend/src/components/ChatGPT/ChatGPT.vue

@@ -7,11 +7,10 @@ import {urlJoin} from '@/lib/helper'
 import {marked} from 'marked'
 import hljs from 'highlight.js'
 import 'highlight.js/styles/vs2015.css'
-import {SendOutlined} from '@ant-design/icons-vue'
+import Icon, {SendOutlined} from '@ant-design/icons-vue'
 import Template from '@/views/template/Template.vue'
 import openai from '@/api/openai'
 import ChatGPT_logo from '@/assets/svg/ChatGPT_logo.svg'
-import Icon from '@ant-design/icons-vue'
 
 const {$gettext} = useGettext()
 

+ 2 - 2
frontend/src/components/NginxControl/NginxControl.vue

@@ -1,7 +1,5 @@
 <script setup lang="ts">
 import gettext from '@/gettext'
-
-const {$gettext} = gettext
 import ngx from '@/api/ngx'
 import logLevel from '@/views/config/constants'
 import {message} from 'ant-design-vue'
@@ -9,6 +7,8 @@ import {ReloadOutlined} from '@ant-design/icons-vue'
 import Template from '@/views/template/Template.vue'
 import {ref, watch} from 'vue'
 
+const {$gettext} = gettext
+
 function get_status() {
     ngx.status().then(r => {
         if (r?.running === true) {

+ 17 - 18
frontend/src/components/StdDataDisplay/StdBatchEdit.vue

@@ -1,12 +1,11 @@
 <script setup lang="ts">
 import {reactive, ref} from 'vue'
 import gettext from '@/gettext'
-
-const {$gettext} = gettext
-
 import StdDataEntry from '@/components/StdDataEntry'
 import {message} from 'ant-design-vue'
 
+const {$gettext} = gettext
+
 const emit = defineEmits(['onSave'])
 
 const props = defineProps(['api', 'beforeSave'])
@@ -49,23 +48,23 @@ async function ok() {
 
 <template>
     <a-modal
-            class="std-curd-edit-modal"
-            :mask="false"
-            :title="$gettext('Batch Modify')"
-            v-model:visible="visible"
-            :cancel-text="$gettext('Cancel')"
-            :ok-text="$gettext('OK')"
-            @ok="ok"
-            :confirm-loading="loading"
-            :width="600"
-            destroyOnClose
+        class="std-curd-edit-modal"
+        :mask="false"
+        :title="$gettext('Batch Modify')"
+        v-model:visible="visible"
+        :cancel-text="$gettext('Cancel')"
+        :ok-text="$gettext('OK')"
+        @ok="ok"
+        :confirm-loading="loading"
+        :width="600"
+        destroyOnClose
     >
 
         <std-data-entry
-                ref="std_data_entry"
-                :data-list="batchColumns"
-                v-model:data-source="data"
-                :error="error"
+            ref="std_data_entry"
+            :data-list="batchColumns"
+            v-model:data-source="data"
+            :error="error"
         />
 
         <slot name="extra"/>
@@ -74,4 +73,4 @@ async function ok() {
 
 <style scoped>
 
-</style>
+</style>

+ 22 - 21
frontend/src/components/StdDataDisplay/StdCurd.vue

@@ -4,7 +4,7 @@ import StdTable from './StdTable.vue'
 
 import StdDataEntry from '@/components/StdDataEntry'
 
-import {reactive, ref} from 'vue'
+import {provide, reactive, ref} from 'vue'
 import {message} from 'ant-design-vue'
 
 const {$gettext} = gettext
@@ -62,6 +62,7 @@ const props = defineProps({
 const visible = ref(false)
 const update = ref(0)
 const data: any = reactive({id: null})
+provide('data', data)
 const error: any = reactive({})
 const selected = ref([])
 
@@ -146,12 +147,12 @@ const selectedRowKeys = ref([])
             </template>
 
             <std-table
-                    ref="table"
-                    v-model:selected-row-keys="selectedRowKeys"
-                    v-bind="props"
-                    @clickEdit="edit"
-                    @selected="onSelect"
-                    :key="update"
+                ref="table"
+                v-model:selected-row-keys="selectedRowKeys"
+                v-bind="props"
+                @clickEdit="edit"
+                @selected="onSelect"
+                :key="update"
             >
                 <template v-slot:actions="slotProps">
                     <slot name="actions" :actions="slotProps.record"/>
@@ -160,26 +161,26 @@ const selectedRowKeys = ref([])
         </a-card>
 
         <a-modal
-                class="std-curd-edit-modal"
-                :mask="false"
-                :title="edit_text?edit_text:(data.id ? $gettext('Modify') : $gettext('Add'))"
-                :visible="visible"
-                :cancel-text="$gettext('Cancel')"
-                :ok-text="$gettext('OK')"
-                @cancel="cancel"
-                @ok="ok"
-                :width="modalWidth"
-                destroyOnClose
+            class="std-curd-edit-modal"
+            :mask="false"
+            :title="edit_text?edit_text:(data.id ? $gettext('Modify') : $gettext('Add'))"
+            :visible="visible"
+            :cancel-text="$gettext('Cancel')"
+            :ok-text="$gettext('OK')"
+            @cancel="cancel"
+            @ok="ok"
+            :width="modalWidth"
+            destroyOnClose
         >
             <div class="before-edit" v-if="$slots.beforeEdit">
                 <slot name="beforeEdit" :data="data"/>
             </div>
 
             <std-data-entry
-                    ref="std_data_entry"
-                    :data-list="editableColumns()"
-                    v-model:data-source="data"
-                    :error="error"
+                ref="std_data_entry"
+                :data-list="editableColumns()"
+                v-model:data-source="data"
+                :error="error"
             />
 
             <slot name="edit" :data="data"/>

+ 5 - 5
frontend/src/components/StdDataDisplay/StdPagination.vue

@@ -24,11 +24,11 @@ const pageSize = computed({
 <template>
     <div class="pagination-container" v-if="pagination.total>pagination.per_page">
         <a-pagination
-                :current="pagination.current_page"
-                v-model:pageSize="pageSize"
-                :size="size"
-                :total="pagination.total"
-                @change="change"
+            :current="pagination.current_page"
+            v-model:pageSize="pageSize"
+            :size="size"
+            :total="pagination.total"
+            @change="change"
         />
     </div>
 </template>

+ 4 - 4
frontend/src/components/StdDataEntry/components/StdPassword.vue

@@ -34,9 +34,9 @@ function handle_generate() {
 <template>
     <a-input-group compact>
         <a-input-password
-                v-if="!visibility"
-                :class="{compact: generate}"
-                v-model:value="M_value" :placeholoder="placeholder"/>
+            v-if="!visibility"
+            :class="{compact: generate}"
+            v-model:value="M_value" :placeholoder="placeholder"/>
         <a-input v-else :class="{compact: generate}" v-model:value="M_value" :placeholoder="placeholder"/>
         <a-button @click="handle_generate" v-if="generate" type="primary">
             <translate>Generate</translate>
@@ -48,4 +48,4 @@ function handle_generate() {
 .compact {
     width: calc(100% - 91px)
 }
-</style>
+</style>

+ 1 - 1
frontend/src/components/StdDataEntry/index.tsx

@@ -1,6 +1,6 @@
 import StdDataEntry from './StdDataEntry.js'
 import {h} from 'vue'
-import {Input, Textarea, InputPassword, InputNumber} from 'ant-design-vue'
+import {Input, InputNumber, Textarea} from 'ant-design-vue'
 import StdSelector from './components/StdSelector.vue'
 import StdSelect from './components/StdSelect.vue'
 import StdPassword from './components/StdPassword.vue'

+ 18 - 6
frontend/src/routes/index.ts

@@ -1,4 +1,4 @@
-import {createRouter, createWebHashHistory, createWebHistory} from 'vue-router'
+import {createRouter, createWebHashHistory} from 'vue-router'
 import gettext from '../gettext'
 import {useUserStore} from '@/pinia'
 
@@ -6,12 +6,12 @@ import {
     CloudOutlined,
     CodeOutlined,
     FileOutlined,
+    FileTextOutlined,
     HomeOutlined,
     InfoCircleOutlined,
-    UserOutlined,
-    FileTextOutlined,
+    SafetyCertificateOutlined,
     SettingOutlined,
-    SafetyCertificateOutlined
+    UserOutlined
 } from '@ant-design/icons-vue'
 import NProgress from 'nprogress'
 import 'nprogress/nprogress.css'
@@ -87,10 +87,22 @@ export const routes = [
             {
                 path: 'cert',
                 name: () => $gettext('Certification'),
-                component: () => import('@/views/cert/Cert.vue'),
+                component: () => import('@/layouts/BaseRouterView.vue'),
                 meta: {
                     icon: SafetyCertificateOutlined
-                }
+                },
+                children: [
+                    {
+                        path: 'list',
+                        name: () => $gettext('Certification List'),
+                        component: () => import('@/views/cert/Cert.vue')
+                    },
+                    {
+                        path: 'dns_credential',
+                        name: () => $gettext('DNS Credentials'),
+                        component: () => import('@/views/cert/DNSCredential.vue')
+                    }
+                ]
             },
             {
                 path: 'terminal',

+ 62 - 0
frontend/src/views/cert/DNSCredential.vue

@@ -0,0 +1,62 @@
+<script setup lang="tsx">
+import {useGettext} from 'vue3-gettext'
+import {datetime} from '@/components/StdDataDisplay/StdTableTransformer'
+import dns_credential from '@/api/dns_credential'
+import StdCurd from '@/components/StdDataDisplay/StdCurd.vue'
+import Template from '@/views/template/Template.vue'
+import DNSChallenge from '@/views/domain/cert/components/DNSChallenge.vue'
+import {input} from '@/components/StdDataEntry'
+
+const {$gettext, interpolate} = useGettext()
+
+const columns = [{
+    title: () => $gettext('Name'),
+    dataIndex: 'name',
+    sorter: true,
+    pithy: true,
+    edit: {
+        type: input
+    }
+}, {
+    title: () => $gettext('Provider'),
+    dataIndex: ['config', 'name'],
+    sorter: true,
+    pithy: true
+}, {
+    title: () => $gettext('Updated at'),
+    dataIndex: 'updated_at',
+    customRender: datetime,
+    sorter: true,
+    pithy: true
+}, {
+    title: () => $gettext('Action'),
+    dataIndex: 'action'
+}]
+</script>
+
+<template>
+    <std-curd :title="$gettext('DNS Credentials')" :api="dns_credential" :columns="columns"
+              row-key="name"
+    >
+        <template #beforeEdit>
+            <a-alert type="info" show-icon :message="$gettext('Note')">
+                <template #description>
+                    <p v-translate>
+                        Please fill in the API authentication credentials provided by your DNS provider.
+                        We will add one or more TXT records to the DNS records of your domain for ownership
+                        verification.
+                        Once the verification is complete, the records will be removed.
+                        Please note that the time configurations below are all in seconds.
+                    </p>
+                </template>
+            </a-alert>
+        </template>
+        <template #edit="{data}">
+            <d-n-s-challenge/>
+        </template>
+    </std-curd>
+</template>
+
+<style lang="less" scoped>
+
+</style>

+ 5 - 7
frontend/src/views/config/Config.vue

@@ -2,19 +2,17 @@
 import StdTable from '@/components/StdDataDisplay/StdTable.vue'
 import gettext from '@/gettext'
 import config from '@/api/config'
-import {customRender, datetime} from '@/components/StdDataDisplay/StdTableTransformer'
-import {computed, h, nextTick, ref, watch} from 'vue'
-
-const {$gettext} = gettext
-
-const api = config
-
+import {computed, ref, watch} from 'vue'
 import configColumns from '@/views/config/config'
 import {useRoute} from 'vue-router'
 import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
 import router from '@/routes'
 import InspectConfig from '@/views/config/InspectConfig.vue'
 
+const {$gettext} = gettext
+
+const api = config
+
 const table = ref(null)
 const route = useRoute()
 

+ 1 - 2
frontend/src/views/config/config.ts

@@ -1,10 +1,9 @@
 import {customRender, datetime} from '@/components/StdDataDisplay/StdTableTransformer'
 import gettext from '@/gettext'
+import {h} from 'vue'
 
 const {$gettext} = gettext
 
-import {h} from 'vue'
-
 const configColumns = [{
     title: () => $gettext('Name'),
     dataIndex: 'name',

+ 0 - 1
frontend/src/views/domain/DomainAdd.vue

@@ -8,7 +8,6 @@ import ngx from '@/api/ngx'
 import {computed, reactive, ref} from 'vue'
 import {message} from 'ant-design-vue'
 import {useRouter} from 'vue-router'
-import template from '@/api/template'
 
 const {$gettext, interpolate} = useGettext()
 

+ 2 - 3
frontend/src/views/domain/DomainList.vue

@@ -3,15 +3,14 @@ import StdTable from '@/components/StdDataDisplay/StdTable.vue'
 
 import {customRender, datetime} from '@/components/StdDataDisplay/StdTableTransformer'
 import {useGettext} from 'vue3-gettext'
-
-const {$gettext, interpolate} = useGettext()
-
 import domain from '@/api/domain'
 import {Badge, message} from 'ant-design-vue'
 import {h, ref} from 'vue'
 import {input} from '@/components/StdDataEntry'
 import SiteDuplicate from '@/views/domain/SiteDuplicate.vue'
 
+const {$gettext, interpolate} = useGettext()
+
 const columns = [{
     title: () => $gettext('Name'),
     dataIndex: 'name',

+ 1 - 2
frontend/src/views/domain/cert/Cert.vue

@@ -1,10 +1,9 @@
 <script setup lang="ts">
 import CertInfo from '@/views/domain/cert/CertInfo.vue'
 import IssueCert from '@/views/domain/cert/IssueCert.vue'
-import {computed, ref} from 'vue'
+import {computed} from 'vue'
 import {useGettext} from 'vue3-gettext'
 import ChangeCert from '@/views/domain/cert/ChangeCert.vue'
-import DNSChallenge from '@/views/domain/cert/components/DNSChallenge.vue'
 
 const {$gettext} = useGettext()
 

+ 1 - 1
frontend/src/views/domain/cert/CertInfo.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import {CloseCircleOutlined, CheckCircleOutlined} from '@ant-design/icons-vue'
+import {CheckCircleOutlined, CloseCircleOutlined} from '@ant-design/icons-vue'
 import dayjs from 'dayjs'
 
 const props = defineProps(['cert'])

+ 1 - 1
frontend/src/views/domain/cert/ChangeCert.vue

@@ -3,7 +3,7 @@ import {useGettext} from 'vue3-gettext'
 import {h, ref} from 'vue'
 import StdTable from '@/components/StdDataDisplay/StdTable.vue'
 import cert from '@/api/cert'
-import {customRender, datetime} from '@/components/StdDataDisplay/StdTableTransformer'
+import {customRender} from '@/components/StdDataDisplay/StdTableTransformer'
 import {input} from '@/components/StdDataEntry'
 import {Badge} from 'ant-design-vue'
 

+ 1 - 1
frontend/src/views/domain/cert/IssueCert.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
 import {useGettext} from 'vue3-gettext'
-import {computed, inject, nextTick, provide, ref, watch} from 'vue'
+import {computed, nextTick, provide, ref, watch} from 'vue'
 import Template from '@/views/template/Template.vue'
 import ObtainCert from '@/views/domain/cert/components/ObtainCert.vue'
 

+ 1 - 1
frontend/src/views/domain/cert/components/AutoCertStepOne.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import {inject, ref, Ref} from 'vue'
+import {inject, Ref} from 'vue'
 import {useGettext} from 'vue3-gettext'
 import DNSChallenge from '@/views/domain/cert/components/DNSChallenge.vue'
 

+ 19 - 1
frontend/src/views/domain/cert/components/DNSChallenge.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import {computed, inject, Ref, ref, watch} from 'vue'
+import {computed, inject, ref, watch} from 'vue'
 import auto_cert from '@/api/auto_cert'
 import {useGettext} from 'vue3-gettext'
 import {SelectProps} from 'ant-design-vue'
@@ -9,8 +9,22 @@ const providers: any = ref([])
 
 const data: any = inject('data')!
 
+const code = computed(() => {
+    return data.code
+})
+
+function init() {
+    providers.value?.forEach((v: any, k: number) => {
+        if (v.code === code.value) {
+            provider_idx.value = k
+        }
+    })
+}
+
 auto_cert.get_dns_providers().then(r => {
     providers.value = r
+}).then(() => {
+    init()
 })
 
 const provider_idx = ref()
@@ -19,8 +33,12 @@ const current: any = computed(() => {
     return providers.value?.[provider_idx.value]
 })
 
+
+watch(code, init)
+
 watch(current, () => {
     data.code = current.value.code
+    data.provider = current.value.name
     auto_cert.get_dns_provider(current.value.code).then(r => {
         Object.assign(current.value, r)
     })

+ 1 - 1
frontend/src/views/domain/cert/components/ObtainCert.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
 import {useGettext} from 'vue3-gettext'
-import {computed, inject, nextTick, provide, reactive, Ref, ref, watch} from 'vue'
+import {computed, inject, nextTick, provide, reactive, Ref, ref} from 'vue'
 import websocket from '@/lib/websocket'
 import {message, Modal} from 'ant-design-vue'
 import template from '@/api/template'

+ 0 - 1
frontend/src/views/domain/ngx_conf/LocationEditor.vue

@@ -3,7 +3,6 @@ import CodeEditor from '@/components/CodeEditor'
 import {useGettext} from 'vue3-gettext'
 import {reactive, ref} from 'vue'
 import {DeleteOutlined, HolderOutlined} from '@ant-design/icons-vue'
-import draggable from 'vuedraggable'
 
 const {$gettext} = useGettext()
 

+ 1 - 1
frontend/src/views/domain/ngx_conf/LogEntry.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import {FileTextOutlined, FileExclamationOutlined} from '@ant-design/icons-vue'
+import {FileExclamationOutlined, FileTextOutlined} from '@ant-design/icons-vue'
 import {computed, ref} from 'vue'
 import {useRouter} from 'vue-router'
 

+ 1 - 2
frontend/src/views/domain/ngx_conf/NgxConfigEditor.vue

@@ -8,10 +8,9 @@ import Cert from '@/views/domain/cert/Cert.vue'
 import LogEntry from '@/views/domain/ngx_conf/LogEntry.vue'
 import ConfigTemplate from '@/views/domain/ngx_conf/config_template/ConfigTemplate.vue'
 import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
-import {PlusOutlined} from '@ant-design/icons-vue'
+import {MoreOutlined, PlusOutlined} from '@ant-design/icons-vue'
 import {Modal} from 'ant-design-vue'
 import template from '@/api/template'
-import {MoreOutlined} from '@ant-design/icons-vue'
 
 const {$gettext} = useGettext()
 

+ 1 - 3
frontend/src/views/domain/ngx_conf/config_template/ConfigTemplate.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts">
 import {useGettext} from 'vue3-gettext'
 import template from '@/api/template'
-import {computed, provide, ref, watch} from 'vue'
+import {computed, provide, ref} from 'vue'
 import {storeToRefs} from 'pinia'
 import {useSettingsStore} from '@/pinia'
 import Template from '@/views/template/Template.vue'
@@ -9,8 +9,6 @@ import DirectiveEditor from '@/views/domain/ngx_conf/directive/DirectiveEditor.v
 import LocationEditor from '@/views/domain/ngx_conf/LocationEditor.vue'
 import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
 import TemplateForm from '@/views/domain/ngx_conf/config_template/TemplateForm.vue'
-import * as wasi from 'wasi'
-import _ from 'lodash'
 
 const {$gettext} = useGettext()
 const {language} = storeToRefs(useSettingsStore())

+ 0 - 1
frontend/src/views/domain/ngx_conf/directive/DirectiveAdd.vue

@@ -1,5 +1,4 @@
 <script setup lang="ts">
-import {If} from '@/views/domain/ngx_conf'
 import CodeEditor from '@/components/CodeEditor'
 import {reactive, ref} from 'vue'
 import {useGettext} from 'vue3-gettext'

+ 0 - 1
frontend/src/views/domain/ngx_conf/directive/DirectiveEditor.vue

@@ -2,7 +2,6 @@
 import DirectiveAdd from '@/views/domain/ngx_conf/directive/DirectiveAdd'
 import {useGettext} from 'vue3-gettext'
 import {reactive, ref} from 'vue'
-import draggable from 'vuedraggable'
 import DirectiveEditorItem from '@/views/domain/ngx_conf/directive/DirectiveEditorItem.vue'
 
 const {$gettext} = useGettext()

+ 0 - 1
frontend/src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue

@@ -1,7 +1,6 @@
 <script setup lang="ts">
 import CodeEditor from '@/components/CodeEditor'
 import {DeleteOutlined, HolderOutlined} from '@ant-design/icons-vue'
-import {If} from '@/views/domain/ngx_conf'
 
 import {useGettext} from 'vue3-gettext'
 import {onMounted, ref, watch} from 'vue'

+ 6 - 6
frontend/src/views/other/Install.vue

@@ -5,7 +5,7 @@ import {reactive, ref} from 'vue'
 import gettext from '@/gettext'
 import install from '@/api/install'
 import {useRoute, useRouter} from 'vue-router'
-import {MailOutlined, UserOutlined, LockOutlined, DatabaseOutlined} from '@ant-design/icons-vue'
+import {DatabaseOutlined, LockOutlined, MailOutlined, UserOutlined} from '@ant-design/icons-vue'
 
 const {$gettext, interpolate} = gettext
 
@@ -33,19 +33,19 @@ const rulesRef = reactive({
         {
             required: true,
             type: 'email',
-            message: () => $gettext('Please input your E-mail!'),
+            message: () => $gettext('Please input your E-mail!')
         }
     ],
     username: [
         {
             required: true,
-            message: () => $gettext('Please input your username!'),
+            message: () => $gettext('Please input your username!')
         }
     ],
     password: [
         {
             required: true,
-            message: () => $gettext('Please input your password!'),
+            message: () => $gettext('Please input your password!')
         }
     ],
     database: [
@@ -53,9 +53,9 @@ const rulesRef = reactive({
             message: () => interpolate(
                 $gettext('The filename cannot contain the following characters: %{c}'),
                 {c: '& &quot; ? < > # {} % ~ / \\'}
-            ),
+            )
         }
-    ],
+    ]
 })
 
 const {validate, validateInfos} = Form.useForm(modelRef, rulesRef)

+ 2 - 3
frontend/src/views/other/Login.vue

@@ -1,8 +1,5 @@
 <script setup lang="ts">
 import {useUserStore} from '@/pinia'
-
-const thisYear = new Date().getFullYear()
-
 import {LockOutlined, UserOutlined} from '@ant-design/icons-vue'
 import {reactive, ref, watch} from 'vue'
 import {useRoute, useRouter} from 'vue-router'
@@ -12,6 +9,8 @@ import auth from '@/api/auth'
 import install from '@/api/install'
 import SetLanguage from '@/components/SetLanguage/SetLanguage.vue'
 
+const thisYear = new Date().getFullYear()
+
 const route = useRoute()
 const router = useRouter()
 

+ 1 - 1
frontend/src/views/preference/Preference.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
 import {useGettext} from 'vue3-gettext'
-import {provide, reactive, ref} from 'vue'
+import {provide, ref} from 'vue'
 import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
 import {useSettingsStore} from '@/pinia'
 import {dark_mode} from '@/lib/theme'

+ 122 - 0
server/api/dns_credential.go

@@ -0,0 +1,122 @@
+package api
+
+import (
+	"github.com/0xJacky/Nginx-UI/server/model"
+	"github.com/0xJacky/Nginx-UI/server/pkg/cert/dns"
+	"github.com/0xJacky/Nginx-UI/server/query"
+	"github.com/gin-gonic/gin"
+	"github.com/spf13/cast"
+	"net/http"
+)
+
+func GetDnsCredential(c *gin.Context) {
+	id := cast.ToInt(c.Param("id"))
+
+	d := query.DnsCredential
+
+	dnsCredential, err := d.FirstByID(id)
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+	type apiDnsCredential struct {
+		model.Model
+		Name string `json:"name"`
+		dns.Config
+	}
+	c.JSON(http.StatusOK, apiDnsCredential{
+		Model:  dnsCredential.Model,
+		Name:   dnsCredential.Name,
+		Config: *dnsCredential.Config,
+	})
+}
+
+func GetDnsCredentialList(c *gin.Context) {
+	d := query.DnsCredential
+	data, err := d.Find()
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+	c.JSON(http.StatusOK, gin.H{
+		"data": data,
+	})
+}
+
+type DnsCredentialManageJson struct {
+	Name     string `json:"name" binding:"required"`
+	Provider string `json:"provider"`
+	dns.Config
+}
+
+func AddDnsCredential(c *gin.Context) {
+	var json DnsCredentialManageJson
+	if !BindAndValid(c, &json) {
+		return
+	}
+
+	json.Config.Name = json.Provider
+	dnsCredential := model.DnsCredential{
+		Name:     json.Name,
+		Config:   &json.Config,
+		Provider: json.Provider,
+	}
+
+	d := query.DnsCredential
+
+	err := d.Create(&dnsCredential)
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, dnsCredential)
+}
+
+func EditDnsCredential(c *gin.Context) {
+	id := cast.ToInt(c.Param("id"))
+
+	var json DnsCredentialManageJson
+	if !BindAndValid(c, &json) {
+		return
+	}
+
+	d := query.DnsCredential
+
+	dnsCredential, err := d.FirstByID(id)
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	json.Config.Name = json.Provider
+	_, err = d.Where(d.ID.Eq(dnsCredential.ID)).Updates(&model.DnsCredential{
+		Name:     json.Name,
+		Config:   &json.Config,
+		Provider: json.Provider,
+	})
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	GetDnsCredential(c)
+}
+
+func DeleteDnsCredential(c *gin.Context) {
+	id := cast.ToInt(c.Param("id"))
+	d := query.DnsCredential
+
+	dnsCredential, err := d.FirstByID(id)
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+	err = d.DeleteByID(dnsCredential.ID)
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+	c.JSON(http.StatusNoContent, nil)
+}

+ 0 - 0
server/model/config-backup.go → server/model/config_backup.go


+ 12 - 0
server/model/dns_credential.go

@@ -0,0 +1,12 @@
+package model
+
+import (
+	"github.com/0xJacky/Nginx-UI/server/pkg/cert/dns"
+)
+
+type DnsCredential struct {
+	Model
+	Name     string      `json:"name"`
+	Config   *dns.Config `json:"config,omitempty" gorm:"serializer:json"`
+	Provider string      `json:"provider"`
+}

+ 1 - 0
server/model/model.go

@@ -31,6 +31,7 @@ func GenerateAllModel() []any {
 		Cert{},
 		ChatGPTLog{},
 		Site{},
+		DnsCredential{},
 	}
 }
 

+ 374 - 0
server/query/dns_credentials.gen.go

@@ -0,0 +1,374 @@
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+
+package query
+
+import (
+	"context"
+	"strings"
+
+	"gorm.io/gorm"
+	"gorm.io/gorm/clause"
+	"gorm.io/gorm/schema"
+
+	"gorm.io/gen"
+	"gorm.io/gen/field"
+
+	"gorm.io/plugin/dbresolver"
+
+	"github.com/0xJacky/Nginx-UI/server/model"
+)
+
+func newDnsCredential(db *gorm.DB, opts ...gen.DOOption) dnsCredential {
+	_dnsCredential := dnsCredential{}
+
+	_dnsCredential.dnsCredentialDo.UseDB(db, opts...)
+	_dnsCredential.dnsCredentialDo.UseModel(&model.DnsCredential{})
+
+	tableName := _dnsCredential.dnsCredentialDo.TableName()
+	_dnsCredential.ALL = field.NewAsterisk(tableName)
+	_dnsCredential.ID = field.NewInt(tableName, "id")
+	_dnsCredential.CreatedAt = field.NewTime(tableName, "created_at")
+	_dnsCredential.UpdatedAt = field.NewTime(tableName, "updated_at")
+	_dnsCredential.DeletedAt = field.NewTime(tableName, "deleted_at")
+	_dnsCredential.Name = field.NewString(tableName, "name")
+	_dnsCredential.Config = field.NewField(tableName, "config")
+
+	_dnsCredential.fillFieldMap()
+
+	return _dnsCredential
+}
+
+type dnsCredential struct {
+	dnsCredentialDo
+
+	ALL       field.Asterisk
+	ID        field.Int
+	CreatedAt field.Time
+	UpdatedAt field.Time
+	DeletedAt field.Time
+	Name      field.String
+	Config    field.Field
+
+	fieldMap map[string]field.Expr
+}
+
+func (d dnsCredential) Table(newTableName string) *dnsCredential {
+	d.dnsCredentialDo.UseTable(newTableName)
+	return d.updateTableName(newTableName)
+}
+
+func (d dnsCredential) As(alias string) *dnsCredential {
+	d.dnsCredentialDo.DO = *(d.dnsCredentialDo.As(alias).(*gen.DO))
+	return d.updateTableName(alias)
+}
+
+func (d *dnsCredential) updateTableName(table string) *dnsCredential {
+	d.ALL = field.NewAsterisk(table)
+	d.ID = field.NewInt(table, "id")
+	d.CreatedAt = field.NewTime(table, "created_at")
+	d.UpdatedAt = field.NewTime(table, "updated_at")
+	d.DeletedAt = field.NewTime(table, "deleted_at")
+	d.Name = field.NewString(table, "name")
+	d.Config = field.NewField(table, "config")
+
+	d.fillFieldMap()
+
+	return d
+}
+
+func (d *dnsCredential) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
+	_f, ok := d.fieldMap[fieldName]
+	if !ok || _f == nil {
+		return nil, false
+	}
+	_oe, ok := _f.(field.OrderExpr)
+	return _oe, ok
+}
+
+func (d *dnsCredential) fillFieldMap() {
+	d.fieldMap = make(map[string]field.Expr, 6)
+	d.fieldMap["id"] = d.ID
+	d.fieldMap["created_at"] = d.CreatedAt
+	d.fieldMap["updated_at"] = d.UpdatedAt
+	d.fieldMap["deleted_at"] = d.DeletedAt
+	d.fieldMap["name"] = d.Name
+	d.fieldMap["config"] = d.Config
+}
+
+func (d dnsCredential) clone(db *gorm.DB) dnsCredential {
+	d.dnsCredentialDo.ReplaceConnPool(db.Statement.ConnPool)
+	return d
+}
+
+func (d dnsCredential) replaceDB(db *gorm.DB) dnsCredential {
+	d.dnsCredentialDo.ReplaceDB(db)
+	return d
+}
+
+type dnsCredentialDo struct{ gen.DO }
+
+// FirstByID Where("id=@id")
+func (d dnsCredentialDo) FirstByID(id int) (result *model.DnsCredential, err error) {
+	var params []interface{}
+
+	var generateSQL strings.Builder
+	params = append(params, id)
+	generateSQL.WriteString("id=? ")
+
+	var executeSQL *gorm.DB
+	executeSQL = d.UnderlyingDB().Where(generateSQL.String(), params...).Take(&result) // ignore_security_alert
+	err = executeSQL.Error
+
+	return
+}
+
+// DeleteByID update @@table set deleted_at=NOW() where id=@id
+func (d dnsCredentialDo) DeleteByID(id int) (err error) {
+	var params []interface{}
+
+	var generateSQL strings.Builder
+	params = append(params, id)
+	generateSQL.WriteString("update dns_credentials set deleted_at=NOW() where id=? ")
+
+	var executeSQL *gorm.DB
+	executeSQL = d.UnderlyingDB().Exec(generateSQL.String(), params...) // ignore_security_alert
+	err = executeSQL.Error
+
+	return
+}
+
+func (d dnsCredentialDo) Debug() *dnsCredentialDo {
+	return d.withDO(d.DO.Debug())
+}
+
+func (d dnsCredentialDo) WithContext(ctx context.Context) *dnsCredentialDo {
+	return d.withDO(d.DO.WithContext(ctx))
+}
+
+func (d dnsCredentialDo) ReadDB() *dnsCredentialDo {
+	return d.Clauses(dbresolver.Read)
+}
+
+func (d dnsCredentialDo) WriteDB() *dnsCredentialDo {
+	return d.Clauses(dbresolver.Write)
+}
+
+func (d dnsCredentialDo) Session(config *gorm.Session) *dnsCredentialDo {
+	return d.withDO(d.DO.Session(config))
+}
+
+func (d dnsCredentialDo) Clauses(conds ...clause.Expression) *dnsCredentialDo {
+	return d.withDO(d.DO.Clauses(conds...))
+}
+
+func (d dnsCredentialDo) Returning(value interface{}, columns ...string) *dnsCredentialDo {
+	return d.withDO(d.DO.Returning(value, columns...))
+}
+
+func (d dnsCredentialDo) Not(conds ...gen.Condition) *dnsCredentialDo {
+	return d.withDO(d.DO.Not(conds...))
+}
+
+func (d dnsCredentialDo) Or(conds ...gen.Condition) *dnsCredentialDo {
+	return d.withDO(d.DO.Or(conds...))
+}
+
+func (d dnsCredentialDo) Select(conds ...field.Expr) *dnsCredentialDo {
+	return d.withDO(d.DO.Select(conds...))
+}
+
+func (d dnsCredentialDo) Where(conds ...gen.Condition) *dnsCredentialDo {
+	return d.withDO(d.DO.Where(conds...))
+}
+
+func (d dnsCredentialDo) Exists(subquery interface{ UnderlyingDB() *gorm.DB }) *dnsCredentialDo {
+	return d.Where(field.CompareSubQuery(field.ExistsOp, nil, subquery.UnderlyingDB()))
+}
+
+func (d dnsCredentialDo) Order(conds ...field.Expr) *dnsCredentialDo {
+	return d.withDO(d.DO.Order(conds...))
+}
+
+func (d dnsCredentialDo) Distinct(cols ...field.Expr) *dnsCredentialDo {
+	return d.withDO(d.DO.Distinct(cols...))
+}
+
+func (d dnsCredentialDo) Omit(cols ...field.Expr) *dnsCredentialDo {
+	return d.withDO(d.DO.Omit(cols...))
+}
+
+func (d dnsCredentialDo) Join(table schema.Tabler, on ...field.Expr) *dnsCredentialDo {
+	return d.withDO(d.DO.Join(table, on...))
+}
+
+func (d dnsCredentialDo) LeftJoin(table schema.Tabler, on ...field.Expr) *dnsCredentialDo {
+	return d.withDO(d.DO.LeftJoin(table, on...))
+}
+
+func (d dnsCredentialDo) RightJoin(table schema.Tabler, on ...field.Expr) *dnsCredentialDo {
+	return d.withDO(d.DO.RightJoin(table, on...))
+}
+
+func (d dnsCredentialDo) Group(cols ...field.Expr) *dnsCredentialDo {
+	return d.withDO(d.DO.Group(cols...))
+}
+
+func (d dnsCredentialDo) Having(conds ...gen.Condition) *dnsCredentialDo {
+	return d.withDO(d.DO.Having(conds...))
+}
+
+func (d dnsCredentialDo) Limit(limit int) *dnsCredentialDo {
+	return d.withDO(d.DO.Limit(limit))
+}
+
+func (d dnsCredentialDo) Offset(offset int) *dnsCredentialDo {
+	return d.withDO(d.DO.Offset(offset))
+}
+
+func (d dnsCredentialDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *dnsCredentialDo {
+	return d.withDO(d.DO.Scopes(funcs...))
+}
+
+func (d dnsCredentialDo) Unscoped() *dnsCredentialDo {
+	return d.withDO(d.DO.Unscoped())
+}
+
+func (d dnsCredentialDo) Create(values ...*model.DnsCredential) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return d.DO.Create(values)
+}
+
+func (d dnsCredentialDo) CreateInBatches(values []*model.DnsCredential, batchSize int) error {
+	return d.DO.CreateInBatches(values, batchSize)
+}
+
+// Save : !!! underlying implementation is different with GORM
+// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
+func (d dnsCredentialDo) Save(values ...*model.DnsCredential) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return d.DO.Save(values)
+}
+
+func (d dnsCredentialDo) First() (*model.DnsCredential, error) {
+	if result, err := d.DO.First(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.DnsCredential), nil
+	}
+}
+
+func (d dnsCredentialDo) Take() (*model.DnsCredential, error) {
+	if result, err := d.DO.Take(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.DnsCredential), nil
+	}
+}
+
+func (d dnsCredentialDo) Last() (*model.DnsCredential, error) {
+	if result, err := d.DO.Last(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.DnsCredential), nil
+	}
+}
+
+func (d dnsCredentialDo) Find() ([]*model.DnsCredential, error) {
+	result, err := d.DO.Find()
+	return result.([]*model.DnsCredential), err
+}
+
+func (d dnsCredentialDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.DnsCredential, err error) {
+	buf := make([]*model.DnsCredential, 0, batchSize)
+	err = d.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
+		defer func() { results = append(results, buf...) }()
+		return fc(tx, batch)
+	})
+	return results, err
+}
+
+func (d dnsCredentialDo) FindInBatches(result *[]*model.DnsCredential, batchSize int, fc func(tx gen.Dao, batch int) error) error {
+	return d.DO.FindInBatches(result, batchSize, fc)
+}
+
+func (d dnsCredentialDo) Attrs(attrs ...field.AssignExpr) *dnsCredentialDo {
+	return d.withDO(d.DO.Attrs(attrs...))
+}
+
+func (d dnsCredentialDo) Assign(attrs ...field.AssignExpr) *dnsCredentialDo {
+	return d.withDO(d.DO.Assign(attrs...))
+}
+
+func (d dnsCredentialDo) Joins(fields ...field.RelationField) *dnsCredentialDo {
+	for _, _f := range fields {
+		d = *d.withDO(d.DO.Joins(_f))
+	}
+	return &d
+}
+
+func (d dnsCredentialDo) Preload(fields ...field.RelationField) *dnsCredentialDo {
+	for _, _f := range fields {
+		d = *d.withDO(d.DO.Preload(_f))
+	}
+	return &d
+}
+
+func (d dnsCredentialDo) FirstOrInit() (*model.DnsCredential, error) {
+	if result, err := d.DO.FirstOrInit(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.DnsCredential), nil
+	}
+}
+
+func (d dnsCredentialDo) FirstOrCreate() (*model.DnsCredential, error) {
+	if result, err := d.DO.FirstOrCreate(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.DnsCredential), nil
+	}
+}
+
+func (d dnsCredentialDo) FindByPage(offset int, limit int) (result []*model.DnsCredential, count int64, err error) {
+	result, err = d.Offset(offset).Limit(limit).Find()
+	if err != nil {
+		return
+	}
+
+	if size := len(result); 0 < limit && 0 < size && size < limit {
+		count = int64(size + offset)
+		return
+	}
+
+	count, err = d.Offset(-1).Limit(-1).Count()
+	return
+}
+
+func (d dnsCredentialDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
+	count, err = d.Count()
+	if err != nil {
+		return
+	}
+
+	err = d.Offset(offset).Limit(limit).Scan(result)
+	return
+}
+
+func (d dnsCredentialDo) Scan(result interface{}) (err error) {
+	return d.DO.Scan(result)
+}
+
+func (d dnsCredentialDo) Delete(models ...*model.DnsCredential) (result gen.ResultInfo, err error) {
+	return d.DO.Delete(models)
+}
+
+func (d *dnsCredentialDo) withDO(do gen.Dao) *dnsCredentialDo {
+	d.DO = *do.(*gen.DO)
+	return d
+}

+ 54 - 46
server/query/gen.go

@@ -16,13 +16,14 @@ import (
 )
 
 var (
-	Q            = new(Query)
-	Auth         *auth
-	AuthToken    *authToken
-	Cert         *cert
-	ChatGPTLog   *chatGPTLog
-	ConfigBackup *configBackup
-	Site         *site
+	Q             = new(Query)
+	Auth          *auth
+	AuthToken     *authToken
+	Cert          *cert
+	ChatGPTLog    *chatGPTLog
+	ConfigBackup  *configBackup
+	DnsCredential *dnsCredential
+	Site          *site
 )
 
 func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
@@ -32,43 +33,47 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
 	Cert = &Q.Cert
 	ChatGPTLog = &Q.ChatGPTLog
 	ConfigBackup = &Q.ConfigBackup
+	DnsCredential = &Q.DnsCredential
 	Site = &Q.Site
 }
 
 func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
 	return &Query{
-		db:           db,
-		Auth:         newAuth(db, opts...),
-		AuthToken:    newAuthToken(db, opts...),
-		Cert:         newCert(db, opts...),
-		ChatGPTLog:   newChatGPTLog(db, opts...),
-		ConfigBackup: newConfigBackup(db, opts...),
-		Site:         newSite(db, opts...),
+		db:            db,
+		Auth:          newAuth(db, opts...),
+		AuthToken:     newAuthToken(db, opts...),
+		Cert:          newCert(db, opts...),
+		ChatGPTLog:    newChatGPTLog(db, opts...),
+		ConfigBackup:  newConfigBackup(db, opts...),
+		DnsCredential: newDnsCredential(db, opts...),
+		Site:          newSite(db, opts...),
 	}
 }
 
 type Query struct {
 	db *gorm.DB
 
-	Auth         auth
-	AuthToken    authToken
-	Cert         cert
-	ChatGPTLog   chatGPTLog
-	ConfigBackup configBackup
-	Site         site
+	Auth          auth
+	AuthToken     authToken
+	Cert          cert
+	ChatGPTLog    chatGPTLog
+	ConfigBackup  configBackup
+	DnsCredential dnsCredential
+	Site          site
 }
 
 func (q *Query) Available() bool { return q.db != nil }
 
 func (q *Query) clone(db *gorm.DB) *Query {
 	return &Query{
-		db:           db,
-		Auth:         q.Auth.clone(db),
-		AuthToken:    q.AuthToken.clone(db),
-		Cert:         q.Cert.clone(db),
-		ChatGPTLog:   q.ChatGPTLog.clone(db),
-		ConfigBackup: q.ConfigBackup.clone(db),
-		Site:         q.Site.clone(db),
+		db:            db,
+		Auth:          q.Auth.clone(db),
+		AuthToken:     q.AuthToken.clone(db),
+		Cert:          q.Cert.clone(db),
+		ChatGPTLog:    q.ChatGPTLog.clone(db),
+		ConfigBackup:  q.ConfigBackup.clone(db),
+		DnsCredential: q.DnsCredential.clone(db),
+		Site:          q.Site.clone(db),
 	}
 }
 
@@ -82,33 +87,36 @@ func (q *Query) WriteDB() *Query {
 
 func (q *Query) ReplaceDB(db *gorm.DB) *Query {
 	return &Query{
-		db:           db,
-		Auth:         q.Auth.replaceDB(db),
-		AuthToken:    q.AuthToken.replaceDB(db),
-		Cert:         q.Cert.replaceDB(db),
-		ChatGPTLog:   q.ChatGPTLog.replaceDB(db),
-		ConfigBackup: q.ConfigBackup.replaceDB(db),
-		Site:         q.Site.replaceDB(db),
+		db:            db,
+		Auth:          q.Auth.replaceDB(db),
+		AuthToken:     q.AuthToken.replaceDB(db),
+		Cert:          q.Cert.replaceDB(db),
+		ChatGPTLog:    q.ChatGPTLog.replaceDB(db),
+		ConfigBackup:  q.ConfigBackup.replaceDB(db),
+		DnsCredential: q.DnsCredential.replaceDB(db),
+		Site:          q.Site.replaceDB(db),
 	}
 }
 
 type queryCtx struct {
-	Auth         *authDo
-	AuthToken    *authTokenDo
-	Cert         *certDo
-	ChatGPTLog   *chatGPTLogDo
-	ConfigBackup *configBackupDo
-	Site         *siteDo
+	Auth          *authDo
+	AuthToken     *authTokenDo
+	Cert          *certDo
+	ChatGPTLog    *chatGPTLogDo
+	ConfigBackup  *configBackupDo
+	DnsCredential *dnsCredentialDo
+	Site          *siteDo
 }
 
 func (q *Query) WithContext(ctx context.Context) *queryCtx {
 	return &queryCtx{
-		Auth:         q.Auth.WithContext(ctx),
-		AuthToken:    q.AuthToken.WithContext(ctx),
-		Cert:         q.Cert.WithContext(ctx),
-		ChatGPTLog:   q.ChatGPTLog.WithContext(ctx),
-		ConfigBackup: q.ConfigBackup.WithContext(ctx),
-		Site:         q.Site.WithContext(ctx),
+		Auth:          q.Auth.WithContext(ctx),
+		AuthToken:     q.AuthToken.WithContext(ctx),
+		Cert:          q.Cert.WithContext(ctx),
+		ChatGPTLog:    q.ChatGPTLog.WithContext(ctx),
+		ConfigBackup:  q.ConfigBackup.WithContext(ctx),
+		DnsCredential: q.DnsCredential.WithContext(ctx),
+		Site:          q.Site.WithContext(ctx),
 	}
 }
 

+ 8 - 0
server/router/routers.go

@@ -96,6 +96,14 @@ func InitRouter() *gin.Engine {
 			g.DELETE("auto_cert/:name", api.RemoveDomainFromAutoCert)
 			g.GET("auto_cert/dns/providers", api.GetDNSProvidersList)
 			g.GET("auto_cert/dns/provider/:code", api.GetDNSProvider)
+
+			// DNS Credential
+			g.GET("dns_credentials", api.GetDnsCredentialList)
+			g.GET("dns_credential/:id", api.GetDnsCredential)
+			g.POST("dns_credential", api.AddDnsCredential)
+			g.POST("dns_credential/:id", api.EditDnsCredential)
+			g.DELETE("dns_credential/:id", api.DeleteDnsCredential)
+
 			// pty
 			g.GET("pty", api.Pty)