Bladeren bron

Merge pull request #26 from 0xJacky/frontend-next

Refactored install and 404 pages
Jacky 2 jaren geleden
bovenliggende
commit
2b800f41d5

+ 0 - 1
frontend/.env.development

@@ -1,2 +1 @@
 VITE_API_ROOT = /api
-VITE_API_WSS_ROOT = wss://nginx.jackyu.cn/api

+ 1 - 2
frontend/.env.production

@@ -1,2 +1 @@
-VUE_APP_API_ROOT = /api
-VUE_APP_API_WSS_ROOT = /api
+VITE_API_ROOT = /api

+ 1 - 1
frontend/frontend.go

@@ -4,5 +4,5 @@ import (
 	"embed"
 )
 
-//go:embed dist
+//go:embed dist/* dist/*/*
 var DistFS embed.FS

+ 0 - 18
frontend/public/vite.svg

@@ -1,18 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img"
-     class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257">
-    <defs>
-        <linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%">
-            <stop offset="0%" stop-color="#41D1FF"></stop>
-            <stop offset="100%" stop-color="#BD34FE"></stop>
-        </linearGradient>
-        <linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%">
-            <stop offset="0%" stop-color="#FFEA83"></stop>
-            <stop offset="8.333%" stop-color="#FFDD35"></stop>
-            <stop offset="100%" stop-color="#FFA800"></stop>
-        </linearGradient>
-    </defs>
-    <path fill="url(#IconifyId1813088fe1fbc01fb466)"
-          d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path>
-    <path fill="url(#IconifyId1813088fe1fbc01fb467)"
-          d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path>
-</svg>

+ 12 - 0
frontend/src/api/index.ts

@@ -0,0 +1,12 @@
+import http from '@/lib/http'
+
+const install = {
+    get_lock() {
+        return http.get('/install')
+    },
+    install_nginx_ui(data: any) {
+        return http.post('/install', data)
+    }
+}
+
+export default install

+ 13 - 21
frontend/src/components/StdDataDisplay/StdPagination.vue

@@ -1,36 +1,28 @@
+<script setup lang="ts">
+import {useGettext} from 'vue3-gettext'
+
+const {pagination, size} = defineProps(['pagination', 'size'])
+const emit = defineEmits(['changePage'])
+const {$gettext} = useGettext()
+
+function changePage(num: number) {
+    emit('changePage', num)
+}
+</script>
+
 <template>
-    <div v-if="Object.keys(pagination).length !== 0">
+    <div v-if="pagination.total>pagination.per_page">
         <a-pagination
             :current="pagination.current_page"
-            :hideOnSinglePage="true"
             :pageSize="pagination.per_page"
             :size="size"
             :total="pagination.total"
-            :show-total="(total, range) => `当前显示${range[0]}-${range[1]}条数据,共${total}条数据`"
             class="pagination"
             @change="changePage"
         />
-        <div class="clear"></div>
     </div>
 </template>
 
-<script>
-export default {
-    name: 'StdPagination',
-    props: {
-        pagination: Object,
-        size: {
-            default: ''
-        }
-    },
-    methods: {
-        changePage(num) {
-            return this.$emit('changePage', num)
-        }
-    }
-}
-</script>
-
 <style lang="less">
 .ant-pagination-total-text {
     @media (max-width: 450px) {

+ 12 - 7
frontend/src/components/StdDataDisplay/StdTable.vue

@@ -5,7 +5,7 @@ const {$gettext, interpolate} = gettext
 
 import StdDataEntry from '@/components/StdDataEntry'
 import StdPagination from './StdPagination.vue'
-import {nextTick, reactive, ref} from 'vue'
+import {nextTick, reactive, ref, watch} from 'vue'
 import {useRoute, useRouter} from 'vue-router'
 import {message} from 'ant-design-vue'
 
@@ -61,9 +61,9 @@ const props = defineProps({
 })
 
 
-const data_source = reactive([])
+const data_source = ref([])
 const loading = ref(true)
-const pagination = ({
+const pagination = reactive({
     total: 1,
     per_page: 10,
     current_page: 1,
@@ -80,7 +80,6 @@ const rowSelection = reactive({})
 const searchColumns = getSearchColumns()
 const pithyColumns = getPithyColumns()
 
-
 get_list()
 
 defineExpose({
@@ -102,7 +101,7 @@ function get_list(page_num = null) {
         params['page'] = page_num
     }
     props.api!.get_list(params).then((r: any) => {
-        Object.assign(data_source, r.data)
+        data_source.value = r.data
 
         if (r.pagination !== undefined) {
             Object.assign(pagination, r.pagination)
@@ -161,10 +160,17 @@ function onSelect(record: any) {
 const router = useRouter()
 
 const reset_search = async () => {
-    params = reactive({})
+    Object.keys(params).forEach(v => {
+        delete params[v]
+    })
     router.push({query: {}}).catch(() => {
     })
 }
+
+watch(params, () => {
+    router.push({query: params})
+    get_list()
+})
 </script>
 
 <template>
@@ -212,7 +218,6 @@ const reset_search = async () => {
                     </template>
                 </template>
             </template>
-
         </a-table>
         <std-pagination :pagination="pagination" @changePage="get_list"/>
     </div>

+ 4 - 0
frontend/src/dark.less

@@ -1 +1,5 @@
 @import "ant-design-vue/dist/antd.dark";
+
+.directive-editor-extra {
+    background-color: rgba(0, 0, 0, 0.84) !important;
+}

+ 4 - 6
frontend/src/lib/http/index.ts

@@ -2,15 +2,12 @@ import axios, {AxiosRequestConfig} from 'axios'
 import {useUserStore} from '@/pinia'
 import {storeToRefs} from 'pinia'
 
+import router from '@/routes'
+
 const user = useUserStore()
 
 const {token} = storeToRefs(user)
 
-declare module 'axios' {
-    export interface AxiosResponse<T = any> extends Promise<T> {
-    }
-}
-
 let instance = axios.create({
     baseURL: import.meta.env.VITE_API_ROOT,
     timeout: 50000,
@@ -38,7 +35,6 @@ instance.interceptors.request.use(
     }
 )
 
-
 instance.interceptors.response.use(
     response => {
         return Promise.resolve(response.data)
@@ -47,6 +43,8 @@ instance.interceptors.response.use(
         switch (error.response.status) {
             case 401:
             case 403:
+                user.logout()
+                await router.push('/login')
                 break
         }
         return Promise.reject(error.response.data)

+ 1 - 1
frontend/src/main.ts

@@ -3,7 +3,7 @@ import {createPinia} from 'pinia'
 import gettext from './gettext'
 import App from './App.vue'
 import router from './routes'
-import 'ant-design-vue/dist/antd.less'
+//import 'ant-design-vue/dist/antd.less'
 import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
 import {useSettingsStore} from '@/pinia'
 

+ 5 - 28
frontend/src/routes/index.ts

@@ -100,7 +100,7 @@ export const routes = [
     {
         path: '/install',
         name: () => $gettext('Install'),
-        // component: () => import('@/views/other/Install.vue'),
+        component: () => import('@/views/other/Install.vue'),
         meta: {noAuth: true}
     },
     {
@@ -110,16 +110,10 @@ export const routes = [
         meta: {noAuth: true}
     },
     {
-        path: '/404',
-        name: () => $gettext('404 Not Found'),
-        component: () => import('@/views/other/Error.vue'),
-        meta: {noAuth: true, status_code: 404, error: 'Not Found'}
-    },
-    {
-        path: '/*',
+        path: '/:pathMatch(.*)*',
         name: () => $gettext('Not Found'),
-        redirect: '/404',
-        meta: {noAuth: true}
+        component: () => import('@/views/other/Error.vue'),
+        meta: {noAuth: true, status_code: 404, error: () => $gettext('Not Found')}
     }
 ]
 
@@ -130,25 +124,8 @@ const router = createRouter({
 })
 
 router.beforeEach((to, from, next) => {
-
     // @ts-ignore
-    document.title = to.name() + ' | Nginx UI'
-
-    if (import.meta.env.MODE === 'production') {
-        // axios.get('/version.json?' + Date.now()).then(r => {
-        //     if (!(process.env.VUE_APP_VERSION === r.data.version
-        //         && Number(process.env.VUE_APP_BUILD_ID) === r.data.build_id)) {
-        //         Vue.prototype.$info({
-        //             title: $gettext('System message'),
-        //             content: $gettext('Detected version update, this page will refresh.'),
-        //             onOk() {
-        //                 location.reload()
-        //             },
-        //             okText: $gettext('OK')
-        //         })
-        //     }
-        // })
-    }
+    document.title = to.name?.() + ' | Nginx UI'
 
     const user = useUserStore()
     const {is_login} = user

+ 1 - 3
frontend/src/style.less

@@ -1,3 +1 @@
-@import "ant-design-vue/dist/antd.variable";
-
-@border-radius-base: 4px;
+@import 'ant-design-vue/dist/antd.less';

+ 28 - 30
frontend/src/views/domain/cert/Cert.vue

@@ -1,44 +1,42 @@
+<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'
+
+const {directivesMap, current_server_directives, enabled} = defineProps<{
+    directivesMap: any
+    current_server_directives: Array<any>
+    enabled: boolean
+}>()
+
+const info = ref(null)
+
+interface Info {
+    get(): void
+}
+
+function callback() {
+    const t: Info | null = info.value
+    t!.get()
+}
+
+const name = computed(() => {
+    return directivesMap['server_name'][0].params.trim()
+})
+</script>
+
 <template>
     <div>
         <cert-info ref="info" :domain="name" v-if="name"/>
         <issue-cert
             :current_server_directives="current_server_directives"
             :directives-map="directivesMap"
-            v-model="auto_cert"
+            v-model:enabled="enabled"
             @callback="callback"
         />
     </div>
 </template>
 
-<script>
-import CertInfo from '@/views/domain/cert/CertInfo'
-import IssueCert from '@/views/domain/cert/IssueCert'
-
-export default {
-    name: 'Cert',
-    components: {IssueCert, CertInfo},
-    props: {
-        directivesMap: Object,
-        current_server_directives: Array,
-        auto_cert: Boolean
-    },
-    model: {
-        prop: 'auto_cert',
-        event: 'change_auto_cert'
-    },
-    methods: {
-        callback() {
-            this.$refs.info.get()
-        }
-    },
-    computed: {
-        name() {
-            return this.directivesMap['server_name'][0].params.trim()
-        }
-    }
-}
-</script>
-
 <style scoped>
 
 </style>

+ 4 - 0
frontend/src/views/domain/cert/CertInfo.vue

@@ -19,6 +19,10 @@ function get() {
         ok.value = false
     })
 }
+
+defineExpose({
+    get
+})
 </script>
 
 <template>

+ 116 - 119
frontend/src/views/domain/cert/IssueCert.vue

@@ -1,9 +1,123 @@
+<script setup lang="ts">
+import {issue_cert} from '../methods'
+import {useGettext} from 'vue3-gettext'
+import {computed, nextTick, ref, watch} from 'vue'
+import {message} from 'ant-design-vue'
+import domain from '@/api/domain'
+
+const {$gettext, interpolate} = useGettext()
+
+const {directivesMap, current_server_directives, enabled} = defineProps<{
+    directivesMap: any
+    current_server_directives: Array<any>
+    enabled: boolean
+}>()
+
+const emit = defineEmits(['changeEnabled', 'callback', 'update:enabled'])
+
+const issuing_cert = ref(false)
+
+function onchange(r: boolean) {
+    emit('changeEnabled', r)
+    change_auto_cert(r)
+    if (r) {
+        job()
+    }
+}
+
+function job() {
+    issuing_cert.value = true
+
+    if (no_server_name.value) {
+        message.error($gettext('server_name not found in directives'))
+        issuing_cert.value = false
+        return
+    }
+
+    if (server_name_more_than_one.value) {
+        message.error($gettext('server_name parameters more than one'))
+        issuing_cert.value = false
+        return
+    }
+
+    const server_name = directivesMap['server_name'][0]
+
+    if (!directivesMap['ssl_certificate']) {
+        current_server_directives.splice(server_name.idx + 1, 0, {
+            directive: 'ssl_certificate',
+            params: ''
+        })
+    }
+
+    nextTick(() => {
+        if (!directivesMap['ssl_certificate_key']) {
+            const ssl_certificate = directivesMap['ssl_certificate'][0]
+            current_server_directives.splice(ssl_certificate.idx + 1, 0, {
+                directive: 'ssl_certificate_key',
+                params: ''
+            })
+        }
+    })
+
+    setTimeout(() => {
+        issue_cert(name.value, callback)
+    }, 100)
+}
+
+function callback(ssl_certificate: string, ssl_certificate_key: string) {
+    directivesMap['ssl_certificate'][0]['params'] = ssl_certificate
+    directivesMap['ssl_certificate_key'][0]['params'] = ssl_certificate_key
+
+    issuing_cert.value = false
+    emit('callback')
+}
+
+function change_auto_cert(r: boolean) {
+    if (r) {
+        domain.add_auto_cert(name.value).then(() => {
+            message.success(interpolate($gettext('Auto-renewal enabled for %{name}'), {name: name.value}))
+        }).catch(e => {
+            message.error(e.message ?? interpolate($gettext('Enable auto-renewal failed for %{name}'), {name: name.value}))
+        })
+    } else {
+        domain.remove_auto_cert(name.value).then(() => {
+            message.success(interpolate($gettext('Auto-renewal disabled for %{name}'), {name: name.value}))
+        }).catch(e => {
+            message.error(e.message ?? interpolate($gettext('Disable auto-renewal failed for %{name}'), {name: name.value}))
+        })
+    }
+}
+
+const server_name_more_than_one = computed(() => {
+    return directivesMap['server_name'] && (directivesMap['server_name'].length > 1 ||
+        directivesMap['server_name'][0].params.trim().indexOf(' ') > 0)
+})
+
+const no_server_name = computed(() => {
+    return directivesMap['server_name'].length === 0
+})
+
+const name = computed(() => {
+    return directivesMap['server_name'][0].params.trim()
+})
+
+watch(server_name_more_than_one, () => {
+    emit('update:enabled', false)
+    onchange(false)
+})
+
+watch(no_server_name, () => {
+    emit('update:enabled', false)
+    onchange(false)
+})
+</script>
+
 <template>
     <div>
         <a-form-item :label="$gettext('Encrypt website with Let\'s Encrypt')">
             <a-switch
                 :loading="issuing_cert"
-                v-model="M_enabled"
+                v-model:checked="enabled"
                 @change="onchange"
                 :disabled="no_server_name||server_name_more_than_one"
             />
@@ -27,7 +141,7 @@
             Note: The server_name in the current configuration must be the domain name
             you need to get the certificate.
         </p>
-        <p v-if="enabled" v-translate>
+        <p v-translate>
             The certificate for the domain will be checked every hour,
             and will be renewed if it has been more than 1 month since it was last issued.
         </p>
@@ -38,123 +152,6 @@
     </div>
 </template>
 
-<script>
-import {issue_cert} from '@/views/domain/methods'
-import $gettext, {$interpolate} from '@/lib/translate/gettext'
-
-export default {
-    name: 'IssueCert',
-    props: {
-        directivesMap: Object,
-        current_server_directives: Array,
-        enabled: Boolean
-    },
-    model: {
-        prop: 'enabled',
-        event: 'changeEnabled'
-    },
-    data() {
-        return {
-            issuing_cert: false,
-            M_enabled: this.enabled,
-        }
-    },
-    methods: {
-        onchange(r) {
-            this.$emit('changeEnabled', r)
-            this.change_auto_cert(r)
-            if (r) {
-                this.job()
-            }
-        },
-        job() {
-            this.issuing_cert = true
-
-            if (this.no_server_name) {
-                this.$message.error($gettext('server_name not found in directives'))
-                this.issuing_cert = false
-                return
-            }
-
-            if (this.server_name_more_than_one) {
-                this.$message.error($gettext('server_name parameters more than one'))
-                this.issuing_cert = false
-                return
-            }
-
-            const server_name = this.directivesMap['server_name'][0]
-
-            if (!this.directivesMap['ssl_certificate']) {
-                this.current_server_directives.splice(server_name.idx + 1, 0, {
-                    directive: 'ssl_certificate',
-                    params: ''
-                })
-            }
-
-            this.$nextTick(() => {
-                if (!this.directivesMap['ssl_certificate_key']) {
-                    const ssl_certificate = this.directivesMap['ssl_certificate'][0]
-                    this.current_server_directives.splice(ssl_certificate.idx + 1, 0, {
-                        directive: 'ssl_certificate_key',
-                        params: ''
-                    })
-                }
-            })
-
-            setTimeout(() => {
-                issue_cert(this.name, this.callback)
-            }, 100)
-        },
-        callback(ssl_certificate, ssl_certificate_key) {
-            this.$set(this.directivesMap['ssl_certificate'][0], 'params', ssl_certificate)
-            this.$set(this.directivesMap['ssl_certificate_key'][0], 'params', ssl_certificate_key)
-            this.issuing_cert = false
-            this.$emit('callback')
-        },
-        change_auto_cert(r) {
-            if (r) {
-                this.$api.domain.add_auto_cert(this.name).then(() => {
-                    this.$message.success($interpolate($gettext('Auto-renewal enabled for %{name}'), {name: this.name}))
-                }).catch(e => {
-                    this.$message.error(e.message ?? $interpolate($gettext('Enable auto-renewal failed for %{name}'), {name: this.name}))
-                })
-            } else {
-                this.$api.domain.remove_auto_cert(this.name).then(() => {
-                    this.$message.success($interpolate($gettext('Auto-renewal disabled for %{name}'), {name: this.name}))
-                }).catch(e => {
-                    this.$message.error(e.message ?? $interpolate($gettext('Disable auto-renewal failed for %{name}'), {name: this.name}))
-                })
-            }
-        },
-    },
-    watch: {
-        server_name_more_than_one() {
-            this.M_enabled = false
-            this.onchange(false)
-        },
-        no_server_name() {
-            this.M_enabled = false
-            this.onchange(false)
-        }
-    },
-    computed: {
-        is_demo() {
-            return this.$store.getters.env.demo === true
-        },
-        server_name_more_than_one() {
-            return this.directivesMap['server_name'] && (this.directivesMap['server_name'].length > 1 ||
-                this.directivesMap['server_name'][0].params.trim().indexOf(' ') > 0)
-        },
-        no_server_name() {
-            return !this.directivesMap['server_name']
-        },
-        name() {
-            return this.directivesMap['server_name'][0].params.trim()
-        }
-    }
-}
-</script>
-
 <style lang="less" scoped>
 .switch-wrapper {
     position: relative;

+ 0 - 37
frontend/src/views/domain/methods.js

@@ -1,37 +0,0 @@
-import $gettext from '@/lib/translate/gettext'
-import store from '@/lib/store'
-import Vue from 'vue'
-
-const issue_cert = (server_name, callback) => {
-    Vue.prototype.$message.info($gettext('Getting the certificate, please wait...'), 15)
-    const ws = new WebSocket(Vue.prototype.getWebSocketRoot() + '/cert/issue/' + server_name
-        + '?token=' + btoa(store.state.user.token))
-
-    ws.onopen = () => {
-        ws.send('go')
-    }
-
-    ws.onmessage = m => {
-        const r = JSON.parse(m.data)
-        switch (r.status) {
-            case 'success':
-                Vue.prototype.$message.success(r.message, 10)
-                break
-            case 'info':
-                Vue.prototype.$message.info(r.message, 10)
-                break
-            case 'error':
-                Vue.prototype.$message.error(r.message, 10)
-                break
-        }
-
-        if (r.status === 'success' && r.ssl_certificate !== undefined && r.ssl_certificate_key !== undefined) {
-            callback(r.ssl_certificate, r.ssl_certificate_key)
-        }
-    }
-    // setTimeout(() => {
-    //     callback('a', 'b')
-    // }, 10000)
-}
-
-export {issue_cert}

+ 40 - 0
frontend/src/views/domain/methods.ts

@@ -0,0 +1,40 @@
+import gettext from '@/gettext'
+import websocket from '@/lib/websocket'
+import ReconnectingWebSocket from 'reconnecting-websocket'
+import {message} from 'ant-design-vue'
+
+const {$gettext} = gettext
+
+const issue_cert = async (server_name: string, callback: Function) => {
+    // message.info($gettext('Getting the certificate, please wait...'), 15)
+    //
+    // const ws: ReconnectingWebSocket = websocket('/api/cert/issue/' + server_name)
+    //
+    // ws.onopen = () => {
+    //     ws.send('go')
+    // }
+    //
+    // ws.onmessage = m => {
+    //     const r = JSON.parse(m.data)
+    //     switch (r.status) {
+    //         case 'success':
+    //             message.success(r.message, 10)
+    //             break
+    //         case 'info':
+    //             message.info(r.message, 10)
+    //             break
+    //         case 'error':
+    //             message.error(r.message, 10)
+    //             break
+    //     }
+    //
+    //     if (r.status === 'success' && r.ssl_certificate !== undefined && r.ssl_certificate_key !== undefined) {
+    //         callback(r.ssl_certificate, r.ssl_certificate_key)
+    //     }
+    // }
+    setTimeout(() => {
+        callback('a', 'b')
+    }, 10000)
+}
+
+export {issue_cert}

+ 4 - 4
frontend/src/views/domain/ngx_conf/LocationEditor.vue

@@ -28,11 +28,11 @@ function add() {
 
 function save() {
     adding.value = false
-    locations.push(this.location)
+    locations?.push(location)
 }
 
-function remove(index) {
-    locations.splice(index, 1)
+function remove(index: number) {
+    locations?.splice(index, 1)
 }
 </script>
 
@@ -43,7 +43,7 @@ function remove(index) {
             :title="$gettext('Location')" size="small">
         <a-form layout="vertical">
             <a-form-item :label="$gettext('Comments')">
-                <a-textarea v-model:value="v.comments"/>
+                <a-textarea v-model:value="v.comments" :bordered="false"/>
             </a-form-item>
             <a-form-item :label="$gettext('Path')">
                 <a-input addon-before="location" v-model:value="v.path"/>

+ 7 - 18
frontend/src/views/domain/ngx_conf/NgxConfigEditor.vue

@@ -1,11 +1,10 @@
 <script setup lang="ts">
-import CertInfo from '@/views/domain/cert/CertInfo'
-// import IssueCert from '@/views/domain/cert/IssueCert'
 import DirectiveEditor from '@/views/domain/ngx_conf/directive/DirectiveEditor'
 import LocationEditor from '@/views/domain/ngx_conf/LocationEditor'
 import {computed, ref} from 'vue'
 import {useRoute} from 'vue-router'
 import {useGettext} from 'vue3-gettext'
+import Cert from '@/views/domain/cert/Cert.vue'
 
 const {$gettext} = useGettext()
 
@@ -16,15 +15,6 @@ const route = useRoute()
 const current_server_index = ref(0)
 const name = ref(route.params.name)
 
-const init_ssl_status = ref(false)
-
-function update_cert_info() {
-    // TODO
-    // if (name.value && this.$refs['cert-info' + this.current_server_index]) {
-    //     this.$refs['cert-info' + this.current_server_index].get()
-    // }
-}
-
 function change_tls(r: any) {
     if (r) {
         // deep copy servers[0] to servers[1]
@@ -140,17 +130,16 @@ const current_support_ssl = computed(() => {
 
                 <div class="tab-content">
                     <template v-if="current_support_ssl&&enabled">
-                        <cert-info :domain="name" v-if="current_support_ssl"/>
-                        <!--                        <issue-cert-->
-                        <!--                            :current_server_directives="current_server_directives"-->
-                        <!--                            :directives-map="directivesMap"-->
-                        <!--                            v-model="auto_cert"-->
-                        <!--                        />-->
+                        <cert
+                            v-if="current_support_ssl"
+                            :current_server_directives="current_server_directives"
+                            :directives-map="directivesMap"
+                            v-model:enabled="auto_cert"/>
                     </template>
 
                     <template v-if="v.comments">
                         <h3 v-translate>Comments</h3>
-                        <p style="white-space: pre-wrap;">{{ v.comments }}</p>
+                        <a-textarea v-model:value="v.comments" :bordered="false"/>
                     </template>
 
                     <directive-editor :ngx_directives="v.directives"/>

+ 16 - 12
frontend/src/views/domain/ngx_conf/directive/DirectiveAdd.vue

@@ -3,7 +3,7 @@ import {If} from '@/views/domain/ngx_conf'
 import CodeEditor from '@/components/CodeEditor'
 import {reactive, ref} from 'vue'
 import {useGettext} from 'vue3-gettext'
-import {CloseOutlined} from '@ant-design/icons-vue'
+import {DeleteOutlined} from '@ant-design/icons-vue'
 
 const {$gettext} = useGettext()
 
@@ -52,17 +52,18 @@ function save() {
             </a-form-item>
             <a-form-item>
                 <code-editor v-if="mode===If" default-height="100px" v-model:content="directive.params"/>
-
-                <a-input-group compact v-else>
-
-                    <a-input style="width: 30%" :placeholder="$gettext('Directive')" v-model="directive.directive"/>
-
-                    <a-input style="width: 70%" :placeholder="$gettext('Params')" v-model="directive.params">
-                        <template #suffix>
-                            <CloseOutlined @click="adding=false" style="color: rgba(0,0,0,.45);font-size: 10px;"/>
+                <div class="input-wrapper" v-else>
+                    <a-input-group compact>
+                        <a-input style="width: 30%" :placeholder="$gettext('Directive')" v-model="directive.directive"/>
+                        <a-input style="width: 70%" :placeholder="$gettext('Params')" v-model="directive.params"/>
+                    </a-input-group>
+                    <a-button @click="adding=false">
+                        <template #icon>
+                            <DeleteOutlined style="font-size: 14px;"/>
                         </template>
-                    </a-input>
-                </a-input-group>
+                    </a-button>
+
+                </div>
             </a-form-item>
         </div>
         <a-button block v-if="!adding" @click="add">{{ $gettext('Add Directive Below') }}</a-button>
@@ -73,5 +74,8 @@ function save() {
 </template>
 
 <style lang="less" scoped>
-
+.input-wrapper {
+    display: flex;
+    gap: 10px;
+}
 </style>

+ 23 - 14
frontend/src/views/domain/ngx_conf/directive/DirectiveEditor.vue

@@ -4,7 +4,7 @@ import {If} from '@/views/domain/ngx_conf'
 import DirectiveAdd from '@/views/domain/ngx_conf/directive/DirectiveAdd'
 import {useGettext} from 'vue3-gettext'
 import {reactive, ref} from 'vue'
-import {CloseOutlined} from '@ant-design/icons-vue'
+import {DeleteOutlined} from '@ant-design/icons-vue'
 
 const {$gettext} = useGettext()
 
@@ -45,19 +45,23 @@ function onSave(idx: number) {
     <a-form-item v-for="(directive,index) in ngx_directives" @click="current_idx=index">
         <code-editor v-if="directive.directive === If" v-model:content="directive.params"
                      defaultHeight="100px"/>
-        <a-input :addon-before="directive.directive" v-model:value="directive.params" @click="current_idx=k"
-                 v-else>
-            <template #suffix>
-                <a-popconfirm @confirm="remove(index)"
-                              :title="$gettext('Are you sure you want to remove this directive?')"
-                              :ok-text="$gettext('Yes')"
-                              :cancel-text="$gettext('No')">
-                    <CloseOutlined style="color: rgba(0,0,0,.45);font-size: 10px;"/>
-                </a-popconfirm>
-            </template>
-        </a-input>
+        <div class="input-wrapper" v-else>
+            <a-input :addon-before="directive.directive" v-model:value="directive.params" @click="current_idx=k"
+            >
+            </a-input>
+            <a-popconfirm @confirm="remove(index)"
+                          :title="$gettext('Are you sure you want to remove this directive?')"
+                          :ok-text="$gettext('Yes')"
+                          :cancel-text="$gettext('No')">
+                <a-button>
+                    <template #icon>
+                        <DeleteOutlined style="font-size: 14px;"/>
+                    </template>
+                </a-button>
+            </a-popconfirm>
+        </div>
         <transition name="slide">
-            <div v-if="current_idx===index" class="extra">
+            <div v-if="current_idx===index" class="directive-editor-extra">
                 <div class="extra-content">
                     <a-form layout="vertical">
                         <a-form-item :label="$gettext('Comments')">
@@ -74,7 +78,7 @@ function onSave(idx: number) {
 </template>
 
 <style lang="less" scoped>
-.extra {
+.directive-editor-extra {
     background-color: #fafafa;
     padding: 10px 20px 20px;
     margin-bottom: 10px;
@@ -91,4 +95,9 @@ function onSave(idx: number) {
 .slide-enter-to, .slide-leave-from {
     max-height: 600px;
 }
+
+.input-wrapper {
+    display: flex;
+    gap: 10px;
+}
 </style>

+ 11 - 9
frontend/src/views/other/Error.vue

@@ -1,16 +1,17 @@
+<script setup lang="ts">
+import {useGettext} from 'vue3-gettext'
+
+const {$gettext} = useGettext()
+</script>
+
 <template>
     <div class="wrapper">
-        <h1 class="title">{{ $route.meta.status_code ? $route.meta.status_code : 404 }}</h1>
-        <p>{{ $route.meta.error ? $route.meta.error : $gettext('File Not Found') }}</p>
+        <h1 class="title">{{ $route.meta.status_code || 404 }}</h1>
+        <p>{{ $route.meta.error?.() ?? $gettext('File Not Found') }}</p>
+        <a-button type="primary" v-translate @click="$router.push('/')">Back Home</a-button>
     </div>
 </template>
 
-<script>
-export default {
-    name: 'Error'
-}
-</script>
-
 <style lang="less" scoped>
 body, div, h1, html {
     padding: 0;
@@ -27,7 +28,8 @@ body, html {
 
 h1 {
     font-size: 8em;
-    font-weight: 100
+    font-weight: 100;
+    margin: 10px 0;
 }
 
 a {

+ 102 - 84
frontend/src/views/other/Install.vue

@@ -1,72 +1,130 @@
+<script setup lang="ts">
+import {Form, message} from 'ant-design-vue'
+import SetLanguage from '@/components/SetLanguage/SetLanguage.vue'
+import {reactive, ref} from 'vue'
+import gettext from '@/gettext'
+import install from '@/api'
+import {useRoute, useRouter} from 'vue-router'
+import {MailOutlined, UserOutlined, LockOutlined, DatabaseOutlined} from '@ant-design/icons-vue'
+
+const {$gettext, interpolate} = gettext
+
+const thisYear = new Date().getFullYear()
+const loading = ref(false)
+
+const route = useRoute()
+const router = useRouter()
+
+install.get_lock().then(async (r: { lock: boolean }) => {
+    if (r.lock) {
+        await router.push('/login')
+    }
+})
+
+const modelRef = reactive({
+    email: '',
+    username: '',
+    password: '',
+    database: ''
+})
+
+const rulesRef = reactive({
+    email: [
+        {
+            required: true,
+            type: 'email',
+            message: () => $gettext('Please input your E-mail!'),
+        }
+    ],
+    username: [
+        {
+            required: true,
+            message: () => $gettext('Please input your username!'),
+        }
+    ],
+    password: [
+        {
+            required: true,
+            message: () => $gettext('Please input your password!'),
+        }
+    ],
+    database: [
+        {
+            message: () => interpolate(
+                $gettext('The filename cannot contain the following characters: %{c}'),
+                {c: '& &quot; ? < > # {} % ~ / \\'}
+            ),
+        }
+    ],
+})
+
+const {validate, validateInfos} = Form.useForm(modelRef, rulesRef)
+
+const onSubmit = () => {
+    validate().then(() => {
+        // modelRef
+        loading.value = true
+        install.install_nginx_ui(modelRef).then(async () => {
+            message.success($gettext('Install successfully'))
+            await router.push('/login')
+        }).catch(e => {
+            message.error(e.message ?? $gettext('Server error'))
+        }).finally(() => {
+            loading.value = false
+        })
+    })
+}
+</script>
+
 <template>
     <div class="login-form">
         <div class="project-title">
             <h1>Nginx UI</h1>
         </div>
-        <a-form
-            id="components-form-install"
-            :form="form"
-            class="login-form"
-            @submit="handleSubmit"
-        >
-            <a-form-item>
+        <a-form id="components-form-install" class="login-form">
+            <a-form-item v-bind="validateInfos.email">
                 <a-input
-                    v-decorator="[
-          'email',
-          { rules: [{
-                type: 'email',
-                message: $gettext('Invalid E-mail!'),
-              },
-              {
-                required: true,
-                message: $gettext('Please input your E-mail!'),
-              },] },
-        ]"
+                    v-model:value="modelRef.email"
                     :placeholder="$gettext('Email (*)')"
                 >
-                    <a-icon slot="prefix" type="mail" style="color: rgba(0,0,0,.25)"/>
+                    <template #prefix>
+                        <MailOutlined/>
+                    </template>
                 </a-input>
             </a-form-item>
-            <a-form-item>
+            <a-form-item v-bind="validateInfos.username">
                 <a-input
-                    v-decorator="[
-          'username',
-          { rules: [{ required: true, message: $gettext('Please input your username!') }] },
-        ]"
+                    v-model:value="modelRef.username"
                     :placeholder="$gettext('Username (*)')"
                 >
-                    <a-icon slot="prefix" type="user" style="color: rgba(0,0,0,.25)"/>
+                    <template #prefix>
+                        <UserOutlined/>
+                    </template>
                 </a-input>
             </a-form-item>
-            <a-form-item>
-                <a-input
-                    v-decorator="[
-          'password',
-          { rules: [{ required: true, message: $gettext('Please input your password!') }] },
-        ]"
-                    type="password"
+            <a-form-item v-bind="validateInfos.password">
+                <a-input-password
+                    v-model:value="modelRef.password"
                     :placeholder="$gettext('Password (*)')"
                 >
-                    <a-icon slot="prefix" type="lock" style="color: rgba(0,0,0,.25)"/>
-                </a-input>
+                    <template #prefix>
+                        <LockOutlined/>
+                    </template>
+                </a-input-password>
             </a-form-item>
             <a-form-item>
                 <a-input
-                    v-decorator="[
-          'database',
-          { rules: [{ pattern: /^[^\\/:*?\x22<>|]{1,120}$/,
-          message: $gettextInterpolate(
-              $gettext('The filename cannot contain the following characters: %{c}'),
-              {c: '& &quot; ? < > # {} % ~ / \\'}
-          )}] },
-        ]"
+                    v-bind="validateInfos.database"
+                    v-model:value="modelRef.database"
                     :placeholder="$gettext('Database (Optional, default: database)')"
                 >
-                    <a-icon slot="prefix" type="database" style="color: rgba(0,0,0,.25)"/>
+                    <template #prefix>
+                        <DatabaseOutlined/>
+                    </template>
                 </a-input>
             </a-form-item>
             <a-form-item>
-                <a-button type="primary" :block="true" html-type="submit" :loading="loading">
+                <a-button type="primary" :block="true" @click="onSubmit" html-type="submit" :loading="loading">
                     <translate>Install</translate>
                 </a-button>
             </a-form-item>
@@ -79,46 +137,6 @@
 
 </template>
 
-<script>
-import SetLanguage from '@/components/SetLanguage/SetLanguage'
-
-export default {
-    name: 'Login',
-    components: {SetLanguage},
-    data() {
-        return {
-            form: {},
-            lock: true,
-            thisYear: new Date().getFullYear(),
-            loading: false
-        }
-    },
-    created() {
-        this.form = this.$form.createForm(this)
-    },
-    mounted() {
-        this.$api.install.get_lock().then(r => {
-            if (r.lock) {
-                this.$router.push('/login')
-            }
-        })
-    },
-    methods: {
-        handleSubmit: async function (e) {
-            e.preventDefault()
-            this.loading = true
-            await this.form.validateFields(async (err, values) => {
-                if (!err) {
-                    this.$api.install.install_nginx_ui(values).then(() => {
-                        this.$router.push('/login')
-                    })
-                }
-                this.loading = false
-            })
-        },
-    },
-}
-</script>
 <style lang="less">
 .project-title {
     margin: 50px;

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

@@ -1,4 +1,6 @@
 <script setup lang="ts">
+import {useUserStore} from '@/pinia'
+
 const thisYear = new Date().getFullYear()
 
 import {LockOutlined, UserOutlined} from '@ant-design/icons-vue'
@@ -23,13 +25,13 @@ const rulesRef = reactive({
     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!'),
         }
     ]
 })
@@ -44,11 +46,18 @@ const onSubmit = () => {
             const next = (route.query?.next || '').toString() || '/'
             await router.push(next)
         }).catch(e => {
-            message.error(e.message)
+            message.error(e.message ?? $gettext('Server error'))
         })
     })
 }
 
+const user = useUserStore()
+
+if (user.is_login) {
+    const next = (route.query?.next || '').toString() || '/dashboard'
+    router.push(next)
+}
+
 </script>
 
 <template>

+ 1 - 1
frontend/src/views/user/User.vue

@@ -45,7 +45,7 @@ const columns = [{
 </script>
 
 <template>
-    <std-curd :columns="columns" :api="user"/>
+    <std-curd :title="$gettext('Manage Users')" :columns="columns" :api="user"/>
 </template>
 
 <style scoped>

+ 1 - 1
frontend/version.json

@@ -1 +1 @@
-{"version":"1.5.0","build_id":11,"total_build":81}
+{"version":"1.5.0","build_id":14,"total_build":84}

+ 3 - 0
frontend/vite.config.ts

@@ -58,6 +58,9 @@ export default defineConfig({
     css: {
         preprocessorOptions: {
             less: {
+                modifyVars: {
+                    'border-radius-base': '4px',
+                },
                 javascriptEnabled: true,
             }
         },

+ 53 - 52
server/api/install.go

@@ -1,73 +1,74 @@
 package api
 
 import (
-    "github.com/0xJacky/Nginx-UI/server/model"
-    "github.com/0xJacky/Nginx-UI/server/settings"
-    "github.com/gin-gonic/gin"
-    "github.com/google/uuid"
-    "golang.org/x/crypto/bcrypt"
-    "net/http"
+	"github.com/0xJacky/Nginx-UI/server/model"
+	"github.com/0xJacky/Nginx-UI/server/settings"
+	"github.com/gin-gonic/gin"
+	"github.com/google/uuid"
+	"golang.org/x/crypto/bcrypt"
+	"net/http"
 )
 
 func installLockStatus() bool {
-    return "" != settings.ServerSettings.JwtSecret
+	return "" != settings.ServerSettings.JwtSecret
 }
 
 func InstallLockCheck(c *gin.Context) {
-    c.JSON(http.StatusOK, gin.H{
-        "lock": installLockStatus(),
-    })
+	c.JSON(http.StatusOK, gin.H{
+		"lock": installLockStatus(),
+	})
 }
 
 type InstallJson struct {
-    Email    string `json:"email" binding:"required,email"`
-    Username string `json:"username" binding:"required,max=255"`
-    Password string `json:"password" binding:"required,max=255"`
-    Database string `json:"database"`
+	Email    string `json:"email" binding:"required,email"`
+	Username string `json:"username" binding:"required,max=255"`
+	Password string `json:"password" binding:"required,max=255"`
+	Database string `json:"database"`
 }
 
 func InstallNginxUI(c *gin.Context) {
-    // 安装过就别访问了
-    if installLockStatus() {
-        c.JSON(http.StatusForbidden, gin.H{
-            "message": "installed",
-        })
-        return
-    }
-    var json InstallJson
-    ok := BindAndValid(c, &json)
-    if !ok {
-        return
-    }
+	// Visit this api after installed is forbidden
+	if installLockStatus() {
+		c.JSON(http.StatusForbidden, gin.H{
+			"error": "installed",
+		})
+		return
+	}
+	var json InstallJson
+	ok := BindAndValid(c, &json)
+	if !ok {
+		return
+	}
 
-    settings.ServerSettings.JwtSecret = uuid.New().String()
-    settings.ServerSettings.Email = json.Email
-    if "" != json.Database {
-        settings.ServerSettings.Database = json.Database
-    }
-    settings.ReflectFrom()
+	settings.ServerSettings.JwtSecret = uuid.New().String()
+	settings.ServerSettings.Email = json.Email
+	if "" != json.Database {
+		settings.ServerSettings.Database = json.Database
+	}
+	settings.ReflectFrom()
 
-    err := settings.Save()
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
+	err := settings.Save()
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
 
-    // Init model
-    model.Init()
+	// Init model
+	model.Init()
 
-    curd := model.NewCurd(&model.Auth{})
-    pwd, _ := bcrypt.GenerateFromPassword([]byte(json.Password), bcrypt.DefaultCost)
-    err = curd.Add(&model.Auth{
-        Name:     json.Username,
-        Password: string(pwd),
-    })
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-    c.JSON(http.StatusOK, gin.H{
-        "message": "ok",
-    })
+	curd := model.NewCurd(&model.Auth{})
+	pwd, _ := bcrypt.GenerateFromPassword([]byte(json.Password), bcrypt.DefaultCost)
+	err = curd.Add(&model.Auth{
+		Name:     json.Username,
+		Password: string(pwd),
+	})
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+	c.JSON(http.StatusOK, gin.H{
+		"message": "ok",
+	})
 
 }

+ 2 - 11
server/api/user.go

@@ -11,18 +11,9 @@ import (
 )
 
 func GetUsers(c *gin.Context) {
-	curd := model.NewCurd(&model.Auth{})
-
-	var list []model.Auth
-	err := curd.GetList(&list)
+	data := model.GetUserList(c, c.Query("name"))
 
-	if err != nil {
-		ErrHandler(c, err)
-		return
-	}
-	c.JSON(http.StatusOK, gin.H{
-		"data": list,
-	})
+	c.JSON(http.StatusOK, data)
 }
 
 func GetUser(c *gin.Context) {

+ 18 - 0
server/model/auth.go

@@ -2,6 +2,7 @@ package model
 
 import (
 	"github.com/0xJacky/Nginx-UI/server/settings"
+	"github.com/gin-gonic/gin"
 	"github.com/golang-jwt/jwt"
 	"time"
 )
@@ -30,6 +31,23 @@ func GetUser(name string) (user Auth, err error) {
 	return user, err
 }
 
+func GetUserList(c *gin.Context, username interface{}) (data DataList) {
+	var total int64
+	db.Model(&Auth{}).Count(&total)
+	var users []Auth
+
+	result := db.Model(&Auth{}).Scopes(orderAndPaginate(c))
+
+	if username != "" {
+		result = result.Where("name LIKE ?", "%"+username.(string)+"%")
+	}
+
+	result.Find(&users)
+
+	data = GetListWithPagination(&users, c, total)
+	return
+}
+
 func DeleteToken(token string) error {
 	return db.Where("token = ?", token).Delete(&AuthToken{}).Error
 }

+ 116 - 0
server/model/model.go

@@ -0,0 +1,116 @@
+package model
+
+import (
+	"fmt"
+	"github.com/0xJacky/Nginx-UI/server/settings"
+	"github.com/gin-gonic/gin"
+	"github.com/spf13/cast"
+	"gorm.io/driver/sqlite"
+	"gorm.io/gorm"
+	"gorm.io/gorm/logger"
+	"log"
+	"path"
+	"time"
+)
+
+var db *gorm.DB
+
+type Model struct {
+	ID        uint       `gorm:"primary_key" json:"id"`
+	CreatedAt time.Time  `json:"created_at"`
+	UpdatedAt time.Time  `json:"updated_at"`
+	DeletedAt *time.Time `gorm:"index" json:"deleted_at"`
+}
+
+func Init() {
+	dbPath := path.Join(path.Dir(settings.ConfPath), fmt.Sprintf("%s.db", settings.ServerSettings.Database))
+	var err error
+	db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{
+		Logger:      logger.Default.LogMode(logger.Info),
+		PrepareStmt: true,
+	})
+	if err != nil {
+		log.Println(err)
+	}
+	// Migrate the schema
+	AutoMigrate(&ConfigBackup{})
+	AutoMigrate(&Auth{})
+	AutoMigrate(&AuthToken{})
+	AutoMigrate(&Cert{})
+}
+
+func AutoMigrate(model interface{}) {
+	err := db.AutoMigrate(model)
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+
+func orderAndPaginate(c *gin.Context) func(db *gorm.DB) *gorm.DB {
+	return func(db *gorm.DB) *gorm.DB {
+		sort := c.DefaultQuery("sort", "desc")
+		order := c.DefaultQuery("order_by", "id") +
+			" " + sort
+
+		page := cast.ToInt(c.Query("page"))
+		if page == 0 {
+			page = 1
+		}
+		pageSize := settings.ServerSettings.PageSize
+		reqPageSize := c.Query("page_size")
+		if reqPageSize != "" {
+			pageSize = cast.ToInt(reqPageSize)
+		}
+		offset := (page - 1) * pageSize
+
+		return db.Order(order).Offset(offset).Limit(pageSize)
+	}
+}
+
+func totalPage(total int64, pageSize int) int64 {
+	n := total / int64(pageSize)
+	if total%int64(pageSize) > 0 {
+		n++
+	}
+	return n
+}
+
+type Pagination struct {
+	Total       int64 `json:"total"`
+	PerPage     int   `json:"per_page"`
+	CurrentPage int   `json:"current_page"`
+	TotalPages  int64 `json:"total_pages"`
+}
+
+type DataList struct {
+	Data       interface{} `json:"data"`
+	Pagination Pagination  `json:"pagination,omitempty"`
+}
+
+func GetListWithPagination(models interface{},
+	c *gin.Context, totalRecords int64) (result DataList) {
+
+	page := cast.ToInt(c.Query("page"))
+	if page == 0 {
+		page = 1
+	}
+
+	result = DataList{}
+
+	result.Data = models
+
+	pageSize := settings.ServerSettings.PageSize
+	reqPageSize := c.Query("page_size")
+	if reqPageSize != "" {
+		pageSize = cast.ToInt(reqPageSize)
+	}
+
+	result.Pagination = Pagination{
+		Total:       totalRecords,
+		PerPage:     pageSize,
+		CurrentPage: page,
+		TotalPages:  totalPage(totalRecords, pageSize),
+	}
+
+	return
+}

+ 0 - 45
server/model/models.go

@@ -1,45 +0,0 @@
-package model
-
-import (
-	"fmt"
-	"github.com/0xJacky/Nginx-UI/server/settings"
-	"gorm.io/driver/sqlite"
-	"gorm.io/gorm"
-	"gorm.io/gorm/logger"
-	"log"
-	"path"
-	"time"
-)
-
-var db *gorm.DB
-
-type Model struct {
-	ID        uint       `gorm:"primary_key" json:"id"`
-	CreatedAt time.Time  `json:"created_at"`
-	UpdatedAt time.Time  `json:"updated_at"`
-	DeletedAt *time.Time `gorm:"index" json:"deleted_at"`
-}
-
-func Init() {
-	dbPath := path.Join(path.Dir(settings.ConfPath), fmt.Sprintf("%s.db", settings.ServerSettings.Database))
-	var err error
-	db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{
-		Logger:      logger.Default.LogMode(logger.Info),
-		PrepareStmt: true,
-	})
-	if err != nil {
-		log.Println(err)
-	}
-	// Migrate the schema
-	AutoMigrate(&ConfigBackup{})
-	AutoMigrate(&Auth{})
-	AutoMigrate(&AuthToken{})
-	AutoMigrate(&Cert{})
-}
-
-func AutoMigrate(model interface{}) {
-	err := db.AutoMigrate(model)
-	if err != nil {
-		log.Fatal(err)
-	}
-}

+ 2 - 0
server/settings/settings.go

@@ -25,6 +25,7 @@ type Server struct {
 	Database          string
 	StartCmd          string
 	Demo              bool
+	PageSize          int
 }
 
 var ServerSettings = &Server{
@@ -34,6 +35,7 @@ var ServerSettings = &Server{
 	Database:          "database",
 	StartCmd:          "login",
 	Demo:              false,
+	PageSize:          10,
 }
 
 var ConfPath string