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

Refactored install and 404 pages
This commit is contained in:
Jacky 2022-08-02 14:05:05 +08:00 committed by GitHub
commit 2b800f41d5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 613 additions and 528 deletions

View file

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

View file

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

View file

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

View file

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

Before

Width:  |  Height:  |  Size: 1.6 KiB

12
frontend/src/api/index.ts Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,18 +11,9 @@ import (
)
func GetUsers(c *gin.Context) {
curd := model.NewCurd(&model.Auth{})
data := model.GetUserList(c, c.Query("name"))
var list []model.Auth
err := curd.GetList(&list)
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) {

View file

@ -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
server/model/model.go Normal file
View file

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

View file

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

View file

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